import string
import types
import tkinter
import Pmw

class NoteBook(Pmw.MegaArchetype):

    def __init__(self, parent = None, **kw):

        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('hull_highlightthickness',  0,           None),
            ('hull_borderwidth',         0,           None),
            ('arrownavigation',          1,           INITOPT),
            ('borderwidth',              2,           INITOPT),
            ('createcommand',            None,        None),
            ('lowercommand',             None,        None),
            ('pagemargin',               4,           INITOPT),
            ('raisecommand',             None,        None),
            ('tabpos',                   'n',         INITOPT),
        )
        self.defineoptions(kw, optiondefs, dynamicGroups = ('Page', 'Tab'))

        # Initialise the base class (after defining the options).
        Pmw.MegaArchetype.__init__(self, parent, tkinter.Canvas)

        self.bind('<Map>', self._handleMap)
        self.bind('<Configure>', self._handleConfigure)

        tabpos = self['tabpos']
        if tabpos is not None and tabpos != 'n':
            raise ValueError('bad tabpos option %s:  should be n or None' % repr(tabpos))
        self._withTabs = (tabpos is not None)
        self._pageMargin = self['pagemargin']
        self._borderWidth = self['borderwidth']

        # Use a dictionary as a set of bits indicating what needs to
        # be redisplayed the next time _layout() is called.  If
        # dictionary contains 'topPage' key, the value is the new top
        # page to be displayed.  None indicates that all pages have
        # been deleted and that _layout() should draw a border under where
        # the tabs should be.
        self._pending = {}
        self._pending['size'] = 1
        self._pending['borderColor'] = 1
        self._pending['topPage'] = None
        if self._withTabs:
            self._pending['tabs'] = 1

        self._canvasSize = None       # This gets set by <Configure> events

        # Set initial height of space for tabs
        if self._withTabs:
            self.tabBottom = 35
        else:
            self.tabBottom = 0

        self._lightBorderColor, self._darkBorderColor = \
                Pmw.Color.bordercolors(self, self['hull_background'])

        self._pageNames   = []        # List of page names

        # Map from page name to page info.  Each item is itself a
        # dictionary containing the following items:
        #   page           the Tkinter.Frame widget for the page
        #   created        set to true the first time the page is raised
        #   tabbutton      the Tkinter.Button widget for the button (if any)
        #   tabreqwidth    requested width of the tab
        #   tabreqheight   requested height of the tab
        #   tabitems       the canvas items for the button: the button
        #                  window item, the lightshadow and the darkshadow
        #   left           the left and right canvas coordinates of the tab
        #   right
        self._pageAttrs   = {}

        # Name of page currently on top (actually displayed, using
        # create_window, not pending).  Ignored if current top page
        # has been deleted or new top page is pending.  None indicates
        # no pages in notebook.
        self._topPageName = None

        # Canvas items used:
        #   Per tab:
        #       top and left shadow
        #       right shadow
        #       button
        #   Per notebook:
        #       page
        #       top page
        #       left shadow
        #       bottom and right shadow
        #       top (one or two items)

        # Canvas tags used:
        #   lighttag      - top and left shadows of tabs and page
        #   darktag       - bottom and right shadows of tabs and page
        #                   (if no tabs then these are reversed)
        #                   (used to color the borders by recolorborders)

        # Create page border shadows.
        if self._withTabs:
            self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._lightBorderColor, tags = 'lighttag')
            self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')
            self._pageTop1Border = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'lighttag')
            self._pageTop2Border = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'lighttag')
        else:
            self._pageLeftBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')
            self._pageBottomRightBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._lightBorderColor, tags = 'lighttag')
            self._pageTopBorder = self.create_polygon(0, 0, 0, 0, 0, 0,
                fill = self._darkBorderColor, tags = 'darktag')

        # Check keywords and initialise options.
        self.initialiseoptions()

    def insert(self, pageName, before = 0, **kw):
        if pageName in self._pageAttrs:
            msg = 'Page "%s" already exists.' % pageName
            raise ValueError(msg)

        # Do this early to catch bad <before> spec before creating any items.
        beforeIndex = self.index(before, 1)

        pageOptions = {}
        if self._withTabs:
            # Default tab button options.
            tabOptions = {
                'text' : pageName,
                'borderwidth' : 0,
            }

        # Divide the keyword options into the 'page_' and 'tab_' options.
        for key in list(kw.keys()):
            if key[:5] == 'page_':
                pageOptions[key[5:]] = kw[key]
                del kw[key]
            elif self._withTabs and key[:4] == 'tab_':
                tabOptions[key[4:]] = kw[key]
                del kw[key]
            else:
                raise KeyError('Unknown option "' + key + '"')

        # Create the frame to contain the page.
        page = self.createcomponent(*(pageName,
                (), 'Page',
                tkinter.Frame, self._hull), **pageOptions)

        attributes = {}
        attributes['page'] = page
        attributes['created'] = 0

        if self._withTabs:
            # Create the button for the tab.
            def raiseThisPage(self = self, pageName = pageName):
                self.selectpage(pageName)
            tabOptions['command'] = raiseThisPage
            tab = self.createcomponent(*(pageName + '-tab',
                    (), 'Tab',
                    tkinter.Button, self._hull), **tabOptions)

            if self['arrownavigation']:
                # Allow the use of the arrow keys for Tab navigation:
                def next(event, self = self, pageName = pageName):
                    self.nextpage(pageName)
                def prev(event, self = self, pageName = pageName):
                    self.previouspage(pageName)
                tab.bind('<Left>', prev)
                tab.bind('<Right>', next)

            attributes['tabbutton'] = tab
            attributes['tabreqwidth'] = tab.winfo_reqwidth()
            attributes['tabreqheight'] = tab.winfo_reqheight()

            # Create the canvas item to manage the tab's button and the items
            # for the tab's shadow.
            windowitem = self.create_window(0, 0, window = tab, anchor = 'nw')
            lightshadow = self.create_polygon(0, 0, 0, 0, 0, 0,
                tags = 'lighttag', fill = self._lightBorderColor)
            darkshadow = self.create_polygon(0, 0, 0, 0, 0, 0,
                tags = 'darktag', fill = self._darkBorderColor)
            attributes['tabitems'] = (windowitem, lightshadow, darkshadow)
            self._pending['tabs'] = 1

        self._pageAttrs[pageName] = attributes
        self._pageNames.insert(beforeIndex, pageName)

        # If this is the first page added, make it the new top page
        # and call the create and raise callbacks.
        if self.getcurselection() is None:
            self._pending['topPage'] = pageName
            self._raiseNewTop(pageName)

        self._layout()
        return page

    def add(self, pageName, **kw):
        return self.insert(*(pageName, len(self._pageNames)), **kw)

    def delete(self, *pageNames):
        newTopPage = 0
        for page in pageNames:
            pageIndex = self.index(page)
            pageName = self._pageNames[pageIndex]
            pageInfo = self._pageAttrs[pageName]

            if self.getcurselection() == pageName:
                if len(self._pageNames) == 1:
                    newTopPage = 0
                    self._pending['topPage'] = None
                elif pageIndex == len(self._pageNames) - 1:
                    newTopPage = 1
                    self._pending['topPage'] = self._pageNames[pageIndex - 1]
                else:
                    newTopPage = 1
                    self._pending['topPage'] = self._pageNames[pageIndex + 1]

            if self._topPageName == pageName:
                self._hull.delete(self._topPageItem)
                self._topPageName = None

            if self._withTabs:
                self.destroycomponent(pageName + '-tab')
                self._hull.delete(*pageInfo['tabitems'])
            self.destroycomponent(pageName)
            del self._pageAttrs[pageName]
            del self._pageNames[pageIndex]

        # If the old top page was deleted and there are still pages
        # left in the notebook, call the create and raise callbacks.
        if newTopPage:
            pageName = self._pending['topPage']
            self._raiseNewTop(pageName)

        if self._withTabs:
            self._pending['tabs'] = 1
        self._layout()

    def page(self, pageIndex):
        pageName = self._pageNames[self.index(pageIndex)]
        return self._pageAttrs[pageName]['page']

    def pagenames(self):
        return list(self._pageNames)

    def getcurselection(self):
        if 'topPage' in self._pending:
            return self._pending['topPage']
        else:
            return self._topPageName

    def tab(self, pageIndex):
        if self._withTabs:
            pageName = self._pageNames[self.index(pageIndex)]
            return self._pageAttrs[pageName]['tabbutton']
        else:
            return None

    def index(self, index, forInsert = 0):
        listLength = len(self._pageNames)
        if type(index) == int:
            if forInsert and index <= listLength:
                return index
            elif not forInsert and index < listLength:
                return index
            else:
                raise ValueError('index "%s" is out of range' % index)
        elif index is Pmw.END:
            if forInsert:
                return listLength
            elif listLength > 0:
                return listLength - 1
            else:
                raise ValueError('NoteBook has no pages')
        elif index is Pmw.SELECT:
            if listLength == 0:
                raise ValueError('NoteBook has no pages')
            return self._pageNames.index(self.getcurselection())
        else:
            if index in self._pageNames:
                return self._pageNames.index(index)
            validValues = 'a name, a number, Pmw.END or Pmw.SELECT'
            raise ValueError('bad index "%s": must be %s' % (index, validValues))

    def selectpage(self, page):
        pageName = self._pageNames[self.index(page)]
        oldTopPage = self.getcurselection()
        if pageName != oldTopPage:
            self._pending['topPage'] = pageName
            if oldTopPage == self._topPageName:
                self._hull.delete(self._topPageItem)
            cmd = self['lowercommand']
            if cmd is not None:
                cmd(oldTopPage)
            self._raiseNewTop(pageName)

            self._layout()

        # Set focus to the tab of new top page:
        if self._withTabs and self['arrownavigation']:
            self._pageAttrs[pageName]['tabbutton'].focus_set()

    def previouspage(self, pageIndex = None):
        if pageIndex is None:
            curpage = self.index(Pmw.SELECT)
        else:
            curpage = self.index(pageIndex)
        if curpage > 0:
            self.selectpage(curpage - 1)

    def nextpage(self, pageIndex = None):
        if pageIndex is None:
            curpage = self.index(Pmw.SELECT)
        else:
            curpage = self.index(pageIndex)
        if curpage < len(self._pageNames) - 1:
            self.selectpage(curpage + 1)

    def setnaturalsize(self, pageNames = None):
        self.update_idletasks()
        maxPageWidth = 1
        maxPageHeight = 1
        if pageNames is None:
            pageNames = self.pagenames()
        for pageName in pageNames:
            pageInfo = self._pageAttrs[pageName]
            page = pageInfo['page']
            w = page.winfo_reqwidth()
            h = page.winfo_reqheight()
            if maxPageWidth < w:
                maxPageWidth = w
            if maxPageHeight < h:
                maxPageHeight = h
        pageBorder = self._borderWidth + self._pageMargin
        width = maxPageWidth + pageBorder * 2
        height = maxPageHeight + pageBorder * 2

        if self._withTabs:
            maxTabHeight = 0
            for pageInfo in list(self._pageAttrs.values()):
                if maxTabHeight < pageInfo['tabreqheight']:
                    maxTabHeight = pageInfo['tabreqheight']
            height = height + maxTabHeight + self._borderWidth * 1.5

        # Note that, since the hull is a canvas, the width and height
        # options specify the geometry *inside* the borderwidth and
        # highlightthickness.
        self.configure(hull_width = width, hull_height = height)

    def recolorborders(self):
        self._pending['borderColor'] = 1
        self._layout()

    def _handleMap(self, event):
        self._layout()

    def _handleConfigure(self, event):
        self._canvasSize = (event.width, event.height)
        self._pending['size'] = 1
        self._layout()

    def _raiseNewTop(self, pageName):
        if not self._pageAttrs[pageName]['created']:
            self._pageAttrs[pageName]['created'] = 1
            cmd = self['createcommand']
            if cmd is not None:
                cmd(pageName)
        cmd = self['raisecommand']
        if cmd is not None:
            cmd(pageName)

    # This is the vertical layout of the notebook, from top (assuming
    # tabpos is 'n'):
    #     hull highlightthickness (top)
    #     hull borderwidth (top)
    #     borderwidth (top border of tabs)
    #     borderwidth * 0.5 (space for bevel)
    #     tab button (maximum of requested height of all tab buttons)
    #     borderwidth (border between tabs and page)
    #     pagemargin (top)
    #     the page itself
    #     pagemargin (bottom)
    #     borderwidth (border below page)
    #     hull borderwidth (bottom)
    #     hull highlightthickness (bottom)
    #
    # canvasBorder is sum of top two elements.
    # tabBottom is sum of top five elements.
    #
    # Horizontal layout (and also vertical layout when tabpos is None):
    #     hull highlightthickness
    #     hull borderwidth
    #     borderwidth
    #     pagemargin
    #     the page itself
    #     pagemargin
    #     borderwidth
    #     hull borderwidth
    #     hull highlightthickness
    #
    def _layout(self):
        if not self.winfo_ismapped() or self._canvasSize is None:
            # Don't layout if the window is not displayed, or we
            # haven't yet received a <Configure> event.
            return

        hullWidth, hullHeight = self._canvasSize
        borderWidth = self._borderWidth
        canvasBorder = int(self._hull['borderwidth']) + \
            int(self._hull['highlightthickness'])
        if not self._withTabs:
            self.tabBottom = canvasBorder
        oldTabBottom = self.tabBottom

        if 'borderColor' in self._pending:
            self._lightBorderColor, self._darkBorderColor = \
                    Pmw.Color.bordercolors(self, self['hull_background'])

        # Draw all the tabs.
        if self._withTabs and ('tabs' in self._pending or
                'size' in self._pending):
            # Find total requested width and maximum requested height
            # of tabs.
            sumTabReqWidth = 0
            maxTabHeight = 0
            for pageInfo in list(self._pageAttrs.values()):
                sumTabReqWidth = sumTabReqWidth + pageInfo['tabreqwidth']
                if maxTabHeight < pageInfo['tabreqheight']:
                    maxTabHeight = pageInfo['tabreqheight']
            if maxTabHeight != 0:
                # Add the top tab border plus a bit for the angled corners
                self.tabBottom = canvasBorder + maxTabHeight + borderWidth * 1.5

            # Prepare for drawing the border around each tab button.
            tabTop = canvasBorder
            tabTop2 = tabTop + borderWidth
            tabTop3 = tabTop + borderWidth * 1.5
            tabBottom2 = self.tabBottom
            tabBottom = self.tabBottom + borderWidth

            numTabs = len(self._pageNames)
            availableWidth = hullWidth - 2 * canvasBorder - \
                numTabs * 2 * borderWidth
            x = canvasBorder
            cumTabReqWidth = 0
            cumTabWidth = 0

            # Position all the tabs.
            for pageName in self._pageNames:
                pageInfo = self._pageAttrs[pageName]
                (windowitem, lightshadow, darkshadow) = pageInfo['tabitems']
                if sumTabReqWidth <= availableWidth:
                    tabwidth = pageInfo['tabreqwidth']
                else:
                    # This ugly calculation ensures that, when the
                    # notebook is not wide enough for the requested
                    # widths of the tabs, the total width given to
                    # the tabs exactly equals the available width,
                    # without rounding errors.
                    cumTabReqWidth = cumTabReqWidth + pageInfo['tabreqwidth']
                    tmp = (2*cumTabReqWidth*availableWidth + sumTabReqWidth) \
                            / (2 * sumTabReqWidth)
                    tabwidth = tmp - cumTabWidth
                    cumTabWidth = tmp

                # Position the tab's button canvas item.
                self.coords(windowitem, x + borderWidth, tabTop3)
                self.itemconfigure(windowitem,
                    width = tabwidth, height = maxTabHeight)

                # Make a beautiful border around the tab.
                left = x
                left2 = left + borderWidth
                left3 = left + borderWidth * 1.5
                right = left + tabwidth + 2 * borderWidth
                right2 = left + tabwidth + borderWidth
                right3 = left + tabwidth + borderWidth * 0.5

                self.coords(lightshadow,
                    left, tabBottom2, left, tabTop2, left2, tabTop,
                    right2, tabTop, right3, tabTop2, left3, tabTop2,
                    left2, tabTop3, left2, tabBottom,
                    )
                self.coords(darkshadow,
                    right2, tabTop, right, tabTop2, right, tabBottom2,
                    right2, tabBottom, right2, tabTop3, right3, tabTop2,
                    )
                pageInfo['left'] = left
                pageInfo['right'] = right

                x = x + tabwidth + 2 * borderWidth

        # Redraw shadow under tabs so that it appears that tab for old
        # top page is lowered and that tab for new top page is raised.
        if self._withTabs and ('topPage' in self._pending or
                'tabs' in self._pending or 'size' in self._pending):

            if self.getcurselection() is None:
                # No pages, so draw line across top of page area.
                self.coords(self._pageTop1Border,
                    canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    borderWidth + canvasBorder, self.tabBottom + borderWidth,
                    )

                # Ignore second top border.
                self.coords(self._pageTop2Border, 0, 0, 0, 0, 0, 0)
            else:
                # Draw two lines, one on each side of the tab for the
                # top page, so that the tab appears to be raised.
                pageInfo = self._pageAttrs[self.getcurselection()]
                left = pageInfo['left']
                right = pageInfo['right']
                self.coords(self._pageTop1Border,
                    canvasBorder, self.tabBottom,
                    left, self.tabBottom,
                    left + borderWidth, self.tabBottom + borderWidth,
                    canvasBorder + borderWidth, self.tabBottom + borderWidth,
                    )

                self.coords(self._pageTop2Border,
                    right, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    right - borderWidth, self.tabBottom + borderWidth,
                    )

            # Prevent bottom of dark border of tabs appearing over
            # page top border.
            self.tag_raise(self._pageTop1Border)
            self.tag_raise(self._pageTop2Border)

        # Position the page border shadows.
        if 'size' in self._pending or oldTabBottom != self.tabBottom:

            self.coords(self._pageLeftBorder,
                canvasBorder, self.tabBottom,
                borderWidth + canvasBorder,
                    self.tabBottom + borderWidth,
                borderWidth + canvasBorder,
                    hullHeight - canvasBorder - borderWidth,
                canvasBorder, hullHeight - canvasBorder,
                )

            self.coords(self._pageBottomRightBorder,
                hullWidth - canvasBorder, self.tabBottom,
                hullWidth - canvasBorder, hullHeight - canvasBorder,
                canvasBorder, hullHeight - canvasBorder,
                borderWidth + canvasBorder,
                    hullHeight - canvasBorder - borderWidth,
                hullWidth - canvasBorder - borderWidth,
                    hullHeight - canvasBorder - borderWidth,
                hullWidth - canvasBorder - borderWidth,
                    self.tabBottom + borderWidth,
                )

            if not self._withTabs:
                self.coords(self._pageTopBorder,
                    canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder, self.tabBottom,
                    hullWidth - canvasBorder - borderWidth,
                        self.tabBottom + borderWidth,
                    borderWidth + canvasBorder, self.tabBottom + borderWidth,
                    )

        # Color borders.
        if 'borderColor' in self._pending:
            self.itemconfigure('lighttag', fill = self._lightBorderColor)
            self.itemconfigure('darktag', fill = self._darkBorderColor)

        newTopPage = self._pending.get('topPage')
        pageBorder = borderWidth + self._pageMargin

        # Raise new top page.
        if newTopPage is not None:
            self._topPageName = newTopPage
            self._topPageItem = self.create_window(
                pageBorder + canvasBorder, self.tabBottom + pageBorder,
                window = self._pageAttrs[newTopPage]['page'],
                anchor = 'nw',
                )

        # Change position of top page if tab height has changed.
        if self._topPageName is not None and oldTabBottom != self.tabBottom:
            self.coords(self._topPageItem,
                    pageBorder + canvasBorder, self.tabBottom + pageBorder)

        # Change size of top page if,
        #   1) there is a new top page.
        #   2) canvas size has changed, but not if there is no top
        #      page (eg:  initially or when all pages deleted).
        #   3) tab height has changed, due to difference in the height of a tab
        if (newTopPage is not None or \
                'size' in self._pending and self._topPageName is not None
                or oldTabBottom != self.tabBottom):
            self.itemconfigure(self._topPageItem,
                width = hullWidth - 2 * canvasBorder - pageBorder * 2,
                height = hullHeight - 2 * canvasBorder - pageBorder * 2 -
                    (self.tabBottom - canvasBorder),
                )

        self._pending = {}

# Need to do forwarding to get the pack, grid, etc methods.
# Unfortunately this means that all the other canvas methods are also
# forwarded.
Pmw.forwardmethods(NoteBook, tkinter.Canvas, '_hull')
