from tugo.encoders.base import Encoder
from tugo.encoders.utils import is_ladder_escape, is_ladder_capture
from tugo.gotypes import Point, Player
from tugo.goboard_fast import Move
from tugo.agent.helpers_fast import is_point_an_eye
import numpy as np
import torch

"""
Feature name            num of planes   Description
Stone colour            3               Player stone / opponent stone / empty
Ones                    1               A constant plane filled with 1
Zeros                   1               A constant plane filled with 0
Sensibleness            1               Whether a move is legal and does not fill its own eyes
Turns since             8               How many turns since a move was played
Liberties               8               Number of liberties (empty adjacent points)
Liberties after move    8               Number of liberties after this move is played
Capture size            8               How many opponent stones would be captured
Self-atari size         8               How many of own stones would be captured
Ladder capture          1               Whether a move at this point is a successful ladder capture
Ladder escape           1               Whether a move at this point is a successful ladder escape
"""

FEATURE_OFFSETS = {
    "stone_color": 0,
    "ones": 3,
    "zeros": 4,
    "sensibleness": 5,
    "turns_since": 6,
    "liberties": 14,
    "liberties_after": 22,
    "capture_size": 30,
    "self_atari_size": 38,
    "ladder_capture": 46,
    "ladder_escape": 47,
    "current_player_color": 48,
}


def offset(feature):
    return FEATURE_OFFSETS[feature]


class AlphaGoEncoder(Encoder):
    def __init__(self, board_size=(19, 19), use_player_plane=True):
        self.board_width, self.board_height = board_size
        self.use_player_plane = use_player_plane
        self.num_planes = 48 + use_player_plane

    def name(self):
        return "alphago"

    def encode(self, game_state):
        board_tensor = torch.zeros((self.num_planes, self.board_height, self.board_width))
        for r in range(self.board_height):
            for c in range(self.board_width):
                point = Point(row=r + 1, col=c + 1)

                go_string = game_state.board.get_go_string(point)
                if go_string and go_string.color == game_state.next_player:
                    board_tensor[offset("stone_color"), r, c] = 1
                elif go_string and go_string.color == game_state.next_player.other:
                    board_tensor[offset("stone_color") + 1, r, c] = 1
                else:
                    board_tensor[offset("stone_color") + 2, r, c] = 1

                board_tensor[offset("ones")] = self.ones()
                board_tensor[offset("zeros")] = self.zeros()

                if not is_point_an_eye(game_state.board, point, game_state.next_player):
                    board_tensor[offset("sensibleness"), r, c] = 1

                ages = min(game_state.board.move_ages.get(r, c), 8)
                if ages > 0:
                    board_tensor[offset("turns_since") + int(ages), r, c] = 1

                if game_state.board.get_go_string(point):
                    liberties = min(game_state.board.get_go_string(point).num_liberties, 8)
                    board_tensor[offset("liberties") + liberties, r, c] = 1

                move = Move(point)
                if game_state.is_valid_move(move):
                    new_state = game_state.apply_move(move)
                    liberties = min(new_state.board.get_go_string(point).num_liberties, 8)
                    board_tensor[offset("liberties_after") + liberties, r, c] = 1

                    adjacent_strings = [game_state.board.get_go_string(nb) for nb in point.neighbors()]
                    capture_count = 0
                    for go_string in adjacent_strings:
                        other_player = game_state.next_player.other
                        if go_string and go_string.num_liberties == 1 and go_string.color == other_player:
                            capture_count += len(go_string.stones)
                    capture_count = min(capture_count, 8)
                    board_tensor[offset("capture_size") + capture_count, r, c] = 1

                if go_string and go_string.num_liberties == 1:
                    if go_string := game_state.board.get_go_string(point):
                        num_atari_stones = min(len(go_string.stones), 8)
                        board_tensor[offset("self_atari_size") + num_atari_stones, r, c] = 1

                if is_ladder_capture(game_state, point):
                    board_tensor[offset("ladder_capture"), r, c] = 1

                if is_ladder_escape(game_state, point):
                    board_tensor[offset("ladder_escape"), r, c] = 1

                if self.use_player_plane:
                    if game_state.next_player == Player.black:
                        board_tensor[offset("ones")] = self.ones()
                    else:
                        board_tensor[offset("zeros")] = self.zeros()

        return board_tensor

    def ones(self):
        return torch.ones((1, self.board_height, self.board_width))

    def zeros(self):
        return torch.zeros((1, self.board_height, self.board_width))

    def capture_size(self, game_state, num_planes=8):
        pass

    def encode_point(self, point):
        return self.board_width * (point.row - 1) + (point.col - 1)

    def decode_point_index(self, index):
        row = index // self.board_width
        col = index % self.board_width
        return Point(row=row + 1, col=col + 1)

    def num_points(self):
        return self.board_width * self.board_height

    def shape(self):
        return self.num_planes, self.board_height, self.board_width


def create(board_size):
    return AlphaGoEncoder(board_size)

