from chess_server.core.models.position import Position
from chess_server.core.models.movement import Movement
from chess_server.core.models.piece import Piece
from chess_server.core.models.pawn import Pawn
from chess_server.core.models.knight import Knight
from chess_server.core.models.queen import Queen
from chess_server.core.models.bishop import Bishop
from chess_server.core.models.rook import Rook
from chess_server.core.models.king import King

from chess_server.error.game_error import *


class Board:

    def __init__(self, game):
        self.__game = game
        self.__piece_list = dict()
        self.__init_board()

    def __str__(self):
        string = ""

        for y in range(7, -1, -1):  # Start at top of board
            for x in range(8):
                position = Position(x, y)

                if self.piece_at(position):
                    string += str(self.__piece_list[position])
                else:
                    string += "X"

                string += "  "

            string += "\n"

        return string

    @property
    def piece_list(self):
        return self.__piece_list

    @piece_list.setter
    def piece_list(self, value):
        self.__piece_list = value

    def __init_board(self):

        # Pawns

        for x in range(8):
            self.__piece_list[Position(x, 1)] = Pawn(Piece.WHITE)
            self.__piece_list[Position(x, 6)] = Pawn(Piece.BLACK)

        # White knights

        self.__piece_list[Position(1, 0)] = Knight(Piece.WHITE)
        self.__piece_list[Position(6, 0)] = Knight(Piece.WHITE)

        # Black knights

        self.__piece_list[Position(1, 7)] = Knight(Piece.BLACK)
        self.__piece_list[Position(6, 7)] = Knight(Piece.BLACK)

        # White bishops

        self.__piece_list[Position(2, 0)] = Bishop(Piece.WHITE)
        self.__piece_list[Position(5, 0)] = Bishop(Piece.WHITE)

        # Black bishops

        self.__piece_list[Position(2, 7)] = Bishop(Piece.BLACK)
        self.__piece_list[Position(5, 7)] = Bishop(Piece.BLACK)

        # White rooks

        self.__piece_list[Position(0, 0)] = Rook(Piece.WHITE)
        self.__piece_list[Position(7, 0)] = Rook(Piece.WHITE)

        # Black rooks

        self.__piece_list[Position(0, 7)] = Rook(Piece.BLACK)
        self.__piece_list[Position(7, 7)] = Rook(Piece.BLACK)

        # Kings

        self.__piece_list[Position(4, 0)] = King(Piece.WHITE)
        self.__piece_list[Position(4, 7)] = King(Piece.BLACK)

        # Queens

        self.__piece_list[Position(3, 0)] = Queen(Piece.WHITE)
        self.__piece_list[Position(3, 7)] = Queen(Piece.BLACK)

    def piece_at(self, position):
        try:
            piece = self.__piece_list[position]
        except KeyError:
            pass
        else:
            return piece

        return None

    def get_piece_position(self, wanted_piece):
        for position, piece in self.__piece_list.items():
            if piece == wanted_piece:
                return position

        return PieceNotFound

    def set_piece_position(self, piece, new_position):
        if piece is not None:
            self.__piece_list[new_position] = self.delete_piece(piece)

    def get_piece_by_id(self, piece_id):
        for piece in self.__piece_list.values():
            if piece.id == piece_id:
                return piece

        raise PieceNotFound

    def get_pieces(self, piece_type, color):
        pieces_list = list()

        for position, piece in self.__piece_list.items():
            if piece.color == color and isinstance(piece, piece_type):
                if piece_type == King:  # Always only one king on board
                    return piece

                pieces_list.append(position)

        return pieces_list

    def get_pieces_next_to(self, position):
        if position.x == 0:  # Left border
            return None, self.piece_at(Position(position.x + 1, position.y))

        if position.x == 7:  # Right border
            return self.piece_at(Position(position.x - 1, position.y)), None

        # Return tuple with the piece on two possible position
        return self.piece_at(Position(position.x - 1, position.y)), self.piece_at(Position(position.x + 1, position.y))

    def delete_piece(self, piece):
        return self.__piece_list.pop(self.get_piece_position(piece))

    def move_piece(self, piece_id, new_position):
        piece = self.get_piece_by_id(piece_id)
        piece_position = self.get_piece_position(piece)

        if piece is None:
            raise PieceNotFound

        if piece_position == new_position:
            raise NoMovement

        if isinstance(piece, Pawn):  # Use specific pawn method
            return self.move_pawn(piece, piece_position, new_position)

        if piece.move(piece_position, new_position):
            if not isinstance(piece, Knight):
                collision = self.detect_collision(piece_position, new_position)

                if collision is not None:
                    raise PieceCollision(self.piece_at(collision).id)

            piece_at_new_position = self.piece_at(new_position)

            if piece_at_new_position is not None:
                if piece_at_new_position.color != piece.color:
                    raise OwnPiece

                raise PieceCaptured(piece_at_new_position.id)

    def move_pawn(self, pawn, current_position, new_position):  # Specific method for pawns
        movement = Movement(current_position, new_position)
        piece_at_new_position = self.piece_at(new_position)

        if movement.x == 0:  # Linear movement
            if pawn.move(current_position, new_position):
                collision = self.detect_collision(current_position, new_position)

                if collision is not None:
                    raise PieceCollision(self.piece_at(collision).id)

                if piece_at_new_position is not None:
                    raise PieceCollision(piece_at_new_position.id)

                self.set_piece_position(pawn, new_position)
                self.en_passant_handler(pawn, new_position)

                if pawn.is_promotion_available(new_position):
                    raise PawnPromoted

        else:  # Diagonal movement (should capture a piece)
            if pawn.move_eat(current_position, new_position):

                if piece_at_new_position is None:
                    # Piece next to this pawn's current position and behind wanted position
                    piece_next_to_y = new_position.y - 1 if pawn.color == Piece.WHITE else new_position.y + 1
                    piece_next_to = self.piece_at(Position(new_position.x, piece_next_to_y))

                    if piece_next_to is None or piece_next_to.color == pawn.color:
                        raise MovementNotAllowed

                    if pawn.en_passant[piece_next_to.id] + 1 == self.__game.turn:  # If the move is done the next turn
                        self.delete_piece(piece_next_to)
                        del pawn.en_passant[piece_next_to.id]

                        self.set_piece_position(pawn, new_position)
                        self.en_passant_handler(pawn, new_position)

                        if pawn.is_promotion_available(new_position):
                            raise PawnPromotedAndPieceCaptured(piece_next_to.id)
                        else:
                            raise PieceCaptured(piece_next_to.id)

                    raise MovementNotAllowed

                if pawn.color != piece_at_new_position.color:
                    self.delete_piece(piece_at_new_position)
                    self.en_passant_handler(pawn, new_position)
                    self.set_piece_position(pawn, new_position)

                    if pawn.is_promotion_available(new_position):
                        raise PawnPromotedAndPieceCaptured(piece_at_new_position.id)
                    else:
                        raise PieceCaptured(piece_at_new_position.id)

    def en_passant_handler(self, pawn, new_position):
        pieces_next_to_new_position = self.get_pieces_next_to(new_position)

        if pieces_next_to_new_position[0] is not None:
            pawn.en_passant[pieces_next_to_new_position[0].id] = self.__game.turn

        if pieces_next_to_new_position[1] is not None:
            pawn.en_passant[pieces_next_to_new_position[1].id] = self.__game.turn

    def detect_collision(self, current_position, new_position):
        movement = Movement(current_position, new_position)
        absolute_movement = abs(Movement(current_position, new_position))

        if absolute_movement.x in range(2) or absolute_movement.y in range(2):  # No collision possible
            return None

        x_range = None
        y_range = None

        if movement.x > 1:
            x_range = range(current_position.x + 1, new_position.x)
        elif movement.x < -1:
            x_range = range(new_position.x + 1, current_position.x)

        if movement.y > 1:
            y_range = range(current_position.y + 1, new_position.y)
        elif movement.y < -1:
            y_range = range(new_position.y + 1, current_position.y)

        if absolute_movement.x > 1 and absolute_movement.y > 1:  # Diagonal movement
            for x in x_range:
                for y in y_range:
                    if self.piece_at(Position(x, y)) is not None:
                        return Position(x, y)
        else:  # Linear movement
            if absolute_movement.x > 1:  # Movement on X
                for x in x_range:
                    if self.piece_at(Position(x, current_position.y)) is not None:
                        return Position(x, current_position.y)
            else:  # Movement on Y
                for y in y_range:
                    if self.piece_at(Position(current_position.x, y)) is not None:
                        return Position(current_position.x, y)

        return None

    def is_king_in_check(self, color):
        king = self.get_pieces(King, color)
        king_position = self.get_piece_position(king)
        knight_list = self.get_pieces(Knight, Piece.WHITE if color == Piece.BLACK else Piece.BLACK)

        # List of all pieces that can capture the king at next turn
        piece_list = list()

        # In check detection
        # At first, check if enemy knights can capture the king
        for knight in knight_list:
            knight_position = self.get_piece_position(knight)

            if knight.move(knight_position, king_position):
                piece_list.append(knight)

        # Secondly, check collisions on every diagonals and lines then check if the piece can move to king's position
        collision_position_list = self.get_all_collisions_from_position(king_position)

        for position in collision_position_list:
            piece = self.piece_at(position)

            if piece.color != king.color and piece.move(position, king_position):
                piece_list.append(piece)

        if len(piece_list) > 0:
            return piece_list

        return None

    def get_all_collisions_from_position(self, position):
        position_list = list()

        # Lines

        # +X
        position = self.get_collision_to_max_position(position, Position(7, position.y))

        if position is not None:
            position_list.append(position)

        # -X
        position = self.get_collision_to_max_position(position, Position(0, position.y))

        if position is not None:
            position_list.append(position)

        # +Y
        position = self.get_collision_to_max_position(position, Position(position.x, 7))

        if position is not None:
            position_list.append(position)

        # -Y
        position = self.get_collision_to_max_position(position, Position(position.x, 0))

        if position is not None:
            position_list.append(position)

        # Diagonals

        # +X +Y
        position = self.get_collision_to_max_position(position, Position(7, 7))

        if position is not None:
            position_list.append(position)

        # +X -Y
        position = self.get_collision_to_max_position(position, Position(7, 0))

        if position is not None:
            position_list.append(position)

        # -X +Y
        position = self.get_collision_to_max_position(position, Position(0, 7))

        if position is not None:
            position_list.append(position)

        return position_list

    def get_collision_to_max_position(self, initial_position, max_position):
        if initial_position != max_position:
            collision_position = self.detect_collision(initial_position, max_position)

            if collision_position is None:
                collision_position = self.piece_at(max_position)

            if collision_position is not None:
                return collision_position

        return None
