from typing import Generator

import prismatui as pr

# //////////////////////////////////////////////////////////////////////////////
class Section:
    """A Section is a container for layers and child sections.
    It can be used to create a hierarchical structure of sections, each with its own layers.
    Each section has a size and position relative to its parent section
    (or the terminal, in case of the root section)."""
    def __init__(self):
        self._parent: "Section" = None

        self.h: int; self.w: int
        self.y: int; self.x: int
        self.hrel: int|float = 1.0
        self.wrel: int|float = 1.0
        self.yrel: int|float = 0
        self.xrel: int|float = 0
        self._update_dimensions()

        self._children: list["Section"] = []
        self._layers = [pr.Layer(self.h, self.w)]


    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def set_parent(self, parent: "Section") -> None:
        """Set the parent section of this section."""
        self._parent = parent
        parent._children.append(self)
        self._update_dimensions()

    # --------------------------------------------------------------------------
    def create_child(self,
        hrel: int|float, wrel: int|float,
        yrel: int|float, xrel: int|float
    ) -> "Section":
        """Create a child section with the specified relative dimensions and position."""
        child = Section()
        child.hrel = hrel
        child.wrel = wrel
        child.yrel = yrel
        child.xrel = xrel
        child.set_parent(self)
        return child

    # --------------------------------------------------------------------------
    def create_layer(self) -> "pr.Layer":
        """Create a new layer in this section."""
        layer = pr.Layer(self.h, self.w)
        self._layers.append(layer)
        return layer

    # --------------------------------------------------------------------------
    def create_mosaic(self, layout: str, divider = '\n') -> dict:
        """Create a mosaic of sections based on the provided 'mosaic' layout
        string, where each character represents a section. Takes inspiration
        from matplotlib's `subplot_mosaic` function."""
        section_dict = {}
        for char, hwyx in pr.mosaic_parser(layout, divider).items():
            section = self.create_child(*hwyx)
            section_dict[char] = section
        return section_dict


    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def get_size(self) -> tuple[int, int]:
        """Get the size of this section."""
        return self.h, self.w

    # --------------------------------------------------------------------------
    def get_position(self) -> tuple[int, int]:
        """Get the position of this section relative to its parent."""
        return self.y, self.x

    # --------------------------------------------------------------------------
    def get_bottom_layer(self) -> "pr.Layer":
        """Get the bottom layer of this section."""
        return self._layers[0]

    # --------------------------------------------------------------------------
    def get_top_layer(self) -> "pr.Layer":
        """Get the top layer of this section."""
        return self._layers[-1]


    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def iter_children(self) -> Generator["Section", None, None]:
        """Iterate over the child sections of this section."""
        return iter(self._children)

    # --------------------------------------------------------------------------
    def iter_layers(self) -> Generator["pr.Layer", None, None]:
        """Iterate over the layers of this section."""
        return iter(self._layers)

    # --------------------------------------------------------------------------
    def clear(self) -> None:
        """Clear all layers and child sections in this section."""
        for layer in self.iter_layers():
            layer.clear()

        for child in self.iter_children():
            child.clear()

    # --------------------------------------------------------------------------
    def aggregate_layers(self, agg_layer: "pr.Layer") -> None:
        """Aggregate all layers and child sections into the provided aggregate layer."""
        for layer in self.iter_layers():
            agg_layer.draw_layer(self.y, self.x, layer)

        for child in self.iter_children():
            child.aggregate_layers(agg_layer)

    # --------------------------------------------------------------------------
    def update_size(self) -> None:
        """Update the size of this section and its layers, as well as of children sections, based on the relative dimensions."""
        self._update_dimensions()

        for layer in self.iter_layers():
            layer.set_size(self.h, self.w)

        for child in self.iter_children():
            child.update_size()


    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def draw_layer(self, y: int | str, x: int | str, layer: "pr.Layer") -> None:
        """Draw another layer onto the top layer of this section, at the specified coordinates."""
        self.get_top_layer().draw_layer(y, x, layer)


    # --------------------------------------------------------------------------
    def draw_matrix(self, y: int | str, x: int | str, chars: list[str], attrs: list[list[int]], blend = pr.BlendMode.OVERLAY) -> None:
        """Draw a character matrix on the top layer of this section, at the specified coordinates with optional blending mode."""
        self.get_top_layer().draw_matrix(y, x, chars, attrs, blend)

    # --------------------------------------------------------------------------
    def draw_text(self,
        y: int | str, x: int | str, string,
        attr: int | list[list[int]] = None,
        blend = pr.BlendMode.OVERLAY,
        cut: dict[str, str] = {}
    ) -> int:
        """
        Draw a string on the top layer of this section, at the specified coordinates with optional attributes and blending mode.
        Returns the length of the given raw string (including newlines).
        """
        return self.get_top_layer().draw_text(y, x, string, attr, blend, cut)

    # --------------------------------------------------------------------------
    def draw_border(self,
        ls = '│', rs = '│', ts = '─', bs = '─',
        tl = '┌', tr = '┐', bl = '└', br = '┘',
        attr = None, blend = pr.BlendMode.OVERLAY
    ) -> None:
        """Draw a border around the top layer of this section with specified characters and attribute."""
        self.get_top_layer().draw_border(ls, rs, ts, bs, tl, tr, bl, br, attr, blend)


    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def _update_dimensions(self) -> None:
        """Update the dimensions of this section based on its relative size and position."""
        if self._parent is None: # root section
            self.h, self.w = pr._CURRENT_BACKEND.get_size()
            self.y, self.x = 0, 0
            return

        h = self.hrel; w = self.wrel
        y = self.yrel; x = self.xrel

        if isinstance(h, float):
            self.h = round(h * self._parent.h)
        elif isinstance(h, int):
            if h < 0: h += self._parent.h
            self.h = min(h, self._parent.h)

        if isinstance(w, float):
            self.w = round(w * self._parent.w)
        elif isinstance(w, int):
            if w < 0: w += self._parent.w
            self.w = min(w, self._parent.w)

        if isinstance(y, float):
            self.y = self._parent.y + round(y * self._parent.h)
        elif isinstance(y, int):
            if y < 0: y += self._parent.h
            self.y = y + self._parent.y

        if isinstance(x, float):
            self.x = self._parent.x + round(x * self._parent.w)
        elif isinstance(x, int):
            if x < 0: x += self._parent.w
            self.x = x + self._parent.x

        y_outbounds = (self.y + self.h) - (self._parent.y + self._parent.h)
        x_outbounds = (self.x + self.w) - (self._parent.x + self._parent.w)

        if y_outbounds > 0: self.y -= y_outbounds
        if x_outbounds > 0: self.x -= x_outbounds

        for layer in self._layers:
            layer.set_size(self.h, self.w)


# //////////////////////////////////////////////////////////////////////////////
