#
# Copyright (c) Daniel Sheffield 2021 - 2023
#
# All rights reserved
#
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
from decimal import Decimal
from typing import Any, Callable, Iterable, Union
from itertools import chain, product
from additional_urwid_widgets import IndicativeListBox

from urwid import (
    AttrMap,
    AttrWrap,
    BarGraph,
    Button,
    connect_signal,
    CheckBox,
    Columns,
    Edit,
    GraphVScale,
    Pile,
    SimpleFocusListWalker,
    Text,
    WidgetWrap,
)
from urwid.numedit import (
    FloatEdit,
)
from urwid.wimp import PopUpLauncher

class AutoCompleteEdit(Edit):
    signals = [ *Edit.signals, 'apply', 'open' ]

    def __init__(self, name: str, *args: Iterable, **kwargs):
        if isinstance(name, tuple):
            pallete, title = name
            self.name = title
            title = title.title()
            passthrough = (pallete, u'  ' if title.lower() == 'unit' else u'')
        else:
            self.name = name
            title = name.title()
            passthrough = u'  ' if name.lower() == 'unit' else u''

        super().__init__(passthrough, *args, **kwargs)

    def keypress(self, size, key):
        if key == 'enter':
            self._emit('apply', self.name)
            return None
        if key == 'delete':
            self.set_edit_text('')
            return None

        return super().keypress(size, key)

class AutoCompleteFloatEdit(FloatEdit):
    signals = [ *FloatEdit.signals, 'apply', 'open' ]

    def __init__(self, name: str, *args: Iterable, **kwargs: dict):
        self.last_val = None
        self.op = '='
        self.pallete = None
        if isinstance(name, tuple):
            self.pallete, self.name = name
            title = self.name.title()
            passthrough = (self.pallete, f'{self.op} ')
        else:
            self.name = name
            title = name.title()
            passthrough = title

        super().__init__(passthrough, *args, **kwargs)

    def update_caption(self):
        if self.pallete is not None:
            self.set_caption((self.pallete, f'{self.op} '))
        else:
            self.set_caption(f'{self.op} ')

    def set_op(self, op):
        self.op = op
        self.last_val = self.value()
        self.set_edit_text('')
        self.update_caption()

    def calc(self):
        x = self.last_val
        op = self.op
        if op in ('+', '-',):
            y = self.value() or Decimal(0.0)
            if op == '+':
                z = x + y
            else:
                z = x - y
        elif op in ('*', '/'):
            y = self.value() or Decimal(1.0)
            if op == '*':
                z = x * y
            else:
                z = x / y
        else:
            y = self.value() or Decimal(0.0)
            z = y

        self.op = '='
        self.update_caption()
        self.set_edit_text(f'{z:.2f}')

    def keypress(self, size, key):
        if isinstance(key, tuple):
            return
        ops = ('+', '-', '*', '/',)

        if key in ops:
            if self.op in ops:
                self.calc()
            self.set_op(key)
            return None
        if key == 'enter':
            if self.get_edit_text() == '' or self.value() == Decimal(0.0):
                self._emit('apply', self.name)
                return None
            self.calc()
            return None
        if key == '=':
            self.calc()
            return None
        if key == 'delete':
            self.set_edit_text('')
            self.op = '='
            self.update_caption()
            return None

        return super().keypress(size, key)

class NoTabCheckBox(CheckBox):
    
    def keypress(self, size, key):
        if not isinstance(key, tuple) and key == 'tab':
            return None
        return super().keypress(size, key)

class FocusWidget(WidgetWrap):
    ignore_focus = True
    def __init__(self, widget, focus_order, focus_widgets):
        super().__init__(widget)
        self._focus_order = focus_order
        self._focus_widgets = focus_widgets
        p = next(p for (k,w), p in self.iter_focus_paths() if k == focus_order[0])
        self.container.set_focus_path(p)

    @property
    def container(self):
        return self._w.original_widget.original_widget

    def find_widget(self, widget=None):
        found = next((x for x in self._focus_widgets() if x[1] is widget), None)
        if found is not None:
            return found
        _w1 = getattr(widget, 'original_widget', None)
        _w2 = getattr(widget, 'contents', None)
        if _w2 is not None:
            return None
        if _w1 is not None:
            return self.find_widget(widget=_w1)

    def iter_containers(self, path=None, widget=None):
        widget = widget or self.container
        path = path or []
        _w1 = getattr(widget, 'original_widget', None)
        _w2 = getattr(widget, 'contents', None)
        _chain = []
        if _w2 is not None:
            yield widget, path
            for idx, c in enumerate(_w2):
                _chain.append(self.iter_containers(path=[*path, idx], widget=c[0]))

        if _w1 is not None:
            _chain.append(self.iter_containers(path=[*path], widget=_w1))
        yield from chain(*_chain)

    def focus_on(self, w):
        _, p = next(filter(lambda x: x[0][1] is w, self.iter_focus_paths()))
        path = self.container.get_focus_path()
        while path != p:
            self.advance_focus()
            path = self.container.get_focus_path()

    def iter_focus_paths(self):
        for c, path in self.iter_containers():
            for idx, w in enumerate(c.contents):
                r = self.find_widget(widget=w[0])
                if r is not None:
                    yield r, [*path, idx]

    def advance_focus(self, reverse=False):
        _c = self.container.get_focus_path()
        _p = [ next((x[1] for x in self.iter_focus_paths() if x[0][1] is w)) for k,(n,w) in product(
            self._focus_order,
            self._focus_widgets()
        ) if k == n]

        _prev, _, _next = next(filter(
            lambda x: _c == list(x[1]),
            zip([_p[-1], *_p[:-1]], _p, [*_p[1:], _p[0]])
        ))
        if reverse:
            self.container.set_focus_path(_prev)
        else:
            self.container.set_focus_path(_next)


class AutoCompletePopUp(PopUpLauncher):
    _default_pop_up_parameters = {
        'left': 0,
        'top': 1,
        'overlay_width': 30,
        'overlay_height': 15,
    }
    def __init__(self,
        widget: Union[AutoCompleteEdit, AutoCompleteFloatEdit],
        apply_choice_cb: Callable[[str, str], None],
        close_cb: Callable[[], Any]
    ):
        super().__init__(widget)
        self._pop_up_parameters = dict(**self._default_pop_up_parameters)
        self.apply_choice_cb = apply_choice_cb
        self.close_cb = close_cb
        connect_signal(self._original_widget, 'open', lambda _, options: self._open_pop_up(options))

    def _open_pop_up(self, options):
        self.options = options
        max_height = self._default_pop_up_parameters['overlay_height']
        height = min(
            len(options),
            self._default_pop_up_parameters['overlay_height'],
        )
        if 0 < len(options) - height < max_height//3:
            height = 4 * max_height // 6

        self._pop_up_parameters = dict(**self._default_pop_up_parameters)
        self._pop_up_parameters.update({
            'overlay_width': max(
                self._original_widget._cache_maxcol,
                self._default_pop_up_parameters['overlay_width'],
            ),
            # account for header and footer rows
            'overlay_height': height+2,
        })
        self.open_pop_up()

    def _close_pop_up(self):
        self.close_pop_up()
        self.close_cb()

    def create_pop_up(self):
        pop_up = SuggestionPopup(
            self._original_widget.name, self.options,
            self.apply_choice_cb,
        )
        connect_signal(pop_up, 'close', lambda _: self._close_pop_up())
        return pop_up

    def get_pop_up_parameters(self):
        return self._pop_up_parameters

class SuggestionPopup(WidgetWrap):

    signals = ['close']

    def __init__(self,
        name: str,
        options: Iterable,
        apply_cb: Callable[[str, str], None]
    ):
        body = []
        for c in options:
            button = Button(c)
            connect_signal(button, 'click', lambda w: apply_cb(name, w.get_label()))
            connect_signal(button, 'click', lambda _: self._emit("close"))
            body.append(AttrMap(button, None, focus_map='popup_focus'))
        walker = SimpleFocusListWalker(body, wrap_around=False)
        super().__init__(AttrWrap(IndicativeListBox(walker,
            topBar_endExposed_prop=("───", None, None),
            topBar_endCovered_prop=("▲ {} more ▲", None, None),
            bottomBar_endCovered_prop=("▼ {} more ▼", None, None),
            bottomBar_endExposed_prop=(f"─── {len(options)} ───", None, None),
        ), 'popup'))


    def keypress(self, size, key):
        if key == 'esc':
            self._emit("close")
            return None
        if key == 'tab':
            return None

        return super().keypress(size, key)

class FlowBarGraph(BarGraph):
    ignore_focus = True
    def __init__(self, height, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.height = height

    def rows(self, size, focus=False):
        return self.height

    def render(self, size, focus=False):
        _size = (size[0], self.rows(size))
        return super().render(_size,focus=focus)

class FlowGraphVScale(GraphVScale):
    ignore_focus = True
    def __init__(self, graph, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.graph = graph

    def rows(self, size, focus=False):
        return self.graph.rows(size)

    def render(self, size, focus=False):
        _size = (size[0], self.rows(size))
        return super().render(_size,focus=focus)

class FlowBarGraphWithVScale(Columns):
    ignore_focus = True

    @property
    def total_width(self):
        return self._total_width

    @property
    def canvas_width(self):
        return self._canvas_width

    @property
    def height(self):
        return self.graph.height

    @height.setter
    def height(self, value):
        self.graph.height = value

    def set_bar_width(self, width):
        self.graph.set_bar_width(width)

    def set_caption(self, text):
        self.graph_caption.set_text(text)

    def set_data(
            self, data: Iterable[Iterable], _max: float,
            vscale: Iterable[float] = None,
            caption: str = None
        ):
        labels = vscale or []
        caption = caption or u''
        top = self.height
        scale = _max*(top+1)/top
        self.graph.set_data(data, scale)
        self.graph_vscale._invalidate()
        self.graph_vscale.set_scale([
            ((x*top)//scale, f'{x:>5.2f} ') for x in labels
        ], top)
        self.graph_caption.set_text(caption)

    def __init__(self, cols, rows, attr, *args, hatt=None, **kwargs):
        self._total_width = cols
        assert cols > 6
        self._vscale_width = 6
        self._canvas_width = cols - 6
        self.graph = FlowBarGraph(rows, attr, hatt=hatt)
        self.graph_vscale = FlowGraphVScale(self.graph, [], 0)
        self.graph_caption = Text(u'')
        super().__init__([
            (self._vscale_width, self.graph_vscale),
            (self._canvas_width, Pile([
                self.graph,
                self.graph_caption
            ]))
        ], *args, **kwargs)

