from __future__ import annotations

from itertools import chain
from pathlib import Path
from typing import Optional

from PIL import Image, ImageDraw, ImageStat

from ._resources import get_blank_image
from .Component import Component
from .Node import Node
from .XMLElement import XMLElement, XMLParserElement

# Control, if _state elements should be added when serializing to XML.
# These are not needed for Stormworks to load the microcontroller,
# but they are normally being generated by the in-game editor.
# Vehicles lack these elements.
# They are completely redundant.
ADD_STATE: bool = False


class MCImage:
    """
    A Stormworks Microcontroller Image
    """

    def __init__(self, pixels: Optional[list[list[bool]]] = None) -> None:
        if pixels is not None:
            if len(pixels) != 16 or any(len(row) != 16 for row in pixels):
                raise ValueError("pixels must be a 16x16 list of booleans")
        self.pixels = pixels or [[False] * 16 for _ in range(16)]

    @staticmethod
    def from_png(path: Path, threshold: Optional[int] = None) -> MCImage:
        """
        Load the image from a PNG file. The image must be 16x16 pixels, and only black and white pixels are supported.

        :param path: Path to the PNG file
        :param threshold: Optional threshold to convert greyscale to black and white. If None, the mean pixel value is used.
        :return: None
        """
        img = Image.open(path).convert("L").resize((16, 16))
        stat = ImageStat.Stat(img)
        if threshold is None:
            threshold = max(int(stat.mean[0]), 1)
        pixels = [
            [img.getpixel((x, y)) >= threshold for x in range(16)] for y in range(16)  # type: ignore
        ]
        img.close()
        return MCImage(pixels)

    def to_png(self, path: Path) -> None:
        """
        Save the image to a PNG file.

        :param path: Path to the PNG file
        :return: None
        """
        img = Image.new("L", (16, 16))
        for y in range(16):
            for x in range(16):
                img.putpixel((x, y), 255 if self.pixels[y][x] else 0)
        img.save(path)

    def to_sw_png(self, path: Path) -> None:
        """
        Save the image to a PNG file in the Stormworks format. This is a 32x32 image with a 1 pixel border.

        :param path: Path to the PNG file
        :return: None
        """
        img = Image.open(get_blank_image())
        x_offset = 150
        y_offset = 150
        draw = ImageDraw.Draw(img)
        for y in range(16):
            for x in range(16):
                x_pos = x_offset + x * 13
                y_pos = y_offset + y * 13
                color = "#a9a9a9" if self.pixels[y][x] else "#636363"
                draw.rectangle((x_pos, y_pos, x_pos + 12, y_pos + 12), fill=color)
        img.save(path)

    def __repr__(self) -> str:
        return f"MCImage(pixels={self.pixels!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, MCImage):
            return False
        return self.pixels == other.pixels

    def __hash__(self) -> int:
        return hash(tuple(tuple(row) for row in self.pixels))


class Microcontroller(XMLElement):
    """
    A Stormworks Microcontroller
    """

    def __init__(
        self,
        name: str,
        description: str,
        width: int,
        length: int,
        nodes: list[Node],
        components: list[Component],
        image: Optional[MCImage] = None,
    ):
        self.name: str = name
        self.description: str = description
        self.width: int = width
        self.length: int = length
        self.nodes: list[Node] = nodes
        self.components: list[Component] = components
        self.image: MCImage = image or MCImage()

    @staticmethod
    def from_xml(element: XMLParserElement) -> Microcontroller:
        # pylint: disable=too-many-locals
        assert element.tag in ("microprocessor", "microprocessor_definition")
        name: str = element.attributes.get("name", "")
        description: str = element.attributes.get("description", "")
        width: int = int(element.attributes.get("width", "0"))
        length: int = int(element.attributes.get("length", "0"))
        image_rows: list[list[bool]] = []
        for i in range(15, -1, -1):
            row_str = element.attributes.get(f"sym{i}", "0")
            row = [(int(row_str) & (1 << j)) != 0 for j in range(16)]
            image_rows.append(row)
        image = MCImage(image_rows)
        nodes: list[Node] = []
        nodes_elem: XMLParserElement = element.children[0]
        assert nodes_elem.tag == "nodes"
        for node in nodes_elem.children:
            nodes.append(Node.from_xml(node))
        group_elem: XMLParserElement = element.children[1]
        assert group_elem.tag == "group"
        components_elem: XMLParserElement = group_elem.children[1]
        if components_elem.tag == "data":
            group_elem.children.pop(1)
            components_elem = group_elem.children[1]
        assert components_elem.tag == "components"
        components: list[Component] = []
        for component in components_elem.children:
            components.append(Component.from_xml(component))
        component_bridge_elem: XMLParserElement = group_elem.children[2]
        assert component_bridge_elem.tag == "components_bridge"
        for child in component_bridge_elem.children:
            component_id = int(child.children[0].attributes.get("id", "0"))
            for xml_node in nodes:
                if xml_node.component_id == component_id:
                    xml_node.add_component_bridge(child)
                    break
        return Microcontroller(
            name, description, width, length, nodes, components, image
        )

    def to_xml(self) -> XMLParserElement:
        nodes_elem: XMLParserElement = XMLParserElement("nodes")
        for node in self.nodes:
            nodes_elem.children.append(node.to_xml())
        group_elem: XMLParserElement = XMLParserElement("group")
        group_elem.children.append(
            XMLParserElement(
                "data", {}, [XMLParserElement("inputs"), XMLParserElement("outputs")]
            )
        )
        components_elem: XMLParserElement = XMLParserElement("components")
        for component in self.components:
            components_elem.children.append(component.to_xml())
        group_elem.children.append(components_elem)
        component_bridge_elem: XMLParserElement = XMLParserElement("components_bridge")
        for node in self.nodes:
            component_bridge_elem.children.append(node.to_component_bridge())
        group_elem.children.append(component_bridge_elem)
        group_elem.children.append(XMLParserElement("groups"))
        if ADD_STATE:
            components_states_elem: XMLParserElement = XMLParserElement(
                "component_states"
            )
            for i, component in enumerate(self.components):
                components_states_elem.children.append(component.to_state_xml(i))
            group_elem.children.append(components_states_elem)
            component_bridge_states_elem: XMLParserElement = XMLParserElement(
                "component_bridge_states"
            )
            for i, node in enumerate(self.nodes):
                component_bridge_states_elem.children.append(
                    node.to_component_bridge_state(i)
                )
            group_elem.children.append(component_bridge_states_elem)
            group_elem.children.append(XMLParserElement("group_states"))
        image_rows: dict[str, str] = {}
        for i, row in enumerate(self.image.pixels):
            row_value = sum((1 << j) for j, pixel in enumerate(row) if pixel)
            if row_value != 0:
                image_rows[f"sym{15 - i}"] = str(row_value)
        attributes: dict[str, str] = {
            "name": self.name,
            "description": self.description,
            "width": str(self.width),
            "length": str(self.length),
            "id_counter": str(self.id_counter),
            "id_counter_node": str(self.node_counter),
            **image_rows,
        }
        return XMLParserElement("microprocessor", attributes, [nodes_elem, group_elem])

    def add_new_component(self, component: Component) -> None:
        """
        Adds a new :class:`Component <sw_mc_lib.Component.Component>`. Sets the right `component_id`.

        :param component: The component to add, `component_id` may be any value
        :return: None
        """
        component.component_id = self.id_counter + 1
        self.components.append(component)

    def add_new_node(self, node: Node) -> None:
        """
        Adds a new :class:`Node <sw_mc_lib.Node.Node>`. Sets the right `component_id` and `node_id`.

        :param node: The Node to add
        :return: None
        """
        node.component_id = self.id_counter + 1
        node.node_id = self.node_counter + 1
        self.nodes.append(node)

    @property
    def id_counter(self) -> int:
        """
        Stormworks internal counter of what the largest component_id is.

        :return: The currently largest component_id
        """
        components = (comp.component_id for comp in self.components)
        nodes = (node.component_id for node in self.nodes)
        return max(chain(components, nodes, (0,)))

    @property
    def node_counter(self) -> int:
        """
        Stormworks internal counter of what the largest node_id is.

        :return: The currently largest node_id
        """
        nodes = (node.node_id for node in self.nodes)
        return max(chain(nodes, (0,)))
