from app import db
from collections import namedtuple, Counter
from enum import Enum, auto
from typing import Optional, List, Set

Ship = namedtuple('Ship', ['name', 'symbol', 'size'])

symbol_to_ship = {
    'C': Ship('Carrier', 'C', 5),
    'B': Ship('Battleship', 'B', 4),
    'R': Ship('Cruiser', 'R', 3),
    'S': Ship('Submarine', 'S', 3),
    'D': Ship('Destroyer', 'D', 2)
}


class Board(db.Model):
    SIZE = 10
    HIT, MISS, SUNK = '+', '-', 'x'
    ACTIONS = {HIT, MISS, SUNK}
    NO_ACTION = '.'

    __tablename__ = 'boards'

    id = db.Column(db.Integer, primary_key=True)
    starting_grid = db.Column(db.String(100))
    grid = db.Column(db.String(100))

    player = db.relationship('Player', back_populates='board')
    player_id = db.Column(db.Integer, db.ForeignKey('players.id'))

    def __init__(self, starting_grid: str):
        self.validate_starting_grid(starting_grid)

        super().__init__(starting_grid=starting_grid,
                         grid=starting_grid)

    def enemy_view(self) -> str:
        """How enemy views this board."""
        if self.no_ships(): return self.grid

        return ''.join([self.grid[xy] if self.grid[xy] in Board.ACTIONS else ' ' for xy in range(Board.SIZE * Board.SIZE)])

    def can_shoot_at(self, x: int, y: int) -> bool:
        """Is it legal to shoot at field."""
        xy = self._to_1d(x, y)

        return self.grid[xy] == self.starting_grid[xy]

    def shoot(self, x: int, y: int) -> str:
        """Shoot at a field and return string with a result.
        Returns:
            Result of an action as string
                - Miss
                - Hit {ship_name}
                - Sunk {ship_name}
        Raises:
            ValueError if action can't take place on this field (x,y was already shot at).
        """
        if not self.can_shoot_at(x, y): raise ValueError(f"Field ({x}, {y}) was already acted upon.")

        xy = self._to_1d(x, y)

        if self.grid[xy] == Board.NO_ACTION:
            self.grid = self.grid[:xy] + self.MISS + self.grid[xy + 1:]
            return 'Miss'
        else:
            symbol = self.grid[xy]
            self.grid = self.grid[:xy] + self.HIT + self.grid[xy + 1:]

            if symbol in self.grid:
                return f'Hit {symbol_to_ship[symbol].name}'
            else:
                self.grid = ''.join([self.grid[i] if self.starting_grid[i] != symbol else self.SUNK for i in range(Board.SIZE * Board.SIZE)])

                return f'Sunk {symbol_to_ship[symbol].name}'

    def no_ships(self) -> bool:
        """All ships sunk?"""
        return set(self.grid) <= Board.ACTIONS | set(Board.NO_ACTION)

    def remaining_ships(self) -> Set[str]:
        """Return set of symbols of remaining ships."""
        return set(self.grid) & set(symbol_to_ship.keys())

    def sunk_ships(self) -> Set[str]:
        """Return set of symbols of sunk ships."""
        return set(symbol_to_ship.keys()) - self.remaining_ships()

    @staticmethod
    def _to_1d(x: int, y: int) -> int: return y*Board.SIZE + x

    @staticmethod
    def grid_as_matrix(grid) -> List[str]:
        """Transforms grid into 2d representation."""
        return [grid[i:i + Board.SIZE] for i in range(0, len(grid), Board.SIZE)]

    @staticmethod
    def validate_starting_grid(grid: str):
        """Raises ValueError if grid is not valid starting grid.
        Valid starting grid has:
            - len == 100
            - only ships and empty fields
            - ships are of valid sizes
            - ships are placed horizontally or vertically
        Raises:
            ValueError if one of the conditions is not True.
        """
        counter = Counter(grid)

        if len(grid) != Board.SIZE * Board.SIZE:
            raise ValueError(f"Grid has to be of length 100.")

        for _, v in symbol_to_ship.items():
            if v.size != counter[v.symbol]:
                raise ValueError(f"Grid has ships of illegal sizes.")

        if set(grid) != (set(symbol_to_ship.keys()) | set(Board.NO_ACTION)):
            raise ValueError(f"Grid has more than ships and empty fields.")

        for symbol, s in symbol_to_ship.items():
            g = Board.grid_as_matrix(grid)
            row, col = divmod(grid.index(symbol), Board.SIZE)
            symbols_in_row = g[row].count(symbol)

            if symbols_in_row == 1:  # must be vertically placed
                g = [''.join(r) for r in zip(*g)]  # transposing matrix to make horizontal placement out of vertical

                for r in range(Board.SIZE):
                    if symbol in g[r]:
                        row, col = r, g[r].index(symbol)  # new row, col pair of a first symbol
                        break

            if col + s.size > Board.SIZE or g[row][col:col+s.size] != symbol * s.size:
                raise ValueError(f"Ship {s.symbol} is not placed correctly.")


class Player(db.Model):
    """Player ties name and board together."""

    __tablename__ = 'players'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    board = db.relationship('Board', back_populates='player', uselist=False)
    game_id = db.Column(db.Integer, db.ForeignKey('games.id'))
    game = db.relationship('Game', back_populates='players')


class Game(db.Model):
    """Game is an object which couples players and their actions.
    When both players are in game, they shoot in turns until one of the players have no ships.
    Once the game is finished no player can shoot.
    At first game is in State.NEW. After another player joins in, it is in State.PLAYING. When one of the players have
    no ships left it switches to State.FINISHED.
    """

    class State(Enum):
        NEW = auto()
        PLAYING = auto()
        FINISHED = auto()

    __tablename__ = 'games'

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(db.Enum(State))

    players = db.relationship('Player', back_populates='game', order_by='Player.id')
    current_idx = db.Column(db.Integer)

    def __init__(self):
        super().__init__()

        self.state = self.State.NEW

    @property
    def current(self):
        return self.players[self.current_idx] if len(self.players) else None

    @property
    def other(self):
        return self.players[(self.current_idx + 1) % 2] if len(self.players) == 2 else None

    def join(self, player: Player):
        """Join new player to the game."""
        if self.state != self.State.NEW:
            raise ValueError(f"Can't join players when game is not in state NEW. State={self.state}")

        if self.current_idx is None:
            self.current_idx = 0
        elif self.players[self.current_idx].id is None:
            raise ValueError("No database id for current player - commit first.")
        elif self.players[self.current_idx].id == player.id:
            raise ValueError("Player can't join the same game twice.")
        else:
            self.state = self.State.PLAYING

        player.game = self

    def shoot(self, player_name: str, x: int, y: int) -> str:
        """Try to shoot as player.
        Returns:
            Action result as a string.
        Raises:
            ValueError if shot cannot be taken.
        """
        if self.state != self.State.PLAYING:
            raise ValueError(f"Game is in state '{self.state}' - can't shoot.")

        if self.current.name != player_name:
            raise ValueError(f"It's '{self.current.name}' turn")

        result = self.other.board.shoot(x, y)

        if self.other.board.no_ships():
            self.state = self.State.FINISHED
        else:
            self.current_idx = (self.current_idx + 1) % 2

        return result

    def winner(self) -> Optional[Player]:
        return self.current if self.state == self.State.FINISHED else None

    def save_to_db(self):
        db.session.add(self)
        db.session.commit()

    @staticmethod
    def get_new_game():
        g = Game.query.filter_by(state=Game.State.NEW).first()
        if g is None:
            g = Game()

        return g