"""
Deals with commands and logic for hangman games.

*Classes*
---------
  Hangman()
    Deals with the game logic of hangman.
  DrawHangman()
    Draws the image shown to the player.
"""
import os
import datetime  # Used for generating the game id
import string  # string.ascii_uppercase used
import math  # Used by DrawHangman(), mainly for drawing circles
import random  # Used to draw poorly
import requests  # Used for getting the word in Hangman.start()
import discord  # Used for discord.file and type hints

from interactions.utils.manage_components import (create_button,
create_actionrow)
from interactions.model import ButtonStyle
from interactions import SlashContext, ComponentContext
# Used for typehints
from PIL import ImageDraw, Image, ImageFont  # Used to draw the image

from gwendolyn_old.utils import encode_id

class Hangman():
  """
  Controls hangman commands and game logic.

  *Methods*
  ---------
    start(ctx: SlashContext)
    stop(ctx: SlashContext)
    guess(message: discord.message, user: str, guess: str)
  """

  def __init__(self, bot):
    """
    Initialize the class.

    *Attributes*
    ------------
      draw: DrawHangman
        The DrawHangman used to draw the hangman image.
      API_url: str
        The url to get the words from.
      APIPARAMS: dict
        The parameters to pass to every api call.
    """
    self.bot = bot
    self._draw = DrawHangman(bot)
    self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
    api_key = self.bot.credentials["wordnik_key"]
    self._APIPARAMS = { # pylint: disable=invalid-name
      "hasDictionaryDef": True,
      "minCorpusCount": 5000,
      "maxCorpusCount": -1,
      "minDictionaryCount": 1,
      "maxDictionaryCount": -1,
      "minLength": 3,
      "maxLength": 11,
      "limit": 1,
      "api_key": api_key
    }

  async def start(self, ctx: SlashContext):
    """
    Start a game of hangman.

    *Parameters*
    ------------
      ctx: SlashContext
        The context of the command.
    """
    await self.bot.defer(ctx)

    word = "-"
    while "-" in word or "." in word:
      response = requests.get(self._API_url, params=self._APIPARAMS)
      word = list(response.json()[0]["word"].upper())

    self.bot.log("Found the word \""+"".join(word)+"\"")
    guessed = [False] * len(word)
    game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")

    remaining_letters = list(string.ascii_uppercase)

    self._draw.draw_image(game_id, 0, word, guessed, [])

    send_message = f"{ctx.author.display_name} started a game of hangman."

    self.bot.log("Game started")
    await ctx.send(send_message)

    boards_path = "gwendolyn/resources/games/hangman_boards/"
    file_path = f"{boards_path}hangman_board{game_id}.png"
    image_message = await ctx.channel.send(
      file=discord.File(file_path)
    )

    blank_message_one = await ctx.channel.send("_ _")
    blank_message_two = await ctx.channel.send("_ _")

    buttons = []
    for letter in remaining_letters:
      custom_id = encode_id([
        "hangman",
        "guess",
        str(ctx.author.id),
        letter,
        "".join(word),
        "",
        game_id,
        str(image_message.id),
        str(blank_message_one.id),
        str(blank_message_two.id)
      ])
      buttons.append(create_button(
        style=ButtonStyle.blue,
        label=letter,
        custom_id=custom_id
      ))

    custom_id = encode_id([
      "hangman",
      "end",
      str(ctx.author.id),
      game_id,
      str(image_message.id),
      str(blank_message_one.id),
      str(blank_message_two.id)
    ])
    buttons.append(create_button(
      style=ButtonStyle.red,
      label="End game",
      custom_id=custom_id
    ))

    action_row_lists = []
    for i in range(((len(buttons)-1)//20)+1):
      action_rows = []
      available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
      for x in range(((len(available_buttons)-1)//4)+1):
        row_buttons = available_buttons[
          (x*4):(min(len(available_buttons),x*4+4))
        ]
        action_rows.append(create_actionrow(*row_buttons))
      action_row_lists.append(action_rows)


    await blank_message_one.edit(components=action_row_lists[0])
    await blank_message_two.edit(components=action_row_lists[1])


  async def stop(self, ctx: ComponentContext, game_id: str,
           *messages: list[str]):
    """
    Stop the game of hangman.

    *Parameters*
    ------------
      ctx: SlashContext
        The context of the command.
    """

    for msg_id in messages:
      msg = await ctx.channel.fetch_message(msg_id)
      await msg.delete()

    self.bot.log("Deleting old messages")

    await ctx.channel.send("Hangman game stopped")
    boards_path = "gwendolyn/resources/games/hangman_boards/"
    file_path = f"{boards_path}hangman_board{game_id}.png"
    os.remove(file_path)

  async def guess(self, ctx: ComponentContext, guess: str, word: str,
          guessed_letters: str, game_id: str, *messages: list[str]):
    """
    Guess a letter.

    *Parameters*
    ------------
      message: discord.Message
        The message that the reaction was placed on.
      user: str
        The id of the user.
      guess: str
        The guess.
    """
    for msg_id in messages:
      msg = await ctx.channel.fetch_message(msg_id)
      await msg.delete()

    misses = 0
    guessed_letters += guess
    guessed = [False for i in range(len(word))]
    for guessed_letter in guessed_letters:
      correct_guess = 0
      for i, letter in enumerate(word):
        if guessed_letter == letter:
          correct_guess += 1
          guessed[i] = True

      if correct_guess == 0:
        misses += 1


    remaining_letters = list(string.ascii_uppercase)
    for letter in guessed_letters:
      remaining_letters.remove(letter)

    if correct_guess == 1:
      send_message = "Guessed {}. There was 1 {} in the word."
      send_message = send_message.format(guess, guess)
    else:
      send_message = "Guessed {}. There were {} {}s in the word."
      send_message = send_message.format(guess, correct_guess, guess)

    self._draw.draw_image(game_id, misses, word, guessed, guessed_letters)

    if misses == 6:
      send_message += self.bot.long_strings["Hangman lost game"]
      remaining_letters = []
    elif all(guessed):
      self.bot.money.addMoney(f"#{ctx.author_id}", 15)
      send_message += self.bot.long_strings["Hangman guessed word"]
      remaining_letters = []


    if remaining_letters != []:
      await ctx.channel.send(send_message)
      boards_path = "gwendolyn/resources/games/hangman_boards/"
      file_path = f"{boards_path}hangman_board{game_id}.png"
      image_message = await ctx.channel.send(
        file=discord.File(file_path)
      )
      blank_message_one = await ctx.channel.send("_ _")
      blank_message_two = await ctx.channel.send("_ _")
      buttons = []
      for letter in list(string.ascii_uppercase):
        custom_id = encode_id([
          "hangman",
          "guess",
          str(ctx.author.id),
          letter,
          word,
          guessed_letters,
          game_id,
          str(image_message.id),
          str(blank_message_one.id),
          str(blank_message_two.id)
        ])
        buttons.append(create_button(
          style=ButtonStyle.blue,
          label=letter,
          custom_id=custom_id,
          disabled=(letter not in remaining_letters)
        ))
      custom_id = encode_id([
        "hangman",
        "end",
        str(ctx.author.id),
        game_id,
        str(image_message.id),
        str(blank_message_one.id),
        str(blank_message_two.id)
      ])
      buttons.append(create_button(
        style=ButtonStyle.red,
        label="End game",
        custom_id=custom_id
      ))

      action_row_lists = []
      for i in range(((len(buttons)-1)//20)+1):
        action_rows = []
        available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
        for x in range(((len(available_buttons)-1)//4)+1):
          row_buttons = available_buttons[
            (x*4):(min(len(available_buttons),x*4+4))
          ]
          action_rows.append(create_actionrow(*row_buttons))
        action_row_lists.append(action_rows)

      await blank_message_one.edit(components=action_row_lists[0])
      await blank_message_two.edit(components=action_row_lists[1])
    else:
      boards_path = "gwendolyn/resources/games/hangman_boards/"
      file_path = f"{boards_path}hangman_board{game_id}.png"
      await ctx.channel.send(send_message, file=discord.File(file_path))

      os.remove(file_path)




class DrawHangman():
  """
  Draws the image of the hangman game.

  *Methods*
  ---------
    draw_image(channel: str)
  """

  def __init__(self, bot):
    """
    Initialize the class.

    *Attributes*
    ------------
      CIRCLESIZE
      LINEWIDTH
      BODYSIZE
      LIMBSIZE
      ARMPOSITION
      MANX, MANY
      LETTERLINELENGTH
      LETTERLINEDISTANCE
      GALLOWX, GALLOWY
      PHI
      FONT
      SMALLFONT
    """
    self._bot = bot
    # pylint: disable=invalid-name
    self._CIRCLESIZE = 120
    self._LINEWIDTH = 12

    self._BODYSIZE = 210
    self._LIMBSIZE = 60
    self._ARMPOSITION = 60

    self._MANX = (self._LIMBSIZE*2)
    self._MANY = (self._CIRCLESIZE+self._BODYSIZE+self._LIMBSIZE)
    MANPADDING = self._LINEWIDTH*4
    self._MANX += MANPADDING
    self._MANY += MANPADDING

    self._LETTERLINELENGTH = 90
    self._LETTERLINEDISTANCE = 30

    self._GALLOWX, self._GALLOWY = 360, 600
    self._PHI = 1-(1 / ((1 + 5 ** 0.5) / 2))

    LETTERSIZE = 75  # Wrong guesses letter size
    WORDSIZE = 70  # Correct guesses letter size

    FONTPATH = "gwendolyn/resources/fonts/comic-sans-bold.ttf"
    self._FONT = ImageFont.truetype(FONTPATH, LETTERSIZE)
    self._SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE)
    # pylint: enable=invalid-name

  def _deviate(self, pre_deviance: int, pre_deviance_accuracy: int,
          position_change: float, maxmin: int,
          max_acceleration: float):
    random_deviance = random.uniform(-position_change, position_change)
    deviance_accuracy = pre_deviance_accuracy + random_deviance
    if deviance_accuracy > maxmin * max_acceleration:
      deviance_accuracy = maxmin * max_acceleration
    elif deviance_accuracy < -maxmin * max_acceleration:
      deviance_accuracy = -maxmin * max_acceleration

    deviance = pre_deviance + deviance_accuracy
    if deviance > maxmin:
      deviance = maxmin
    elif deviance < -maxmin:
      deviance = -maxmin
    return deviance, deviance_accuracy

  def _bad_circle(self):
    circle_padding = (self._LINEWIDTH*3)
    image_width = self._CIRCLESIZE+circle_padding
    image_size = (image_width, image_width)
    background = Image.new("RGBA", image_size, color=(0, 0, 0, 0))

    drawer = ImageDraw.Draw(background, "RGBA")
    middle = (self._CIRCLESIZE+(self._LINEWIDTH*3))/2
    deviance_x = 0
    deviance_y = 0
    deviance_accuracy_x = 0
    deviance_accuracy_y = 0
    start = random.randint(-100, -80)
    degrees_amount = 360 + random.randint(-10, 30)

    for degree in range(degrees_amount):
      deviance_x, deviance_accuracy_x = self._deviate(
        deviance_x,
        deviance_accuracy_x,
        self._LINEWIDTH/100,
        self._LINEWIDTH,
        0.03
      )
      deviance_y, deviance_accuracy_y = self._deviate(
        deviance_y,
        deviance_accuracy_y,
        self._LINEWIDTH/100,
        self._LINEWIDTH,
        0.03
      )

      radians = math.radians(degree+start)
      circle_x = (math.cos(radians) * (self._CIRCLESIZE/2))
      circle_y = (math.sin(radians) * (self._CIRCLESIZE/2))

      position_x = middle + circle_x - (self._LINEWIDTH/2) + deviance_x
      position_y = middle + circle_y - (self._LINEWIDTH/2) + deviance_y

      circle_position = [
        (position_x, position_y),
        (position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
      ]
      drawer.ellipse(circle_position, fill=(0, 0, 0, 255))

    return background

  def _bad_line(self, length: int, rotated: bool = False):
    if rotated:
      width, height = length+self._LINEWIDTH*3, self._LINEWIDTH*3
    else:
      width, height = self._LINEWIDTH*3, length+self._LINEWIDTH*3
    background = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))

    drawer = ImageDraw.Draw(background, "RGBA")

    possible_deviance = int(self._LINEWIDTH/3)
    deviance_x = random.randint(-possible_deviance, possible_deviance)
    deviance_y = 0
    deviance_accuracy_x = 0
    deviance_accuracy_y = 0

    for pixel in range(length):
      deviance_x, deviance_accuracy_x = self._deviate(
        deviance_x,
        deviance_accuracy_x,
        self._LINEWIDTH/1000,
        self._LINEWIDTH,
        0.004
      )
      deviance_y, deviance_accuracy_y = self._deviate(
        deviance_y,
        deviance_accuracy_y,
        self._LINEWIDTH/1000,
        self._LINEWIDTH,
        0.004
      )

      if rotated:
        position_x = self._LINEWIDTH + pixel + deviance_x
        position_y = self._LINEWIDTH + deviance_y
      else:
        position_x = self._LINEWIDTH + deviance_x
        position_y = self._LINEWIDTH + pixel + deviance_y

      circle_position = [
        (position_x, position_y),
        (position_x+self._LINEWIDTH, position_y+self._LINEWIDTH)
      ]
      drawer.ellipse(circle_position, fill=(0, 0, 0, 255))

    return background

  def _draw_man(self, misses: int, seed: str):
    random.seed(seed)
    man_size = (self._MANX, self._MANY)
    background = Image.new("RGBA", man_size, color=(0, 0, 0, 0))

    if misses >= 1:
      head = self._bad_circle()
      paste_x = (self._MANX-(self._CIRCLESIZE+(self._LINEWIDTH*3)))//2
      paste_position = (paste_x, 0)
      background.paste(head, paste_position, head)
    if misses >= 2:
      body = self._bad_line(self._BODYSIZE)
      paste_x = (self._MANX-(self._LINEWIDTH*3))//2
      paste_position = (paste_x, self._CIRCLESIZE)
      background.paste(body, paste_position, body)

    if misses >= 3:
      limbs = random.sample(["rl", "ll", "ra", "la"], min(misses-2, 4))
    else:
      limbs = []

    random.seed(seed)

    for limb in limbs:
      limb_drawing = self._bad_line(self._LIMBSIZE, True)
      x_position = (self._MANX-(self._LINEWIDTH*3))//2

      if limb[1] == "a":
        rotation = random.randint(-45, 45)
        shift = math.sin(math.radians(rotation))
        line_length = self._LIMBSIZE+(self._LINEWIDTH*3)
        compensation = int(shift*line_length)
        limb_drawing = limb_drawing.rotate(rotation, expand=1)
        y_position = self._CIRCLESIZE + self._ARMPOSITION
        if limb == "ra":
          compensation = min(-compensation, 0)
        else:
          x_position -= self._LIMBSIZE
          compensation = min(compensation, 0)

        y_position += compensation
      else:
        rotation = random.randint(-15, 15)
        y_position = self._CIRCLESIZE+self._BODYSIZE-self._LINEWIDTH
        if limb == "rl":
          limb_drawing = limb_drawing.rotate(rotation-45, expand=1)
        else:
          x_position += -limb_drawing.size[0]+self._LINEWIDTH*3
          limb_drawing = limb_drawing.rotate(rotation+45, expand=1)

      paste_position = (x_position, y_position)
      background.paste(limb_drawing, paste_position, limb_drawing)

    return background

  def _bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)):
    if big:
      font = self._FONT
    else:
      font = self._SMALLFONT
    width, height = font.getsize(text)
    img = Image.new("RGBA", (width, height), color=(0, 0, 0, 0))
    drawer = ImageDraw.Draw(img, "RGBA")

    drawer.text((0, 0), text, font=font, fill=color)
    return img

  def _draw_gallows(self):
    gallow_size = (self._GALLOWX, self._GALLOWY)
    background = Image.new("RGBA", gallow_size, color=(0, 0, 0, 0))

    bottom_line = self._bad_line(int(self._GALLOWX * 0.75), True)
    bottom_line_x = int(self._GALLOWX * 0.125)
    bottom_line_y = self._GALLOWY-(self._LINEWIDTH*4)
    paste_position = (bottom_line_x, bottom_line_y)
    background.paste(bottom_line, paste_position, bottom_line)

    line_two = self._bad_line(self._GALLOWY-self._LINEWIDTH*6)
    line_two_x = int(self._GALLOWX*(0.75*self._PHI))
    line_two_y = self._LINEWIDTH*2
    paste_position = (line_two_x, line_two_y)
    background.paste(line_two, paste_position, line_two)

    top_line = self._bad_line(int(self._GALLOWY*0.30), True)
    paste_x = int(self._GALLOWX*(0.75*self._PHI))-self._LINEWIDTH
    paste_position = (paste_x, self._LINEWIDTH*3)
    background.paste(top_line, paste_position, top_line)

    last_line = self._bad_line(int(self._GALLOWY*0.125))
    paste_x += int(self._GALLOWY*0.30)
    background.paste(last_line, (paste_x, self._LINEWIDTH*3), last_line)
    return background

  def _draw_letter_lines(self, word: str, guessed: list, misses: int):
    letter_width = self._LETTERLINELENGTH+self._LETTERLINEDISTANCE
    image_width = letter_width*len(word)
    image_size = (image_width, self._LETTERLINELENGTH+self._LINEWIDTH*3)
    letter_lines = Image.new("RGBA", image_size, color=(0, 0, 0, 0))
    for i, letter in enumerate(word):
      line = self._bad_line(self._LETTERLINELENGTH, True)
      paste_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
      paste_position = (paste_x, self._LETTERLINELENGTH)
      letter_lines.paste(line, paste_position, line)
      if guessed[i]:
        letter_drawing = self._bad_text(letter, True)
        letter_width = self._FONT.getsize(letter)[0]
        letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
        letter_x -= (letter_width//2)
        letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
        letter_lines.paste(
          letter_drawing,
          (letter_x, 0),
          letter_drawing
        )
      elif misses == 6:
        letter_drawing = self._bad_text(letter, True, (242, 66, 54))
        letter_width = self._FONT.getsize(letter)[0]
        letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE)
        letter_x -= (letter_width//2)
        letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2)
        letter_lines.paste(
          letter_drawing,
          (letter_x, 0),
          letter_drawing
        )

    return letter_lines

  def _shortest_dist(self, positions: list, new_position: tuple):
    shortest_dist = math.inf
    for i, j in positions:
      x_distance = abs(i-new_position[0])
      y_distance = abs(j-new_position[1])
      dist = math.sqrt(x_distance**2+y_distance**2)
      if shortest_dist > dist:
        shortest_dist = dist
    return shortest_dist

  def _draw_misses(self, guesses: list, word: str):
    background = Image.new("RGBA", (600, 400), color=(0, 0, 0, 0))
    pos = []
    for guess in guesses:
      if guess not in word:
        placed = False
        while not placed:
          letter = self._bad_text(guess, True)
          w, h = self._FONT.getsize(guess)
          x = random.randint(0, 600-w)
          y = random.randint(0, 400-h)
          if self._shortest_dist(pos, (x, y)) > 70:
            pos.append((x, y))
            background.paste(letter, (x, y), letter)
            placed = True
    return background

  def draw_image(self, game_id: str, misses: int, word: str,
           guessed: list[bool], guessed_letters: str):
    """
    Draw a hangman Image.

    *Parameters*
    ------------
      channel: str
        The id of the channel the game is in.
    """
    self._bot.log("Drawing hangman image")

    random.seed(game_id)

    background = Image.open("gwendolyn/resources/games/default_images/hangman.png")
    gallow = self._draw_gallows()
    man = self._draw_man(misses, game_id)

    random.seed(game_id)
    letter_line_parameters = [word, guessed, misses]
    letter_lines = self._draw_letter_lines(*letter_line_parameters)

    random.seed(game_id)
    misses = self._draw_misses(list(guessed_letters), word)

    background.paste(gallow, (100, 100), gallow)
    background.paste(man, (300, 210), man)
    background.paste(letter_lines, (120, 840), letter_lines)
    background.paste(misses, (600, 150), misses)

    misses_text = self._bad_text("MISSES", False)
    misses_text_width = misses_text.size[0]
    background.paste(misses_text, (850-misses_text_width//2, 50), misses_text)

    boards_path = "gwendolyn/resources/games/hangman_boards/"
    image_path = f"{boards_path}hangman_board{game_id}.png"
    background.save(image_path)
