from __future__ import annotations

import typing
import warnings

from urwid.canvas import CompositeCanvas
from urwid.split_repr import remove_defaults
from urwid.util import int_scale

from .constants import (
    RELATIVE_100,
    Sizing,
    VAlign,
    WHSettings,
    normalize_height,
    normalize_valign,
    simplify_height,
    simplify_valign,
)
from .widget_decoration import WidgetDecoration

if typing.TYPE_CHECKING:
    from typing_extensions import Literal

    from .widget import Widget


class FillerError(Exception):
    pass


class Filler(WidgetDecoration):
    def __init__(
        self,
        body: Widget,
        valign: (
            Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]
        ) = VAlign.MIDDLE,
        height: int | Literal["pack"] | tuple[Literal["relative"], int] | None = WHSettings.PACK,
        min_height: int | None = None,
        top: int = 0,
        bottom: int = 0,
    ) -> None:
        """
        :param body: a flow widget or box widget to be filled around (stored
            as self.original_widget)
        :type body: Widget

        :param valign: one of:
            ``'top'``, ``'middle'``, ``'bottom'``,
            (``'relative'``, *percentage* 0=top 100=bottom)

        :param height: one of:

            ``'pack'``
              if body is a flow widget

            *given height*
              integer number of rows for self.original_widget

            (``'relative'``, *percentage of total height*)
              make height depend on container's height

        :param min_height: one of:

            ``None``
              if no minimum or if body is a flow widget

            *minimum height*
              integer number of rows for the widget when height not fixed

        :param top: a fixed number of rows to fill at the top
        :type top: int
        :param bottom: a fixed number of rows to fill at the bottom
        :type bottom: int

        If body is a flow widget then height must be ``'flow'`` and
        *min_height* will be ignored.

        Filler widgets will try to satisfy height argument first by
        reducing the valign amount when necessary.  If height still
        cannot be satisfied it will also be reduced.
        """
        super().__init__(body)

        # convert old parameters to the new top/bottom values
        if isinstance(height, tuple):
            if height[0] == "fixed top":
                if not isinstance(valign, tuple) or valign[0] != "fixed bottom":
                    raise FillerError("fixed top height may only be used with fixed bottom valign")
                top = height[1]
                height = RELATIVE_100
            elif height[0] == "fixed bottom":
                if not isinstance(valign, tuple) or valign[0] != "fixed top":
                    raise FillerError("fixed bottom height may only be used with fixed top valign")
                bottom = height[1]
                height = RELATIVE_100
        if isinstance(valign, tuple):
            if valign[0] == "fixed top":
                top = valign[1]
                valign = VAlign.TOP
            elif valign[0] == "fixed bottom":
                bottom = valign[1]
                valign = VAlign.BOTTOM

        # convert old flow mode parameter height=None to height='flow'
        if height is None or height == Sizing.FLOW:
            height = WHSettings.PACK

        self.top = top
        self.bottom = bottom
        self.valign_type, self.valign_amount = normalize_valign(valign, FillerError)
        self.height_type, self.height_amount = normalize_height(height, FillerError)

        if self.height_type not in (WHSettings.GIVEN, WHSettings.PACK):
            self.min_height = min_height
        else:
            self.min_height = None

    def sizing(self):
        return {Sizing.BOX}  # always a box widget

    def _repr_attrs(self):
        attrs = dict(
            super()._repr_attrs(),
            valign=simplify_valign(self.valign_type, self.valign_amount),
            height=simplify_height(self.height_type, self.height_amount),
            top=self.top,
            bottom=self.bottom,
            min_height=self.min_height,
        )
        return remove_defaults(attrs, Filler.__init__)

    @property
    def body(self):
        """backwards compatibility, widget used to be stored as body"""
        warnings.warn(
            "backwards compatibility, widget used to be stored as body",
            PendingDeprecationWarning,
            stacklevel=2,
        )
        return self.original_widget

    @body.setter
    def body(self, new_body):
        warnings.warn(
            "backwards compatibility, widget used to be stored as body",
            PendingDeprecationWarning,
            stacklevel=2,
        )
        self.original_widget = new_body

    def get_body(self):
        """backwards compatibility, widget used to be stored as body"""
        warnings.warn(
            "backwards compatibility, widget used to be stored as body",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.original_widget

    def set_body(self, new_body):
        warnings.warn(
            "backwards compatibility, widget used to be stored as body",
            DeprecationWarning,
            stacklevel=2,
        )
        self.original_widget = new_body

    def selectable(self) -> bool:
        """Return selectable from body."""
        return self._original_widget.selectable()

    def filler_values(self, size: tuple[int, int], focus: bool) -> tuple[int, int]:
        """
        Return the number of rows to pad on the top and bottom.

        Override this method to define custom padding behaviour.
        """
        (maxcol, maxrow) = size

        if self.height_type == WHSettings.PACK:
            height = self._original_widget.rows((maxcol,), focus=focus)
            return calculate_top_bottom_filler(
                maxrow, self.valign_type, self.valign_amount, WHSettings.GIVEN, height, None, self.top, self.bottom
            )

        return calculate_top_bottom_filler(
            maxrow,
            self.valign_type,
            self.valign_amount,
            self.height_type,
            self.height_amount,
            self.min_height,
            self.top,
            self.bottom,
        )

    def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
        """Render self.original_widget with space above and/or below."""
        (maxcol, maxrow) = size
        top, bottom = self.filler_values(size, focus)

        if self.height_type == WHSettings.PACK:
            canv = self._original_widget.render((maxcol,), focus)
        else:
            canv = self._original_widget.render((maxcol, maxrow - top - bottom), focus)
        canv = CompositeCanvas(canv)

        if maxrow and canv.rows() > maxrow and canv.cursor is not None:
            cx, cy = canv.cursor
            if cy >= maxrow:
                canv.trim(cy - maxrow + 1, maxrow - top - bottom)
        if canv.rows() > maxrow:
            canv.trim(0, maxrow)
            return canv
        canv.pad_trim_top_bottom(top, bottom)
        return canv

    def keypress(self, size: tuple[int, int], key: str) -> str | None:
        """Pass keypress to self.original_widget."""
        (maxcol, maxrow) = size
        if self.height_type == WHSettings.PACK:
            return self._original_widget.keypress((maxcol,), key)

        top, bottom = self.filler_values((maxcol, maxrow), True)
        return self._original_widget.keypress((maxcol, maxrow - top - bottom), key)

    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
        """Return cursor coords from self.original_widget if any."""
        (maxcol, maxrow) = size
        if not hasattr(self._original_widget, "get_cursor_coords"):
            return None

        top, bottom = self.filler_values(size, True)
        if self.height_type == WHSettings.PACK:
            coords = self._original_widget.get_cursor_coords((maxcol,))
        else:
            coords = self._original_widget.get_cursor_coords((maxcol, maxrow - top - bottom))
        if not coords:
            return None
        x, y = coords
        if y >= maxrow:
            y = maxrow - 1
        return x, y + top

    def get_pref_col(self, size: tuple[int, int]) -> int:
        """Return pref_col from self.original_widget if any."""
        (maxcol, maxrow) = size
        if not hasattr(self._original_widget, "get_pref_col"):
            return None

        if self.height_type == WHSettings.PACK:
            x = self._original_widget.get_pref_col((maxcol,))
        else:
            top, bottom = self.filler_values(size, True)
            x = self._original_widget.get_pref_col((maxcol, maxrow - top - bottom))

        return x

    def move_cursor_to_coords(self, size: tuple[int, int], col: int, row: int) -> bool:
        """Pass to self.original_widget."""
        (maxcol, maxrow) = size
        if not hasattr(self._original_widget, "move_cursor_to_coords"):
            return True

        top, bottom = self.filler_values(size, True)
        if row < top or row >= maxcol - bottom:
            return False

        if self.height_type == WHSettings.PACK:
            return self._original_widget.move_cursor_to_coords((maxcol,), col, row - top)
        return self._original_widget.move_cursor_to_coords((maxcol, maxrow - top - bottom), col, row - top)

    def mouse_event(
        self,
        size: tuple[int, int],
        event,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool:
        """Pass to self.original_widget."""
        (maxcol, maxrow) = size
        if not hasattr(self._original_widget, "mouse_event"):
            return False

        top, bottom = self.filler_values(size, True)
        if row < top or row >= maxrow - bottom:
            return False

        if self.height_type == WHSettings.PACK:
            return self._original_widget.mouse_event((maxcol,), event, button, col, row - top, focus)
        return self._original_widget.mouse_event((maxcol, maxrow - top - bottom), event, button, col, row - top, focus)


def calculate_top_bottom_filler(
    maxrow: int,
    valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
    valign_amount: int,
    height_type: Literal["given", "relative", "clip", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.CLIP],
    height_amount: int,
    min_height: int | None,
    top: int,
    bottom: int,
) -> tuple[int, int]:
    """
    Return the amount of filler (or clipping) on the top and
    bottom part of maxrow rows to satisfy the following:

    valign_type -- 'top', 'middle', 'bottom', 'relative'
    valign_amount -- a percentage when align_type=='relative'
    height_type -- 'given', 'relative', 'clip'
    height_amount -- a percentage when width_type=='relative'
        otherwise equal to the height of the widget
    min_height -- a desired minimum width for the widget or None
    top -- a fixed number of rows to fill on the top
    bottom -- a fixed number of rows to fill on the bottom

    >>> ctbf = calculate_top_bottom_filler
    >>> ctbf(15, 'top', 0, 'given', 10, None, 2, 0)
    (2, 3)
    >>> ctbf(15, 'relative', 0, 'given', 10, None, 2, 0)
    (2, 3)
    >>> ctbf(15, 'relative', 100, 'given', 10, None, 2, 0)
    (5, 0)
    >>> ctbf(15, 'middle', 0, 'given', 4, None, 2, 0)
    (6, 5)
    >>> ctbf(15, 'middle', 0, 'given', 18, None, 2, 0)
    (0, 0)
    >>> ctbf(20, 'top', 0, 'relative', 60, None, 0, 0)
    (0, 8)
    >>> ctbf(20, 'relative', 30, 'relative', 60, None, 0, 0)
    (2, 6)
    >>> ctbf(20, 'relative', 30, 'relative', 60, 14, 0, 0)
    (2, 4)
    """
    if height_type == WHSettings.RELATIVE:
        maxheight = max(maxrow - top - bottom, 0)
        height = int_scale(height_amount, 101, maxheight + 1)
        if min_height is not None:
            height = max(height, min_height)
    else:
        height = height_amount

    standard_alignments = {VAlign.TOP: 0, VAlign.MIDDLE: 50, VAlign.BOTTOM: 100}
    valign = standard_alignments.get(valign_type, valign_amount)

    # add the remainder of top/bottom to the filler
    filler = maxrow - height - top - bottom
    bottom += int_scale(100 - valign, 101, filler + 1)
    top = maxrow - height - bottom

    # reduce filler if we are clipping an edge
    if bottom < 0 < top:
        shift = min(top, -bottom)
        top -= shift
        bottom += shift
    elif top < 0 < bottom:
        shift = min(bottom, -top)
        bottom -= shift
        top += shift

    # no negative values for filler at the moment
    top = max(top, 0)
    bottom = max(bottom, 0)

    return top, bottom
