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

'''
Root-most (what they call a "central widget") of every Qt-UI-app's
window.

'''
from __future__ import annotations
from typing import (
    Iterator,
    TYPE_CHECKING,
)

import trio

from piker.ui.qt import (
    QtCore,
    Qt,
    QWidget,
    QHBoxLayout,
    QVBoxLayout,
)
from ..log import get_logger

if TYPE_CHECKING:
    from ._gaps import GapOverlayMngr
    from ._search import SearchWidget
    from ._chart import (
        LinkedSplits,
    )
    from ._cursor import (
        Cursor,
    )


log = get_logger(__name__)

_godw: GodWidget|None = None

def get_godw() -> GodWidget:
    '''
    Get the top level "god widget", the root/central-most Qt
    widget-object set as `QMainWindow.setCentralWidget(_godw)`.

    See `piker.ui._exec` for the runtime init details and all the
    machinery for running `trio` on the Qt event loop in guest mode.

    '''
    if _godw is None:
        raise RuntimeError(
            'No god-widget initialized ??\n'
            'Have you called `run_qtractor()` yet?\n'
        )
    return _godw


class GodWidget(QWidget):
    '''
    "Our lord and savior, the holy child of window-shua, there is no
    widget above thee." - 6|6

    The highest level composed widget which contains layouts for
    organizing charts as well as other sub-widgets used to control or
    modify them.

    '''
    search: SearchWidget
    mode_name: str = 'god'

    def __init__(

        self,
        parent=None,

    ) -> None:

        super().__init__(parent)

        self.search: SearchWidget|None = None

        self.hbox = QHBoxLayout(self)
        self.hbox.setContentsMargins(0, 0, 0, 0)
        self.hbox.setSpacing(6)
        self.hbox.setAlignment(Qt.AlignTop)

        self.vbox = QVBoxLayout()
        self.vbox.setContentsMargins(0, 0, 0, 0)
        self.vbox.setSpacing(2)
        self.vbox.setAlignment(Qt.AlignTop)

        self.hbox.addLayout(self.vbox)

        self._chart_cache: dict[
            str,
            tuple[LinkedSplits, LinkedSplits],
        ] = {}

        self.hist_linked: LinkedSplits|None = None
        self.rt_linked: LinkedSplits|None = None
        self._active_cursor: Cursor|None = None

        # assigned in the startup func `_async_main()`
        self._root_n: trio.Nursery = None
        self.gapman: GapOverlayMngr|None = None

        self._widgets: dict[str, QWidget] = {}
        self._resizing: bool = False

        # TODO: do we need this, when would god get resized
        # and the window does not? Never right?!
        # self.reg_for_resize(self)

    # TODO: strat loader/saver that we don't need yet.
    # def init_strategy_ui(self):
    #     self.toolbar_layout = QHBoxLayout()
    #     self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
    #     self.vbox.addLayout(self.toolbar_layout)
    #     self.strategy_box = StrategyBoxWidget(self)
    #     self.toolbar_layout.addWidget(self.strategy_box)

    @property
    def linkedsplits(self) -> LinkedSplits:
        return self.rt_linked

    def set_chart_symbols(
        self,
        group_key: tuple[str],  # of form <fqme>.<providername>
        all_linked: tuple[LinkedSplits, LinkedSplits],  # type: ignore

    ) -> None:
        # re-sort org cache symbol list in LIFO order
        cache = self._chart_cache
        cache.pop(group_key, None)
        cache[group_key] = all_linked

    def get_chart_symbols(
        self,
        symbol_key: str,

    ) -> tuple[LinkedSplits, LinkedSplits]:  # type: ignore
        return self._chart_cache.get(symbol_key)

    async def load_symbols(
        self,
        fqmes: list[str],
        loglevel: str,
        reset: bool = False,

    ) -> trio.Event:
        '''
        Load a new contract into the charting app.

        Expects a ``numpy`` structured array containing all the ohlcv fields.

        '''
        # NOTE: for now we use the first symbol in the set as the "key"
        # for the overlay of feeds on the chart.
        group_key: tuple[str] = tuple(fqmes)

        all_linked = self.get_chart_symbols(group_key)
        order_mode_started = trio.Event()

        if not self.vbox.isEmpty():

            # XXX: seems to make switching slower?
            # qframe = self.hist_linked.chart.qframe
            # if qframe.sidepane is self.search:
            #     qframe.hbox.removeWidget(self.search)

            for linked in [self.rt_linked, self.hist_linked]:
                # XXX: this is CRITICAL especially with pixel buffer caching
                linked.hide()
                linked.unfocus()

                # XXX: pretty sure we don't need this
                # remove any existing plots?
                # XXX: ahh we might want to support cache unloading..
                # self.vbox.removeWidget(linked)

        # switching to a new viewable chart
        if all_linked is None or reset:
            from ._display import display_symbol_data

            # we must load a fresh linked charts set
            from ._chart import LinkedSplits
            self.rt_linked = rt_charts = LinkedSplits(self)
            self.hist_linked = hist_charts = LinkedSplits(self)

            # spawn new task to start up and update new sub-chart instances
            self._root_n.start_soon(
                display_symbol_data,
                self,
                fqmes,
                loglevel,
                order_mode_started,
            )

            # self.vbox.addWidget(hist_charts)
            self.vbox.addWidget(rt_charts)
            self.set_chart_symbols(
                group_key,
                (hist_charts, rt_charts),
            )

            for linked in [hist_charts, rt_charts]:
                linked.show()
                linked.focus()

            await trio.sleep(0)

        else:
            # symbol is already loaded and ems ready
            order_mode_started.set()

            self.hist_linked, self.rt_linked = all_linked

            for linked in all_linked:
                # TODO:
                # - we'll probably want per-instrument/provider state here?
                #   change the order config form over to the new chart

                # chart is already in memory so just focus it
                linked.show()
                linked.focus()
                linked.graphics_cycle()
                await trio.sleep(0)

                # resume feeds *after* rendering chart view asap
                chart = linked.chart
                if chart:
                    chart.resume_all_feeds()

            # TODO: we need a check to see if the chart
            # last had the xlast in view, if so then shift so it's
            # still in view, if the user was viewing history then
            # do nothing yah?
            self.rt_linked.chart.main_viz.default_view(
                do_min_bars=True,
            )

        # if a history chart instance is already up then
        # set the search widget as its sidepane.
        hist_chart = self.hist_linked.chart
        if hist_chart:
            hist_chart.qframe.set_sidepane(self.search)

            # NOTE: this is really stupid/hard to follow.
            # we have to reposition the active position nav
            # **AFTER** applying the search bar as a sidepane
            # to the newly switched to symbol.
            await trio.sleep(0)

            # TODO: probably stick this in some kinda `LooknFeel` API?
            for tracker in self.rt_linked.mode.trackers.values():
                pp_nav = tracker.nav
                if tracker.live_pp.cumsize:
                    pp_nav.show()
                    pp_nav.hide_info()
                else:
                    pp_nav.hide()

        # set window titlebar info
        symbol = self.rt_linked.mkt
        if symbol is not None:
            self.window.setWindowTitle(
                f'{symbol.fqme} '
                f'tick:{symbol.size_tick}'
            )

        return order_mode_started

    def focus(self) -> None:
        '''
        Focus the top level widget which in turn focusses the chart
        ala "view mode".

        '''
        # go back to view-mode focus (aka chart focus)
        self.clearFocus()
        chart = self.rt_linked.chart
        if chart:
            chart.setFocus()

    def reg_for_resize(
        self,
        widget: QWidget,
    ) -> None:
        getattr(widget, 'on_resize')
        self._widgets[widget.mode_name] = widget

    def on_win_resize(
        self,
        event: QtCore.QEvent,
    ) -> None:
        '''
        Top level god widget handler from window (the real yaweh) resize
        events such that any registered widgets which wish to be
        notified are invoked using our pythonic `.on_resize()` method
        api.

        Where we do UX magic to make things not suck B)

        '''
        if self._resizing:
            return

        self._resizing = True

        log.debug(
            f'God widget resize\n'
            f'{event}\n'
        )
        for name, widget in self._widgets.items():
            widget.on_resize()

        self._resizing = False

    # on_resize = on_win_resize

    def get_cursor(self) -> Cursor:
        return self._active_cursor

    def iter_linked(self) -> Iterator[LinkedSplits]:
        for linked in [self.hist_linked, self.rt_linked]:
            yield linked

    def resize_all(self) -> None:
        '''
        Dynamic resize sequence: adjusts all sub-widgets/charts to
        sensible default ratios of what space is detected as available
        on the display / window.

        '''
        rt_linked = self.rt_linked
        rt_linked.set_split_sizes()
        self.rt_linked.resize_sidepanes()
        self.hist_linked.resize_sidepanes(from_linked=rt_linked)
        self.search.on_resize()
