from __future__ import annotations

import math
import random
from dataclasses import dataclass, field
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple

from .data import (
    CornerCoord,
    EdgeID,
    NUMBER_TOKENS,
    PORT_SEQUENCE,
    Port,
    Resource,
    RESOURCE_TOKENS,
)

NeighborVector = Tuple[int, int, int]

NEIGHBOR_VECTORS: Sequence[NeighborVector] = [
    (1, -1, 0),
    (1, 0, -1),
    (0, 1, -1),
    (-1, 1, 0),
    (-1, 0, 1),
    (0, -1, 1),
]

CORNER_OFFSETS: Sequence[NeighborVector] = [
    (2, -1, -1),
    (1, 1, -2),
    (-1, 2, -1),
    (-2, 1, 1),
    (-1, -1, 2),
    (1, -2, 1),
]

SQRT_3 = math.sqrt(3.0)


def cube_to_pixel(coord: Tuple[float, float, float], scale: float = 1.0) -> Tuple[float, float]:
    x, _, z = coord
    q = x
    r = z
    px = scale * (SQRT_3 * q + (SQRT_3 / 2) * r)
    py = scale * (1.5 * r)
    return px, py


@dataclass
class HexTile:
    id: int
    coord: Tuple[int, int, int]
    resource: Resource
    number_token: Optional[int] = None
    has_robber: bool = False
    corners: List[CornerCoord] = field(default_factory=list)
    neighbors: Set[int] = field(default_factory=set)


@dataclass
class Corner:
    id: CornerCoord
    adjacent_hexes: Set[int] = field(default_factory=set)
    neighbors: Set[CornerCoord] = field(default_factory=set)
    port: Optional[Port] = None
    owner: Optional[str] = None
    building: Optional[str] = None  # settlement or city
    edges: Set[EdgeID] = field(default_factory=set)


@dataclass
class Edge:
    id: EdgeID
    corners: Tuple[CornerCoord, CornerCoord]
    adjacent_hexes: Set[int] = field(default_factory=set)
    owner: Optional[str] = None
    port: Optional[Port] = None


@dataclass
class Board:
    hexes: Dict[int, HexTile]
    corners: Dict[CornerCoord, Corner]
    edges: Dict[EdgeID, Edge]
    robber_hex: int
    seed: Optional[int] = None

    def __post_init__(self) -> None:
        self._corner_order: List[CornerCoord] = sorted(
            self.corners.keys(), key=lambda c: cube_to_pixel(c)
        )
        self._corner_labels: Dict[CornerCoord, int] = {
            coord: idx + 1 for idx, coord in enumerate(self._corner_order)
        }
        self._edge_order: List[EdgeID] = sorted(
            self.edges.keys(),
            key=lambda e: cube_to_pixel(tuple((a + b) / 2 for a, b in zip(*e))),
        )
        self._edge_labels: Dict[EdgeID, int] = {
            edge: idx + 1 for idx, edge in enumerate(self._edge_order)
        }

    def corner_label(self, corner_id: CornerCoord) -> int:
        return self._corner_labels[corner_id]

    def corner_from_label(self, label: int) -> CornerCoord:
        return self._corner_order[label - 1]

    def edge_label(self, edge_id: EdgeID) -> int:
        return self._edge_labels[edge_id]

    def edge_from_label(self, label: int) -> EdgeID:
        return self._edge_order[label - 1]

    def edge_between(self, a: CornerCoord, b: CornerCoord) -> Optional[Edge]:
        edge_id = frozenset({a, b})
        return self.edges.get(edge_id)

    def adjacent_corners(self, corner_id: CornerCoord) -> Set[CornerCoord]:
        return self.corners[corner_id].neighbors

    def can_place_settlement(
        self, corner_id: CornerCoord, check_distance: bool = True
    ) -> bool:
        corner = self.corners[corner_id]
        if corner.owner is not None:
            return False
        if check_distance:
            for neighbor in corner.neighbors:
                if self.corners[neighbor].owner is not None:
                    return False
        return True

    def connected_corners(self, start: CornerCoord) -> Iterable[CornerCoord]:
        return self.corners[start].neighbors

    def serialize(self) -> Dict[str, object]:
        return {
            "hexes": {
                hid: {
                    "coord": tile.coord,
                    "resource": tile.resource.value,
                    "number": tile.number_token,
                    "robber": tile.has_robber,
                }
                for hid, tile in self.hexes.items()
            },
            "corners": {
                self.corner_label(cid): {
                    "coord": cid,
                    "adjacent_hexes": list(self.corners[cid].adjacent_hexes),
                    "neighbors": [self.corner_label(n) for n in self.corners[cid].neighbors],
                    "owner": self.corners[cid].owner,
                    "building": self.corners[cid].building,
                    "port": self.corners[cid].port.kind.value if self.corners[cid].port else None,
                }
                for cid in self.corners
            },
            "edges": {
                self.edge_label(edge_id): {
                    "a": self.corner_label(coords[0]),
                    "b": self.corner_label(coords[1]),
                    "owner": edge.owner,
                }
                for edge_id, edge in self.edges.items()
                for coords in [list(edge_id)]
            },
        }


def generate_hex_coords(radius: int = 2) -> List[Tuple[int, int, int]]:
    coords: List[Tuple[int, int, int]] = []
    for x in range(-radius, radius + 1):
        for y in range(-radius, radius + 1):
            z = -x - y
            if -radius <= z <= radius:
                coords.append((x, y, z))
    return coords


def create_board(seed: Optional[int] = None) -> Board:
    rng = random.Random(seed)
    hex_coords = generate_hex_coords()
    rng.shuffle(hex_coords)
    resources = RESOURCE_TOKENS.copy()
    rng.shuffle(resources)
    hexes: Dict[int, HexTile] = {}
    coord_map: Dict[Tuple[int, int, int], int] = {}
    for idx, coord in enumerate(hex_coords):
        resource = resources[idx]
        hexes[idx] = HexTile(id=idx, coord=coord, resource=resource)
        coord_map[coord] = idx

    for tile in hexes.values():
        for vec in NEIGHBOR_VECTORS:
            neighbor_coord = (
                tile.coord[0] + vec[0],
                tile.coord[1] + vec[1],
                tile.coord[2] + vec[2],
            )
            neighbor_id = coord_map.get(neighbor_coord)
            if neighbor_id is not None:
                tile.neighbors.add(neighbor_id)

    assign_number_tokens(hexes, rng)

    corner_data: Dict[CornerCoord, Dict[str, Set]] = {}
    edge_data: Dict[EdgeID, Dict[str, Set]] = {}
    robber_hex = next(
        tile.id for tile in hexes.values() if tile.resource == Resource.DESERT
    )
    hexes[robber_hex].has_robber = True

    for tile in hexes.values():
        corners: List[CornerCoord] = []
        for offset in CORNER_OFFSETS:
            corner_coord = (
                3 * tile.coord[0] + offset[0],
                3 * tile.coord[1] + offset[1],
                3 * tile.coord[2] + offset[2],
            )
            data = corner_data.setdefault(
                corner_coord, {"hexes": set(), "neighbors": set()}
            )
            data["hexes"].add(tile.id)
            corners.append(corner_coord)
        tile.corners = corners
        for i in range(len(corners)):
            a = corners[i]
            b = corners[(i + 1) % len(corners)]
            edge_id = frozenset({a, b})
            edge_info = edge_data.setdefault(
                edge_id, {"hexes": set(), "corners": (a, b)}
            )
            edge_info["hexes"].add(tile.id)
            corner_data[a]["neighbors"].add(b)
            corner_data[b]["neighbors"].add(a)
            corner_data[a].setdefault("edges", set()).add(edge_id)
            corner_data[b].setdefault("edges", set()).add(edge_id)

    corners_obj = {
        coord: Corner(
            id=coord,
            adjacent_hexes=data["hexes"],
            neighbors=data["neighbors"],
            edges=data.get("edges", set()),
        )
        for coord, data in corner_data.items()
    }
    edges_obj = {
        edge_id: Edge(
            id=edge_id,
            corners=tuple(sorted(edge_id, key=lambda c: cube_to_pixel(c))),
            adjacent_hexes=data["hexes"],
        )
        for edge_id, data in edge_data.items()
    }

    assign_ports(edges_obj, corners_obj)

    return Board(
        hexes=hexes,
        corners=corners_obj,
        edges=edges_obj,
        robber_hex=robber_hex,
        seed=seed,
    )


def assign_number_tokens(hexes: Dict[int, HexTile], rng: random.Random) -> None:
    tokens = NUMBER_TOKENS.copy()
    attempts = 0
    while True:
        attempts += 1
        rng.shuffle(tokens)
        idx = 0
        placement_failed = False
        for tile in hexes.values():
            if tile.resource == Resource.DESERT:
                tile.number_token = None
                continue
            token = tokens[idx]
            idx += 1
            tile.number_token = token
            if token in (6, 8):
                for neighbor_id in tile.neighbors:
                    neighbor = hexes[neighbor_id]
                    if neighbor.number_token in (6, 8):
                        placement_failed = True
                        break
            if placement_failed:
                break
        if not placement_failed:
            return
        if attempts > 5000:
            raise RuntimeError("Unable to place number tokens with 6/8 spacing rule")
        for tile in hexes.values():
            tile.number_token = None


def assign_ports(edges: Dict[EdgeID, Edge], corners: Dict[CornerCoord, Corner]) -> None:
    boundary_edges = [
        edge for edge in edges.values() if len(edge.adjacent_hexes) == 1
    ]
    if not boundary_edges:
        return
    boundary_edges.sort(
        key=lambda e: math.atan2(
            cube_to_pixel(tuple((a + b) / 2 for a, b in zip(*e.corners)))[1],
            cube_to_pixel(tuple((a + b) / 2 for a, b in zip(*e.corners)))[0],
        )
    )
    step = len(boundary_edges) / len(PORT_SEQUENCE)
    for idx, port in enumerate(PORT_SEQUENCE):
        edge_index = min(int(idx * step), len(boundary_edges) - 1)
        edge = boundary_edges[edge_index]
        edge.port = port
        for corner_id in edge.corners:
            corners[corner_id].port = port
