/*
 * jdMenu 1.3.beta2 (2007-03-06) (mod by dimka)
 *
 * Copyright (c) 2006,2007 Jonathan Sharp (http://jdsharp.us)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 */
(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')});
      this.$settings = $.extend({}, settings);
      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: 2,
    showDelay: 0,
    hideDelay: 500,
    onShow: null,
    onHideCheck: null,
    onHide: null,
    onAnimate: null,
    onClick: null,
    offsetX: -1,
    offsetY: 0,
    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)
      .bind('mouseenter', hoverOverLI).bind('mouseleave', hoverOutLI)
      .bind('click', itemClick)
      .find('> a.accessible')
      .bind('click', accessibleClick);
  };

  // Remove all events for this menu
  function removeEvents(ul) {
    $('> li', ul)
      .unbind('mouseenter').unbind('mouseleave')
      .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).width();
    var wh = $(window).height();

    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);
