# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for piker0)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""
Non-shitty labels that don't re-invent the wheel.

"""
from inspect import isfunction
from typing import (
    Callable,
    Any,
)

import pyqtgraph as pg

from piker.ui.qt import (
    px_cache_mode,
    QtGui,
    QtWidgets,
    QLabel,
    size_policy,
    QPointF,
    QRectF,
    Qt,
)
from ._style import (
    DpiAwareFont,
    hcolor,
    _font,
)


class Label:
    '''
    A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.

    After hacking for many days on multiple "label" systems inside
    ``pyqtgraph`` yet again we're left writing our own since it seems
    all of those are over complicated, ad-hoc, transform-mangling,
    messes which can't accomplish the simplest things via their inputs
    (such as pinning to the left hand side of a view box).

    Here we do the simple thing where the label uses callables to figure
    out the (x, y) coordinate "pin point": nice and simple.

    This type is another effort (see our graphics) to start making
    small, re-usable label components that can actually be used to build
    production grade UIs...

    '''
    def __init__(
        self,
        view: pg.ViewBox,
        fmt_str: str,

        color: str = 'default_light',
        x_offset: float = 0,
        font_size: str = 'small',
        opacity: float = 1,
        fields: dict = {},
        parent: pg.GraphicsObject = None,
        update_on_range_change: bool = True,

    ) -> None:

        vb = self.vb = view
        self._fmt_str = fmt_str
        self._view_xy = QPointF(0, 0)

        self.scene_anchor: Callable[..., QPointF] | None = None

        self._x_offset = x_offset

        txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent)
        txt.setCacheMode(px_cache_mode.DeviceCoordinateCache)

        vb.scene().addItem(txt)

        # configure font size based on DPI
        dpi_font = DpiAwareFont(
            _font_size_key=font_size,
        )
        dpi_font.configure_to_dpi()
        txt.setFont(dpi_font.font)
        txt.setOpacity(opacity)

        # register viewbox callbacks
        if update_on_range_change:
            vb.sigRangeChanged.connect(self.on_sigrange_change)

        self._hcolor: str = ''
        self.color = color

        self.fields = fields
        self.orient_v = 'bottom'

        self._anchor_func = self.txt.pos().x

        # not sure if this makes a diff
        self.txt.setCacheMode(px_cache_mode.DeviceCoordinateCache)

        # TODO: edit and selection support
        # https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
        # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)

    @property
    def color(self) -> str:
        return self._hcolor

    @color.setter
    def color(self, color: str) -> None:
        self.txt.setDefaultTextColor(pg.mkColor(hcolor(color)))
        self._hcolor = color

    def update(self) -> None:
        '''
        Update this label either by invoking its user defined anchoring
        function, or by positioning to the last recorded data view
        coordinates.

        '''
        # move label in scene coords to desired position
        anchor = self.scene_anchor
        if anchor:
            self.txt.setPos(anchor())

        else:
            # position based on last computed view coordinate
            self.set_view_pos(self._view_xy.y())

    def on_sigrange_change(self, vr, r) -> None:
        return self.update()

    @property
    def w(self) -> float:
        return self.txt.boundingRect().width()

    def scene_br(self) -> QRectF:
        txt = self.txt
        return txt.mapToScene(
            txt.boundingRect()
        ).boundingRect()

    @property
    def h(self) -> float:
        return self.txt.boundingRect().height()

    def vbr(self) -> QRectF:
        return self.vb.boundingRect()

    def set_x_anchor_func(
        self,
        func: Callable,
    ) -> None:
        assert isinstance(func(), float)
        self._anchor_func = func

    def set_view_pos(
        self,

        y: float,
        x: float | None = None,

    ) -> None:

        if x is None:
            scene_x = self._anchor_func() or self.txt.pos().x()
            x = self.vb.mapToView(QPointF(scene_x, scene_x)).x()

        # get new (inside the) view coordinates / position
        self._view_xy = QPointF(x, y)

        # map back to the outer UI-land "scene" coordinates
        s_xy = self.vb.mapFromView(self._view_xy)

        if self.orient_v == 'top':
            s_xy = QPointF(s_xy.x(), s_xy.y() - self.h)

        # move label in scene coords to desired position
        self.txt.setPos(s_xy)

        assert s_xy == self.txt.pos()

    @property
    def fmt_str(self) -> str:
        return self._fmt_str

    @fmt_str.setter
    def fmt_str(self, fmt_str: str) -> None:
        self._fmt_str = fmt_str

    def format(
        self,
        **fields: dict

    ) -> str:

        out = {}

        # this is hacky support for single depth
        # calcs of field data from field data
        # ex. to calculate a $value = price * size
        for k, v in fields.items():

            if isfunction(v):
                out[k] = v(fields)

            else:
                out[k] = v

        text = self._fmt_str.format(**out)

        # for large numbers with a thousands place
        text = text.replace(',', ' ')

        self.txt.setPlainText(text)

    def render(self) -> None:
        self.format(**self.fields)

    def show(self) -> None:
        self.txt.show()
        self.txt.update()

    def hide(self) -> None:
        self.txt.hide()

    def delete(self) -> None:
        self.vb.scene().removeItem(self.txt)

    # NOTE: pulled out from ``ChartPlotWidget`` from way way old code.
    # def _label_h(self, yhigh: float, ylow: float) -> float:
    #     # compute contents label "height" in view terms
    #     # to avoid having data "contents" overlap with them
    #     if self._labels:
    #         label = self._labels[self.name][0]

    #         rect = label.itemRect()
    #         tl, br = rect.topLeft(), rect.bottomRight()
    #         vb = self.plotItem.vb

    #         try:
    #             # on startup labels might not yet be rendered
    #             top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())

    #             # XXX: magic hack, how do we compute exactly?
    #             label_h = (top - bottom) * 0.42

    #         except np.linalg.LinAlgError:
    #             label_h = 0
    #     else:
    #         label_h = 0

    #     # print(f'label height {self.name}: {label_h}')

    #     if label_h > yhigh - ylow:
    #         label_h = 0

    #     print(f"bounds (ylow, yhigh): {(ylow, yhigh)}")


class FormatLabel(QLabel):
    '''
    Kinda similar to above but using the widget apis.

    '''
    def __init__(
        self,

        fmt_str: str,
        font: QtGui.QFont,
        font_size: int,
        font_color: str,

        use_md: bool = True,

        parent=None,

    ) -> None:

        super().__init__(parent)

        # by default set the format string verbatim and expect user
        # to call ``.format()`` later (presumably they'll notice the
        # unformatted content if ``fmt_str`` isn't meant to be
        # unformatted).
        self.fmt_str = fmt_str
        # self.setText(fmt_str)  # ?TODO, why here?

        self.setStyleSheet(
            f"""QLabel {{
                color : {hcolor(font_color)};
                font-size : {font_size}px;
            }}
            """
        )
        self.setFont(_font.font)
        if use_md:
            self.setTextFormat(
                Qt.TextFormat.MarkdownText
            )
        self.setMargin(0)

        self.setSizePolicy(
            size_policy.Expanding,
            size_policy.Expanding,
        )
        self.setAlignment(
            Qt.AlignLeft
            |
            Qt.AlignBottom
            # Qt.AlignVCenter
        )
        self.setText(self.fmt_str)

    def format(
        self,
        **fields: dict[str, Any],

    ) -> str:
        out = self.fmt_str.format(**fields)
        self.setText(out)
        return out
