# Released under the MIT License. See LICENSE for details.
#
"""Prep functionality for our UI.

We do all layout math and bake out partial ui calls in a background
thread so there's as little work to do in the ui thread as possible.
"""

from functools import partial
from typing import TYPE_CHECKING, assert_never

from efro.util import pairs_from_flat
from efro.dataclassio import dataclass_to_json
import bacommon.displayitem as ditm
import bacommon.docui.v2 as dui2
import bauiv1 as bui
from bauiv1 import builtinassets
from bauiv1 import classicassets

from bauiv1lib.docui.prep._types import DecorationPrep

if TYPE_CHECKING:
    from typing import Any, Callable

    from bacommon.langstr import LangStrSpec
    from bauiv1lib.docui import DocUIWindow


def _native(lstr: 'LangStrSpec', packages: list[str]) -> bui.LangStr:
    """Native handle bound against a payload's package list."""
    return bui.LangStr(dataclass_to_json(lstr), packages=packages)


def _btex(name: str) -> str:
    """Qualified ref for a texture in the builtin asset-package."""
    return f'{builtinassets.__asset_package__}:textures/{name}'


def _stex(name: str) -> str:
    """Qualified classicassets texture ref."""
    return f'{classicassets.__asset_package__}:textures/{name}'


def _refstr(ref: 'Any') -> str:
    """Qualified engine name for a typed asset ref."""
    return f'{ref.apverid}:{ref.name}'


def prep_decorations(
    decorations: list[dui2.Decoration],
    center_x: float,
    center_y: float,
    scale: float,
    tdelay: float | None,
    *,
    packages: list[str],
    highlight: bool,
    out_decoration_preps: list[DecorationPrep],
) -> None:
    """Prep appropriate decoration types for a list of decorations."""
    for decoration in decorations:
        dectypeid = decoration.get_type_id()
        if dectypeid is dui2.DecorationTypeID.UNKNOWN:
            if bui.do_once():
                bui.uilog.exception(
                    'DocUI receieved unknown decoration;'
                    ' this is likely a server error.'
                )
        elif dectypeid is dui2.DecorationTypeID.TEXT:
            assert isinstance(decoration, dui2.Text)
            prep_text(
                decoration,
                (center_x, center_y),
                scale,
                tdelay,
                out_decoration_preps,
                packages=packages,
                highlight=highlight,
            )

        elif dectypeid is dui2.DecorationTypeID.IMAGE:
            assert isinstance(decoration, dui2.Image)
            prep_image(
                decoration,
                (center_x, center_y),
                scale,
                tdelay,
                out_decoration_preps,
                highlight=highlight,
            )
        elif dectypeid is dui2.DecorationTypeID.DISPLAY_ITEM:
            assert isinstance(decoration, dui2.DisplayItem)
            prep_display_item(
                decoration,
                (center_x, center_y),
                scale,
                tdelay,
                out_decoration_preps,
                highlight=highlight,
            )
        else:
            assert_never(dectypeid)


def prep_text(
    text: dui2.Text,
    bcenter: tuple[float, float],
    bscale: float,
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
    *,
    packages: list[str],
    highlight: bool,
) -> None:
    """Prep decorations for text."""
    # pylint: disable=too-many-branches
    xoffs = bcenter[0] + text.position[0] * bscale
    yoffs = bcenter[1] + text.position[1] * bscale

    if text.h_align is dui2.HAlign.LEFT:
        h_align = 'left'
    elif text.h_align is dui2.HAlign.CENTER:
        h_align = 'center'
    elif text.h_align is dui2.HAlign.RIGHT:
        h_align = 'right'
    else:
        assert_never(text.h_align)

    if text.v_align is dui2.VAlign.TOP:
        v_align = 'top'
    elif text.v_align is dui2.VAlign.CENTER:
        v_align = 'center'
    elif text.v_align is dui2.VAlign.BOTTOM:
        v_align = 'bottom'
    else:
        assert_never(text.v_align)

    out_decoration_preps.append(
        DecorationPrep(
            call=partial(
                bui.textwidget,
                position=(xoffs, yoffs),
                scale=text.scale * bscale,
                maxwidth=text.size[0] * bscale,
                max_height=text.size[1] * bscale,
                flatness=text.flatness,
                shadow=text.shadow,
                h_align=h_align,
                v_align=v_align,
                size=(0, 0),
                color=text.color,
                text=_native(text.text, packages),
                literal=True,
                transition_delay=tdelay,
                transition_type='scale',
                depth_range=text.depth_range,
            ),
            textures={},
            meshes={},
            highlight=highlight and text.highlight,
        )
    )
    # Draw square around max width/height in debug mode.
    if text.debug:
        mwfull = bscale * text.size[0]
        mhfull = bscale * text.size[1]

        if text.h_align is dui2.HAlign.LEFT:
            mwxoffs = xoffs
        elif text.h_align is dui2.HAlign.CENTER:
            mwxoffs = xoffs - mwfull * 0.5
        elif text.h_align is dui2.HAlign.RIGHT:
            mwxoffs = xoffs - mwfull
        else:
            assert_never(text.h_align)

        if text.v_align is dui2.VAlign.TOP:
            mwyoffs = yoffs - mhfull
        elif text.v_align is dui2.VAlign.CENTER:
            mwyoffs = yoffs - mhfull * 0.5
        elif text.v_align is dui2.VAlign.BOTTOM:
            mwyoffs = yoffs
        else:
            assert_never(text.v_align)

        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.imagewidget,
                    position=(mwxoffs, mwyoffs),
                    size=(mwfull, mhfull),
                    color=(1, 0, 0),
                    opacity=0.2,
                    transition_delay=tdelay,
                    transition_type='scale',
                ),
                textures={'texture': _btex('white')},
                meshes={},
                highlight=True,
            )
        )


def prep_image(
    image: dui2.Image,
    bcenter: tuple[float, float],
    bscale: float,
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
    *,
    highlight: bool,
) -> None:
    """Prep decorations for an image."""
    xoffs = bcenter[0] + image.position[0] * bscale
    yoffs = bcenter[1] + image.position[1] * bscale

    widthfull = bscale * image.size[0]
    heightfull = bscale * image.size[1]

    if image.h_align is dui2.HAlign.LEFT:
        xoffsfin = xoffs
    elif image.h_align is dui2.HAlign.CENTER:
        xoffsfin = xoffs - widthfull * 0.5
    elif image.h_align is dui2.HAlign.RIGHT:
        xoffsfin = xoffs - widthfull
    else:
        assert_never(image.h_align)

    if image.v_align is dui2.VAlign.TOP:
        yoffsfin = yoffs - heightfull
    elif image.v_align is dui2.VAlign.CENTER:
        yoffsfin = yoffs - heightfull * 0.5
    elif image.v_align is dui2.VAlign.BOTTOM:
        yoffsfin = yoffs
    else:
        assert_never(image.v_align)

    textures: dict[str, str] = {'texture': _refstr(image.texture)}
    if image.tint_texture is not None:
        textures['tint_texture'] = _refstr(image.tint_texture)
    if image.mask_texture is not None:
        textures['mask_texture'] = _refstr(image.mask_texture)

    meshes: dict[str, str] = {}
    if image.mesh_opaque is not None:
        meshes['mesh_opaque'] = _refstr(image.mesh_opaque)
    if image.mesh_transparent is not None:
        meshes['mesh_transparent'] = _refstr(image.mesh_transparent)

    out_decoration_preps.append(
        DecorationPrep(
            call=partial(
                bui.imagewidget,
                position=(xoffsfin, yoffsfin),
                size=(widthfull, heightfull),
                color=None if image.color is None else image.color[:3],
                opacity=1.0 if image.color is None else image.color[3],
                tint_color=image.tint_color,
                tint2_color=image.tint2_color,
                transition_delay=tdelay,
                transition_type='scale',
                depth_range=image.depth_range,
            ),
            textures=textures,
            meshes=meshes,
            highlight=highlight and image.highlight,
        )
    )


def prep_row_debug(
    size: tuple[float, float],
    pos: tuple[float, float],
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
) -> None:
    """Prep debug decorations for a row."""

    textures: dict[str, str] = {'texture': _btex('white')}

    # Shrink the square we draw a tiny bit so rows butted up to
    # eachother can be seen.
    border_shrink = 1.0

    out_decoration_preps.append(
        DecorationPrep(
            call=partial(
                bui.imagewidget,
                position=(pos[0], pos[1] + border_shrink),
                size=(size[0], size[1] - 2.0 * border_shrink),
                color=(0, 0, 1.0),
                opacity=0.1,
                transition_delay=tdelay,
                transition_type='scale',
            ),
            textures=textures,
            meshes={},
            highlight=True,
        )
    )


def prep_row_debug_button(
    bsize: tuple[float, float],
    bcorner: tuple[float, float],
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
) -> None:
    """Prep debug decorations for a button."""
    xoffs = bcorner[0]
    yoffs = bcorner[1]

    textures: dict[str, str] = {'texture': _btex('white')}

    out_decoration_preps.append(
        DecorationPrep(
            call=partial(
                bui.imagewidget,
                position=(xoffs, yoffs),
                size=bsize,
                color=(0.0, 0.0, 1),
                opacity=0.15,
                transition_delay=tdelay,
                transition_type='scale',
            ),
            textures=textures,
            meshes={},
            highlight=True,
        )
    )


def prep_button_debug(
    bsize: tuple[float, float],
    bcenter: tuple[float, float],
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
) -> None:
    """Prep debug decorations for a button."""
    textures: dict[str, str] = {'texture': _btex('white')}

    out_decoration_preps.append(
        DecorationPrep(
            call=partial(
                bui.imagewidget,
                position=(
                    bcenter[0] - bsize[0] * 0.5,
                    bcenter[1] - bsize[1] * 0.5,
                ),
                size=bsize,
                color=(0, 1, 0),
                opacity=0.1,
                transition_delay=tdelay,
                transition_type='scale',
            ),
            textures=textures,
            meshes={},
            highlight=True,
        )
    )


def prep_display_item(
    display_item: dui2.DisplayItem,
    parent_center: tuple[float, float],
    parent_scale: float,
    tdelay: float | None,
    out_decoration_preps: list[DecorationPrep],
    *,
    highlight: bool,
) -> None:
    # pylint: disable=too-many-statements
    """Prep decorations for a display-item."""
    # pylint: disable=too-many-branches
    # pylint: disable=too-many-locals

    # Calc center and size of our bounds based on parent.
    our_center = (
        parent_center[0] + display_item.position[0] * parent_scale,
        parent_center[1] + display_item.position[1] * parent_scale,
    )
    bounds_size = (
        parent_scale * display_item.size[0],
        parent_scale * display_item.size[1],
    )

    wrapper = display_item.wrapper
    item = wrapper.item
    itemtype = item.get_type_id()

    # Draw our bounds if debug mode is enabled (or we're a test-item).
    if display_item.debug or itemtype is ditm.ItemTypeID.TEST:
        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.imagewidget,
                    color=(1, 1, 0),
                    opacity=0.1,
                    position=(
                        our_center[0] - bounds_size[0] * 0.5,
                        our_center[1] - bounds_size[1] * 0.5,
                    ),
                    size=bounds_size,
                    transition_delay=tdelay,
                    transition_type='scale',
                ),
                textures={'texture': _btex('white')},
                meshes={},
                highlight=highlight and display_item.highlight,
            )
        )

    # Calc our width and height based on our aspect ratio so we fit in
    # the provided bounds.
    if display_item.style is dui2.DisplayItemStyle.FULL:
        aspect_ratio = 0.75  # Bit less tall than wide (graphic centric).
        compact = False
        icon = False
    elif display_item.style is dui2.DisplayItemStyle.COMPACT:
        aspect_ratio = 0.5  # Significantly wider (text centric)
        compact = True
        icon = False
    elif display_item.style is dui2.DisplayItemStyle.ICON:
        aspect_ratio = 1.0  # Square
        compact = False
        icon = True
    else:
        # Make sure we cover all possibilities.
        assert_never(display_item.style)

    if bounds_size[0] * aspect_ratio > bounds_size[1]:
        height = bounds_size[1]
        width = height / aspect_ratio
    else:
        width = bounds_size[0]
        height = width * aspect_ratio

    # Show our constrained bounds in debug mode.
    if display_item.debug or itemtype is ditm.ItemTypeID.TEST:
        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.imagewidget,
                    color=(1, 0.5, 0),
                    opacity=0.2,
                    position=(
                        our_center[0] - width * 0.5,
                        our_center[1] - height * 0.5,
                    ),
                    size=(width, height),
                    transition_delay=tdelay,
                    transition_type='scale',
                ),
                textures={'texture': _btex('white')},
                meshes={},
                highlight=highlight and display_item.highlight,
            )
        )

    img: str | None = None
    img_x_offs = 0.0
    img_y_offs = 0.0
    imgsize = width * (0.5 if compact else 1.0 if icon else 0.33)

    show_text = True
    text_mult = 0.006
    text: str | None = None  # Uses default if None
    text_x_offs = 0.0
    text_y_offs = 0.0
    text_align = 'center'
    text_max_width: float | None = width * 0.9

    if itemtype is ditm.ItemTypeID.CHEST:
        from baclassic import (
            CHEST_APPEARANCE_DISPLAY_INFOS,
            CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
        )
        import bacommon.classic

        assert isinstance(item, bacommon.classic.ClassicChestDisplayItem)

        img = None
        show_text = False
        c_info = CHEST_APPEARANCE_DISPLAY_INFOS.get(
            item.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
        )
        c_size = width * (0.66 if compact else 1.05 if icon else 0.83)
        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.imagewidget,
                    position=(
                        our_center[0] - c_size * 0.5,
                        our_center[1] - c_size * 0.5,
                    ),
                    size=(c_size, c_size),
                    transition_delay=tdelay,
                    transition_type='scale',
                    tint_color=c_info.tint,
                    tint2_color=c_info.tint2,
                    depth_range=display_item.depth_range,
                ),
                textures={
                    'texture': c_info.texclosed,
                    'tint_texture': c_info.texclosedtint,
                },
                meshes={},
                highlight=highlight and display_item.highlight,
            )
        )
    elif itemtype is ditm.ItemTypeID.TEST:
        assert isinstance(item, ditm.Test)
        # Nothing to do here. This is just another way to enable debug
        # drawing.
        if icon or compact:
            text_mult = 0.02  # Very large text.

    elif (
        itemtype is ditm.ItemTypeID.TOKENS
        or itemtype is ditm.ItemTypeID.TICKETS
        or itemtype is ditm.ItemTypeID.TICKETS_PURPLE
    ):
        if itemtype is ditm.ItemTypeID.TOKENS:
            assert isinstance(item, ditm.Tokens)
            img = _stex('coin')
            if compact:
                text = str(item.count)
        elif itemtype is ditm.ItemTypeID.TICKETS:
            assert isinstance(item, ditm.Tickets)
            img = _stex('tickets')
            if compact:
                text = str(item.count)
        elif itemtype is ditm.ItemTypeID.TICKETS_PURPLE:
            assert isinstance(item, ditm.PurpleTickets)
            img = _stex('tickets_purple')
            if compact:
                text = str(item.count)
        else:
            assert_never(itemtype)

        if compact:
            imgamt = 0.85  # How much of img dimensions we measure.

            assert text is not None
            text_mult = 0.01
            strwidth = (
                width
                * bui.get_string_width(text, suppress_warning=True)
                * text_mult
            )
            totwidth = strwidth + imgsize * imgamt

            maxwidth = width * 0.95
            if totwidth > maxwidth:
                mult = maxwidth / totwidth
                text_mult *= mult
                strwidth *= mult
                totwidth *= mult
                imgsize *= mult

            text_max_width = None  # We calc this fully ourself.
            # Move to right and then left by half img width.
            img_x_offs = totwidth * 0.5 - imgsize * imgamt * 0.5
            # Move to left and then right by half text width.
            text_x_offs = totwidth * -0.5 + strwidth * 0.5
        elif icon:
            img_y_offs = 0.0
            show_text = False
        else:
            img_y_offs = width * 0.11
            text_y_offs = width * -0.15
    elif itemtype is ditm.ItemTypeID.UNKNOWN:
        assert isinstance(item, ditm.Unknown)
        # Just do default text here.
        if icon:
            text_mult = 0.02  # Very large text.
    else:
        # Make sure we cover all possibilities.
        assert_never(itemtype)

    if img is not None:
        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.imagewidget,
                    position=(
                        our_center[0] - imgsize * 0.5 + img_x_offs,
                        our_center[1] - imgsize * 0.5 + img_y_offs,
                    ),
                    size=(imgsize, imgsize),
                    transition_delay=tdelay,
                    transition_type='scale',
                    depth_range=display_item.depth_range,
                ),
                textures={'texture': img},
                meshes={},
                highlight=highlight and display_item.highlight,
            )
        )
    if show_text:
        if text is None:
            subs = wrapper.description_subs
            if subs is None:
                subs = []
            text = bui.Lstr(
                translate=('displayItemNames', wrapper.description),
                subs=pairs_from_flat(subs),
            ).as_json()

        out_decoration_preps.append(
            DecorationPrep(
                call=partial(
                    bui.textwidget,
                    position=(
                        our_center[0] + text_x_offs,
                        our_center[1] + text_y_offs,
                    ),
                    scale=width * text_mult,
                    maxwidth=text_max_width,
                    h_align=text_align,
                    v_align='center',
                    size=(0, 0),
                    color=(
                        (1, 1, 1)
                        if display_item.text_color is None
                        else display_item.text_color
                    ),
                    text=text,
                    flatness=1.0,
                    shadow=1.0,
                    literal=False,
                    transition_delay=tdelay,
                    transition_type='scale',
                    depth_range=display_item.depth_range,
                ),
                textures={},
                meshes={},
                highlight=highlight and display_item.highlight,
            )
        )
