###############################################################################
# Name: ed_shelf.py                                                           #
# Purpose: Editra Shelf container                                             #
# Author: Cody Precord <cprecord@editra.org>                                  #
# Copyright: (c) 2009 Cody Precord <staff@editra.org>                         #
# License: wxWindows License                                                  #
###############################################################################

"""
Shelf plugin and control implementation

@summary: Shelf Implementation

"""

__author__ = "Cody Precord <cprecord@editra.org>"
__svnid__ = "$Id: ed_shelf.py 72821 2012-10-29 22:48:49Z CJP $"
__revision__ = "$Revision: 72821 $"

#-----------------------------------------------------------------------------#
# Imports
import re
import wx

# Editra Imports
import ed_menu
import ed_glob
from profiler import Profile_Get
import ed_msg
import plugin
import iface
import ed_fmgr
from extern import aui
import ed_book
import ebmlib

#--------------------------------------------------------------------------#
# Globals
PGNUM_PAT = re.compile(' - [0-9]+')
_ = wx.GetTranslation

#-----------------------------------------------------------------------------#

class Shelf(plugin.Plugin):
    """Plugin that creates a notebook for holding the various Shelf items
    implemented by L{ShelfI}.

    """
    SHELF_NAME = u'Shelf'
    observers = plugin.ExtensionPoint(iface.ShelfI)

    def GetUiHandlers(self, delegate):
        """Gets the update ui handlers for the shelf's menu
        @param delegate: L{EdShelfDelegate} instance
        @return: [(ID, handler),]

        """
        handlers = [ (item.GetId(), delegate.UpdateShelfMenuUI)
                     for item in self.observers ]
        return handlers

    def Init(self, parent):
        """Mixes the shelf into the parent window
        @param parent: Reference to MainWindow
        @return: L{EdShelfDelegate}

        """
        # First check if the parent has an instance already
        mgr = parent.GetFrameManager()
        if mgr.GetPane(Shelf.SHELF_NAME).IsOk():
            return

        # HACK - fixes mouse event issues that result in wrong
        #        tab indexes being reported in the notebook.
        wrapper = ShelfWrapper(parent)
        shelf = wrapper.GetShelf()
        mgr.AddPane(wrapper,
                    ed_fmgr.EdPaneInfo().Name(Shelf.SHELF_NAME).\
                            Caption(_("Shelf")).Bottom().Layer(0).\
                            CloseButton(True).MaximizeButton(True).\
                            BestSize(wx.Size(500,250)))

        # Hide the pane and let the perspective manager take care of it
        mgr.GetPane(Shelf.SHELF_NAME).Hide()
        mgr.Update()

        # Create the delegate
        delegate = EdShelfDelegate(shelf, self)

        # Install Shelf menu under View and bind event handlers
        view = parent.GetMenuBar().GetMenuByName("view")
        menu = delegate.GetMenu()
        pos = 0
        for pos in range(view.GetMenuItemCount()):
            mitem = view.FindItemByPosition(pos)
            if mitem.GetId() == ed_glob.ID_PERSPECTIVES:
                break

        view.InsertMenu(pos + 1, ed_glob.ID_SHELF, _("Shelf"), 
                        menu, _("Put an item on the Shelf"))

        for item in menu.GetMenuItems():
            if item.IsSeparator():
                continue
            parent.Bind(wx.EVT_MENU, delegate.OnGetShelfItem, item)

        if menu.GetMenuItemCount() < 3:
            view.Enable(ed_glob.ID_SHELF, False)

        # Check for any other plugin specific install needs
        for observer in self.observers:
            if not observer.IsInstalled() and \
               hasattr(observer, 'InstallComponents'):
                observer.InstallComponents(parent)

        # Only Load Perspective if all items are loaded
        if delegate.StockShelf(Profile_Get('SHELF_ITEMS', 'list', [])):
            delegate.SetPerspective(Profile_Get('SHELF_LAYOUT', 'str', u""))
            delegate.SetSelection(Profile_Get('SHELF_SELECTION', 'int', -1))
        return delegate

#--------------------------------------------------------------------------#

class EdShelfBook(ed_book.EdBaseBook):
    ID_SHELF_SUBMENU = wx.NewId()
    ID_CLOSE_LIKE_TABS = wx.NewId()
    """Shelf notebook control"""
    def __init__(self, parent):
        style = aui.AUI_NB_BOTTOM | \
                aui.AUI_NB_TAB_SPLIT | \
                aui.AUI_NB_SCROLL_BUTTONS | \
                aui.AUI_NB_CLOSE_ON_ACTIVE_TAB | \
                aui.AUI_NB_TAB_MOVE | \
                aui.AUI_NB_DRAW_DND_TAB
        super(EdShelfBook, self).__init__(parent, style=style)

        # Attributes
        self._open = dict()
        self._name2idx = dict() # For settings maintenance
        self._menu = ebmlib.ContextMenuManager()
        self._mcback = None
        self._mw = None

        # Event Handlers
        # HACK: binding to self works differently than parent catching it
        #       binding here causes incorrect mouse coords to be sent to
        #       event handler during click events!
#        self.Bind(aui.EVT_AUINOTEBOOK_TAB_RIGHT_UP, self.OnTabRightUp)
        self.Bind(aui.EVT_AUINOTEBOOK_BG_RIGHT_UP, self.OnBgRightUp)
        self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnItemClose)
        self.Bind(wx.EVT_MENU, self.OnTabMenu)
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy, self)

        # Message handlers
        ed_msg.Subscribe(self.OnUpdateTabs, ed_msg.EDMSG_THEME_NOTEBOOK)
        ed_msg.Subscribe(self.OnUpdateTabs, ed_msg.EDMSG_THEME_CHANGED)

    def OnDestroy(self, evt):
        if evt.GetId() == self.GetId():
            self._menu.Clear()
            ed_msg.Unsubscribe(self.OnUpdateTabs)
        evt.Skip()

    def OnTabRightUp(self, evt):
        """Tab right click handler"""
        self._menu.Clear()
        if self.MenuCallback:
            sel = self.GetPageIndex(evt.Page)
            s_lbl = self.GetPageText(sel)
            s_lbl = s_lbl.rsplit('-')[0].strip()
            tab_menu = ed_menu.EdMenu()
            tab_menu.Append(EdShelfBook.ID_CLOSE_LIKE_TABS,
                            _("Close All '%s'") % s_lbl)
            self._menu.AddHandler(EdShelfBook.ID_CLOSE_LIKE_TABS,
                                  lambda tab, evt: self.CloseAll(s_lbl))
            tab_menu.AppendSeparator()
            shelf_menu = self.MenuCallback()
            tab_menu.AppendMenu(EdShelfBook.ID_SHELF_SUBMENU, 
                                _("Open"), shelf_menu)
            self._menu.Menu = tab_menu
            self.PopupMenu(self._menu.Menu)

    def OnBgRightUp(self, evt):
        """Show context menu"""
        self._menu.Clear()
        if self.MenuCallback:
            self._menu.Menu = self.MenuCallback()
            self.PopupMenu(self._menu.Menu)

    def OnTabMenu(self, evt):
        """Handle tab menu events"""
        handler = self._menu.GetHandler(evt.Id)
        if handler is not None:
            handler(self.GetCurrentPage(), evt)
        else:
            evt.Skip()

    MainWindow = property(lambda self: self._mw,
                          lambda self, mw: setattr(self, '_mw', mw))
    BitmapCallbacks = property(lambda self: self._name2idx)
    MenuCallback = property(lambda self: self._mcback,
                            lambda self, funct: setattr(self, '_mcback', funct))

    def AddItem(self, item, name, bmp=wx.NullBitmap):
        """Add an item to the shelf's notebook. This is useful for interacting
        with the Shelf from outside its interface. It may be necessary to
        call L{EnsureShelfVisible} before or after adding an item if you wish
        the shelf to be shown when the item is added.
        @param item: A panel like instance to add to the shelf's notebook
        @param name: Items name used for page text in notebook
        @keyword bmp: Tab bitmap to display

        """
        self.AddPage(item, 
                     u"%s - %d" % (name, self._open.get(name, 0)),
                     select=True)

        # Set the tab icon
        self.SetPageBitmap(self.GetPageCount()-1, bmp)
        self._open[name] = self._open.get(name, 0) + 1

    def OnItemClose(self, evt):
        """Callback for when pages are closed"""
#        name = self.GetPageText(evt.GetSelection())
#        name = name.rsplit('-')[0].strip()
#        self._open[name] = self._open.get(name, 1) - 1
        evt.Skip()

    def CloseAll(self, name):
        """Close all of 'name' shelf items
        @param name: shelf item name (string)

        """
        idxs = self.GetAllIndexes(name)
        idxs.reverse()
        for idx in idxs:
            self.DeletePage(idx)

    def GetAllIndexes(self, name):
        """Get all the indexes of pages of the item identified
        by 'name'.
        @param name: shelf item name (string)

        """
        idxs = list()
        for pg in range(self.GetPageCount()):
            lbl = self.GetPageText(pg)
            lbl = PGNUM_PAT.sub(u"", lbl)
            if lbl == name:
                idxs.append(pg)
        return idxs

    def EnsureShelfVisible(self):
        """Make sure the Shelf is visible
        @precondition: Shelf.Init has been called
        @postcondition: Shelf is shown

        """
        mgr = self.MainWindow.GetFrameManager()
        pane = mgr.GetPane(Shelf.SHELF_NAME)
        if not pane.IsShown():
            pane.Show()
            mgr.Update()

    def GetCount(self, item_name):
        """Get the number of open instances of a given Shelf Item
        @param item_name: Name of the Shelf item
        @return: number of instances on the Shelf

        """
        count = 0
        for page in range(self.GetPageCount()):
            if self.GetPageText(page).startswith(item_name):
                count = count + 1
        return count

    def GetMainWindow(self):
        """Get the main window that this shelf instance was created for
        @return: ed_main.MainWindow

        """
        return self.MainWindow

    def GetOpen(self):
        """Get the list of open shelf items
        @return: list

        """
        return self._open

    def Hide(self):
        """Hide the shelf
        @postcondition: Shelf is hidden by aui manager

        """
        mgr = self.MainWindow.GetFrameManager()
        pane = mgr.GetPane(Shelf.SHELF_NAME)
        if pane.IsOk():
            pane.Hide()
            mgr.Update()

    def ItemIsOnShelf(self, item_name):
        """Check if at least one instance of a given item
        is currently on the Shelf.
        @param item_name: name of Item to look for

        """
        for page in range(self.GetPageCount()):
            if self.GetPageText(page).startswith(item_name):
                return True
        return False

    def IsShown(self):
        """Is the shelf visible?
        @return: bool

        """
        mgr = self.MainWindow.GetFrameManager()
        pane = mgr.GetPane(Shelf.SHELF_NAME)
        if pane.IsOk():
            return pane.IsShown()
        else:
            return False

    def OnUpdateTabs(self, msg):
        """Update all tab images depending upon current settings"""
        if not self:
            return
        
        if not Profile_Get('TABICONS', default=True):
            for page in range(self.GetPageCount()):
                self.SetPageBitmap(page, wx.NullBitmap)
        else:
            # Show the icons
            for pnum in range(self.GetPageCount()):
                page = self.GetPage(pnum)
                bmp = self.BitmapCallbacks.get(repr(page.__class__), lambda:wx.NullBitmap)()
                self.SetPageBitmap(pnum, bmp)

#--------------------------------------------------------------------------#

class EdShelfDelegate(object):
    """Delegate class to mediate between the plugin singleton object and the
    UI implementation.

    """
    def __init__(self, shelf, pobject):
        """Create the delegate object
        @param shelf: Ui component instance
        @param pobject: Reference to the plugin object

        """
        super(EdShelfDelegate, self).__init__()

        # Attributes
        self._log = wx.GetApp().GetLog()
        self._shelf = shelf
        self._pin = pobject

        # Setup
        self._shelf.MenuCallback = getattr(self, 'GetShelfObjectMenu')

    @property
    def observers(self):
        return self._pin.observers

    def AddItem(self, item, name, bmp=wx.NullBitmap):
        """Add an item to the shelf"""
        self._shelf.AddItem(item, name, bmp)

    def CanStockItem(self, item_name):
        """See if a named item can be stocked or not, meaning if it
        can be saved and opened in the next session or not.
        @param item_name: name of item to check
        @return: bool whether item can be stocked or not

        """
        for item in self.observers:
            if item_name == item.GetName():
                if hasattr(item, 'IsStockable'):
                    return item.IsStockable()
                else:
                    break
        return False

    def EnsureShelfVisible(self):
        """Ensure the shelf is visible"""
        self._shelf.EnsureShelfVisible()

    def GetItemById(self, itemid):
        """Get the shelf item by its id
        @param itemid: Shelf item id
        @return: reference to a ShelfI object

        """
        for item in self.observers:
            if item.GetId() == itemid:
                return item
        return None

    def GetItemId(self, item_name):
        """Get the id that identifies a given item
        @param item_name: name of item to get ID for
        @return: integer id or None if not found

        """
        for item in self.observers:
            if item_name == item.GetName():
                return item.GetId()
        return None

    def GetItemStack(self):
        """Returns a list of ordered named items that are open in the shelf
        @return: list of strings

        """
        rval = list()
        if self._shelf is not None:
            for page in range(self._shelf.GetPageCount()):
                rval.append(re.sub(PGNUM_PAT, u'', 
                            self._shelf.GetPageText(page), 1))
        return rval

    def GetSelection(self):
        """Get the index of the currently selected tab"""
        if self._shelf:
            return self._shelf.GetSelection()
        return -1

    def GetPerspective(self):
        """Get the auinotebook perspective data
        @return: string

        """
        return self._shelf.SavePerspective()

    def SetPerspective(self, pdata):
        """Set the aui notebooks perspective and layout
        @param pdata: perspective data string

        """
        if pdata:
            try:
                self._shelf.LoadPerspective(pdata)
                self._shelf.Update()
            except Exception, msg:
                self._log("[shelf][err] Failed LoadPerspective: %s" % msg)

    def GetShelfObjectMenu(self):
        """Get the minimal menu that lists all Shelf objects
        without the 'Show Shelf' item.
        @return: ed_menu.EdMenu

        """
        menu = ed_menu.EdMenu()
        menu_items = list()
        open_items = self._shelf.GetOpen()
        for observer in self.observers:
            # Register Observers
            open_items[observer.GetName()] = 0
            try:
                menu_i = observer.GetMenuEntry(menu)
                if menu_i is not None:
                    menu_items.append((menu_i.GetItemLabel(), menu_i))
            except Exception, msg:
                self._log("[shelf][err] %s" % str(msg))
        menu_items.sort()

        combo = 0
        for item in menu_items:
            combo += 1
            shortcut = u""
            if combo < 10:
                shortcut = u"\tCtrl+Alt+" + unicode(combo)
            nitem = menu.Append(item[1].Id, item[1].GetText() + shortcut)
            if item[1].Bitmap.IsOk():
                nitem.SetBitmap(item[1].Bitmap)
            item[1].Destroy()
        return menu

    def GetMenu(self):
        """Return the menu of this object
        @return: ed_menu.EdMenu()

        """
        menu = self.GetShelfObjectMenu()
        menu.Insert(0, wx.ID_SEPARATOR)
        menu.Insert(0, ed_glob.ID_SHOW_SHELF, _("Show Shelf") + \
                    ed_menu.EdMenuBar.keybinder.GetBinding(ed_glob.ID_SHOW_SHELF), 
                    _("Show the Shelf"))
        return menu

    def GetOwnerWindow(self):
        """Return the L{ed_main.MainWindow} instance that owns/created
        this Shelf.
        @return: reference to ed_main.MainWindow or None

        """
        return self._shelf.GetMainWindow()

    def GetWindow(self):
        """Return reference to the Shelfs window component
        @return: AuiNotebook

        """
        return self._shelf

    def OnGetShelfItem(self, evt):
        """Handles menu events that have been registered
        by the Shelf Items on the Shelf.
        @param evt: Event that called this handler

        """
        e_id = evt.GetId()
        if e_id == ed_glob.ID_SHOW_SHELF:
            parent = self.GetOwnerWindow()
            if self._shelf.IsShown():
                self._shelf.Hide()
                nb = parent.GetNotebook()
                nb.GetCurrentCtrl().SetFocus()
            else:
                self._shelf.EnsureShelfVisible()
                mgr = parent.GetFrameManager()
                pane = mgr.GetPane(Shelf.SHELF_NAME)
                if pane is not None:
                    page = pane.window.Book.GetCurrentPage()
                    if hasattr(page, 'SetFocus'):
                        page.SetFocus()
        else:
            self.PutItemOnShelf(evt.GetId())

    def OnPutShelfItemAway(self, evt):
        """Handles when an item is closed
        @param evt: event that called this handler
        @todo: is this needed?

        """
        raise NotImplementedError

    def PutItemOnShelf(self, shelfid):
        """Put an item on the shelf by using its unique shelf id.
        This is only for use with loading items implementing the
        L{ShelfI} interface. See L{AddItem} if you wish to pass
        a panel to the shelf to add.
        @param shelfid: id of the ShelfItem to open

        """
        item = None
        for shelfi in self.observers:
            if shelfi.GetId() == shelfid:
                item = shelfi
                break

        if item is None:
            return

        name = item.GetName()
        if self._shelf.ItemIsOnShelf(name) and \
            not item.AllowMultiple() or self._shelf is None:
            return
        else:
            self.EnsureShelfVisible()
            # Guard against crashes in creating plugin derived objects
            # log error to log and continue running.
            window = None
            try:
                window = item.CreateItem(self._shelf)
            except Exception, msg:
                self._log("[shelf][err] CreateItem failed: %s" % msg)
                return

            bmp = wx.NullBitmap
            if hasattr(item, 'GetBitmap'):
                self._shelf.BitmapCallbacks[repr(window.__class__)] = item.GetBitmap
                bmp = item.GetBitmap()
            else:
                self._shelf.BitmapCallbacks[repr(window.__class__)] = lambda:wx.NullBitmap
            self.AddItem(window, name, bmp)

    def RaiseItem(self, item_name):
        """Set the selection in the notebook to be the that of the first
        instance of item_name that is found in the shelf.
        @param item_name: ShelfI name
        @return: reference to the selected page or None if no instance is

        """
        for page in range(self._shelf.GetPageCount()):
            if self._shelf.GetPageText(page).startswith(item_name):
                self._shelf.SetSelection(page)
                return self._shelf.GetPage(page)
        else:
            return None

    def RaiseWindow(self, window):
        """Set the selection in the notebook to be the that of the given
        window. Mostly used internally by items implementing L{ShelfI}.
        @param window: Window object
        @return: reference to the selected page or None if no instance is

        """
        for page in range(self._shelf.GetPageCount()):
            ctrl = self._shelf.GetPage(page)
            if window == ctrl:
                self._shelf.SetSelection(page)
                return ctrl
        else:
            return None

    def SetSelection(self, index):
        """Select an item in the Shelf window
        @param index: shelf tab index

        """
        if self._shelf and index > 0 and index < self._shelf.GetPageCount():
            try:
                self._shelf.SetSelection(index)
            except Exception, msg:
                self._log("[shelf][err] Failed SetSelection: %s" % msg)

    def StockShelf(self, i_list):
        """Fill the shelf by opening an ordered list of items
        @param i_list: List of named L{ShelfI} instances
        @return: bool (True if all loaded / False otherwise)

        """
        bLoaded = True
        for item in i_list:
            if self.CanStockItem(item):
                itemid = self.GetItemId(item)
                if itemid:
                    self.PutItemOnShelf(itemid)
                else:
                    bLoaded = False
            else:
                bLoaded = False
        return bLoaded

    def UpdateShelfMenuUI(self, evt):
        """Enable/Disable shelf items based on whether they support
        muliple instances or not.
        @param evt: wxEVT_UPDATEUI

        """
        item = self.GetItemById(evt.GetId())
        if item is None:
            evt.Skip()
            return

        count = self._shelf.GetCount(item.GetName())
        if count and not item.AllowMultiple():
            evt.Enable(False)
        else:
            evt.Enable(True)

#-----------------------------------------------------------------------------#

class ShelfWrapper(wx.Panel):
    def __init__(self, parent):
        super(ShelfWrapper, self).__init__(parent)

        self._shelf = EdShelfBook(self)
        self._shelf.MainWindow = parent

        # Setup
        sizer = wx.BoxSizer()
        sizer.Add(self._shelf, 1, wx.EXPAND)
        self.SetSizer(sizer)

        self.Bind(aui.EVT_AUINOTEBOOK_TAB_RIGHT_UP, self.OnTabRightUp)

    Book = property(lambda self: self._shelf)

    def GetShelf(self):
        return self._shelf

    def OnTabRightUp(self, evt):
        self._shelf.OnTabRightUp(evt)
