import chess
import pwd
import os

from chs.client.ending import GameOver
from chs.utils.core import Colors, Styles


def disjoin(a, b):
  result = [a.lower() for a in list(a)]
  for c in b:
    try:
      result.remove(c.lower())
    except ValueError:
      pass
  return ''.join(result)

def flatten(l):
  return [item for sublist in l for item in sublist]

def safe_pop(l):
  try:
    return l.pop()
  except IndexError:
    return None

def round_to_nearest(x, base=25):
  return base * round(x / base)

class Board(object):
  def __init__(self, level, play_as):
    self._play_as = play_as
    self._level = level
    self._score = 0
    self._cp = 0

  FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

  def generate(self, fen, board, engine, game_over=None):
    if board.turn:
      # Print board before generating the score
      board_loading = self._generate(fen, board, game_over, True)
      print(board_loading)
      print('\n{}{}{}┏━━━━━━━━━━━━━━━━━━━━━━━┓ \n{}┗{}{}{}waiting{}'.format(
        Styles.PADDING_SMALL, Colors.WHITE, Colors.BOLD,\
        Styles.PADDING_SMALL, Styles.PADDING_SMALL, Colors.RESET, Colors.GRAY, Colors.RESET)
      )
      # Analyze the score and print the board again when we're done
      new_cp = engine.score(board)
      new_score = engine.normalize(new_cp)
      self._score = new_score if new_score is not None else self._score
      self._cp = new_cp if new_cp is not None else self._cp
      board_loaded = self._generate(fen, board, game_over)
      print(board_loaded)
    else:
      # Print board without generating the score
      board_loading = self._generate(fen, board, game_over)
      print(board_loading)

  def _generate(self, fen, board, game_over, loading=False):
    self.clear()
    is_check = board.is_check()
    loading_text = '   {}↻{}\n'.format(Colors.GRAY, Colors.RESET) if loading else '\n'

    # Label who's turn it is to move
    turn = fen.split(' ')[1]
    ui_board = self.get_title_from_move(turn)
    ui_board += '{}\n'.format(loading_text)

    position_changes = None
    try:
      san = board.peek()
      uci = board.uci(san)
      starting_position = uci[0:2]
      ending_position = uci[2:4]
      position_changes = (starting_position, ending_position)
    except IndexError:
      position_changes = None

    hint_positions = None
    try:
      uci = board.help_engine_hint
      starting_position = uci[0:2]
      ending_position = uci[2:4]
      hint_positions = (starting_position, ending_position)
    except TypeError:
      hint_positions = None

    # Draw the board and pieces
    fen_positions = fen.split(' ')[0]

    # If user is black, reverse the positions so we draw black first
    positions = self.white_or_black(fen_positions, fen_positions[::-1])
    ranks = positions.split('/')
    rank_i = self.white_or_black(8, 1)
    rank_i_meta = 8

    def get_piece_composed(piece):
      if turn == 'b':
        return self.get_piece_colored(piece, is_check, False)
      else:
        return self.get_piece_colored(piece, False, is_check)

    for rank in ranks:
      file_i = self.white_or_black(1, 8)
      file_i_meta = 1
      pieces = flatten(map(get_piece_composed, list(rank)))
      ui_board += '{}{}{} '.format(Styles.PADDING_MEDIUM, Colors.GRAY, str(rank_i))
      # Add each piece + tile
      for piece in pieces:
        color = self.get_tile_color_from_position(rank_i, file_i, position_changes, hint_positions)
        ui_board += '{}{}'.format(color, piece)
        file_i = self.white_or_black(file_i + 1, file_i - 1)
        file_i_meta = file_i_meta + 1
      # Finish the rank
      ui_board += '{}  {}{}\n'.format(Colors.RESET, self.get_bar_section(rank_i_meta), self.get_meta_section(board, fen, rank_i_meta, game_over))
      rank_i = self.white_or_black(rank_i - 1, rank_i + 1)
      rank_i_meta = rank_i_meta - 1

    # Add files label - If user is black, reverse the file numbering since board is flipped
    ui_board += ' {}{}'.format(Styles.PADDING_MEDIUM, Colors.GRAY)
    files_ui = self.white_or_black(self.FILES, self.FILES[::-1])
    for f in files_ui:
      ui_board += ' {}'.format(f)
    # Extra meta text
    ui_board += '{}{}\n{}'.format(' ' * 6, self.get_meta_section(board, fen, 0, game_over), Colors.RESET)
    return ui_board

  def get_meta_section(self, board, fen, rank, game_over):
    padding = '    '
    padding_alt = '   '
    just_played = game_over or (
      chess.WHITE
      if len(board.san_move_stack_white) > len(board.san_move_stack_black)
      else chess.BLACK
    )
    if rank == 0:
      positions = fen.split(' ')[0]
      ranks = positions.split('/')
      # Calculate advantage pieces
      (captured_white, captured_black) = self._get_captured_pieces(positions)
      (white_advantage, black_advantage) = self._diff_pieces(captured_white, captured_black)
      advantage_text = ''.join(map(
        self.get_piece,
        list(self.white_or_black(white_advantage, black_advantage))
      ))
      # Calculate advantage score
      diff_score = self.white_or_black(
        self._score_pieces(white_advantage) - self._score_pieces(black_advantage),
        self._score_pieces(black_advantage) - self._score_pieces(white_advantage)
      )
      score_text = '+{}'.format(diff_score) if diff_score > 0 else ''
      return '{}{}{}{}'.format(padding, Colors.DULL_GRAY, advantage_text, score_text)
    if rank == 1:
      return '  {}'.format(self.get_user())
    if rank == 2:
      if isinstance(just_played, GameOver):
        text = '{}{}'.format(Colors.ORANGE, self.string_of_game_over(game_over))
        return '{}{}'.format(padding, text)
      else:
        normalized_score = round(self._score * 100, 1)
        prefix = '+' if normalized_score >= 0 else ''
        winning_potential = '{}{}'.format(prefix, normalized_score)
        return '{}{}{} cp:{}'.format(padding, Colors.DULL_GRAY, winning_potential.ljust(11), self._cp)
    if rank == 3:
      return '{}{}┗━━━━━━━━━━━━━━━━━━━┛'.format(padding_alt, Colors.DULL_GRAY)
    if rank == 4:
      white_move = safe_pop(board.san_move_stack_white[-1:]) or ''
      black_move = safe_pop(board.san_move_stack_black[-1:]) or ''
      move_number = len(board.san_move_stack_white)
      move_number_text = '{} '.format((str(move_number) + '.').ljust(3)) if move_number > 0 else '    '
      if just_played is chess.WHITE:
        text = '{}{}{}'.format(Colors.LIGHT, white_move.ljust(7), ''.ljust(7))
      elif just_played is chess.BLACK:
        text = '{}{}{}{}'.format(Colors.GRAY, white_move.ljust(7), Colors.LIGHT, black_move.ljust(7))
      else:
        text = '{}{}{}{}'.format(Colors.GRAY, white_move.ljust(7), Colors.GRAY, black_move.ljust(7))
      return '{}{}┃ {}{}{}┃'.format(padding_alt, Colors.DULL_GRAY, move_number_text, text, Colors.DULL_GRAY)
    if rank == 5:
      white_move = safe_pop(board.san_move_stack_white[-2:-1]) or ''
      black_move = safe_pop(board.san_move_stack_black[-2:-1]) or ''
      move_number = len(board.san_move_stack_white) - 1
      move_number_text = '{} '.format((str(move_number) + '.').ljust(3)) if move_number > 0 else '    '
      if just_played is chess.WHITE:
        black_move = safe_pop(board.san_move_stack_black[-1:]) or ''
      text = '{}{}{}{}'.format(Colors.GRAY, white_move.ljust(7), Colors.GRAY, black_move.ljust(7))
      return '{}{}┃ {}{}{}┃'.format(padding_alt, Colors.DULL_GRAY, move_number_text, text, Colors.DULL_GRAY)
    if rank == 6:
      return '{}{}┏━━━━━━━━━━━━━━━━━━━┓'.format(padding_alt, Colors.DULL_GRAY)
    if rank == 7:
      positions = fen.split(' ')[0]
      ranks = positions.split('/')
      # Calculate advantage pieces
      (captured_white, captured_black) = self._get_captured_pieces(positions)
      (white_advantage, black_advantage) = self._diff_pieces(captured_white, captured_black)
      advantage_text = ''.join(map(
        self.get_piece,
        list(self.white_or_black(black_advantage, white_advantage))
      ))
      # Calculate advantage score
      diff_score = self.white_or_black(
        self._score_pieces(black_advantage) - self._score_pieces(white_advantage),
        self._score_pieces(white_advantage) - self._score_pieces(black_advantage)
      )
      score_text = '+{}'.format(diff_score) if diff_score > 0 else ''
      return '{}{}{}{}'.format(padding, Colors.DULL_GRAY, advantage_text, score_text)
    if rank == 8:
      return '  {}'.format(self.get_user(True))
    return ''

  def get_user(self, is_computer=False):
    title = '{}BOT {}'.format(Colors.ORANGE, Colors.RESET) if is_computer else ''
    name = 'stockfish {}'.format(self._level) if is_computer else pwd.getpwuid(os.getuid()).pw_name
    return '{}● {}{}{}{}'.format(Colors.DULL_GREEN, title, Colors.LIGHT, name, Colors.RESET)

  def get_bar_section(self, rank):
    percentage = ''
    tick = ' '
    color = Colors.DULL_GRAY
    normalized_score = self.white_or_black(
      round_to_nearest((self._score * 100) + 100),
      200 - (round_to_nearest((self._score * 100) + 100))
    )
    block_range = rank * 25

    # Color the bar blocks
    if normalized_score >= block_range:
      if normalized_score == 100:
        color = Colors.GREEN if self._score >= 0 else Colors.RED
      else:
        pos_color = self.white_or_black(Colors.GREEN, Colors.RED)
        neg_color = self.white_or_black(Colors.RED, Colors.GREEN)
        color = pos_color if self._score >= 0 else neg_color

    # Include the tick if we're in the center.
    if block_range == 125:
      tick = '{}_{}'.format(Colors.DULL_GRAY, color)
    return '{}{}█ {}{}'.format(color, tick, percentage, Colors.RESET)

  def get_title_from_move(self, turn):
    player = '{} to move'.format('Black' if turn == 'b' else 'White')
    colors = '{}'.format(\
      Colors.Backgrounds.BLACK + Colors.LIGHT if turn == 'b' else\
      Colors.Backgrounds.WHITE + Colors.DARK)
    return '\n\n {}{}  {}  {}'\
      .format(Styles.PADDING_MEDIUM, colors, player, Colors.RESET)

  def _diff_pieces(self, a, b):
    white = disjoin(b, a)
    black = disjoin(a, b)
    return (white, black)

  def _score_pieces(self, pieces):
    score = 0
    scores = {
      'r': 5,
      'n': 3,
      'b': 3,
      'q': 9,
      'k': 0,
      'p': 1
    }
    for piece in pieces:
      score += scores.get(piece)
    return score

  def _get_captured_pieces(self, positions):
    # White
    w_pawns = 8 - positions.count('P')
    w_rooks = 2 - positions.count('R')
    w_bishops = 2 - positions.count('B')
    w_knights = 2 - positions.count('N')
    w_queens = 1 - positions.count('Q')
    w_kings = 1 - positions.count('K')
    w_pieces = (
      ('P' * w_pawns if w_pawns > 0 else '') +
      ('B' * w_bishops if w_bishops > 0 else '') +
      ('N' * w_knights if w_knights > 0 else '') +
      ('R' * w_rooks if w_rooks > 0 else '') +
      ('Q' * w_queens if w_queens > 0 else '') +
      ('K' * w_kings if w_kings > 0 else '')
    )
    # Black
    b_pawns = 8 - positions.count('p')
    b_rooks = 2 - positions.count('r')
    b_bishops = 2 - positions.count('b')
    b_knights = 2 - positions.count('n')
    b_queens = 1 - positions.count('q')
    b_kings = 1 - positions.count('k')
    b_pieces = (
      ('p' * b_pawns if b_pawns > 0 else '') +
      ('b' * b_bishops if b_bishops > 0 else '') +
      ('n' * b_knights if b_knights > 0 else '') +
      ('r' * b_rooks if b_rooks > 0 else '') +
      ('q' * b_queens if b_queens > 0 else '') +
      ('k' * b_kings if b_kings > 0 else '')
    )
    return (
      w_pieces,
      b_pieces
    )

  def get_tile_color_from_position(self, r, f, pos_delta, hint_delta):
    square_coordinates = self.get_coordinates_from_rank_file(r, f)
    highlight_dark = None
    highlight_light = None
    if pos_delta is not None:
      if square_coordinates in [pos_delta[0], pos_delta[1]]:
        highlight_light = Colors.Backgrounds.GREEN_LIGHT
        highlight_dark = Colors.Backgrounds.GREEN_DARK
    if hint_delta is not None:
      if square_coordinates in [hint_delta[0], hint_delta[1]]:
        highlight_light = Colors.Backgrounds.PURPLE_LIGHT
        highlight_dark = Colors.Backgrounds.PURPLE_DARK
    if r % 2 == 0:
      if f % 2 == 0:
        return highlight_dark or Colors.Backgrounds.DARK
      return highlight_light or Colors.Backgrounds.LIGHT
    if f % 2 == 0:
      return highlight_light or Colors.Backgrounds.LIGHT
    return highlight_dark or Colors.Backgrounds.DARK

  ### TODO maybe make get_piece_thin?
  def get_piece(self, letter):
    pieces = {
      'R': '♜ ',
      'N': '♞ ',
      'B': '♗ ',
      'Q': '♕ ',
      'K': '♔ ',
      'P': '♙ ',
      'r': '♜ ',
      'n': '♞ ',
      'b': '♝ ',
      'q': '♛ ',
      'k': '♚ ',
      'p': '♙ ',
    }
    return pieces.get(letter)

  def get_piece_colored(self, letter, is_black_check, is_white_check):
    black_king_color = Colors.Backgrounds.RED if is_black_check else Colors.DARK
    white_king_color = Colors.Backgrounds.RED if is_white_check else Colors.LIGHT
    pieces = {
      # White
      'R': [Colors.LIGHT + '♜ ' + Colors.RESET],
      'N': [Colors.LIGHT + '♞ ' + Colors.RESET],
      'B': [Colors.LIGHT + '♗ ' + Colors.RESET],
      'Q': [Colors.LIGHT + '♕ ' + Colors.RESET],
      'K': [white_king_color + '♔ ' + Colors.RESET],
      'P': [Colors.LIGHT + '♙ ' + Colors.RESET],
      # Black
      'r': [Colors.DARK + '♜ ' + Colors.RESET],
      'n': [Colors.DARK + '♞ ' + Colors.RESET],
      'b': [Colors.DARK + '♝ ' + Colors.RESET],
      'q': [Colors.DARK + '♛ ' + Colors.RESET],
      'k': [black_king_color + '♚ ' + Colors.RESET],
      'p': [Colors.DARK + '♙ ' + Colors.RESET],
      '1': ['  '],
      '2': ['  ', '  '],
      '3': ['  ', '  ', '  '],
      '4': ['  ', '  ', '  ', '  '],
      '5': ['  ', '  ', '  ', '  ', '  '],
      '6': ['  ', '  ', '  ', '  ', '  ', '  '],
      '7': ['  ', '  ', '  ', '  ', '  ', '  ', '  '],
      '8': ['  ', '  ', '  ', '  ', '  ', '  ', '  ', '  ']
    }
    return pieces.get(letter)

  def get_coordinates_from_rank_file(self, r, f):
    file = self.FILES[f - 1]
    return '{}{}'.format(file, r)

  def string_of_game_over(self, game_over):
    if game_over is GameOver.BLACK_WINS:
      return 'Black wins by checkmate 0-1'
    if game_over is GameOver.WHITE_WINS:
      return 'White wins by checkmate 1-0'
    if game_over is GameOver.DRAW:
      return 'Draw ½ ½'
    if game_over is GameOver.RESIGN:
      return 'White resigns 0-1'
    return 'Game over'

  def is_user_white(self):
    return self._play_as == chess.WHITE

  def white_or_black(self, a, b):
    return a if self.is_user_white() else b

  def clear(self):
    if os.name == 'nt': # For windows
      os.system('cls')
    else: # For mac and linux
      os.system('clear')
