from random import sample
import wx

BTN = (('Down', 50), ('0', 20), ('Up', 50))
SCR_HDR = (
    ('Color', 80), ('No / Grn', 60), ('lv1', 80), ('lv2', 30), ('lv3', 45),
    ('lv4', 90))
LV_COLS = 2, 3, 4, 5
NOR = 200

class ScrollPnl(wx.Panel):

    def __init__(self, parent):
        super().__init__(parent)
        vbox = wx.BoxSizer(wx.VERTICAL)

        hbox = wx.BoxSizer(wx.HORIZONTAL)                   # buttons
        self.btn_pos = {}                                                     # ctrl: pos
        for idx, entry in enumerate(BTN):
            btn = wx.Button(self, -1, entry[0], size=(entry[1], -1))
            btn.Bind(wx.EVT_BUTTON, self.evt_button)
            btn.Bind(wx.EVT_MOUSEWHEEL, self.evt_scrollwin_step)
            hbox.Add(btn)
            self.btn_pos[btn.GetId()] = idx

        self.key_disp = wx.StaticText(self)                     # key display
        self.key_disp.SetBackgroundColour(
            wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX))
        self.key_disp.SetToolTip('Key of cursor position')
        hbox.Add(self.key_disp, 0, wx.ALIGN_CENTRE|wx.LEFT, 10)

        self.scrwin = wx.ScrolledWindow(self)                 # scrollable
        self.scrwin.SetScrollRate(1, 60)
        self.hdr = ScrWinHdr(self, self.scrwin)                     # header
        self.insert_rows()
        self.scrbth = wx.ScrolledWindow(self)                     # thumb
        self.scrbth.SetScrollRate(1, -1)
        self.insert_thn()
        self.conv_fact = 0
        self.set_rows(0)

        grp = wx.Window(self, size=(0, 1))                    # thumb grip
        grp.SetBackgroundColour(wx.BLUE)
        grp.Bind(wx.EVT_ENTER_WINDOW, self.grip_btns)

        self.fgs = wx.FlexGridSizer(1)
        self.fgs.AddMany(
            [(hbox, 0, wx.BOTTOM|wx.EXPAND, 5),
            (self.hdr, 0, wx.EXPAND), (self.scrwin, 0, wx.EXPAND),
            (grp, 0, wx.EXPAND), (self.scrbth, 0, wx.EXPAND)])
        self.fgs.AddGrowableCol(0, 0)
        self.fgs.AddGrowableRow(2, 0)
        vbox.Add(self.fgs, 1, wx.EXPAND)
        self.SetSizer(vbox)

        self.scrwin.Bind(wx.EVT_SCROLLWIN, self.evt_scroll_scrwin)
        self.scrwin_width = self.scrwin.GetSize()[0]
        self.Bind(wx.EVT_SIZE, self.evt_size)
        self.scrbth.Bind(wx.EVT_SCROLLWIN, self.evt_scroll_scrbth)
        self.grip_btns(None)

    def evt_size(self, evt, /):
        self.max_scroll_pos = self.scrwin.GetScrollRange(
                                                    wx.VERTICAL)-\
                    self.scrwin.GetScrollThumb(wx.VERTICAL) - 1
        evt.Skip()

    def evt_scroll_scrbth(self, evt, /):
        if evt.GetOrientation() == wx.HORIZONTAL:
            if pos := evt.GetPosition():
                self.evt_show_key(None,
                    int(pos *\
                        NOR / self.scrbth.GetScrollRange(wx.HORIZONTAL)))
                self.scrwin.Scroll(
                    wx.DefaultCoord, int(pos * self.conv_fact))
                evt.Skip()

    def evt_scroll_scrwin(self, evt, /):
        if evt.GetOrientation() == wx.HORIZONTAL:
            if evt.GetEventType() ==\
                    wx.wxEVT_SCROLLWIN_THUMBRELEASE:
                self.scrwin.Scroll(evt.GetPosition(), wx.DefaultCoord)
            elif evt.GetEventType() ==\
                    wx.wxEVT_SCROLLWIN_THUMBTRACK:
                self.hdr.set_dimension(evt.GetPosition())
                evt.Skip()
        else:
            if evt.GetEventType() ==\
                    wx.wxEVT_SCROLLWIN_LINEDOWN:
                evt.Skip()
                self.scroll_down()
                self.evt_show_key(None)
            elif evt.GetEventType() == wx.wxEVT_SCROLLWIN_LINEUP:
                evt.Skip()
                self.scroll_up()
                self.evt_show_key(None)
            elif evt.GetEventType() in (
                    wx.wxEVT_SCROLLWIN_THUMBTRACK,
                    wx.wxEVT_SCROLLWIN_THUMBRELEASE):
                evt.Skip()
                self.evt_show_key(None)
            self.sync_win_thn()

    def scroll_up(self):
        if not self.scrwin.GetScrollPos(wx.VERTICAL):
            self.next_block(True)
        else:
            self.sync_win_thn()
            self.evt_show_key(None)

    def scroll_down(self):
        if self.scrwin.GetScrollPos(wx.VERTICAL) >= self.max_scroll_pos:
            self.next_block(False)
        else:
            self.sync_win_thn()
            self.evt_show_key(None)

    def next_block(self, up, /):
        """it is a two step browsing
                -   first klick scrolls down to end of block
                -   then starts new block at top of block
                (up in reverse order)
        """
        if up:
            if self.scrwin.GetScrollPos(wx.VERTICAL):
                self.scrwin.Scroll(wx.DefaultCoord, 0)
                if self.scrbth.IsShown():
                    self.scrbth.Scroll(0, wx.DefaultCoord)
            # elif not self.first_block:
            else:
                start = self.last_idx - 2 * NOR
                self.set_rows(start, self.max_scroll_pos)
        else:
            if self.scrwin.GetScrollPos(wx.VERTICAL) >= self.max_scroll_pos:
                self.set_rows(self.last_idx)
            else:
                self.scrwin.Scroll(wx.DefaultCoord, self.max_scroll_pos)
                self.sync_win_thn()
        self.evt_show_key(None)

    def sync_win_thn(self, /):
        if self.scrbth.IsShown() and self.conv_fact:
            self.scrbth.Scroll(
                int(self.scrwin.GetScrollPos(wx.VERTICAL) / self.conv_fact),
                wx.DefaultCoord)

    def evt_scrollwin_step(self, evt, /):
        self.evt_scrollwin(evt, True)

    def evt_scrollwin(self, evt, step=False, /):
        # 'step' means stepping (5 lines) and not scrolling
        ok = False
        scroll_pos = self.scrwin.GetScrollPos(wx.VERTICAL)
        wr = evt.GetWheelRotation()
        if wr > 0:
            if not scroll_pos:
                self.next_block(True)
                ok = True
        elif scroll_pos >= self.max_scroll_pos:
            self.next_block(False)
            ok = True
        if not ok:
            if scroll_pos < 4 or scroll_pos > self.max_scroll_pos - 4 or step:
                y = scroll_pos - 5 if wr > 0 else scroll_pos + 5
                self.scrwin.Scroll((wx.DefaultCoord, y))
            else:
                evt.ResumePropagation(1)
                evt.Skip()

    def evt_button(self, evt, /):
        if (pos := self.btn_pos.get(evt.GetId())) is not None:
            if pos == 1:
                self.set_rows(0)
                self.evt_show_key(None, 0)
            elif pos == 2:
                self.next_block(True)
            else:
                self.next_block(False)

    def evt_show_key(self, evt, idx=None):
        if evt:
            txt = self.key[evt.GetEventObject()]
        elif idx:
            txt = self.idx_key[idx]
        else:
            txt = self.idx_key[int(self.scrwin.GetViewStart()[1] *\
                    NOR / self.scrwin.GetScrollRange(wx.VERTICAL))]
        self.key_disp.SetLabel(txt)

    def insert_rows(self):
        self.ctrl_list = {}              # contains    [row]: (ctrl1, ctrl2, ...)]
        vbox = wx.BoxSizer(wx.VERTICAL)
        for row in range(NOR):
            hbox = wx.BoxSizer(wx.HORIZONTAL)
            ctrls = []
            # 1 hdr col
            ctrl = wx.Button(
                self.scrwin, size=(80, 80), style=wx.BORDER_NONE)
            ctrls.append(ctrl)
            hbox.Add(ctrl)
            # 2 hdr col
            vbox1 = wx.BoxSizer(wx.VERTICAL)
            ctrl = wx.Button(
                self.scrwin, size=(50, wx.DefaultSize[1]),
                style=wx.BORDER_NONE)
            ctrls.append(ctrl)
            vbox1.Add(ctrl, 0, wx.LEFT, 5)
            ctrl = wx.Button(
                self.scrwin, size=(50, wx.DefaultSize[1]),
                style=wx.BORDER_NONE)
            ctrl.SetBackgroundColour(self.scrwin.GetBackgroundColour())
            ctrls.append(ctrl)
            vbox1.Add(ctrl, 0, wx.LEFT, 5)
            hbox.Add(vbox1, 0, wx.ALIGN_CENTRE)
            # 3 hdr col
            x = 0
            for idx in LV_COLS:
                x += SCR_HDR[idx][1]
            ctrl = wx.ListView(
                self.scrwin, size=(x, 80),
                style=wx.LC_REPORT | wx.LC_NO_HEADER | wx.BORDER_NONE)
            ctrls.append(ctrl)
            ctrl.Bind(wx.EVT_MOUSEWHEEL, self.evt_scrollwin)
            ctrl.Bind(wx.EVT_ENTER_WINDOW, self.evt_show_key)
            for idx in LV_COLS:
                ctrl.AppendColumn('', width=SCR_HDR[idx][1])
            hbox.Add(ctrl, 0, wx.LEFT, 5)
            vbox.Add(hbox, 0, wx.TOP, 5)
            self.ctrl_list[row] = ctrls
        self.scrwin.SetSizer(vbox)

    def set_rows(self, first, y_pos=0, /):
        self.scrwin.Freeze()
        # self.first_block = not bool(first)
        self.ctrl_bth_idx = {}                                      # Id: idx
        self.key = {}                                                   # lv: key
        self.idx_key = {}                                            # idx: key
        for idx, row in enumerate(range(NOR)):
            col = sample(range(255), 3)
            self.ctrl_list[row][0].SetBackgroundColour(col)
            self.ctrl_list[row][1].SetLabelMarkup(
                ''.join(('<b><i>', str(first), '</i></b>')))
            grn = str(col[1])
            self.ctrl_list[row][2].SetLabel(grn)
            txt = ''.join((' ', str(first), ' / ', grn, ' '))
            self.key[self.ctrl_list[row][3]] = txt
            self.idx_key[idx] = txt
            # set thumb
            self.ctrl_bth[idx][0].SetBackgroundColour(col)
            self.ctrl_bth[idx][0].SetLabel(grn)
            self.ctrl_bth_idx[self.ctrl_bth[idx][1]] = idx
            first += 1
        self.last_idx = first
        self.scrwin.Scroll(wx.DefaultCoord, y_pos)
        self.sync_win_thn()
        self.scrwin.Thaw()

    def insert_thn(self):
        self.ctrl_bth = {}                                      # idx: (btn, Id)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        for idx, _ in enumerate(range(NOR)):
            btn = wx.Button(
                self.scrbth, -1, size=(20, 20), style=wx.BORDER_NONE)
            self.ctrl_bth[idx] = (btn, btn.GetId())
            btn.Bind(wx.EVT_LEFT_DOWN, self.select_thn)
            hbox.Add(btn, 0, wx.RIGHT, 2)
        self.scrbth.SetSizer(hbox)

    def select_thn(self, evt, /):
        self.evt_show_key(None, self.ctrl_bth_idx[evt.GetId()])
        self.scrwin.Scroll(
            wx.DefaultCoord, int(self.scrwin.GetScrollRange(
                wx.VERTICAL) / NOR * self.ctrl_bth_idx[evt.GetId()]))

    def grip_btns(self, _, /):
        self.hdr.size_grip = True
        self.Freeze()
        if self.scrbth.IsShown():
            self.scrbth.Hide()
        else:
            self.scrbth.Show()
            self.sync_win_thn()
        self.fgs.Layout()
        self.conv_fact = self.scrwin.GetScrollRange(wx.VERTICAL)\
                            / self.scrbth.GetScrollRange(wx.HORIZONTAL)
        self.hdr.reset()
        self.Thaw()
        self.hdr.size_grip = False

class ScrWinHdr(wx.Window):

    def __init__(self, parent, scrwin):
        super().__init__(parent)
        self.scrwin = scrwin
        fgs = wx.FlexGridSizer(len(SCR_HDR), 0, 1)
        for entry in SCR_HDR:
            btn = wx.Button(
                self, label=entry[0], size=(entry[1] - 1, -1),
                style=wx.BORDER_NONE)
            btn.SetBackgroundColour('light blue')
            fgs.Add(btn)
        self.SetSizer(fgs)
        self.Bind(wx.EVT_SIZE, self.evt_size)
        self.init = True
        self.size_grip = False
        self.px = 0

    def evt_size(self, _):
        if self.init:
            self.Layout()
            self.init = False
        if not self.size_grip and self.px:
            self.sync_size()
            wx.CallAfter(self.sync_size)

    def sync_size(self):
        self.px = -self.scrwin.GetViewStart()[0]
        self.SetSize(self.px, -1, -1, -1)

    def reset(self):
        if self.px:
            self.SetSize(self.px, -1, -1, -1)

    def set_dimension(self, x):
        self.px = - x
        self.SetSize(self.px, -1, -1, -1)
