#!/usr/bin/env python3

import curses
import threading

from .parsers import PARSE_MAP, parse_null, make_parse_letter
from ..globals import CURSES_LOCK

class _Node:
    def iter_possible(self):
        if None in self.children:
            yield self
        if [i for i in self.children if i is not None]:
            for c in self.children:
                if c is None:
                    continue
                yield from c.iter_possible()

    def __repr__(self):
        return self.path

    def __init__(self, parent, parse, var: bool = False, f = None, path = ""):
        self.f = f
        self.parent = parent
        self.parse = parse
        self.var = var

        self.children = []
        self.name = path
        if parent is not None:
            parent.children.append(self)
            self.path = parent.path + (' ' if parent.path else "") + path
        else:
            self.path = path

#options_help = {
#    "test": "This is a test command",
#    "set": "Set a channel range to the given value",
#    "reset": "Reset the given channels, or all",
#}
#
#root = parse_options([(o.split(' '), f) for o, f in options_list])

#help_map = {}

options_list = (
    ("test potato $value one two", lambda *args: ('1')),
    ("test potato two", lambda *args: ('2')),
    ("test walnut alpha", lambda *args: ('3')),
    ("test walnut alpha beta", lambda *args: ('4')),
    ("set $channel_range to $value", lambda cr, v: ("Channel range %s at %s" % (repr(cr), repr(v)))),
    ("reset $channel_range", lambda cr: ("reset "+repr(cr))),
    ("reset", lambda: ("reset all")),
)

class Input:
    @staticmethod
    def parse_context(ctx, help_map=None, help_f=None, parent=None):
        if parent is None:
            parent = _Node(None, parse_null)

        start = {}
        for i, f in ctx:
            if not i:
                if None in parent.children:
                    raise ValueError("Duplicate base command")
                parent.children.append(None)
                parent.f = f
                continue

            if isinstance(i, str):
                i = i.split(' ')
            if i[0] not in start:
                if parent.parent is None and i[0][0] == 'h':
                    raise ValueError("No base command may start with h")
                start[i[0]] = []
            start[i[0]].append((i, f))

        for s, ols in start.items():
            n = _Node(parent, PARSE_MAP[s] if s[0] == '$' else make_parse_letter(s[0], s),
                      var=(s[0] == '$'), path=s)
            ols = [(ol[1:], f) for ol, f in ols]
            for l, f in ols:
                if not l:
                    n.f = f
                    n.children.append(None)
                    break
            ols = [i for i in ols if i[0]]
            if ols:
                Input.parse_context(ols, parent=n)

        if parent.parent is None and help_f is not None:
            if help_map is None:
                help_map = {}

            root_commands = tuple((i.name for i in parent.children if i is not None))

            help_node = _Node(parent, make_parse_letter('h', "help"), path="help")
            help_node.children.append(None)

            if None in help_map:
                help_node.f = lambda: help_f(None, root_commands, help_map[None])
            else:
                help_node.f = lambda: help_f(None, root_commands, "No help available!")

            for s in parent.children:
                if s is None or s.name == "help":
                    continue

                options = tuple(s.iter_possible())
                hn = _Node(help_node, make_parse_letter(s.name[0], s.name), path=s.name)
                hn.children.append(None)

                if s.name in help_map:
                    hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), help_map[name])
                else:
                    hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), "No help available!")

        return parent

    def set_dim(self, height, width):
        if height < 4 or width < 10:
            raise ValueError("Size too small")

        with self._lock:
            if (height, width) != (self._height, self._width):
                self.win.erase()
                self.win.noutrefresh()

                self.win.resize(height, width)
                self.win.redrawwin()

            self._height = height
            self._width = width
            self.win.border()
            self._redraw()
            self.win.noutrefresh()
        
    def set_pos(self, y, x):
        with self._lock:
            if (y, x) != (self._y, self._x):
                self.win.mvwin(y, x)
                self._y = y
                self._x = x
                self.win.border()
                self.win.noutrefresh()

    def _redraw(self):
        with self._lock:
            if len(self._l2) > self._width-2:
                l2 = self._l2[:self._width-3] + '…'
            else:
                l2 = self._l2

            if len(self._l1) > self._width-5:
                l1 = ">> …" + self._l1[::-1][:self._width-6][::-1]
            else:
                l1 = ">> " + self._l1
            self.win.addstr(1, 1, ' '*(self._width-2))
            self.win.addstr(2, 1, ' '*(self._width-2))
            self.win.addstr(1, 1, l1)
            self.win.addstr(2, 1, l2, (curses.A_ITALIC if hasattr(curses, "A_ITALIC") else 0))
            self.win.move(1, len(l1)+1)
            self.win.refresh()

    @property
    def context(self):
        return self._context

    @context.setter
    def context(self, ctx):
        with self._ctx_lock:
            self._context = ctx
            self._ctx_changed = True
            with CURSES_LOCK:
                self._l1 = ""
                self._l2 = ""
                self._redraw()

    def main(self, resize=None):
        """Run the input loop.

        If `resize` is given, it will be called should the terminal be resized. 
        """
        with self._ctx_lock:
            current = self._context
        ## In the format:
        ## (input, parsed, display, is variable?)
        path = [["", True, "", False]]
        size_ok = True
        while True:
            with self._ctx_lock:
                self._l1 = "".join((i[2] for i in path if i[2]))
            with CURSES_LOCK:
                if size_ok:
                    self._redraw()
            l = self.win.getch()
            with self._ctx_lock:
                if l == curses.KEY_RESIZE:
                    if resize is not None:
                        size_ok = resize()
                    continue
                if not size_ok:
                    continue
                if self._context is None:
                    continue
                elif self._ctx_changed:
                    path = path[:1]
                    self._ctx_changed = False
                self._l2 = ""
                if l in (127, curses.KEY_BACKSPACE): ## Backspace
                    if current == self._context:
                        continue
                    e = path[-1]
                    e[0] = e[0][:-1]
                    if not e[0]:
                        path.pop(-1)
                        current = current.parent
                        continue
                    e[1], _, e[2] = current.parse(e[0])
                elif l in (10, curses.KEY_ENTER) and None in current.children: ## Enter 
                    ret = current.f(*(i[1] for i in path if i[3]))
                    self._l2 = "OK" if ret is None else str(ret)
                    path = path[:1]
                    current = self._context
                else:
                    e = path[-1]
                    s = e[0] + chr(l)
                    parsed, s, display = current.parse(s)
                    if parsed is None and s: ## Invalid input
                        self._l2 = "Expected \"%s\"" % current.name
                        continue
                    e[1], e[2] = parsed, display
                    if not s: ## We're still working on this one 
                        e[0] += chr(l)
                        continue
                    ## We're done with this one, the only remaining option is to move on
                    for n in current.children:
                        if n is None:
                            continue
                        parsed, cs, display = n.parse(s)
                        if not cs:
                            ## Found it
                            if current.parent is not None:
                                e[2] = e[2] + ' '
                            current = n
                            e = [s, parsed, display, n.var]
                            path.append(e)
                            break
                    else:
                        self._l2 = "Available: %s" % ", ".join((("ENTER" if n is None else '"'+n.name+'"') for n in current.children))

    def __init__(self, y, x, height, width):
        with CURSES_LOCK:
            self.win = curses.newwin(height, width, y, x)
            self.win.keypad(True)
        self._lock =  threading.RLock()
        self._height = height
        self._width = width
        self._y = -1
        self._x = -1
        self._l1 = ""
        self._l2 = ""
        self.set_pos(y, x)
        self.set_dim(height, width)

        self._ctx_lock = threading.RLock()
        self._context = None
        self._ctx_changed = False

        self._redraw()

def main2(stdscr):
    w = Input(0, 0, 4, 100)
    w.context = Input.parse_context(options_list)
    w.main()

if __name__ == "__main__":
    curses.wrapper(main2)