
function MenuImplementation() {

  this.Container = function(ownerNode) {
    var bodyHtmlElement = document.getElementsByTagName("body")[0];

    this.contains = function (testHtmlElement) {
      while (testHtmlElement) {
        if (testHtmlElement == ownerNode) {
          return true;
        }
        testHtmlElement = testHtmlElement.parentNode;
      }
      return false;
    }
    this.getBoundingBox = function () {
      var boundingBox = { left: 0, right: 0, top: 0, bottom: 0 },
          currNode = ownerNode;

      while(currNode) {
        boundingBox.left += currNode.offsetLeft;
        boundingBox.top += currNode.offsetTop;

        currNode = currNode.offsetParent;
      }

      with (boundingBox) {
        right = left + ownerNode.offsetWidth;
        bottom = top + ownerNode.offsetHeight;
      }

      return boundingBox;
    }
  }

  this.custom = { };

  /* -- Custom implementations -- */

  this.custom.DropDown = function (node) {

    var context = this,
        group = node,
        childNodes = node.childNodes,
        hideDelay = node.data.hideDelay,
        baseZIndex = node.data.baseZIndex,
        delayedHideAll = new DelayedStackClearer(null, hideDelay),
        // Stack:
        stack = new menuData.Stack(),
        stackClear = stack.clear,
        stackIsPresent = stack.isPresent,
        stackPut = stack.put,
        // ---
        Container = menuImplementation.Container,
        bodyHtmlElement,
        lastInsertedHtmlElement;

    function DelayedCaller(funcRef, delay) {
      var timerId;

      this.scheduleCall = function () {
        timerId = setTimeout(funcRef, delay);
      }
      this.cancelCall = function () {
        clearTimeout(timerId);
      }
    }

    function DelayedStackClearer(stopNode, delay) {
      DelayedCaller.call(this, function () { stackClear(stopNode) }, delay);

      this.getStopNode = function () {
        return stopNode;
      }
      this.setStopNode = function (newStopNode) {
        stopNode = newStopNode;
      }
    }

    function MenuCoordinates(menu, parentItem, menuBoxGetter, parentItemBoxGetter) {
      var context = this;
      with (parentItem.data.childMenuPosition) {
        var hMuls = this.posMatrix[h][hOptimize],
            vMuls = this.posMatrix[v][vOptimize],
            parentHMuls = this.posMatrix[hParent][hOptimize],
            parentVMuls = this.posMatrix[vParent][vOptimize],
            offsetHMuls = this.offsetMatrix[hOptimize],
            offsetVMuls = this.offsetMatrix[vOptimize],
            hO = hOffset,
            vO = vOffset,
            parentItemBox,
            menuBox;
      }

      function getMenuPosition(viewportLength,
                               parentItemNear,
                               parentItemLength,
                               parentPosMuls,
                               menuLength,
                               menuPosMuls,
                               offset,
                               offsetMuls,
                               scrollDistance) {

        var viewportNear = scrollDistance,
            viewportFar = scrollDistance + viewportLength,
            menuNear = parentItemNear + parentItemLength*parentPosMuls[0] - menuLength*menuPosMuls[0] + offset,
            optimizedMenuNear = parentItemNear + parentItemLength*parentPosMuls[1] - menuLength*menuPosMuls[1] + offset*offsetMuls[1],
            menuFarOverflow = scrollDistance + viewportLength - menuNear - menuLength,
            optimizedMenuFarOverflow = scrollDistance + viewportLength - optimizedMenuNear - menuLength,
            menuFullOverflow = (menuNear < scrollDistance ? menuNear - scrollDistance : 0) + (menuFarOverflow = menuFarOverflow < 0 ? menuFarOverflow : 0),
            optimizedMenuFullOverflow = (optimizedMenuNear < scrollDistance ? optimizedMenuNear - scrollDistance : 0) + (optimizedMenuFarOverflow = optimizedMenuFarOverflow < 0 ? optimizedMenuFarOverflow : 0);

        menuNear += menuFarOverflow*offsetMuls[0];
        optimizedMenuNear += optimizedMenuFarOverflow*offsetMuls[0];

        menuNear = menuFullOverflow < optimizedMenuFullOverflow ? optimizedMenuNear : menuNear;

        return menuNear*(menuNear < 0 ? 1 - offsetMuls[0] : 1);

      }

      this.cacheBoundingBoxes = function () {
        parentItemBox = parentItemBoxGetter();
        menuBox = menuBoxGetter();
      }

      this.getLeft = function () {
        with (parentItemBox) {
          var  parentItemLeft = left,
               parentItemWidth = right - left;
        }

        return getMenuPosition(window.innerWidth,
                               parentItemLeft,
                               parentItemWidth,
                               parentHMuls,
                               context.getWidth(),
                               hMuls,
                               hO,
                               offsetHMuls,
                               window.scrollX);

      }

      this.getTop = function () {
        with (parentItemBox) {
          var  parentItemTop = top,
               parentItemHeight = bottom - top;
        }

        return getMenuPosition(window.innerHeight,
                               parentItemTop,
                               parentItemHeight,
                               parentVMuls,
                               context.getHeight(),
                               vMuls,
                               vO,
                               offsetVMuls,
                               window.scrollY);

      }

      this.getWidth = function () {
        return menuBox.right - menuBox.left;
      }

      this.getHeight = function () {
        return menuBox.bottom - menuBox.top;
      }

    }
    MenuCoordinates.prototype.posMatrix = {
      near: {
        none: [ 0, 0 ],
        offset: [ 0, 0 ],
        mirror: [ 0, 1 ]
      },
      middle: {
        none: [ 0.5, 0.5 ],
        offset: [ 0.5, 0.5 ],
        mirror: [ 0.5, 0.5 ]
      },
      far: {
        none: [ 1, 1 ],
        offset: [ 1, 1 ],
        mirror: [ 1, 0 ]
      }
    };
    MenuCoordinates.prototype.offsetMatrix = {
      none: [ 0, 1 ],
      offset: [ 1, 1 ],
      mirror: [ 1, -1 ]
    };

    function insertHtmlElement(element) {
      return lastInsertedHtmlElement = bodyHtmlElement.insertBefore(element, (lastInsertedHtmlElement ? lastInsertedHtmlElement.nextSibling : bodyHtmlElement.firstChild));
    }

    function processHostItems() {
      var i = hostItems.length;

      if (bodyHtmlElement = document.getElementsByTagName("body")[0]) {
        while (i-- > 0) {
          if (hostItems[i].runtimeData.htmlElement = document.getElementById(hostItems[i].data.id)) {
            hostItems[i].implement(HostItem);
            hostItems.splice(i, 1);
          }
        }
      }
    }

    // -- Initialization --

    var hostItems = [ ];

    // Making a copy of childNodes array.

    (function (i) {
       while (i-- > 0) {
         hostItems[i] = childNodes[i];
       }
     })(childNodes.length);

    // Processing host items

    processHostItems();

    function HostItem(node) {

      var context = this,
          // Node:
          parentNode = node.parentNode,
          childMenu = node.childNodes[0],
          delayedHideToParent = new DelayedStackClearer(parentNode, hideDelay),
          // ---
          htmlElement = node.runtimeData.htmlElement,
          htmlElementStyle = htmlElement.style,
          container = new Container(htmlElement);

      this.container = container;

      function highlight() {
        with (htmlElementStyle) {
          color = "#ffffff";
          textDecoration = "none";
          backgroundColor = "#666666";
        }
      }
      function reset() {
        if (htmlElement.className != "curr") {
          htmlElementStyle.backgroundColor = "transparent";
        }
      }
      function showChildMenu() {
        (showChildMenu = childMenu.implement(Menu).activate)();
      }
      function a() {  // Activates item without child menu
        if (stackIsPresent(node)) {
          stackClear(node);
        }
        else {
          stackClear(parentNode);
          stackPut(node, reset);
      
    highlight();
        }
      }
      this.activate = childMenu ?
        function () { // Activates item with child menu
          if (stackIsPresent(childMenu)) {
            stackClear(childMenu);
          }
          else {
            a();
            showChildMenu();
          }
        } : a;
      this.deactivate = childMenu ?
        function () {  // Deactivates item with child menu
          stackPut("delayedHideToParent", delayedHideToParent.cancelCall);
          delayedHideToParent.scheduleCall();
        }
        :
        function () {  // Deactivates item without child menu
          stackClear(parentNode);
        }

      if (htmlElement) {
        htmlElement.addEventListener("mouseover",
          function () {
            delayedHideAll.cancelCall();
            context.activate();
          }, false);
        htmlElement.addEventListener("mouseout", context.deactivate, false);
      }
    }

    function Item(node) {

      var context = this,
          // Node:
          parentNode = node.parentNode,
          childMenu = node.childNodes[0],
          delayedHideToParent = new DelayedStackClearer(parentNode, hideDelay),
          // ---
          htmlElement = node.runtimeData.htmlElement,
          htmlElementStyle = htmlElement.style,
          container = new Container(htmlElement);

      this.container = container;

      function highlight() {
        with (htmlElementStyle) {
          color = "#FFF";
          backgroundColor = "#285CA2";
        }
      }
      function reset() {
        with (htmlElementStyle) {
          color = "#333";
          backgroundColor = "transparent";
        }
      }
      function showChildMenu() {
        (showChildMenu = childMenu.implement(Menu).activate)();
      }
      function a() {  // Activates item without child menu
        if (stackIsPresent(node)) {
          stackClear(node);
        }
        else {
          stackClear(parentNode);
          stackPut(node, reset);
          highlight();
        }
      }
      this.activate = childMenu ?
        function () { // Activates item with child menu
          if (stackIsPresent(childMenu)) {
            stackClear(childMenu);
          }
          else {
            a();
            showChildMenu();
          }
        } : a;
      this.deactivate = childMenu ?
        function () {  // Deactivates item with child menu
          stackPut("delayedHideToParent", delayedHideToParent.cancelCall);
          delayedHideToParent.scheduleCall();
        }
        :
        function () {  // Deactivates item without child menu
          stackClear(parentNode);
        }

      if (htmlElement) {
        htmlElement.addEventListener("mouseover",
          function () {
            delayedHideAll.cancelCall();
            context.activate();
          }, false);
        htmlElement.addEventListener("mouseout", context.deactivate, false);
      }
    }

    function Menu(node) {

      var context = this,
          // ---
          htmlElement = document.createElement("div"),
          htmlElementStyle = htmlElement.style,
          // Node:
          parentNode = node.parentNode,
          childNodes = node.childNodes,
          container = new Container(htmlElement),
          containerContains = container.contains,
          menuCoordinates = new MenuCoordinates(node, parentNode, container.getBoundingBox, parentNode.implementation.container.getBoundingBox);

      function show() {
        with (htmlElementStyle) {
          zIndex = baseZIndex + 3;

          menuCoordinates.cacheBoundingBoxes(); // You need to display element first!!

          left = menuCoordinates.getLeft() + "px";
          top = menuCoordinates.getTop() + "px";
          display = "block";
        }
      }
      function hide() {
        with (htmlElementStyle) {
          display = "none";
          left = 0;
          top = 0;
        }
      }
      this.activate = function () {
        stackPut(node, hide);
        show();
      }

      htmlElement.addEventListener("mouseover",
        function (event) {
          if (!containerContains(event.relatedTarget)) {
            delayedHideAll.setStopNode(node);
          }
        }, false);
      htmlElement.addEventListener("mouseout",
        function (event) {
          if (!containerContains(event.relatedTarget)) {
            with (delayedHideAll) {
              cancelCall();
              setStopNode(null);
              scheduleCall();
            }
          }
        }, false);

      htmlElement.className = "m2Drop";

      with (htmlElementStyle) {
        position = "absolute";
        display = "none";
      }

      var iMax = childNodes.length,
          menuHtml = "<table cellpadding='0' cellspacing='0' border='0' class='mMozillaDrop1'><tr><td>",
          tmp;

      for (var i = 0; i < iMax; ++i) {
        with (childNodes[i]) {
          menuHtml += "<a href='" + data.href + "'" + data.target + (childNodes.length > 0 ? " class='childMenu'" : "") + ">" + data.itemHtml + "<\/a>";
        }
      }

      htmlElement.innerHTML = menuHtml + "<\/td><\/tr><\/table>";
      insertHtmlElement(htmlElement);
      tmp = htmlElement.getElementsByTagName("a");
      while (iMax-- > 0) {
        childNodes[iMax].runtimeData.htmlElement = tmp[iMax];
        childNodes[iMax].implement(Item);
      }
    }

  }

}

var menuImplementation = new MenuImplementation();

/* Deferred initialization */

jsLoader.processDeferredCalls("menuInit", function (a) {
  a[0].implement(menuImplementation.custom[a[1]]);
});
