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

# 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/>.

'''
Text entry "forms" widgets (mostly for configuration and UI user input).

'''
from __future__ import annotations
from contextlib import asynccontextmanager
from functools import partial
from typing import (
    Any,
    Callable,
    Awaitable,
)

import trio

from piker.ui.qt import (
    keys,
    size_policy,
    QtGui,
    QSize,
    QModelIndex,
    Qt,
    QEvent,
    QWidget,
    QLabel,
    QComboBox,
    QLineEdit,
    QHBoxLayout,
    QVBoxLayout,
    QFormLayout,
    QProgressBar,
    QStyledItemDelegate,
    QStyleOptionViewItem,
)

from ._event import open_handlers
from ._icons import mk_icons
from ._style import hcolor, _font, _font_small, DpiAwareFont
from ._label import FormatLabel


class Edit(QLineEdit):

    def __init__(

        self,
        parent: QWidget,
        # parent_chart: QWidget,  # noqa
        font: DpiAwareFont = _font,
        width_in_chars: int = None,

    ) -> None:

        # self.setContextMenuPolicy(Qt.CustomContextMenu)
        # self.customContextMenuRequested.connect(self.show_menu)
        # self.setStyleSheet(f"font: 18px")

        self.dpi_font = font
        # self.godwidget = parent_chart

        if width_in_chars:
            self._chars = int(width_in_chars)
            x_size_policy = size_policy.Fixed

        else:
            # chart count which will be used to calculate
            # width of input field.
            self._chars: int = 6
            # fit to surroundingn frame width
            x_size_policy = size_policy.Expanding

        super().__init__(parent)

        # size it as we specify
        # https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
        self.setSizePolicy(
            x_size_policy,
            size_policy.Fixed,
        )
        self.setFont(font.font)

        # witty bit of margin
        self.setTextMargins(2, 2, 2, 2)

    def sizeHint(self) -> QSize:
        """
        Scale edit box to size of dpi aware font.

        """
        psh = super().sizeHint()

        dpi_font = self.dpi_font
        psh.setHeight(dpi_font.px_size)

        # make space for ``._chars: int`` for of characters in view
        # TODO: somehow this math ain't right?
        chars_w_pxs = dpi_font.boundingRect('0'*self._chars).width()
        scale = round(dpi_font.scale())
        psh.setWidth(int(chars_w_pxs * scale))
        return psh

    def set_width_in_chars(
        self,
        chars: int,

    ) -> None:
        self._chars = chars
        self.sizeHint()
        self.update()

    def update_fonts(self, font: DpiAwareFont) -> None:
        '''Update font and recalculate widget size.'''
        self.dpi_font = font
        self.setFont(font.font)
        # tell Qt our size hint changed so it recalculates layout
        self.updateGeometry()

    def focus(self) -> None:
        self.selectAll()
        self.show()
        self.setFocus()


class FontScaledDelegate(QStyledItemDelegate):
    '''
    Super simple view delegate to render text in the same
    font size as the search widget.

    '''
    def __init__(
        self,

        parent=None,
        font: DpiAwareFont = _font,

    ) -> None:

        super().__init__(parent)
        self.dpi_font = font

    def sizeHint(
        self,

        option: QStyleOptionViewItem,
        index: QModelIndex,

    ) -> QSize:

        # value = index.data()
        # br = self.dpi_font.boundingRect(value)
        # w, h = br.width(), br.height()
        parent = self.parent()

        if getattr(parent, '_max_item_size', None):
            return QSize(*self.parent()._max_item_size)

        else:
            return super().sizeHint(option, index)

    # NOTE: hack to display icons on RHS
    # TODO: is there a way to set this stype option once?
    # def paint(self, painter, option, index):
    #     # display icons on RHS
    #     # https://stackoverflow.com/a/39943629
    #     option.decorationPosition = QtGui.QStyleOptionViewItem.Right
    #     option.decorationAlignment = Qt.AlignRight | Qt.AlignVCenter
    #     QStyledItemDelegate.paint(self, painter, option, index)


class Selection(QComboBox):

    def __init__(
        self,
        parent=None,
    ) -> None:

        self._items: dict[str, int] = {}
        super().__init__(parent=parent)
        self.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToContents,
        )
        # make line edit expand to surrounding frame
        self.setSizePolicy(
            size_policy.Expanding,
            size_policy.Fixed,
        )
        view = self.view()
        view.setUniformItemSizes(True)

        # TODO: this doesn't seem to work for the currently selected item?
        self.setItemDelegate(FontScaledDelegate(self))

        self.resize()

        self._icons = mk_icons(
            self.style(),
            self.iconSize()
        )

    def set_style(
        self,
        color: str,
        font_size: int,

    ) -> None:

        self.setStyleSheet(
            f"""QComboBox {{
                color : {hcolor(color)};
                font-size : {font_size}px;
            }}
            """
        )

    def resize(
        self,
        char: str = 'W',

    ) -> None:
        br = _font.boundingRect(str(char))
        _, h = br.width(), int(br.height())

        # TODO: something better then this monkey patch
        view = self.view()

        # XXX: see size policy settings of line edit
        # view._max_item_size = w, h

        self.setMinimumHeight(h)  # at least one entry in view
        view.setMaximumHeight(6*h)  # limit to 6 items max in view

        icon_size = round(h * 0.75)
        self.setIconSize(QSize(icon_size, icon_size))

    def update_fonts(self, font: DpiAwareFont) -> None:
        '''Update font and recalculate widget size.'''
        self.setFont(font.font)
        # recalculate heights with new font
        self.resize()
        # tell Qt our size hint changed so it recalculates layout
        self.updateGeometry()

    def set_items(
        self,
        keys: list[str],

    ) -> None:
        '''
        Write keys to the selection verbatim.

        All other items are cleared beforehand.

        '''
        self.clear()
        self._items.clear()

        for i, key in enumerate(keys):
            strkey = str(key)
            self.insertItem(i, strkey)

            # store map of entry keys to row indexes
            self._items[strkey] = i

        # compute max item size so that the weird
        # "style item delegate" thing can then specify
        # that size on each item...
        keys.sort()
        self.resize(keys[-1])

    def set_icon(
        self,
        key: str,
        icon_name: str | None,

    ) -> None:
        self.setItemIcon(
            self._items[key],
            self._icons[icon_name],
        )

    def items(self) -> list[(str, int)]:
        return list(self._items.items())

    # NOTE: in theory we can put icons on the RHS side with this hackery:
    # https://stackoverflow.com/a/64256969
    # def showPopup(self):
    #     print('show')
    #     QComboBox.showPopup(self)

    # def hidePopup(self):
    #     # self.setItemDelegate(FontScaledDelegate(self.parent()))
    #     print('hide')
    #     QComboBox.hidePopup(self)


# slew of resources which helped get this where it is:
# https://stackoverflow.com/questions/20648210/qcombobox-adjusttocontents-changing-height
# https://stackoverflow.com/questions/3151798/how-do-i-set-the-qcombobox-width-to-fit-the-largest-item
# https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content#6370892
# https://stackoverflow.com/questions/25304267/qt-resize-of-qlistview
# https://stackoverflow.com/questions/28227406/how-to-set-qlistview-rows-height-permanently
class FieldsForm(QWidget):

    vbox: QVBoxLayout
    form: QFormLayout

    def __init__(
        self,
        parent=None,

    ) -> None:

        super().__init__(parent)

        # size it as we specify
        self.setSizePolicy(
            size_policy.Expanding,
            size_policy.Expanding,
        )

        # XXX: not sure why we have to create this here exactly
        # (instead of in the pane creation routine) but it's
        # here and is managed by downstream layout routines.
        # best guess is that you have to create layouts in order
        # of hierarchy in order for things to display correctly?
        # TODO: we may want to hand this *down* from some "pane manager"
        # thing eventually?
        self.vbox = QVBoxLayout(self)
        # self.vbox.setAlignment(Qt.AlignVCenter)
        self.vbox.setAlignment(Qt.AlignBottom)
        self.vbox.setContentsMargins(3, 6, 3, 6)
        self.vbox.setSpacing(0)

        # split layout for the (<label>: |<widget>|) parameters entry
        self.form = QFormLayout()
        self.form.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.form.setContentsMargins(0, 0, 3, 0)
        self.form.setSpacing(0)
        self.form.setHorizontalSpacing(0)

        self.vbox.addLayout(self.form, stretch=3)

        self.labels: dict[str, QLabel] = {}
        self.fields: dict[str, QWidget] = {}

        self._font_size = _font_small.px_size - 2
        self._max_item_width: (float, float) = 0, 0

    def add_field_label(
        self,

        name: str,

        font_size: int | None = None,
        font_color: str = 'default_lightest',

    ) -> QtGui.QLabel:

        # add label to left of search bar
        # self.label = label = QtGui.QLabel()
        font_size = font_size or self._font_size - 1

        self.label = label = FormatLabel(
            fmt_str=name,
            font=_font.font,
            font_size=font_size,
            font_color=font_color,
        )

        # for later lookup
        self.labels[name] = label

        return label

    def add_edit_field(
        self,

        key: str,
        label_name: str,
        value: str,
        readonly: bool = False,

    ) -> Edit:

        # TODO: maybe a distint layout per "field" item?
        label = self.add_field_label(label_name)

        edit = Edit(
            parent=self,
            # width_in_chars=6,
        )
        edit.setStyleSheet(
            f"""QLineEdit {{
                color : {hcolor('gunmetal')};
                font-size : {self._font_size}px;
            }}
            """
        )
        edit.setReadOnly(readonly)
        edit.setText(str(value))
        self.form.addRow(label, edit)

        self.fields[key] = edit

        return edit

    def add_select_field(
        self,

        key: str,
        label_name: str,
        values: list[str],

    ) -> Selection:

        # TODO: maybe a distint layout per "field" item?
        label = self.add_field_label(label_name)

        select = Selection(self)
        select.set_style(color='gunmetal', font_size=self._font_size)
        select._key = key
        select.set_items(values)

        self.setSizePolicy(
            size_policy.Fixed,
            size_policy.Fixed,
        )
        select.show()
        self.form.addRow(label, select)
        self.fields[key] = select
        return select

    def update_fonts(self) -> None:
        '''Update font sizes after zoom change.'''
        from ._style import _font, _font_small

        # update stored font size
        self._font_size = _font_small.px_size - 2

        # update all labels
        for name, label in self.labels.items():
            if hasattr(label, 'update_font'):
                label.update_font(_font.font, self._font_size - 1)

        # update all fields (edits, selects)
        for key, field in self.fields.items():
            # first check for our custom update_fonts method (Edit, Selection)
            if hasattr(field, 'update_fonts'):
                field.update_fonts(_font)
            # then handle stylesheet updates for those without custom methods
            elif hasattr(field, 'setStyleSheet'):
                # regenerate stylesheet with new font size
                field.setStyleSheet(
                    f"""QLineEdit {{
                        color : {hcolor('gunmetal')};
                        font-size : {self._font_size}px;
                    }}
                    """
                )
                field.setFont(_font.font)

            # for Selection widgets that need style updates
            if hasattr(field, 'set_style'):
                field.set_style(color='gunmetal', font_size=self._font_size)


async def handle_field_input(

    widget: QWidget,
    recv_chan: trio.abc.ReceiveChannel,
    form: FieldsForm,
    on_value_change: Callable[[str, Any], Awaitable[bool]],
    focus_next: QWidget,

) -> None:

    async for kbmsg in recv_chan:

        if kbmsg.etype in {
            keys.KeyPress,
            keys.KeyRelease,
        }:
            event, etype, key, mods, txt = kbmsg.to_tuple()
            print(f'key: {kbmsg.key}, mods: {kbmsg.mods}, txt: {kbmsg.txt}')

            # default controls set
            ctl = False
            if kbmsg.mods == Qt.ControlModifier:
                ctl = True

            if ctl and key in {  # cancel and refocus

                Qt.Key_C,
                Qt.Key_Space,  # i feel like this is the "native" one
                Qt.Key_Alt,
            }:

                widget.clearFocus()
                # normally the godwidget
                focus_next.focus()
                continue

            # process field input
            if key in (Qt.Key_Enter, Qt.Key_Return):

                key = widget._key
                value = widget.text()
                on_value_change(key, value)


def mk_form(

    parent: QWidget,
    fields_schema: dict,
    font_size: int | None = None,

) -> FieldsForm:

    # TODO: generate components from model
    # instead of schema dict (aka use an ORM)
    form = FieldsForm(parent=parent)
    form._font_size = font_size or _font_small.px_size

    # generate sub-components from schema dict
    for key, conf in fields_schema.items():
        wtype = conf['type']
        label = str(conf.get('label', key))
        kwargs = conf.get('kwargs', {})

        # plain (line) edit field
        if wtype == 'edit':
            w = form.add_edit_field(
                key,
                label,
                conf['default_value'],
                **kwargs,
            )

        # drop-down selection
        elif wtype == 'select':
            values = list(conf['default_value'])
            w = form.add_select_field(
                key,
                label,
                values,
                **kwargs,
            )

        w._key = key

    return form


@asynccontextmanager
async def open_form_input_handling(

    form: FieldsForm,
    focus_next: QWidget,
    on_value_change: Callable[[str, Any], Awaitable[bool]],

) -> FieldsForm:

    async with open_handlers(

        list(form.fields.values()),
        event_types={
            QEvent.KeyPress,
        },

        async_handler=partial(
            handle_field_input,
            form=form,
            focus_next=focus_next,
            on_value_change=on_value_change,
        ),

        # block key repeats?
        filter_auto_repeats=True,
    ):
        yield form


class FillStatusBar(QProgressBar):
    '''
    A status bar for fills up to a position limit.

    '''
    border_px: int = 2
    slot_margin_px: int = 1

    def __init__(
        self,
        approx_height_px: float,
        width_px: float,
        font_size: int,
        parent=None

    ) -> None:
        super().__init__(parent=parent)

        self.approx_h = int(round(approx_height_px))
        self.setMinimumHeight(self.approx_h)
        self.setMaximumHeight(self.approx_h)

        self.font_size = font_size

        self.setFormat('')  # label format
        self.setMinimumWidth(int(width_px))
        self.setMaximumWidth(int(width_px))

    def set_slots(
        self,
        slots: int,
        value: float,

    ) -> None:

        self.setOrientation(Qt.Vertical)
        h = self.height()

        # TODO: compute "used height" thus far and mostly fill the rest
        tot_slot_h, r = divmod(h, slots)

        self.setStyleSheet(
            f"""
            QProgressBar {{

                text-align: center;

                font-size : {self.font_size - 2}px;

                background-color: {hcolor('papas_special')};
                color : {hcolor('papas_special')};

                border: {self.border_px}px solid {hcolor('default_light')};
                border-radius: 2px;
            }}
            QProgressBar::chunk {{

                background-color: {hcolor('default_spotlight')};
                color: {hcolor('bracket')};

                border-radius: 2px;
            }}
            """
        )

        # to set a discrete "block" per slot...
        # XXX: couldn't get the discrete math to work here such
        # that it was always correctly showing a discretized value
        # up to the limit; not sure if it's the ``.setRange()``
        # / ``.setValue()`` api or not but i was able to get something
        # close screwing with the divmod above above but after so large
        # a value it would always be less chunks then the correct
        # value..
        # margin: {self.slot_margin_px}px;
        # height: {slot_height_px}px;


        # margin-bottom: {slot_margin_px*2}px;
        # margin-top: {slot_margin_px*2}px;
        # color: #19232D;
        # width: 10px;

        self.setRange(0, int(slots))
        self.setValue(value)

    def update_fonts(self, font_size: int) -> None:
        '''Update font size after zoom change.'''
        from ._style import _font_small

        self.font_size = font_size
        # regenerate stylesheet with new font size
        self.setStyleSheet(
            f"""
            QProgressBar {{

                text-align: center;

                font-size : {self.font_size - 2}px;

                background-color: {hcolor('papas_special')};
                color : {hcolor('papas_special')};

                border: {self.border_px}px solid {hcolor('default_light')};
                border-radius: 2px;
            }}
            QProgressBar::chunk {{

                background-color: {hcolor('default_spotlight')};
                color: {hcolor('bracket')};

                border-radius: 2px;
            }}
            """
        )
        self.setFont(_font_small.font)


def mk_fill_status_bar(

    parent_pane: QWidget,
    form: FieldsForm,
    pane_vbox: QVBoxLayout,
    label_font_size: int | None = None,

) -> (
    # TODO: turn this into a composite?
    QHBoxLayout,
    QProgressBar,
    QLabel,
    QLabel,
    QLabel,
):
    # indent = 18
    # bracket_val = 0.375 * 0.666 * w
    # indent = bracket_val / (1 + 5/8)

    # TODO: calc this height from the ``ChartnPane``
    chart_h = round(parent_pane.height() * 5/8)
    bar_h = chart_h * 0.375*0.9

    # TODO: once things are sized to screen
    bar_label_font_size = label_font_size or _font.px_size - 2

    label_font = DpiAwareFont()
    label_font._set_qfont_px_size(bar_label_font_size)
    br = label_font.boundingRect(f'{3.32:.1f}% port')
    _, h = br.width(), br.height()
    # text_w = 8/3 * w

    # PnL on lhs
    bar_labels_lhs = QVBoxLayout()
    left_label = form.add_field_label(
        '{pnl:>+.2%} pnl',
        font_size=bar_label_font_size,
        font_color='gunmetal',
    )
    # size according to dpi scaled fonted contents to avoid
    # resizes on magnitude changes (eg. 9 -> 10 %)
    min_w = int(_font.boundingRect('1000.0M% pnl').width())
    left_label.setMinimumWidth(min_w)
    left_label.resize(
        min_w,
        left_label.size().height(),
    )

    bar_labels_lhs.addSpacing(int(5/8 * bar_h))

    bar_labels_lhs.addWidget(
        left_label,
        # XXX: doesn't seem to actually push up against
        # the status bar?
        alignment=Qt.AlignRight | Qt.AlignTop,
    )

    # this hbox is added as a layout by the paner maker/caller
    hbox = QHBoxLayout()

    hbox.addLayout(bar_labels_lhs)
    # hbox.addSpacing(indent)  # push to right a bit

    # config
    # hbox.setSpacing(indent * 0.375)
    hbox.setSpacing(0)
    hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
    hbox.setContentsMargins(0, 0, 0, 0)

    # TODO: use percentage str formatter:
    # https://docs.python.org/3/library/string.html#grammar-token-precision

    top_label = form.add_field_label(
        '{limit}',
        font_size=bar_label_font_size,
        font_color='gunmetal',
    )

    bottom_label = form.add_field_label(
        # 'x: {step_size}',
       '{unit_prefix}: {step_size}',
        font_size=bar_label_font_size,
        font_color='gunmetal',
    )

    bar = FillStatusBar(
        approx_height_px=bar_h,
        width_px=h * (1 + 1/6),
        font_size=form._font_size,
        parent=form
    )

    hbox.addWidget(bar, alignment=Qt.AlignLeft | Qt.AlignTop)

    bar_labels_rhs_vbox = QVBoxLayout()
    bar_labels_rhs_vbox.addWidget(
        top_label,
        alignment=Qt.AlignLeft | Qt.AlignTop
    )
    bar_labels_rhs_vbox.addWidget(
        bottom_label,
        alignment=Qt.AlignLeft | Qt.AlignBottom
    )

    hbox.addLayout(bar_labels_rhs_vbox)

    # compute "chunk" sizes for fill-status-bar based on some static height
    slots = 4
    bar.set_slots(slots, value=0)

    return hbox, bar, left_label, top_label, bottom_label


def mk_order_pane_layout(

    parent: QWidget,
    # accounts: dict[str, str | None],

) -> FieldsForm:

    font_size: int = _font.px_size - 2

    # TODO: maybe just allocate the whole fields form here
    # and expect an async ctx entry?
    form = mk_form(
        parent=parent,
        fields_schema={
            'account': {
                'label': '**accnt**:',
                'type': 'select',
                'default_value': ['paper'],
            },
            'size_unit': {
                'label': '**alloc**:',
                'type': 'select',
                'default_value': [
                    '$ size',
                    '# units',
                    # '% of port',
                ],
            },
            # 'disti_weight': {
            #     'label': '**weighting**:',
            #     'type': 'select',
            #     'default_value': ['uniform'],
            # },
            'limit': {
                'label': '**limit**:',
                'type': 'edit',
                'default_value': 5000,
            },
            'slots': {
                'label': '**slots**:',
                'type': 'edit',
                'default_value': 4,
            },
        },
    )

    # top level pane layout
    # XXX: see ``FieldsForm.__init__()`` for why we can't do basic
    # config of the vbox here
    vbox = form.vbox

    # _, h = form.width(), form.height()
    # print(f'w, h: {w, h}')

    hbox, fill_bar, left_label, top_label, bottom_label = mk_fill_status_bar(
        parent,
        form,
        pane_vbox=vbox,
        label_font_size=font_size,
    )
    # TODO: would be nice to have some better way of reffing these over
    # monkey patching...
    form.fill_bar = fill_bar
    form.left_label = left_label
    form.bottom_label = bottom_label
    form.top_label = top_label

    # add pp fill bar + spacing
    vbox.addLayout(hbox, stretch=3)

    # TODO: handle resize events and appropriately scale this
    # to the sidepane height?
    # https://doc.qt.io/qt-5/layout.html#adding-widgets-to-a-layout
    # vbox.setSpacing(_font.px_size * 1.375)

    form.show()
    return form
