# ===============================================================================
#
# ToDo: Bug - when selecting close on a tab, sometimes the tab to the right is
#       selected, most likely due to determination of mouse position
# ToDo: Tab Selection seems overly complicated. OnLeftDown finds tab at
#       position, and then call's CheckTabSelected which calls TabHitTest (when
#       we are already aware it will return due to FindTabAtPos)
# ToDo: Perhaps a better way of finding tabs at position instead of looping
#       through them and getting their regions. Perhaps some smart trickery with
#       mouse pos x (all tabs have same width, so we divide x by width to find
#       tab index?). This will also help with finding close buttons.
# ToDo: Fix page preview code (PFNotebookPagePreview)
#
# ===============================================================================


import math
from functools import lru_cache

import wx
import wx.lib.newevent

from gui.bitmap_loader import BitmapLoader
from gui.utils import color as color_utils, draw, fonts
from service.fit import Fit

_t = wx.GetTranslation
_PageChanging, EVT_NOTEBOOK_PAGE_CHANGING = wx.lib.newevent.NewEvent()
_PageChanged, EVT_NOTEBOOK_PAGE_CHANGED = wx.lib.newevent.NewEvent()
_PageAdding, EVT_NOTEBOOK_PAGE_ADDING = wx.lib.newevent.NewEvent()
_PageClosing, EVT_NOTEBOOK_PAGE_CLOSING = wx.lib.newevent.NewEvent()
PageAdded, EVT_NOTEBOOK_PAGE_ADDED = wx.lib.newevent.NewEvent()
PageClosed, EVT_NOTEBOOK_PAGE_CLOSED = wx.lib.newevent.NewEvent()


class VetoAble:
    def __init__(self):
        self.__vetoed = False

    def Veto(self):
        self.__vetoed = True

    def isVetoed(self):
        return self.__vetoed


class NotebookTabChangeEvent:
    def __init__(self, old, new):
        self.__old = old
        self.__new = new

    def GetOldSelection(self):
        return self.__old

    def GetSelection(self):
        return self.__new

    OldSelection = property(GetOldSelection)
    Selection = property(GetSelection)


class PageChanging(_PageChanging, NotebookTabChangeEvent, VetoAble):
    def __init__(self, old, new):
        _PageChanging.__init__(self)
        NotebookTabChangeEvent.__init__(self, old, new)
        VetoAble.__init__(self)


class PageChanged(_PageChanged, NotebookTabChangeEvent):

    def __init__(self, old, new, *args, **kwargs):
        _PageChanged.__init__(self, *args, **kwargs)
        NotebookTabChangeEvent.__init__(self, old, new)


class PageClosing(_PageClosing, VetoAble):
    def __init__(self, i):
        _PageClosing.__init__(self)
        self.__index = i
        VetoAble.__init__(self)
        self.Selection = property(self.GetSelection)

    def GetSelection(self):
        return self.__index


class PageAdding(_PageAdding, VetoAble):
    def __init__(self):
        _PageAdding.__init__(self)
        VetoAble.__init__(self)


class ChromeNotebook(wx.Panel):

    def __init__(self, parent, can_add=True, tabWidthMode=0):
        """
        Instance of Notebook. Initializes general layout, includes methods
        for setting current page, replacing pages, any public function for the
        notebook

        width modes:
        - 0: legacy (all tabs have equal width)
        - 1: all tabs take just enough space to fit text
        """
        super().__init__(parent, wx.ID_ANY, size=(-1, -1))

        self._pages = []
        self._active_page = None

        main_sizer = wx.BoxSizer(wx.VERTICAL)

        tabs_sizer = wx.BoxSizer(wx.VERTICAL)
        self.tabs_container = _TabsContainer(self, can_add=can_add, tabWidthMode=tabWidthMode)
        tabs_sizer.Add(self.tabs_container, 0, wx.EXPAND)

        if 'wxMSW' in wx.PlatformInfo:
            style = wx.DOUBLE_BORDER
        else:
            style = wx.SIMPLE_BORDER

        back_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)

        content_sizer = wx.BoxSizer(wx.VERTICAL)
        self.page_container = wx.Panel(self, style=style)
        self.page_container.SetBackgroundColour(back_color)
        content_sizer.Add(self.page_container, 1, wx.EXPAND, 5)

        main_sizer.Add(tabs_sizer, 0, wx.EXPAND, 5)
        main_sizer.Add(content_sizer, 1, wx.EXPAND | wx.BOTTOM, 2)

        self.SetSizer(main_sizer)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Layout()

    def GetPage(self, i):
        return self._pages[i]

    def SetPage(self, i, page):
        if i >= len(self._pages) or i is None or page is None:
            return

        old_page = self._pages[i]
        self._pages[i] = page
        if old_page == self._active_page:
            old_page.Destroy()
            self._active_page = page
        else:
            old_page.Destroy()

        page.Reparent(self.page_container)

        if self._active_page == page:
            self.ShowActive()

    def GetBorders(self):
        """Gets border widths to better determine page size in ShowActive()"""

        bx = wx.SystemSettings.GetMetric(wx.SYS_BORDER_X)
        by = wx.SystemSettings.GetMetric(wx.SYS_BORDER_Y)

        if bx < 0:
            bx = 1
        if by < 0:
            by = 1

        return bx, by

    def ReplaceActivePage(self, page):
        self.SetPage(self.GetSelection(), page)

    def GetSelectedPage(self):
        return self._active_page

    def GetPageIndex(self, page):
        return self._pages.index(page) if page in self._pages else None

    def GetSelection(self):
        return self.GetPageIndex(self._active_page)

    def GetCurrentPage(self):
        return self._active_page

    def GetPageCount(self):
        return len(self._pages)

    def NextPage(self):
        """Used with keyboard shortcut for next page navigation"""
        current_page = self.GetSelection()

        if current_page is None:
            return

        if current_page < self.GetPageCount() - 1:
            self.SetSelection(current_page + 1)
            new_page = current_page + 1
        else:
            self.SetSelection(0)
            new_page = 0

        wx.PostEvent(self, PageChanged(current_page, new_page))

    def PrevPage(self):
        """Used with keyboard shortcut for previous page navigation"""
        current_page = self.GetSelection()

        if current_page is None:
            return

        if current_page > 0:
            self.SetSelection(current_page - 1)
            new_page = current_page - 1
        else:
            self.SetSelection(self.GetPageCount() - 1)
            new_page = self.GetPageCount() - 1

        wx.PostEvent(self, PageChanged(current_page, new_page))

    def AddPage(self, win=None, title=None, image: wx.Image=None, closeable=True):
        title = title or "Empty Tab"
        if self._active_page:
            self._active_page.Hide()

        if not win:
            win = wx.Panel(self)

        win.Reparent(self.page_container)

        self.page_container.Layout()

        self._pages.append(win)
        self.tabs_container.AddTab(title, image, closeable)

        self._active_page = win
        self.ShowActive(True)

    def DisablePage(self, page, toggle):
        idx = self.GetPageIndex(page)

        if toggle and page == self._active_page:
            try:
                # Set page to the first non-disabled page
                self.SetSelection(next(i for i, _ in enumerate(self._pages) if not self.tabs_container.tabs[i].disabled))
            except StopIteration:
                self.SetSelection(0)

        self.tabs_container.DisableTab(idx, toggle)

    def SetSelection(self, page, focus=True):
        old_selection = self.GetSelection()
        if old_selection != page:
            self._active_page.Hide()
            self._active_page = self._pages[page]
            self.tabs_container.SetSelected(page)
            self.ShowActive()
            if focus:
                self._active_page.SetFocus()
            wx.PostEvent(self, PageChanged(old_selection, page))

    def DeletePage(self, n):
        page = self._pages[n]
        self._pages.remove(page)
        page.Destroy()

        self.tabs_container.DeleteTab(n)

        selection = self.tabs_container.GetSelected()
        if selection is not None:
            self._active_page = self._pages[selection]
            self.ShowActive()
            wx.PostEvent(self, PageChanged(-1, selection))
        else:
            self._active_page = None

    def SwitchPages(self, src, dst):
        self._pages[src], self._pages[dst] = self._pages[dst], self._pages[src]

    def ShowActive(self, resize_only=False):
        """
        Sets the size of the page and shows. The sizing logic adjusts for some
        minor sizing errors (scrollbars going beyond bounds)

        resize_only
            if we are not interested in showing the page, only setting the size

        @todo: is resize_only still needed? Was introduced with 8b8b97 in mid
        2011 to fix a resizing bug with blank _pages, cannot reproduce
        13Sept2014
        """

        ww, wh = self.page_container.GetSize()
        bx, by = self.GetBorders()
        ww -= bx * 4
        wh -= by * 4
        self._active_page.SetSize((max(ww, -1), max(wh, -1)))
        self._active_page.SetPosition((0, 0))

        if not resize_only:
            self._active_page.Show()

        self.Layout()

    def IsActive(self, page):
        return self._active_page == page

    def SetPageTitle(self, i, text, refresh=True):
        tab = self.tabs_container.tabs[i]
        tab.baseText = text
        if refresh:
            self.tabs_container.AdjustTabsSize()
            self.Refresh()

    def SetPageTitleExtra(self, i, text, refresh=True):
        tab = self.tabs_container.tabs[i]
        tab.extraText = text
        if refresh:
            self.tabs_container.AdjustTabsSize()
            self.Refresh()

    def SetPageIcon(self, i, icon, refresh=True):
        tab = self.tabs_container.tabs[i]
        tab.tab_img = icon
        if refresh:
            self.tabs_container.AdjustTabsSize()
            self.Refresh()

    def SetPageTextIcon(self, i, text=wx.EmptyString, icon=None):
        self.SetPageTitle(i, text, False)
        self.SetPageIcon(i, icon, False)
        self.tabs_container.AdjustTabsSize()
        self.Refresh()

    def Refresh(self):
        self.tabs_container.Refresh()

    def OnSize(self, event):
        w, h = self.GetSize()
        self.tabs_container.SetSize((w, -1))
        self.tabs_container.UpdateSize()
        self.tabs_container.Refresh()
        self.Layout()

        if self._active_page:
            self.ShowActive()
        event.Skip()


class _TabRenderer:
    def __init__(self, size=(36, 24), text=wx.EmptyString, img: wx.Image=None,
                 closeable=True):

        # tab left/right zones inclination
        self.ctab_left = BitmapLoader.getImage("ctableft", "gui")
        self.ctab_middle = BitmapLoader.getImage("ctabmiddle", "gui")
        self.ctab_right = BitmapLoader.getImage("ctabright", "gui")
        self.ctab_close = BitmapLoader.getImage("ctabclose", "gui")

        self.left_width = self.ctab_left.GetWidth()
        self.right_width = self.ctab_right.GetWidth()
        self.middle_width = self.ctab_middle.GetWidth()
        self.close_btn_width = self.ctab_close.GetWidth()

        width, height = size

        self.min_width = self.left_width + self.right_width + self.middle_width
        self.min_height = self.ctab_middle.GetHeight()

        # set minimum width and height to what is allotted to images
        width = max(width, self.min_width)
        height = max(height, self.min_height)

        self.disabled = False
        self.baseText = text
        self.extraText = ''
        self.tab_size = (width, height)
        self.closeable = closeable
        self.selected = False
        self.close_btn_hovering = False
        self.tab_bitmap = None
        self.tab_back_bitmap = None
        self.padding = 4
        self.font = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)

        self.tab_img = img
        self.position = (0, 0)  # Not used internally for rendering - helper for tab container
        self.InitTab()

    @property
    def text(self):
        return self.baseText + self.extraText

    def SetPosition(self, position):
        self.position = position

    def GetPosition(self):
        return self.position

    def GetSize(self):
        return self.tab_size

    def SetSize(self, size):
        width, height = size

        width = max(width, self.min_width)
        height = max(height, self.min_height)

        self.tab_size = (width, height)
        self.InitTab()

    def SetSelected(self, sel=True):
        self.selected = sel
        self.InitTab()

    def GetSelected(self):
        return self.selected

    def IsSelected(self):
        return self.selected

    def ShowCloseButtonHovering(self, hover=True):
        if self.close_btn_hovering != hover:
            self.close_btn_hovering = hover
            self._Render()

    def GetCloseButtonHoverStatus(self):
        return self.close_btn_hovering

    def GetTabRegion(self):
        new_region = self.CopyRegion(self.tab_region)
        new_region.Subtract(self.close_region) if self.closeable else self.tab_region
        return new_region

    def GetCloseButtonRegion(self):
        return self.CopyRegion(self.close_region)

    def GetMinSize(self):
        ebmp = wx.Bitmap(1, 1)
        mdc = wx.MemoryDC()
        mdc.SelectObject(ebmp)
        mdc.SetFont(self.font)
        textSizeX, textSizeY = mdc.GetTextExtent(self.text)
        totalSize = self.left_width + self.right_width + textSizeX + self.close_btn_width / 2 + 16 + self.padding * 2
        mdc.SelectObject(wx.NullBitmap)
        return totalSize, self.tab_height

    def SetTabImage(self, img):
        self.tab_img = img

    def CopyRegion(self, region):
        rect = region.GetBox()

        newRegion = wx.Region(rect.X, rect.Y, rect.Width, rect.Height)
        newRegion.Intersect(region)

        return newRegion

    def InitTab(self):
        self.tab_width, self.tab_height = self.tab_size

        self.content_width = self.tab_width - self.left_width - self.right_width
        self.tab_region = None
        self.close_region = None

        self.InitColors()
        self.InitBitmaps()

        self.ComposeTabBack()
        self.InitTabRegions()
        self._Render()

    def InitBitmaps(self):
        """
        Creates bitmap for tab

        Takes the bitmaps already set and replaces a known color (black) with
        the needed color, while also considering selected state. Color dependant
        on platform -- see InitColors().
        """
        if self.selected:
            tr, tg, tb, ta = self.selected_color
        else:
            tr, tg, tb, ta = self.inactive_color

        ctab_left = self.ctab_left.Copy()
        ctab_right = self.ctab_right.Copy()
        ctab_middle = self.ctab_middle.Copy()

        ctab_left.Replace(0, 0, 0, tr, tg, tb)
        ctab_right.Replace(0, 0, 0, tr, tg, tb)
        ctab_middle.Replace(0, 0, 0, tr, tg, tb)

        self.ctab_left_bmp = wx.Bitmap(ctab_left)
        self.ctab_right_bmp = wx.Bitmap(ctab_right)
        self.ctab_middle_bmp = wx.Bitmap(ctab_middle)
        self.ctab_close_bmp = wx.Bitmap(self.ctab_close)

    def ComposeTabBack(self):
        """
        Creates the tab background bitmap based upon calculated dimension values
        and modified bitmaps via InitBitmaps()
        """
        bk_bmp = wx.Bitmap(self.tab_width, self.tab_height)

        mdc = wx.MemoryDC()
        mdc.SelectObject(bk_bmp)
        mdc.Clear()

        # draw the left bitmap
        mdc.DrawBitmap(self.ctab_left_bmp, 0, 0)

        # convert middle bitmap and scale to tab width
        cm = self.ctab_middle_bmp.ConvertToImage()
        mimg = cm.Scale(self.content_width, self.ctab_middle.GetHeight(),
                        wx.IMAGE_QUALITY_NORMAL)
        mbmp = wx.Bitmap(mimg)

        # draw middle bitmap, offset by left
        mdc.DrawBitmap(mbmp, self.left_width, 0)

        # draw right bitmap offset by left + middle
        mdc.DrawBitmap(self.ctab_right_bmp,
                       self.content_width + self.left_width, 0)

        mdc.SelectObject(wx.NullBitmap)

        if self.tab_back_bitmap:
            del self.tab_back_bitmap

        self.tab_back_bitmap = bk_bmp

    def InitTabRegions(self):
        """
        Initializes regions for tab, which makes it easier to determine if
        given coordinates are included in a region
        """
        self.tab_region = wx.Region(self.tab_back_bitmap)
        self.close_region = wx.Region(self.ctab_close_bmp)

        x_offset = self.content_width \
            + self.left_width \
            - self.ctab_close_bmp.GetWidth() / 2
        y_offset = (self.tab_height - self.ctab_close_bmp.GetHeight()) / 2
        self.close_region.Offset(x_offset, y_offset)

    def InitColors(self):
        """Determines colors used for tab, based on system settings"""
        self.tab_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
        self.inactive_color = color_utils.GetSuitable(self.tab_color, 0.25)
        self.selected_color = color_utils.GetSuitable(self.tab_color, 0.10)

    def Render(self):
        return self.tab_bitmap

    def _Render(self):
        """Renders the tab, complete with the icon, text, and close button"""
        if self.tab_bitmap:
            del self.tab_bitmap

        height = self.tab_height

        canvas = wx.Bitmap(self.tab_width, self.tab_height, 24)

        mdc = wx.MemoryDC()

        mdc.SelectObject(canvas)
        mdc.Clear()

        mdc.DrawBitmap(self.tab_back_bitmap, 0, 0, True)

        # draw the tab icon
        if self.tab_img:
            bmp = wx.Bitmap(self.tab_img.ConvertToGreyscale() if self.disabled else self.tab_img)
            # @todo: is this conditional relevant anymore?
            if self.content_width > 16:
                # Draw tab icon
                mdc.DrawBitmap(
                    bmp,
                    self.left_width + self.padding - bmp.GetWidth() / 2,
                    (height - bmp.GetHeight()) / 2)

        # draw close button
        if self.closeable:
            if self.close_btn_hovering:
                cbmp = self.ctab_close_bmp
            else:
                cimg = self.ctab_close_bmp.ConvertToImage()
                cimg = cimg.AdjustChannels(0.7, 0.7, 0.7, 0.3)
                cbmp = wx.Bitmap(cimg)

            mdc.DrawBitmap(
                cbmp,
                self.content_width + self.left_width - cbmp.GetWidth() / 2,
                (height - cbmp.GetHeight()) / 2)

        mdc.SelectObject(wx.NullBitmap)

        canvas.SetMaskColour((0x12, 0x23, 0x32))
        img = canvas.ConvertToImage()

        if not img.HasAlpha():
            img.InitAlpha()

        bmp = wx.Bitmap(img)
        self.tab_bitmap = bmp

    # We draw the text separately in order to draw it directly on the native DC, rather than a memory one, because
    # drawing text on a memory DC draws it blurry on HD/Retina screens
    def DrawText(self, dc):
        height = self.tab_height
        dc.SetFont(self.font)

        if self.tab_img:
            text_start = self.left_width + self.padding + self.tab_img.GetWidth() / 2
        else:
            text_start = self.left_width

        maxsize = self.tab_width \
            - text_start \
            - self.right_width \
            - self.padding * 4
        color = self.selected_color if self.selected else self.inactive_color

        dc.SetTextForeground(color_utils.GetSuitable(color, 1))

        # draw text (with no ellipses)
        text = draw.GetPartialText(dc, self.text, maxsize, "")
        tx, ty = dc.GetTextExtent(text)
        dc.DrawText(text, text_start + self.padding, height / 2 - ty / 2)

    def __repr__(self):
        return "_TabRenderer(text={}, disabled={}) at {}".format(
            self.text, self.disabled, hex(id(self))
        )


class _AddRenderer:
    def __init__(self):
        """Renders the add tab button"""
        self.add_img = BitmapLoader.getImage("ctabadd", "gui")
        self.width = self.add_img.GetWidth()
        self.height = self.add_img.GetHeight()

        self.region = None
        self.tbmp = wx.Bitmap(self.add_img)
        self.add_bitmap = None

        self.position = (0, 0)
        self.highlighted = False

        self.InitRenderer()

    def GetPosition(self):
        return self.position

    def SetPosition(self, pos):
        self.position = pos

    def GetSize(self):
        return self.width, self.height

    def GetHeight(self):
        return self.height

    def GetWidth(self):
        return self.width

    def InitRenderer(self):
        self.region = self.CreateRegion()
        self._Render()

    def CreateRegion(self):
        region = wx.Region(self.tbmp)
        return region

    def CopyRegion(self, region):
        rect = region.GetBox()

        new_region = wx.Region(rect.X, rect.Y, rect.Width, rect.Height)
        new_region.Intersect(region)

        return new_region

    def GetRegion(self):
        return self.CopyRegion(self.region)

    def Highlight(self, highlight=False):
        self.highlighted = highlight
        self._Render()

    def IsHighlighted(self):
        return self.highlighted

    def Render(self):
        return self.add_bitmap

    def _Render(self):
        if self.add_bitmap:
            del self.add_bitmap

        alpha = 1 if self.highlighted else 0.3

        img = self.add_img.AdjustChannels(1, 1, 1, alpha)
        bmp = wx.Bitmap(img)
        self.add_bitmap = bmp


class _TabsContainer(wx.Panel):
    def __init__(self, parent, pos=(50, 0), size=(100, 22), id=wx.ID_ANY,
                 can_add=True, tabWidthMode=0):
        """
        Defines the tab container. Handles functions such as tab selection and
        dragging, and defines minimum width of tabs (all tabs are of equal
        width, which is determined via widest tab). Also handles the tab
        preview, if any.
        """
        super().__init__(parent, id, pos, size)
        self.tabWidthMode = tabWidthMode

        self.tabs = []
        self.width, self.height = size
        self.container_height = self.height
        self.start_drag = False
        self.dragging = False

        # amount of overlap of tabs?
        self.inclination = 7

        if can_add:
            self.reserved = 48
        else:
            self.reserved = self.inclination * 4

        # pixel distance to drag before we actually start dragging
        self.drag_trail = 5

        self.dragx = 0
        self.dragy = 0
        self.dragged_tab = None
        self.drag_trigger = self.drag_trail

        self.show_add_button = can_add

        self.tab_container_width = self.width - self.reserved
        self.fxBmps = {}

        self.add_button = _AddRenderer()
        self.add_bitmap = self.add_button.Render()

        self.preview_timer = None
        self.preview_timer_id = wx.ID_ANY
        self.preview_wnd = None
        self.preview_bmp = None
        self.preview_pos = None
        self.preview_tab = None

        self.Bind(wx.EVT_TIMER, self.OnTimer)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MIDDLE_UP, self.OnMiddleUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_SYS_COLOUR_CHANGED, self.OnSysColourChanged)

        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)

    def OnSysColourChanged(self, event):
        for tab in self.tabs:
            tab.InitTab()
        self.Refresh()

    def OnSize(self, event):
        self.UpdateSize()
        event.Skip()

    def UpdateSize(self):
        """Update tab sizes based on new tab container size"""
        width, _ = self.GetSize()
        if width != self.width:
            self.width = width
            self.tab_container_width = self.width - self.reserved
            self.AdjustTabsSize()

    def OnLeftDown(self, event):
        """ Select tab on mouse down and start dragging logic """
        mposx, mposy = event.GetPosition()
        if not self.start_drag:
            tab = self.FindTabAtPos(mposx, mposy)
            if tab:
                self.CheckTabSelected(tab, mposx, mposy)
                if self.show_add_button:
                    # If we can add tabs, we can drag them. Set flag
                    self.start_drag = True
                    tx, ty = tab.GetPosition()
                    self.dragx = mposx - tx
                    self.dragy = self.container_height - self.height
                self.Refresh()

            self.dragged_tab = tab

    def OnMiddleUp(self, event):
        mposx, mposy = event.GetPosition()

        tab = self.FindTabAtPos(mposx, mposy)

        if tab is None or not tab.closeable:  # if not able to close, return False
            return False

        index = self.tabs.index(tab)
        ev = PageClosing(index)
        wx.PostEvent(self.Parent, ev)

        if ev.isVetoed():
            return False

        index = self.GetTabIndex(tab)
        self.Parent.DeletePage(index)
        wx.PostEvent(self.Parent, PageClosed(index=index))

        sel = self.GetSelected()
        if sel is not None:
            wx.PostEvent(self.Parent, PageChanged(-1, sel))

    def OnMotion(self, event):
        """
        Determines what happens when the mouse moves. This handles primarily
        dragging (region tab can be dragged) as well as checking if we are over
        an actionable button.
        """
        mposx, mposy = event.GetPosition()

        if self.start_drag:
            if not self.dragging:
                if self.drag_trigger < 0:
                    self.dragging = True
                    self.drag_trigger = self.drag_trail
                    self.CaptureMouse()
                else:
                    self.drag_trigger -= 1
            if self.dragging:
                # we wish to keep tab within tab container boundaries. To do
                # this, we must detect when mouse moves outside of boundaries.
                # Set hard limits to 0 and the size of tab container.
                dtx = mposx - self.dragx
                w, h = self.dragged_tab.GetSize()

                dtx = max(dtx, 0)

                if dtx + w > self.tab_container_width + self.inclination * 2:
                    dtx = self.tab_container_width - w + self.inclination * 2

                self.dragged_tab.SetPosition((dtx, self.dragy))

                # we must modify the surrounding tabs
                index = self.GetTabIndex(self.dragged_tab)

                left_tab = self.GetTabAtLeft(index)
                right_tab = self.GetTabAtRight(index)

                if left_tab:
                    lw, lh = left_tab.GetSize()
                    lx, ly = left_tab.GetPosition()

                    if lx + lw / 2 - self.inclination * 2 > dtx:
                        self.SwitchTabs(index - 1, index, self.dragged_tab)

                if right_tab:
                    rw, rh = right_tab.GetSize()
                    rx, ry = right_tab.GetPosition()

                    if rx + rw / 2 + self.inclination * 2 < dtx + w:
                        self.SwitchTabs(index + 1, index, self.dragged_tab)

                self.UpdateTabsPosition(self.dragged_tab)
                self.Refresh()
            return

        # If we aren't dragging, check for actionable buttons under mouse
        self.CheckCloseHighlighted(mposx, mposy)
        self.CheckAddHighlighted(mposx, mposy)
        self.CheckTabPreview(mposx, mposy)

        event.Skip()

    def OnLeftUp(self, event):
        """Determines what happens when user left clicks (up)"""
        mposx, mposy = event.GetPosition()
        if self.start_drag and self.dragging:
            self.dragging = False
            self.start_drag = False
            self.dragged_tab = None
            self.drag_trigger = self.drag_trail
            self.UpdateTabsPosition()
            self.Refresh()

            if self.HasCapture():
                self.ReleaseMouse()

            return

        if self.start_drag:
            self.start_drag = False
            self.drag_trigger = self.drag_trail

        # Checks if we selected the add button and, if True, returns
        if self.CheckAddButton(mposx, mposy):
            return

        # If there are no tabs, don't waste time
        if self.GetTabsCount() == 0:
            return

        # Gets selected tab (was set when user down clicked)
        sel_tab = self.GetSelectedTab()

        # Check if we selected close button for selected tab
        if self.CheckTabClose(sel_tab, mposx, mposy):
            return

        # Check if we selected close button for all others
        for tab in self.tabs:
            if self.CheckTabClose(tab, mposx, mposy):
                return

    def DisableTab(self, tab, disabled=True):
        tb_renderer = self.tabs[tab]
        tb_renderer.disabled = disabled

        self.AdjustTabsSize()
        self.Refresh()

    def GetSelectedTab(self):
        for tab in self.tabs:
            if tab.GetSelected():
                return tab
        return None

    def GetSelected(self):
        for tab in self.tabs:
            if tab.GetSelected():
                return self.tabs.index(tab)
        return None

    def SetSelected(self, tabIndex):
        """Set tab as selected given its index"""
        old_sel_tab = self.GetSelectedTab()
        old_sel_tab.SetSelected(False)
        self.tabs[tabIndex].SetSelected(True)
        self.Refresh()

    def CheckTabSelected(self, tab, x, y):
        """
        Selects the tab at x, y. If the tab at x, y is already selected, simply
        return true. Otherwise, perform TabHitTest and set tab at position to
        selected
        """
        old_sel_tab = self.GetSelectedTab()
        if old_sel_tab == tab:
            return True

        if self.TabHitTest(tab, x, y):
            if tab.disabled:
                return
            tab.SetSelected(True)
            old_sel_tab.SetSelected(False)

            ev = PageChanging(self.tabs.index(old_sel_tab), self.tabs.index(tab))
            wx.PostEvent(self.Parent, ev)

            if ev.isVetoed():
                return False

            self.Refresh()
            sel_tab = self.tabs.index(tab)
            self.Parent.SetSelection(sel_tab)

            wx.PostEvent(self.Parent, PageChanged(self.tabs.index(old_sel_tab),
                                                  self.tabs.index(tab)))

            return True

        return False

    def CheckTabClose(self, tab, x, y):
        """
        Determines if close button was selected for the given tab by comparing
        x and y position with known close button region
        """
        if not tab.closeable:  # if not able to close, return False
            return False

        region = tab.GetCloseButtonRegion()
        posx, posy = tab.GetPosition()
        region.Offset(posx, posy)

        if region.Contains(x, y):
            index = self.tabs.index(tab)
            ev = PageClosing(index)
            wx.PostEvent(self.Parent, ev)

            if ev.isVetoed():
                return False

            index = self.GetTabIndex(tab)
            self.Parent.DeletePage(index)
            wx.PostEvent(self.Parent, PageClosed(index=index))

            sel = self.GetSelected()
            if sel is not None:
                wx.PostEvent(self.Parent, PageChanged(-1, sel))

            return True
        return False

    def CheckAddButton(self, x, y):
        """
        Determines if add button was selected by comparing x and y position with
        add button region
        """
        if not self.show_add_button:  # if not able to add, return False
            return

        region = self.add_button.GetRegion()
        ax, ay = self.add_button.GetPosition()
        region.Offset(ax, ay)

        if region.Contains(x, y):
            ev = PageAdding()
            wx.PostEvent(self.Parent, ev)
            if ev.isVetoed():
                return False

            self.Parent.AddPage()
            wx.PostEvent(self.Parent, PageAdded())
            return True

    def CheckCloseHighlighted(self, x, y):
        """
        Checks if mouse pos at x, y is over a close button. If so, set the
        close hovering flag for that tab
        """
        dirty = False

        for tab in self.tabs:
            region = tab.GetCloseButtonRegion()
            posx, posy = tab.GetPosition()
            region.Offset(posx, posy)

            if region.Contains(x, y):
                if not tab.GetCloseButtonHoverStatus():
                    tab.ShowCloseButtonHovering(True)
                    dirty = True
            else:
                if tab.GetCloseButtonHoverStatus():
                    tab.ShowCloseButtonHovering(False)
                    dirty = True
            if dirty:
                self.Refresh()
                break

    def FindTabAtPos(self, x, y):
        if self.GetTabsCount() == 0:
            return None

        # test current tab first
        sel_tab = self.GetSelectedTab()
        if self.TabHitTest(sel_tab, x, y):
            return sel_tab

        # test all other tabs next
        for tab in self.tabs:
            if self.TabHitTest(tab, x, y):
                return tab

        return None

    def TabHitTest(self, tab, x, y):
        """ Test if x and y are contained within a tabs region """
        tabRegion = tab.GetTabRegion()
        tabPos = tab.GetPosition()
        tabPosX, tabPosY = tabPos
        tabRegion.Offset(tabPosX, tabPosY)

        if tabRegion.Contains(x, y):
            return True

        return False

    def GetTabAtLeft(self, tab_index):
        return self.tabs[tab_index - 1] if tab_index > 0 else None

    def GetTabAtRight(self, tab_index):
        if tab_index < self.GetTabsCount() - 1:
            return self.tabs[tab_index + 1]
        else:
            return None

    def SwitchTabs(self, src, dst, dragged_tab=None):
        self.tabs[src], self.tabs[dst] = self.tabs[dst], self.tabs[src]
        self.UpdateTabsPosition(dragged_tab)
        self.Parent.SwitchPages(src, dst)
        self.Refresh()

    def GetTabIndex(self, tab):
        return self.tabs.index(tab)

    def CheckTabPreview(self, mposx, mposy):
        """
        Checks to see if we have a tab preview and sets up the timer for it
        to display
        """
        sFit = Fit.getInstance()
        if not sFit.serviceFittingOptions["showTooltip"] or False:
            return

        if self.preview_timer:
            if self.preview_timer.IsRunning():
                if self.preview_wnd:
                    self.preview_timer.Stop()
                return

        if self.preview_wnd:
            self.preview_wnd.Show(False)
            del self.preview_wnd
            self.preview_wnd = None

        for tab in self.tabs:
            if not tab.GetSelected():
                if self.TabHitTest(tab, mposx, mposy):
                    try:
                        page = self.Parent.GetPage(self.GetTabIndex(tab))
                        if hasattr(page, "Snapshot"):
                            if not self.preview_timer:
                                self.preview_timer = wx.Timer(
                                    self, self.preview_timer_id)

                            self.preview_tab = tab
                            self.preview_timer.Start(500, True)
                            break
                    except (KeyboardInterrupt, SystemExit):
                        raise
                    except:
                        pass

    def CheckAddHighlighted(self, x, y):
        """
        Checks to see if x, y are in add button region, and sets the highlight
        flag
        """
        if not self.show_add_button:
            return

        region = self.add_button.GetRegion()
        ax, ay = self.add_button.GetPosition()
        region.Offset(ax, ay)

        if region.Contains(x, y):
            if not self.add_button.IsHighlighted():
                self.add_button.Highlight(True)
                self.Refresh()
        else:
            if self.add_button.IsHighlighted():
                self.add_button.Highlight(False)
                self.Refresh()

    def OnPaint(self, event):
        mdc = wx.AutoBufferedPaintDC(self)

        # if 'wxMac' in wx.PlatformInfo:
        #     color = wx.Colour(0, 0, 0)
        #     brush = wx.Brush(color)
        #     # @todo: what needs to be changed with wxPheonix?
        #     from Carbon.Appearance import kThemeBrushDialogBackgroundActive
        #     brush.MacSetTheme(kThemeBrushDialogBackgroundActive)
        # else:
        color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE)
        brush = wx.Brush(color)

        if "wxGTK" not in wx.PlatformInfo:
            mdc.SetBackground(brush)
            mdc.Clear()

        selected = None

        if self.show_add_button:
            ax, ay = self.add_button.GetPosition()
            mdc.DrawBitmap(self.add_button.Render(), ax, ay, True)

        for i in range(len(self.tabs) - 1, -1, -1):
            tab = self.tabs[i]
            posx, posy = tab.GetPosition()

            if not tab.IsSelected():
                # drop shadow first
                mdc.DrawBitmap(self.fxBmps[tab], posx, posy, True)
                bmp = tab.Render()
                img = bmp.ConvertToImage()
                img = img.AdjustChannels(1, 1, 1, 0.85)
                bmp = wx.Bitmap(img)
                mdc.DrawBitmap(bmp, posx, posy, True)

                mdc.SetDeviceOrigin(posx, posy)
                tab.DrawText(mdc)
                mdc.SetDeviceOrigin(0, 0)
            else:
                selected = tab

        # we draw selected tab separately (instead of in else) so as to not hide
        # the front bit behind the preceding tab
        if selected:
            posx, posy = selected.GetPosition()
            # drop shadow first
            mdc.DrawBitmap(self.fxBmps[selected], posx, posy, True)

            bmp = selected.Render()

            if self.dragging:
                img = bmp.ConvertToImage()
                img = img.AdjustChannels(1.2, 1.2, 1.2, 0.7)
                bmp = wx.Bitmap(img)

            mdc.DrawBitmap(bmp, posx, posy, True)

            mdc.SetDeviceOrigin(posx, posy)
            selected.DrawText(mdc)
            mdc.SetDeviceOrigin(0, 0)

    def OnErase(self, event):
        pass

    def UpdateTabFX(self):
        """ Updates tab drop shadow bitmap """
        self.fxBmps.clear()
        for tab in self.tabs:
            tabW, tabH = tab.tab_size
            self.fxBmps[tab] = self.GetTabFx(tabW, self.height + 1)

    @lru_cache(maxsize=50)
    def GetTabFx(self, width, height):
        renderer = _TabRenderer((width, height))
        fx_bmp = renderer.Render()
        img = fx_bmp.ConvertToImage()
        if not img.HasAlpha():
            img.InitAlpha()
        img = img.Blur(2)
        img = img.AdjustChannels(0.3, 0.3, 0.3, 0.35)
        return wx.Bitmap(img)

    def AddTab(self, title=wx.EmptyString, img=None, closeable=False):
        self.ClearTabsSelected()

        tab_renderer = _TabRenderer((200, self.height), title, img, closeable)
        tab_renderer.SetSelected(True)

        self.tabs.append(tab_renderer)
        self.AdjustTabsSize()
        self.Refresh()

    def ClearTabsSelected(self):
        for tab in self.tabs:
            tab.SetSelected(False)

    def DeleteTab(self, tab):
        tab_renderer = self.tabs[tab]
        was_selected = tab_renderer.GetSelected()
        self.tabs.remove(tab_renderer)

        if tab_renderer:
            del tab_renderer

        # determine our new selection
        if was_selected and self.GetTabsCount() > 0:
            if tab > self.GetTabsCount() - 1:
                self.tabs[self.GetTabsCount() - 1].SetSelected(True)
            else:
                self.tabs[tab].SetSelected(True)

        self.AdjustTabsSize()
        self.Refresh()

    def GetTabsCount(self):
        return len(self.tabs)

    def AdjustTabsSize(self):
        """
        Adjust tab sizes to ensure that they are all consistent and can fit into
        the tab container.
        """
        if self.tabWidthMode == 1:
            if self.GetTabsCount() > 0:
                availableW = self.tab_container_width
                overlapSavedW = max(0, len(self.tabs)) * self.inclination * 2
                tabsGrouped = {}
                for tab in self.tabs:
                    tabW, _ = tab.GetMinSize()
                    tabsGrouped.setdefault(math.ceil(tabW), []).append(tab)
                clippedTabs = []
                clipW = max(tabsGrouped, default=0)

                def getUnclippedW():
                    unclippedW = 0
                    for w, tabs in tabsGrouped.items():
                        unclippedW += w * len(tabs)
                    return unclippedW
                while tabsGrouped:
                    # Check if we're within width limit
                    neededW = 0
                    for w, tabs in tabsGrouped.items():
                        neededW += w * len(tabs)
                    if clippedTabs:
                        neededW += clipW * len(clippedTabs)
                    if neededW <= availableW + overlapSavedW:
                        break
                    # If we're not, extract widest tab group and mark it for clipping
                    currentTabs = tabsGrouped.pop(max(tabsGrouped))
                    clippedTabs.extend(currentTabs)
                    proposedClipWidth = math.floor((availableW + overlapSavedW - getUnclippedW()) / len(clippedTabs))
                    if not tabsGrouped or proposedClipWidth >= max(tabsGrouped, default=0):
                        clipW = max(0, proposedClipWidth)
                        break
                    else:
                        clipW = max(tabsGrouped)
                # Assign width for unclipped tabs
                for w, tabs in tabsGrouped.items():
                    for tab in tabs:
                        tab.SetSize((w, self.height))
                if clippedTabs:
                    # Some width remains to be used due to rounding to integer
                    extraWTotal = availableW + overlapSavedW - getUnclippedW() - clipW * len(clippedTabs)
                    extraWPerTab = math.ceil(extraWTotal / len(clippedTabs))
                    # Assign width for clipped tabs
                    for tab in clippedTabs:
                        extraW = min(extraWTotal, extraWPerTab)
                        extraWTotal -= extraW
                        tab.SetSize((clipW + extraW, self.height))
        else:
            # first we loop through our tabs and calculate the the largest tab. This
            # is the size that we will base our calculations off
            max_width = 100  # Tab should be at least 100
            for tab in self.tabs:
                mw, _ = tab.GetMinSize()  # Tab min size includes tab contents
                max_width = max(mw, max_width)

            tabWidth = 0
            # Divide tab container by number of tabs and add inclination. This will
            # return the ideal max size for the containers size
            if self.GetTabsCount() > 0:
                dx = self.tab_container_width / self.GetTabsCount() + self.inclination * 2
                tabWidth = min(dx, max_width)

            # Apply new size to all tabs
            for tab in self.tabs:
                tab.SetSize((tabWidth, self.height))

        # update drop shadow based on new sizes
        self.UpdateTabFX()
        self.UpdateTabsPosition()

    def UpdateTabsPosition(self, skip_tab=None):
        tabsWidth = 0
        for tab in self.tabs:
            tabsWidth += tab.tab_width - self.inclination * 2

        pos = tabsWidth
        selected = None
        for i in range(len(self.tabs) - 1, -1, -1):
            tab = self.tabs[i]
            width = tab.tab_width - self.inclination * 2
            pos -= width
            if not tab.IsSelected():
                tab.SetPosition((pos, self.container_height - self.height))
            else:
                selected = tab
                selpos = pos

        if selected is not skip_tab:
            selected.SetPosition((selpos, self.container_height - self.height))

        self.add_button.SetPosition((round(tabsWidth) + self.inclination * 2,
                                     self.container_height - self.height / 2 - self.add_button.GetHeight() / 3))

    def OnLeaveWindow(self, event):
        if self.start_drag and not self.dragging:
            self.dragging = False
            self.start_drag = False
            self.dragged_tab = None
            self.drag_trigger = self.drag_trail
            if self.HasCapture():
                self.ReleaseMouse()

        if self.preview_wnd:
            self.preview_wnd.Show(False)
            del self.preview_wnd
            self.preview_wnd = None
        event.Skip()

    def OnTimer(self, event):
        mposx, mposy = wx.GetMousePosition()
        cposx, cposy = self.ScreenToClient((mposx, mposy))

        if self.FindTabAtPos(cposx, cposy) == self.preview_tab:
            if not self.preview_tab.GetSelected():
                page = self.Parent.GetPage(self.GetTabIndex(self.preview_tab))
                if page.Snapshot():
                    self.preview_wnd = PFNotebookPagePreview(
                        self,
                        (mposx + 3, mposy + 3),
                        page.Snapshot(),
                        self.preview_tab.text)
                    self.preview_wnd.Show()
        event.Skip()


class PFNotebookPagePreview(wx.Frame):
    def __init__(self, parent, pos, bitmap, title):
        super().__init__(parent, id=wx.ID_ANY, title=wx.EmptyString, pos=pos,
                         size=wx.DefaultSize, style=wx.NO_BORDER |
                                                    wx.FRAME_NO_TASKBAR |
                                                    wx.STAY_ON_TOP)
        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
        self.title = title
        self.bitmap = bitmap
        self.SetSize((bitmap.GetWidth(), bitmap.GetHeight()))
        self.Bind(wx.EVT_PAINT, self.OnWindowPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnWindowEraseBk)
        self.Bind(wx.EVT_TIMER, self.OnTimer)

        self.timer = wx.Timer(self, wx.ID_ANY)
        self.timerSleep = None
        self.timerSleepId = wx.NewId()
        self.direction = 1
        self.padding = 15
        self.transp = 0

        hfont = wx.Font(fonts.NORMAL, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
        self.SetFont(hfont)

        tx, ty = self.GetTextExtent(self.title)
        tx += self.padding * 2

        if bitmap.GetWidth() < tx:
            width = tx
        else:
            width = bitmap.GetWidth()

        self.SetSize((width, bitmap.GetHeight() + 16))

        self.SetTransparent(0)
        self.Refresh()

    def OnTimer(self, event):
        self.transp += 20 * self.direction

        if self.transp > 220:
            self.transp = 220
            self.timer.Stop()

        if self.transp < 0:
            self.transp = 0
            self.timer.Stop()
            super().Show(False)
            self.Destroy()
            return
        self.SetTransparent(self.transp)

    def RaiseParent(self):
        wnd = self
        lastwnd = None
        while wnd is not None:
            lastwnd = wnd
            wnd = wnd.Parent
        if lastwnd:
            lastwnd.Raise()

    def Show(self, showWnd=True):
        if showWnd:
            super().Show(showWnd)
            self.RaiseParent()
            self.direction = 1
            self.timer.Start(10)
        else:
            self.direction = -1
            self.timer.Start(10)

    def OnWindowEraseBk(self, event):
        pass

    def OnWindowPaint(self, event):
        rect = self.GetRect()
        canvas = wx.Bitmap(rect.width, rect.height)
        mdc = wx.BufferedPaintDC(self)
        mdc.SelectObject(canvas)
        color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
        mdc.SetBackground(wx.Brush(color))
        mdc.Clear()

        font = wx.Font(11, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
        mdc.SetFont(font)

        x, y = mdc.GetTextExtent(self.title)

        mdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)))
        mdc.DrawRectangle(0, 0, rect.width, 16)

        mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))

        mdc.DrawBitmap(self.bitmap, 0, 16)

        mdc.SetPen(wx.Pen("#000000", width=1))
        mdc.SetBrush(wx.TRANSPARENT_BRUSH)

        mdc.DrawRectangle(0, 16, rect.width, rect.height - 16)
