"""Implements a pop-up menu containing multiple clickable options.

See the :ref:`directoptionmenu` page in the programming manual for a more
in-depth explanation and an example of how to use this class.
"""

__all__ = ['DirectOptionMenu']

from panda3d.core import *
from direct.showbase import ShowBaseGlobal
from . import DirectGuiGlobals as DGG
from .DirectButton import *
from .DirectLabel import *
from .DirectFrame import *


class DirectOptionMenu(DirectButton):
    """
    DirectOptionMenu(parent) - Create a DirectButton which pops up a
    menu which can be used to select from a list of items.
    Execute button command (passing the selected item through) if defined
    To cancel the popup menu click anywhere on the screen outside of the
    popup menu.  No command is executed in this case.
    """
    def __init__(self, parent = None, **kw):
        # Inherits from DirectButton
        optiondefs = (
            # List of items to display on the popup menu
            ('items',       [],             self.setItems),
            # Initial item to display on menu button
            # Can be an integer index or the same string as the button
            ('initialitem', None,           DGG.INITOPT),
            # Amount of padding to place around popup button indicator
            ('popupMarkerBorder', (.1, .1), None),
            # The initial position of the popup marker
            ('popupMarker_pos', None, None),
            # Background color to use to highlight popup menu items
            ('highlightColor', (.5, .5, .5, 1), None),
            # Extra scale to use on highlight popup menu items
            ('highlightScale', (1, 1), None),
            # Alignment to use for text on popup menu button
            # Changing this breaks button layout
            ('text_align',  TextNode.ALeft, None),
            # Remove press effect because it looks a bit funny
            ('pressEffect',     0,          DGG.INITOPT),
           )
        # Merge keyword options with default options
        self.defineoptions(kw, optiondefs)
        # Initialize superclasses
        DirectButton.__init__(self, parent)
        # Record any user specified frame size
        self.initFrameSize = self['frameSize']
        # Create a small rectangular marker to distinguish this button
        # as a popup menu button
        self.popupMarker = self.createcomponent(
            'popupMarker', (), None,
            DirectFrame, (self,),
            frameSize = (-0.5, 0.5, -0.2, 0.2),
            scale = 0.4,
            relief = DGG.RAISED)
        # Record any user specified popup marker position
        self.initPopupMarkerPos = self['popupMarker_pos']
        # This needs to popup the menu too
        self.popupMarker.bind(DGG.B1PRESS, self.showPopupMenu)
        # Check if item is highlighted on release and select it if it is
        self.popupMarker.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
        # Make popup marker have the same click sound
        if self['clickSound']:
            self.popupMarker.guiItem.setSound(
                DGG.B1PRESS + self.popupMarker.guiId, self['clickSound'])
        else:
            self.popupMarker.guiItem.clearSound(DGG.B1PRESS + self.popupMarker.guiId)
        # This is created when you set the menu's items
        self.popupMenu = None
        self.selectedIndex = None
        self.highlightedIndex = None
        if 'item_text_scale' in kw:
            self._prevItemTextScale = kw['item_text_scale']
        else:
            self._prevItemTextScale = (1,1)
        # A big screen encompassing frame to catch the cancel clicks
        self.cancelFrame = self.createcomponent(
            'cancelframe', (), None,
            DirectFrame, (self,),
            frameSize = (-1, 1, -1, 1),
            relief = None,
            state = 'normal')
        # Make sure this is on top of all the other widgets
        self.cancelFrame.setBin('gui-popup', 0)
        self.cancelFrame.node().setBounds(OmniBoundingVolume())
        self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu)
        # Default action on press is to show popup menu
        self.bind(DGG.B1PRESS, self.showPopupMenu)
        # Check if item is highlighted on release and select it if it is
        self.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
        # Call option initialization functions
        self.initialiseoptions(DirectOptionMenu)
        # Need to call this since we explicitly set frame size
        self.resetFrameSize()

    def setItems(self):
        """
        self['items'] = itemList
        Create new popup menu to reflect specified set of items
        """
        # Remove old component if it exits
        if self.popupMenu != None:
            self.destroycomponent('popupMenu')
        # Create new component
        self.popupMenu = self.createcomponent('popupMenu', (), None,
                                              DirectFrame,
                                              (self,),
                                              relief = 'raised',
                                              )
        # Make sure it is on top of all the other gui widgets
        self.popupMenu.setBin('gui-popup', 0)
        self.highlightedIndex = None
        if not self['items']:
            return
        # Create a new component for each item
        # Find the maximum extents of all items
        itemIndex = 0
        self.minX = self.maxX = self.minZ = self.maxZ = None
        for item in self['items']:
            c = self.createcomponent(
                'item%d' % itemIndex, (), 'item',
                DirectButton, (self.popupMenu,),
                text = item, text_align = TextNode.ALeft,
                command = lambda i = itemIndex: self.set(i))
            bounds = c.getBounds()
            if self.minX == None:
                self.minX = bounds[0]
            elif bounds[0] < self.minX:
                self.minX = bounds[0]
            if self.maxX == None:
                self.maxX = bounds[1]
            elif bounds[1] > self.maxX:
                self.maxX = bounds[1]
            if self.minZ == None:
                self.minZ = bounds[2]
            elif bounds[2] < self.minZ:
                self.minZ = bounds[2]
            if self.maxZ == None:
                self.maxZ = bounds[3]
            elif bounds[3] > self.maxZ:
                self.maxZ = bounds[3]
            itemIndex += 1
        # Calc max width and height
        self.maxWidth = self.maxX - self.minX
        self.maxHeight = self.maxZ - self.minZ
        # Adjust frame size for each item and bind actions to mouse events
        for i in range(itemIndex):
            item = self.component('item%d' %i)
            # So entire extent of item's slot on popup is reactive to mouse
            item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
            # Move it to its correct position on the popup
            item.setPos(-self.minX, 0, -self.maxZ - i * self.maxHeight)
            item.bind(DGG.B1RELEASE, self.hidePopupMenu)
            # Highlight background when mouse is in item
            item.bind(DGG.WITHIN,
                      lambda x, i=i, item=item:self._highlightItem(item, i))
            # Restore specified color upon exiting
            fc = item['frameColor']
            item.bind(DGG.WITHOUT,
                      lambda x, item=item, fc=fc: self._unhighlightItem(item, fc))
        # Set popup menu frame size to encompass all items
        f = self.component('popupMenu')
        f['frameSize'] = (0, self.maxWidth, -self.maxHeight * itemIndex, 0)

        # Determine what initial item to display and set text accordingly
        if self['initialitem']:
            self.set(self['initialitem'], fCommand = 0)
        else:
            # No initial item specified, just use first item
            self.set(0, fCommand = 0)

        # Position popup Marker to the right of the button
        pm = self.popupMarker
        pmw = (pm.getWidth() * pm.getScale()[0] +
               2 * self['popupMarkerBorder'][0])
        if self.initFrameSize:
            # Use specified frame size
            bounds = list(self.initFrameSize)
        else:
            # Or base it upon largest item
            bounds = [self.minX, self.maxX, self.minZ, self.maxZ]
        if self.initPopupMarkerPos:
            # Use specified position
            pmPos = list(self.initPopupMarkerPos)
        else:
            # Or base the position on the frame size.
            pmPos = [bounds[1] + pmw/2.0, 0, bounds[2] + (bounds[3] - bounds[2])/2.0]
        pm.setPos(pmPos[0], pmPos[1], pmPos[2])
        # Adjust popup menu button to fit all items (or use user specified
        # frame size
        bounds[1] += pmw
        self['frameSize'] = (bounds[0], bounds[1], bounds[2], bounds[3])
        # Set initial state
        self.hidePopupMenu()

    def showPopupMenu(self, event = None):
        """
        Make popup visible and try to position it just to right of
        mouse click with currently selected item aligned with button.
        Adjust popup position if default position puts it outside of
        visible screen region
        """

        # Needed attributes (such as minZ) won't be set unless the user has specified
        # items to display. Let's assert that we've given items to work with.
        items = self['items']
        assert items and len(items) > 0, 'Cannot show an empty popup menu! You must add items!'

        # Show the menu
        self.popupMenu.show()
        # Make sure its at the right scale
        self.popupMenu.setScale(self, VBase3(1))
        # Compute bounds
        b = self.getBounds()
        fb = self.popupMenu.getBounds()
        # Position menu at midpoint of button
        xPos = (b[1] - b[0])/2.0 - fb[0]
        self.popupMenu.setX(self, xPos)
        # Try to set height to line up selected item with button
        self.popupMenu.setZ(
            self, self.minZ + (self.selectedIndex + 1)*self.maxHeight)
        # Make sure the whole popup menu is visible
        pos = self.popupMenu.getPos(ShowBaseGlobal.render2d)
        scale = self.popupMenu.getScale(ShowBaseGlobal.render2d)
        # How are we doing relative to the right side of the screen
        maxX = pos[0] + fb[1] * scale[0]
        if maxX > 1.0:
            # Need to move menu to the left
            self.popupMenu.setX(ShowBaseGlobal.render2d, pos[0] + (1.0 - maxX))
        # How about up and down?
        minZ = pos[2] + fb[2] * scale[2]
        maxZ = pos[2] + fb[3] * scale[2]
        if minZ < -1.0:
            # Menu too low, move it up
            self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (-1.0 - minZ))
        elif maxZ > 1.0:
            # Menu too high, move it down
            self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (1.0 - maxZ))
        # Also display cancel frame to catch clicks outside of the popup
        self.cancelFrame.show()
        # Position and scale cancel frame to fill entire window
        self.cancelFrame.setPos(ShowBaseGlobal.render2d, 0, 0, 0)
        self.cancelFrame.setScale(ShowBaseGlobal.render2d, 1, 1, 1)

    def hidePopupMenu(self, event = None):
        """ Put away popup and cancel frame """
        self.popupMenu.hide()
        self.cancelFrame.hide()

    def _highlightItem(self, item, index):
        """ Set frame color of highlighted item, record index """
        self._prevItemTextScale = item['text_scale']
        item['frameColor'] = self['highlightColor']
        item['frameSize'] = (self['highlightScale'][0]*self.minX, self['highlightScale'][0]*self.maxX, self['highlightScale'][1]*self.minZ, self['highlightScale'][1]*self.maxZ)
        item['text_scale'] = self['highlightScale']
        self.highlightedIndex = index

    def _unhighlightItem(self, item, frameColor):
        """ Clear frame color, clear highlightedIndex """
        item['frameColor'] = frameColor
        item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
        item['text_scale'] = self._prevItemTextScale
        self.highlightedIndex = None

    def selectHighlightedIndex(self, event = None):
        """
        Check to see if item is highlighted (by cursor being within
        that item).  If so, selected it.  If not, do nothing
        """
        if self.highlightedIndex is not None:
            self.set(self.highlightedIndex)
            self.hidePopupMenu()

    def index(self, index):
        intIndex = None
        if isinstance(index, int):
            intIndex = index
        elif index in self['items']:
            i = 0
            for item in self['items']:
                if item == index:
                    intIndex = i
                    break
                i += 1
        return intIndex

    def set(self, index, fCommand = 1):
        # Item was selected, record item and call command if any
        newIndex = self.index(index)
        if newIndex is not None:
            self.selectedIndex = newIndex
            item = self['items'][self.selectedIndex]
            self['text'] = item
            if fCommand and self['command']:
                # Pass any extra args to command
                self['command'](*[item] + self['extraArgs'])

    def get(self):
        """ Get currently selected item """
        return self['items'][self.selectedIndex]

    def commandFunc(self, event):
        """
        Override popup menu button's command func
        Command is executed in response to selecting menu items
        """
        pass
