//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.flex {

import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;

import flash.events.Event;
import flash.events.IEventDispatcher;

import flash.geom.Point;
import flash.geom.Rectangle;

import mx.controls.Menu;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuListData;

import mx.core.mx_internal;
import mx.core.Application;
import mx.core.ClassFactory;
import mx.core.IFlexDisplayObject;
import mx.core.ScrollPolicy;

import mx.events.MenuEvent;

import mx.managers.PopUpManager;

import com.threerings.util.CommandEvent;

import com.threerings.flex.PopUpUtil;

import com.threerings.flex.menuClasses.CommandMenuItemRenderer;
import com.threerings.flex.menuClasses.CommandListData;

use namespace mx_internal;

/**
 * A pretty standard menu that can submit CommandEvents if menu items have "command" and possibly
 * "arg" properties. Commands are submitted to controllers for processing. Alternatively, you may
 * specify "callback" properties that specify a function closure to call, with the "arg" property
 * containing either a single arg or an array of args.
 *
 * Another property now supported is "iconObject", which is an already-instantiated
 * IFlexDisplayObject to use as the icon. This will only be applied if the "icon" property
 * (which specifies a Class) is not used.
 *
 * Example dataProvider array:
 * [ { label: "Go home", icon: homeIconClass,
 *     command: Controller.GO_HOME, arg: homeId },
 *   { type: "separator"},
     { label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
 *   { label: "Other places", children: subMenuArray }
 * ];
 *
 * See "Defining menu structure and data" in the Flex manual for the full list.
 *
 * CommandMenu is scrollable.  This can be forced by setting verticalScrollPolicy to 
 * ScrollPolicy.AUTO or ScrollPolicy.ON and setting the maxHeight.  Also, if the scrolling isn't
 * forced on, but the menu does not fit within the vertical bounds given (either the stage size by
 * default, or the height of the rectangle given in setBounds()), scrolling will be turned on
 * so that none of the content is lost.
 *
 * Note: we don't extend flexlib's ScrollableMenu (or its sub-class ScrollableArrowMenu) because it
 * does some weird stuff in measure() that forces some of our menus down to a very small height...
 */
public class CommandMenu extends Menu
{
    /**
     * Factory method to create a command menu.
     *
     * @param items an array of menu items.
     * @param dispatcher an override event dispatcher to use for command events, rather than
     * our parent.
     */
    public static function createMenu (
        items :Array, dispatcher :IEventDispatcher = null) :CommandMenu
    {
        var menu :CommandMenu = new CommandMenu();
        menu.owner = DisplayObjectContainer(Application.application);
        menu.tabEnabled = false;
        menu.showRoot = true;
        menu.setDispatcher(dispatcher);
        Menu.popUpMenu(menu, null, items); // does not actually pop up, but needed.
        return menu;
    }

    /**
     * The mx.controls.Menu class overrides setting and getting the verticalScrollPolicy
     * basically setting the verticalScrollPolicy did nothing, and getting it always returned
     * ScrollPolicy.OFF.  So that's not going to work if we want the menu to scroll. Here we 
     * reinstate the verticalScrollPolicy setter, and keep a local copy of the value in a 
     * protected variable _verticalScrollPolicy.
     * 
     * This setter is basically a copy of what ScrollControlBase and ListBase do.
     */
    override public function set verticalScrollPolicy (value :String) :void 
    {
        var newPolicy :String = value.toLowerCase();

        itemsSizeChanged = true;

        if (_verticalScrollPolicy != newPolicy)
        {
            _verticalScrollPolicy = newPolicy;
            dispatchEvent(new Event("verticalScrollPolicyChanged"));
        }

        invalidateDisplayList();
    }

    override public function get verticalScrollPolicy () :String
    {
        return _verticalScrollPolicy;
    }

    public function CommandMenu ()
    {
        super();

        itemRenderer = new ClassFactory(getItemRenderer());

        verticalScrollPolicy = ScrollPolicy.OFF;
        addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
    }

    /** 
     * Called in the CommandMenu constructor, this should return the item rendering class for this
     * CommandMenu.
     */
    protected function getItemRenderer () :Class
    {
        return CommandMenuItemRenderer;
    }

    /**
     * Configures the event dispatcher to be used when dispatching this menu's command events.
     * Normally they will be dispatched on our parent (usually the SystemManager or something).
     */
    public function setDispatcher (dispatcher :IEventDispatcher) :void
    {
        _dispatcher = dispatcher;
    }

    /**
     * Sets a Rectangle (in stage coords) that the menu will attempt to keep submenus positioned 
     * within.  If a submenu is too large to fit, it will position it in the lower right corner
     * of this Rectangle (or top if popping upwards, or left if popping leftwards).   This
     * value defaults to the stage bounds.
     */
    public function setBounds (fitting :Rectangle) :void
    {
        _fitting = fitting;
    }

    /**
     * Actually pop up the menu. This can be used instead of show().
     */
    public function popUp (
        trigger :DisplayObject, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        var r :Rectangle = trigger.getBounds(trigger.stage);
        show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
    }

    /**
     * Shows the menu at the specified mouse coordinates.
     */
    public function popUpAt (
        mx :int, my :int, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(mx, my);
    }

    /**
     * Shows the menu at the current mouse location.
     */
    public function popUpAtMouse (popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(); // our show, with no args, pops at the mouse
    }

    /**
     * Just like our superclass's show(), except that when invoked with no args, causes the menu to
     * show at the current mouse location instead of the top-left corner of the application.
     * Also, we ensure that the resulting menu is in-bounds.
     */
    override public function show (xShow :Object = null, yShow :Object = null) :void
    {
        if (xShow == null) {
            xShow = DisplayObject(Application.application).mouseX;
        }
        if (yShow == null) {
            yShow = DisplayObject(Application.application).mouseY;
        }

        super.show(xShow, yShow);

        // reposition now that we know our size
        if (_lefting) {
            y = x - getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            y = y - getExplicitOrMeasuredHeight();
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(this, fitting);

        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (y < fitting.y) {
            verticalScrollPolicy = ScrollPolicy.AUTO;
            maxHeight = fitting.height;
            y = fitting.y;
        }
    }

    /** 
     * The Menu class overrode configureScrollBars() and made the function do nothing.  That means
     * the scrollbars don't know how to draw themselves, so here we reinstate configureScrollBars.
     * This is basically a copy of the same method from the mx.controls.List class.
     */
    override protected function configureScrollBars () :void
    {
        var rowCount :int = listItems.length;
        if (rowCount == 0) {
            return;
        }

        // if there is more than one row and it is a partial row we don't count it
        if (rowCount > 1 && 
            rowInfo[rowCount - 1].y + rowInfo[rowCount - 1].height > listContent.height) {
            rowCount--;
        }

        // offset, when added to rowCount, is hte index of the dataProvider item for that row.  
        // IOW, row 10 in listItems is showing dataProvider item 10 + verticalScrollPosition - 
        // lockedRowCount - 1
        var offset :int = verticalScrollPosition - lockedRowCount - 1;

        // don't count filler rows at the bottom either.
        var fillerRows :int = 0;
        while (rowCount > 0 && listItems[rowCount - 1].length == 0)
        {
            if (collection && rowCount + offset >= collection.length) {
                rowCount--;
                fillerRows++;
            } else {
                break;
            }
        }

        var colCount :int = listItems[0].length;
        var oldHorizontalScrollBar :Object = horizontalScrollBar;
        var oldVerticalScrollBar :Object = verticalScrollBar;
        var roundedWidth :int = Math.round(unscaledWidth);
        var length :int = collection ? collection.length - lockedRowCount : 0;
        var numRows :int = rowCount - lockedRowCount;

        setScrollBarProperties(Math.round(listContent.width), roundedWidth, length, numRows);
        maxVerticalScrollPosition = Math.max(length - numRows, 0);
    }

    /**
     * Callback for MenuEvent.ITEM_CLICK.
     */
    protected function itemClicked (event :MenuEvent) :void
    {
        var arg :Object = getItemProp(event.item, "arg");
        var cmdOrFn :Object = getItemProp(event.item, "command");
        if (cmdOrFn == null) {
            cmdOrFn = getItemProp(event.item, "callback");
        }
        if (cmdOrFn != null) {
            event.stopImmediatePropagation();
            CommandEvent.dispatch(_dispatcher == null ? mx_internal::parentDisplayObject :
                                  _dispatcher, cmdOrFn, arg);
        }
        // else: no warning. There may be non-command menu items mixed in.
    }

    // from ScrollableArrowMenu..
    override mx_internal function openSubMenu (row :IListItemRenderer) :void
    {
        supposedToLoseFocus = true;

        var r :Menu = getRootMenu();
        var menu :CommandMenu;

        // check to see if the menu exists, if not create it
        if (!IMenuItemRenderer(row).menu) {
            // the only differences between this method and the original method in mx.controls.Menu
            // are these few lines.
            menu = new CommandMenu();
            menu.maxHeight = this.maxHeight;
            menu.verticalScrollPolicy = this.verticalScrollPolicy;
            menu.variableRowHeight = this.variableRowHeight;

            menu.parentMenu = this;
            menu.owner = this;
            menu.showRoot = showRoot;
            menu.dataDescriptor = r.dataDescriptor;
            menu.styleName = r;
            menu.labelField = r.labelField;
            menu.labelFunction = r.labelFunction;
            menu.iconField = r.iconField;
            menu.iconFunction = r.iconFunction;
            menu.itemRenderer = r.itemRenderer;
            menu.rowHeight = r.rowHeight;
            menu.scaleY = r.scaleY;
            menu.scaleX = r.scaleX;

            // if there's data and it has children then add the items
            if (row.data && _dataDescriptor.isBranch(row.data) &&
                    _dataDescriptor.hasChildren(row.data)) {
                menu.dataProvider = _dataDescriptor.getChildren(row.data);
            }
            menu.sourceMenuBar = sourceMenuBar;
            menu.sourceMenuBarItem = sourceMenuBarItem;

            IMenuItemRenderer(row).menu = menu;
            PopUpManager.addPopUp(menu, r, false);
        }

        super.openSubMenu(row);

        // if we're lefting, upping or fitting make sure our submenu does so as well
        var submenu :Menu = IMenuItemRenderer(row).menu;
        if (_lefting) {
            submenu.x -= submenu.getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            var displayObj :DisplayObject = DisplayObject(row);
            var rowLoc :Point = displayObj.localToGlobal(new Point(row.x, row.y));
            submenu.y = rowLoc.y - submenu.getExplicitOrMeasuredHeight() + displayObj.height;
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(submenu, fitting);
        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (submenu.y < fitting.y) {
            submenu.verticalScrollPolicy = ScrollPolicy.AUTO;
            submenu.maxHeight = fitting.height;
            submenu.y = fitting.y;
        }
    }

    override protected function measure () :void
    {
        super.measure();

        if (measuredHeight > this.maxHeight) {
            measuredHeight = this.maxHeight;
        }

        if (verticalScrollPolicy == ScrollPolicy.ON || verticalScrollPolicy == ScrollPolicy.AUTO) {
            if (verticalScrollBar) {
                measuredMinWidth = measuredWidth = measuredWidth + verticalScrollBar.minWidth;
            }
        }

        commitProperties();
    }

    // from List
    override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
    {
        // Oh, FFS.
        // We need to set up these "maxMeasuredIconWidth" fields on the MenuListData, but our
        // superclass has made those variables private.
        // We can get the values out of another MenuListData, so we just always call super()
        // to create one of those, and if we need to make a CommandListData, we construct one
        // from the fields in the MenuListData.

        var menuListData :MenuListData = super.makeListData(data, uid, rowNum) as MenuListData;

        var iconObject :IFlexDisplayObject = getItemProp(data, "iconObject") as IFlexDisplayObject;
        if (iconObject != null) {
            var cmdListData :CommandListData = new CommandListData(menuListData.label,
                menuListData.icon, iconObject, labelField, uid, this, rowNum);

            cmdListData.maxMeasuredIconWidth = menuListData.maxMeasuredIconWidth;
            cmdListData.maxMeasuredTypeIconWidth = menuListData.maxMeasuredTypeIconWidth;
            cmdListData.maxMeasuredBranchIconWidth = menuListData.maxMeasuredBranchIconWidth;
            cmdListData.useTwoColumns = menuListData.useTwoColumns;
            return cmdListData;
        }

        return menuListData;
    }

    /**
     * Get the specified property for the specified item, if any.  Somewhat similar to bits in the
     * DefaultDataDescriptor.
     */
    protected function getItemProp (item :Object, prop :String) :Object
    {
        try {
            if (item is XML) {
                return String((item as XML).attribute(prop));

            } else if (prop in item) {
                return item[prop];
            }

        } catch (e :Error) {
            // alas; fall through
        }
        return null;
    }

    protected var _dispatcher :IEventDispatcher;
    protected var _lefting :Boolean = false;
    protected var _upping :Boolean = false;
    protected var _fitting :Rectangle = null;
    protected var _verticalScrollPolicy :String;
}
}
