/*
 * jdMenu 1.3.beta2 (2007-03-06)
 *
 * Copyright (c) 2006,2007 Jonathan Sharp (http://jdsharp.us)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 * http://jdsharp.us/
 *
 * Built upon jQuery 1.1.1 (http://jquery.com)
 * This also requires the jQuery dimensions plugin
 */
(function($){
    // This will store an element list of all our menu objects
    var jdMenu = [];
    
    // Public methods
    $.fn.jdMenu = function(inSettings) {
        var settings = $.extend({}, arguments.callee.defaults, inSettings);
        return this.each(function() {
            jdMenu.push(this);
            $(this).addClass('jd_menu_flag_root');
            this.$settings = $.extend({}, settings, {isVerticalMenu: $(this).is('.jd_menu_vertical')});
            addEvents(this);
        });
    };
    $.fn.jdMenuShow = function() {
        return this.each(function() {
            showMenuLI.apply(this);
        });
    };
    $.fn.jdMenuHide = function() {
        return this.each(function() {
            hideMenuUL.apply(this);
        });
    };

    // Private methods and logic
    $(window)
        // Bind a click event to hide all visible menus when the document is clicked
        .bind('click', function(){
            $(jdMenu).find('ul:visible').jdMenuHide();
        })
        // Cleanup after ourself by nulling the $settings object
        .bind('unload', function() {
            $(jdMenu).each(function() {
                this.$settings = null;
            });
        });

    // These are our default settings for this plugin
    $.fn.jdMenu.defaults = {
        activateDelay: 750,
        showDelay: 150,
        hideDelay: 550,
        onShow: null,
        onHideCheck: null,
        onHide: null,
        onAnimate: null,
        onClick: null,
        offsetX: 4,
        offsetY: 2,
        iframe: $.browser.msie
    };
    
    // Our special parentsUntil method to get all parents up to and including the matched element
    $.fn.parentsUntil = function(match) {
        var a = [];
        $(this[0]).parents().each(function() {
            a.push(this);
            return !$(this).is(match);
        });
        return this.pushStack(a, arguments);
    };

    // Returns our settings object for this menu
    function getSettings(el) {
        return $(el).parents('ul.jd_menu_flag_root')[0].$settings;
    }

    // Unbind any events and then rebind them
    function addEvents(ul) {
        removeEvents(ul);
        $('> li', ul)
            .hover(hoverOverLI, hoverOutLI)
            .bind('click', itemClick)
            .find('> a.accessible')
                .bind('click', accessibleClick);
    };
    
    // Remove all events for this menu
    function removeEvents(ul) {
        $('> li', ul)
            .unbind('mouseover').unbind('mouseout')
            .unbind('click')
            .find('> a.accessible')
                .unbind('click');
    };
    
    function hoverOverLI() {
        var cls = 'jd_menu_hover' + ($(this).parent().is('.jd_menu_flag_root') ? '_menubar' : '');
        $(this).addClass(cls).find('> a').addClass(cls);
        
        if (this.$timer) {
            clearTimeout(this.$timer);
        }

        // Do we have a sub menu?
        if ($('> ul', this).size() > 0) {
            var settings = getSettings(this);
            
            // Which delay to use, the longer activate one or the shorter show delay if a menu is already visible
            var delay = ($(this).parents('ul.jd_menu_flag_root').find('ul:visible').size() == 0) 
                            ? settings.activateDelay : settings.showDelay;
            var t = this;
            this.$timer = setTimeout(function() {
                showMenuLI.apply(t);
            }, delay);
        }
    };
    
    function hoverOutLI() {
        // Remove both classes so we do not have to test which one we are
        $(this)	.removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
            .find('> a')
                .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar');
        
        if (this.$timer) {
            clearTimeout(this.$timer);
        }

        // TODO: Possible bug with our test for visibility in that parent menus are hidden child menus are not

        // If we have a visible menu, hide it
        if ($(this).is(':visible') && $('> ul', this).size() > 0) {
            var settings = getSettings(this);
            var ul = $('> ul', this)[0];
            this.$timer = setTimeout(function() {
                hideMenuUL.apply(ul);
            }, settings.hideDelay);
        }
    };
    
    // "this" is a reference to the LI element that contains 
    // the UL that will be shown
    function showMenuLI() {
        var ul = $('> ul', this).get(0);
        // We are already visible, just return
        if ($(ul).is(':visible')) {
            return false;
        }

        // Clear our timer if it exists
        if (this.$timer) {
            clearTimeout(this.$timer);
        }

        // Get our settings object
        var settings = getSettings(this);

        // Call our callback
        if (settings.onShow != null && settings.onShow.apply(this) == false) {
            return false;
        }

        // Add hover classes, needed for accessible functionality
        var isRoot = $(this).parent().is('.jd_menu_flag_root');
        var c = 'jd_menu_active' + (isRoot ? '_menubar' : '');
        $(this).addClass(c).find('> a').addClass(c);

        if (!isRoot) {
            // Add the active class to the parent list item which maybe our menubar
            var c = 'jd_menu_active' + ($(this).parent().parent().parent().is('.jd_menu_flag_root') ? '_menubar' : '');
            $(this).parent().parent().addClass(c).find('> a').addClass(c);
        }

        // Hide any existing menues at the same level
        $(this).parent().find('> li > ul:visible').not(ul).each(function() {
            hideMenuUL.apply(this);
        });

        addEvents(ul);

        // Our range object is used in calculating menu positions
        var Range = function(x1, x2, y1, y2) {
            this.x1	= x1;
            this.x2 = x2;
            this.y1 = y1;
            this.y2 = y2;
        }
        Range.prototype.contains = function(range) {
            return 	(this.x1 <= range.x1 && range.x2 <= this.x2) 
                    && 
                    (this.y1 <= range.y1 && range.y2 <= this.y2);
        }
        Range.prototype.transform = function(x, y) {
            return new Range(this.x1 + x, this.x2 + x, this.y1 + y, this.y2 + y);
        }
        Range.prototype.nudgeX = function(range) {
            if (this.x1 < range.x1) {
                return new Range(range.x1, range.x1 + (this.x2 - this.x1), this.y1, this.y2);
            } else if (this.x2 > range.x2) {
                return new Range(range.x2 - (this.x2 - this.x1), range.x2, this.y1, this.y2);
            }
            return this;
        }
        Range.prototype.nudgeY = function(range) {
            if (this.y1 < range.y1) {
                return new Range(this.x1, this.x2, range.y1, range.y1 + (this.y2 - this.y1));
            } else if (this.y2 > range.y2) {
                return new Range(this.x1, this.x2, range.y2 - (this.y2 - this.y1), range.y2);
            }
            return this;
        }

        // window width & scroll offset
        var sx = $(window).scrollLeft()
        var sy = $(window).scrollTop();
        var ww = $(window).innerWidth();
        var wh = $(window).innerHeight();

        var viewport = new Range(	sx, sx + ww, 
                                    sy, sy + wh);

        // "Show" our menu so we can calculate its width, set left and top so that it does not accidentally
        // go offscreen and trigger browser scroll bars
        $(ul).css({visibility: 'hidden', left: 0, top: 0}).show();

        var menuWidth		= $(ul).outerWidth();
        var menuHeight		= $(ul).outerHeight();

        // Get the LI parent UL outerwidth in case borders are applied to it
        var tp 				= $(this).parent();
        var thisWidth		= tp.outerWidth();
        var thisBorderWidth	= parseInt(tp.css('borderLeftWidth')) + parseInt(tp.css('borderRightWidth'));
        //var thisBorderTop 	= parseInt(tp.css('borderTopWidth'));
        var thisHeight		= $(this).outerHeight();
        var thisOffset 		= $(this).offset({border: false});

        $(ul).hide().css({visibility: ''});

        // We define a list of valid positions for our menu and then test against them to find one that works best
        var position = [];
    // Bottom Horizontal
        // Menu is directly below and left edges aligned to parent item
        position[0] = new Range(thisOffset.left, thisOffset.left + menuWidth, 
                                thisOffset.top + thisHeight, thisOffset.top + thisHeight + menuHeight);
        // Menu is directly below and right edges aligned to parent item
        position[1] = new Range((thisOffset.left + thisWidth) - menuWidth, thisOffset.left + thisWidth,
                                position[0].y1, position[0].y2);
        // Menu is "nudged" horizontally below parent item
        position[2] = position[0].nudgeX(viewport);

    // Right vertical
        // Menu is directly right and top edge aligned to parent item
        position[3] = new Range(thisOffset.left + thisWidth - thisBorderWidth, thisOffset.left + thisWidth - thisBorderWidth + menuWidth,
                                thisOffset.top, thisOffset.top + menuHeight);
        // Menu is directly right and bottom edges aligned with parent item
        position[4] = new Range(position[3].x1, position[3].x2, 
                                position[0].y1 - menuHeight, position[0].y1);
        // Menu is "nudged" vertically to right of parent item
        position[5] = position[3].nudgeY(viewport);

    // Top Horizontal
        // Menu is directly top and left edges aligned to parent item
        position[6] = new Range(thisOffset.left, thisOffset.left + menuWidth, 
                                thisOffset.top - menuHeight, thisOffset.top);
        // Menu is directly top and right edges aligned to parent item
        position[7] = new Range((thisOffset.left + thisWidth) - menuWidth, thisOffset.left + thisWidth,
                                position[6].y1, position[6].y2);
        // Menu is "nudged" horizontally to the top of parent item
        position[8] = position[6].nudgeX(viewport);
    
    // Left vertical
        // Menu is directly left and top edges aligned to parent item
        position[9] = new Range(thisOffset.left - menuWidth, thisOffset.left, 
                                position[3].y1, position[3].y2);
        // Menu is directly left and bottom edges aligned to parent item
        position[10]= new Range(position[9].x1, position[9].x2, 
                                position[4].y1 + thisHeight - menuHeight, position[4].y1 + thisHeight);
        // Menu is "nudged" vertically to left of parent item
        position[11]= position[10].nudgeY(viewport);

        // This defines the order in which we test our positions
        var order = [];
        if ($(this).parent().is('.jd_menu_flag_root') && !settings.isVerticalMenu) {
            order = [0, 1, 2, 6, 7, 8, 5, 11];
        } else {
            order = [3, 4, 5, 9, 10, 11, 0, 1, 2, 6, 7, 8];
        }

        // Set our default position (first position) if no others can be found
        var pos = order[0];
        for (var i = 0, j = order.length; i < j; i++) {
            // If this position for our menu is within the viewport of the browser, use this position
            if (viewport.contains(position[order[i]])) {
                pos = order[i];
                break;
            }
        }
        var menuPosition = position[pos];

        // Find if we are absolutely positioned or have an absolutely positioned parent
        $(this).add($(this).parents()).each(function() {
            if ($(this).css('position') == 'absolute') {
                var abs = $(this).offset();
                // Transform our coordinates to be relative to the absolute parent
                menuPosition = menuPosition.transform(-abs.left, -abs.top);
                return false;
            }
        });

        switch (pos) {
            case 3:
                menuPosition.y1 += settings.offsetY;
            case 4:
                menuPosition.x1 -= settings.offsetX;
                break;
            
            case 9:
                menuPosition.y1 += settings.offsetY;
            case 10:
                menuPosition.x1 += settings.offsetX;
                break;
        }

        if (settings.iframe) {
            $(ul).bgiframe();
        }

        if (settings.onAnimate) {
            $(ul).css({left: menuPosition.x1, top: menuPosition.y1});
            // The onAnimate method is expected to "show" the element it is passed
            settings.onAnimate.apply(ul, [true]);
        } else {
            $(ul).css({left: menuPosition.x1, top: menuPosition.y1}).show();
        }

        return true;
    }

    // "this" is a reference to a UL menu to be hidden
    function hideMenuUL(recurse) {
        if (!$(this).is(':visible')) {
            return false;
        }

        var settings = getSettings(this);

        // Test if this menu should get hidden
        if (settings.onHideCheck != null && settings.onHideCheck.apply(this) == false) {
            return false;
        }
        
        // Hide all of our child menus first
        $('> li ul:visible', this).each(function() {
            hideMenuUL.apply(this, [false]);
        });

        // If we are the root, do not hide ourself
        if ($(this).is('.jd_menu_flag_root')) {
            alert('We are root');
            return false;
        }

        var elms = $('> li', this).add($(this).parent());
        elms.removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
            .removeClass('jd_menu_active').removeClass('jd_menu_active_menubar')
            .find('> a')
                .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
                .removeClass('jd_menu_active').removeClass('jd_menu_active_menubar');

        removeEvents(this);
        $(this).each(function() {
            if (settings.onAnimate != null) {
                settings.onAnimate.apply(this, [false]);
            } else {
                $(this).hide();
            }
        }).find('> .bgiframe').remove();
        // Our callback for after our menu is hidden
        if (settings.onHide != null) {
            settings.onHide.apply(this);
        }

        // Recursively hide our parent menus
        if (recurse == true) {
            $(this).parentsUntil('ul.jd_menu_flag_root')
                    .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
                .not('.jd_menu_flag_root').filter('ul')
                    .each(function() {
                        hideMenuUL.apply(this, [false]);
                    });
        }

        return true;
    }

    // Prevent the default (usually following a link)
    function accessibleClick(e) {
        if ($(this).is('.accessible')) {
            // Stop the browser from the default link action allowing the 
            // click event to propagate to propagate to our LI (itemClick function)
            e.preventDefault();
        }
    }

    // Trigger a menu click
    function itemClick(e) {
        e.stopPropagation();

        var settings = getSettings(this);
        if (settings.onClick != null && settings.onClick.apply(this) == false) {
            return false;
        }

        if ($('> ul', this).size() > 0) {
            showMenuLI.apply(this);
        } else {
            if (e.target == this) {
                var link = $('> a', e.target).not('.accessible');
                if (link.size() > 0) {
                    var a = link.get(0);
                    if (!a.onclick) {
                        window.open(a.href, a.target || '_self');
                    } else {
                        $(a).click();
                    }
                }
            }
            
            hideMenuUL.apply($(this).parent(), [true]);
        }
    }
})(jQuery);
