import wx

MRK_COL = 'LIGHT BLUE'

def get_result(query, search_list,/):
    result = []
    for entry in search_list:
        if query in entry.lower():
            result.append(entry)
    return result

class CtxTextCtrl(wx.TextCtrl):
    def __init__(self, parent, width=-1, context=None):
        super().__init__(
            parent, size=(width, -1), style=wx.TE_PROCESS_ENTER)
        if context:
            self.context = context
            self.Bind(wx.EVT_TEXT, self.evt_text)
            self.Bind(wx.EVT_CHAR_HOOK, self.evt_char_hook)
            self.Bind(wx.EVT_ENTER_WINDOW, lambda _: self.SetFocus())
            self.list_id = None
            self.query = 0

    def evt_text(self, _,/):
        query = self.GetValue().rstrip().lower()
        if lq := len(query):
            if 'select' not in vars(self):
                self.select = Select(self)
                if 'list' in vars(self):
                    self.select.set_list(len(self.list))
            if (q0 := query[0]) != self.list_id:        # set up new base list
                self.list = get_result(q0, self.context)
                self.select.set_list(len(self.list))
                self.list_id = q0
                self.result = self.list
            else:
                if lq < self.query:                                # decrementing
                    self.result = self.list
            result = get_result(query, self.result)
            self.select.set_result(result)
            self.result = result
            self.query = len(query)
        else:
            if 'select' in vars(self):
                self.select.set_result([])
        self.SetFocus()

    def evt_char_hook(self, evt,/):
        if 'select' in vars(self):
            if (kc := evt.GetKeyCode()) in (315, 317):
                self.select.move_marker(bool(kc == 315))
            elif kc == 370:
                self.text_enter()
                self.destroy_select()
            elif kc == 9:
                self.destroy_select()
            else:
                evt.Skip()
        else:
            evt.Skip()

    def text_enter(self,/):
        if val := self.select.get_selected():
            self.ChangeValue(val)
            self.result = get_result(val.rstrip().lower(), self.result)
            self.query = len(val)
            if self.query:
                self.SetInsertionPoint(self.query)
                self.SetFocus()

    def destroy_select(self,/):
        if 'select' in vars(self):
            self.select.Destroy()
            del self.select

    def on_quit(self, _,/):
        if 'select' in vars(self):
            self.select.Destroy()
        self.Destroy()

class Select(wx.Dialog):
    def __init__(self, parent):
        super().__init__(
            parent, title='select by keys or mouse, TAB will close',
            style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.CAPTION) | wx.STAY_ON_TOP)

        self.parent = parent
        self.marked = -1
        self.res_len = 0
        self.idx_ctrl = {}                                      # idx: ctrl
        self.ctrl_idx = {}                                      # ctrl: idx
        self.Bind(wx.EVT_CLOSE, lambda _: parent.destroy_select())
        self.Bind(wx.EVT_ACTIVATE, lambda _: None)
        vb = wx.BoxSizer(wx.VERTICAL)
        self.scrwin = wx.ScrolledCanvas(self)
        self.scrwin.SetScrollRate(20, 20)
        vb.Add(self.scrwin, 1, wx.EXPAND)
        self.SetSizer(vb)

    def set_list(self, nol,/):
        if nol:
            vbox = self.scrwin.GetSizer()
            init = not bool(vbox)
            if init:
                vbox = wx.BoxSizer(wx.VERTICAL)
            else:
                vbox.SetMinSize(vbox.GetSize())
            cur_len = len(self.idx_ctrl)
            idx = cur_len
            while idx < nol:
                win = wx.Window(self.scrwin)
                win.Bind(wx.EVT_LEFT_DOWN, self.evt_select)
                win.Bind(
                    wx.EVT_ENTER_WINDOW,
                    lambda evt: self.mark_item(evt.GetEventObject()))
                ctrl = wx.StaticText(win)
                ctrl.Bind(
                    wx.EVT_LEFT_DOWN, lambda evt: evt.Skip())
                ctrl.Bind(
                    wx.EVT_ENTER_WINDOW, lambda evt: evt.Skip())
                vbox.Add(win, 0, wx.EXPAND|wx.LEFT, 5)
                self.idx_ctrl[idx] = ctrl
                idx += 1
            idx = cur_len - 1
            while idx >= nol:
                vbox.Detach(idx)
                self.idx_ctrl[idx].GetParent().Destroy()
                del self.idx_ctrl[idx]
                idx -= 1
            if init:
                self.scrwin.SetSizerAndFit(vbox)
            else:
                vbox.Fit(self.scrwin)
                self.SendSizeEvent()
            self.Show()
        else:
            self.marked = -1
            self.Hide()

    def set_result(self, result,/):
        if result:
            self.ctrl_idx.clear()
            idx = 0
            for entry in result:
                ctrl = self.idx_ctrl[idx]
                ctrl.SetLabel(entry)
                self.ctrl_idx[ctrl] = idx
                if not idx:
                    if 'marked' in vars(self):
                        self.reset_bgc()
                    ctrl.GetParent().SetBackgroundColour(MRK_COL)
                    self.marked = idx
                idx += 1
            while idx < self.res_len:
                if (ctrl := self.idx_ctrl.get(idx)) is None:
                    break
                ctrl.SetLabel('')
                idx += 1
            if self.marked > -1 and not result:
                self.reset_bgc()
                self.marked = -1
            self.result = result
            self.res_len = len(result)
            self.scrwin.Refresh()
            self.scrwin.Scroll(0, 0)
            self.Show()
        else:
            self.marked = -1
            self.Hide()

    def mark_item(self, win,/):
        if (idx := self.ctrl_idx.get(win.GetChildren()[0])) is not None:
            if self.marked != idx:
                self.reset_bgc(idx)
                win.SetBackgroundColour(MRK_COL)
                self.scrwin.Refresh()

    def reset_bgc(self, idx=-1,/):
        if self.marked > -1:
            if item := self.idx_ctrl.get(self.marked):
                item.GetParent().SetBackgroundColour(None)
        if idx > -1:
            self.marked = idx

    def move_marker(self, up,/):
        if up:
            if self.marked:
                self.change_marked(up)
        else:
            if self.marked < self.res_len - 1:
                self.change_marked(up)

    def change_marked(self, up,/):
        self.reset_bgc()
        pm = -1 if up else 1
        self.marked += pm
        self.idx_ctrl[
            self.marked].GetParent().SetBackgroundColour(MRK_COL)
        self.scrwin.Refresh()

    def get_selected(self,/):
        if self.marked > -1:
            return self.result[self.marked]

    def evt_select(self, evt,/):
        self.select_item(evt.GetEventObject().GetChildren()[0])

    def select_item(self, ctrl,/):
        val = self.result[self.ctrl_idx.get(ctrl)]
        self.reset_bgc()
        self.scrwin.Refresh()
        self.parent.ChangeValue(val)
        self.parent.SetInsertionPoint(len(val))
        # self.parent.SetFocus()
        self.parent.destroy_select()
