/**
 * PopupMenu
 * 
 *  This library provides a popup layer beside on button. Popups are shown
 *  when the mouse cursor comes inside of related buttons.
 * 
 * @depends jQuery
 *
 * Example:
 * <script>
 * var mainmenu = new PopupMenu($(#mainmenu), 'up');
 * var submenu1 = new PopupMenu($(#submenu1), 'right');
 * var submenu2 = new PopupMenu($(#submenu2), 'right');
 *
 * mainmenu.bind_to($('#windows_start'));
 * submenu1.append_to(mainmenu).bind_to($('#mainmenu span.item1'));
 * submenu2.append_to(mainmenu).bind_to($('#mainmenu span.item2'));
 * </script>
 *
 * <div id="mainmenu">
 *   <span class="item1">Programs...</span>
 *   <span class="item2">Documents...</span>
 *   <a class="item3" href="...">Shutdown</a>
 * </div>
 * 
 * <div id="submenu1">
 *   <a href="...">Notepad</a>
 *   <a href="...">Explorer</a>
 *   <a href="...">Outlook</a>
 * </div>
 * 
 * <div id="submenu2">
 *   <a href="...">My Picture</a>
 *   <a href="...">My Music</a>
 * </div>
 * 
 * <a id="windows_start" href="">Start</a>
 * 
 * Copyright (c) 2008 Hisateru Tanaka.
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 */
(function() {
    /**
     * Constructor
     * @param popup jQuery node
     * @param layout left,right,up,down or custom function
     * @param duration for effect animation
     */
    window.PopupMenu = function(popup, layout, duration) {
        this.popup = popup;
        this.layout = layout;
        this.duration  = duration != undefined ? duration : 200;
        this.children = [];
        this.parent = null;
        
        if(!is_added(popup)){
            popup.appendTo('body');
        }
        popup.css('overflow', 'hidden');
        popup.css('position', 'absolute');
        popup.css('display',  'none');
    };
    
    /*
     * private 
     */
    function is_added(node){
        var p = node.get()[0].parentNode;
        while(p && p != window) {
            //alert(repr(p));
            if(p == document.body){
                return true;
            }
            p = p.parentNode;
        }
        return false;
    }
    
    var proto = window.PopupMenu.prototype;
    
    /*
     * static fields
     */
    proto.default_layouts = {
        left: function(button, popup){
            return {
                hide:{left:0, top:0, width:0, height:0},
                show:{left:-popup.width, top:0, width:popup.width, height:popup.height}};
        },
        right: function(button, popup){
            return {
                hide:{left:button.width(), top:0, width:0, height:0},
                show:{left:button.width(), top:0, width:popup.width, height:popup.height}};
        },
        up: function(button, popup){
            return {
                hide:{left:0, top:0, width:popup.width, height:0},
                show:{left:0, top:-popup.height, width:popup.width, height:popup.height}};
        },
        down: function(button, popup){
            return {
                hide:{left:0, top:button.height(), width:popup.width, height:0},
                show:{left:0, top:button.height(), width:popup.width, height:popup.height}};
        }
    };
    
    /**
     * binds submenu to button. submenu is adjusted
     *
     * @param button trigger jQuery node to popup me.
     */
    proto.bind_to = function(button) {
        var self = this;
        var popup = this.popup;
        
        // layouting methods
        var initial_state = {
            width:popup.width(),
            height:popup.height()
        };
        function default_layout_of(name){
            for(k in self.default_layouts){
                if(k==name){
                    return self.default_layouts[k];
                }
            }
            return self.default_layouts['down'];  //default
        }
        function layout_info(){
            return $.isFunction(self.layout) ? self.layout(button, initial_state) :
                (default_layout_of(self.layout))(button, initial_state);
        }
        function base_position(){
            var pos = {left:0, top:0};
            pos = button.position();
            if(self.parent){
                var parentpos = self.parent.popup.position();
                pos.left += parentpos.left;
                pos.top  += parentpos.top;
            }
            return pos;
        }
        function shown_state() {
            var base = base_position();
            var lo = layout_info();
            return {
                left:  base.left + lo.show.left,
                top:   base.top  + lo.show.top,
                width: lo.show.width,
                height:lo.show.height,
                opacity: 1.0
            };
        }
        function hidden_state() {
            var base = base_position();
            var lo = layout_info();
            return {
                left:  base.left + lo.hide.left,
                top:   base.top  + lo.hide.top,
                width: lo.hide.width,
                height:lo.hide.height,
                opacity: 0.0
            };
        }
        
        // effect implements for showing and hiding
        var shown = false;
        function show_popup() {
            if(!shown){
                popup.stop();
                popup.css(hidden_state());
                popup.css('display', 'block');
                popup.animate(shown_state(), self.duration);
                shown = true;
                regist_triggers();
            }
        }
        
        function hide_popup() {
            if(shown){
                popup.stop();
                popup.css(shown_state());
                popup.animate(hidden_state(), self.duration, null, function(){
                    popup.css('display', 'none');
                });
                shown = false;
                unregist_triggers();
            }
        }
        
        var mouse_inside = false;
        function delay_hide_popup() {
            setTimeout(function(){
                if(!mouse_inside){ hide_popup(); }
            }, 10);
        }
        
        // binding event to handler
        function mouse_enter(){ mouse_inside = true; }
        function mouse_leave(){ mouse_inside = false; delay_hide_popup(); }
        function regist_trigger_element(elem){
            elem.bind('mouseover', mouse_enter);
            elem.bind('mouseout', mouse_leave);
        }
        function unregist_trigger_element(elem){
            elem.unbind('mouseover', mouse_enter);
            elem.unbind('mouseout', mouse_leave);
        }
        function all_children_recursive(obj) {
            var children = [];
            $.each(obj.children, function(i,e) {
                children.push(e);
                $.merge(e.children, all_children_recursive(e));
            });
            return children;
        }
        function regist_triggers() {
            regist_trigger_element(button);
            regist_trigger_element(popup);
            $.each(all_children_recursive(self), function(i,e){
                regist_trigger_element(e.popup);
            });
        }
        function unregist_triggers() {
            unregist_trigger_element(button);
            unregist_trigger_element(popup);
            $.each(all_children_recursive(self), function(i,e){
                regist_trigger_element(e.popup);
            });
        }
        
        button.bind('mouseover', show_popup);
        
        return this;
    };
    
    /**
     * append child menu to me.
     * @param child PopupMenu instance
     */
    proto.append = function(child) {
        child.parent = this;
        this.children.push(child);
        return this;
    }
    
    /**
     * append me to parent menu.
     * @param parent PopupMenu instance
     */
    proto.append_to = function(parent) {
        parent.children.push(this);
        this.parent = parent;
        return this;
    }
    
    /* utils */
    function _optional_argument(variable_args_part, detector, evaluter, default_value) {
        var found = false;
        var value;
        $.each(variable_args_part, function(i,e){
            if(!found && detector(e)){
                value = e;
                found = true;
            }
        });
        return found ? evaluter(value) : default_value;
    }
    
    function _true_arguments_array(args) {
        var parsed = [];
        $.each(args, function(i,e){
            parsed[i] = e;
        });
        return parsed;
    }
    
    function repr(v, sharrow){
        switch(typeof v){
            case 'object':
                if(sharrow){
                    return '{...}';
                }else{
                    var props = [];
                    for(var k in v) { props.push(k + ':' + repr(v[k], true)); }
                    return '{' + props.join(', ') + '}';
                }
            case 'function':
                return 'function(){...}';
            case 'string':
                v = v.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
                return '"' + v.substr(0,30) + (v.length > 30 ? "..." : "") + '"';
            default:
                s = String(v);
        }
    }
})();
