/*
 * HS Mega Menu - jQuery Plugin
 * @version: 1.0.0 (Sun, 26 Feb 2017)
 * @requires: jQuery v1.6 or later
 * @author: HtmlStream
 * @event-namespace: .HSMegaMenu
 * @browser-support: IE9+
 * @license:
 *
 * Copyright 2017 HtmlStream
 *
 */
;(function ($) {
  'use strict';

  /**
   * Creates a mega menu.
   *
   * @constructor
   * @param {HTMLElement|jQuery} element - The element to create the mega menu for.
   * @param {Object} options - The options
   */

  var MegaMenu = window.MegaMenu || {};

  MegaMenu = (function () {

    var isMenuOpened = false;

    function MegaMenu(element, options) {

      var self = this;

      /**
       * Current element.
       *
       * @public
       */
      this.$element = $(element);

      /**
       * Current options set by the caller including defaults.
       *
       * @public
       */
      this.options = $.extend(true, {}, MegaMenu.defaults, options);


      /**
       * Collection of menu elements.
       *
       * @private
       */
      this._items = $();


      Object.defineProperties(this, {

        /**
         * Contains composed selector of menu items.
         *
         * @public
         */
        itemsSelector: {
          get: function () {
            return self.options.classMap.hasSubMenu + ',' +
              self.options.classMap.hasMegaMenu;
          }
        },

        /**
         * Contains chain of active items.
         *
         * @private
         */
        _tempChain: {
          value: null,
          writable: true
        },

        /**
         * Contains current behavior state.
         *
         * @public
         */
        state: {
          value: null,
          writable: true
        }

      });

      this.initialize();

    }

    return MegaMenu;

  }());


  /**
   * Default options of the mega menu.
   *
   * @public
   */
  MegaMenu.defaults = {
    event: 'hover',
    direction: 'horizontal',
    breakpoint: 991,
    animationIn: false,
    animationOut: false,

    rtl: false,
    hideTimeOut: 300,

    // For 'vertical' direction only
    sideBarRatio: 1 / 4,
    pageContainer: $('body'),

    classMap: {
      initialized: '.hs-menu-initialized',
      mobileState: '.hs-mobile-state',

      subMenu: '.hs-sub-menu',
      hasSubMenu: '.hs-has-sub-menu',
      hasSubMenuActive: '.hs-sub-menu-opened',

      megaMenu: '.hs-mega-menu',
      hasMegaMenu: '.hs-has-mega-menu',
      hasMegaMenuActive: '.hs-mega-menu-opened'
    },

    mobileSpeed: 400,
    mobileEasing: 'linear',

    beforeOpen: function () {},
    beforeClose: function () {},
    afterOpen: function () {},
    afterClose: function () {}
  };

  /**
   * Initialization of the plugin.
   *
   * @protected
   */
  MegaMenu.prototype.initialize = function () {
    var self = this,
      $w = $(window);

    if (this.options.rtl) this.$element.addClass('hs-rtl');

    this.$element
      .addClass(this.options.classMap.initialized.slice(1))
      .addClass('hs-menu-' + this.options.direction);


    // Independent events
    $w.on('resize.HSMegaMenu', function (e) {

      if (self.resizeTimeOutId) clearTimeout(self.resizeTimeOutId);

      self.resizeTimeOutId = setTimeout(function () {

        if (window.innerWidth <= self.options.breakpoint && self.state == 'desktop') self.initMobileBehavior();
        else if (window.innerWidth > self.options.breakpoint && self.state == 'mobile') self.initDesktopBehavior();

        self.refresh();

      }, 50);

    });

    $(document).on('click.HSMegaMenu touchstart.HSMegaMenu', 'body', function (e) {

      var $parents = $(e.target).parents(self.itemsSelector);

      self.closeAll($parents.add($(e.target)));

    });

    $w.on('keyup.HSMegaMenu', function (e) {

      if (e.keyCode && e.keyCode === 27) {

        self.closeAll();

        isMenuOpened = false;
      }

    });

    if (window.innerWidth <= this.options.breakpoint) this.initMobileBehavior();
    else if (window.innerWidth > this.options.breakpoint) this.initDesktopBehavior();

    this.smartPositions();

    return this;

  };

  MegaMenu.prototype.smartPositions = function () {

    var self = this,
      $submenus = this.$element.find(this.options.classMap.subMenu);

    $submenus.each(function (i, el) {

      MenuItem.smartPosition($(el), self.options);

    });

  };

  /**
   * Binding events to menu elements.
   *
   * @protected
   */
  MegaMenu.prototype.bindEvents = function () {

    var self = this,
      selector;

    // Hover case
    if (this.options.event === 'hover' && !_isTouch()) {
      this.$element
        .on(
          'mouseenter.HSMegaMenu',
          this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' +
          this.options.classMap.hasSubMenu + ':not([data-event="click"])',
          function (e) {

            var $this = $(this),
              $chain = $this.parents(self.itemsSelector);

            // Lazy initialization
            if (!$this.data('HSMenuItem')) {
              self.initMenuItem($this, self.getType($this));
            }

            $chain = $chain.add($this);

            self.closeAll($chain);

            $chain.each(function (i, el) {

              var HSMenuItem = $(el).data('HSMenuItem');

              if (HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId);
              HSMenuItem.desktopShow();

            });

            self._items = self._items.not($chain);
            self._tempChain = $chain;

            e.preventDefault();
            e.stopPropagation();
          }
        )
        .on(
          'mouseleave.HSMegaMenu',
          this.options.classMap.hasMegaMenu + ':not([data-event="click"]),' +
          this.options.classMap.hasSubMenu + ':not([data-event="click"])',
          function (e) {

            var $this = $(this),
              HSMenuItem = $this.data('HSMenuItem'),
              $chain = $(e.relatedTarget).parents(self.itemsSelector);

            HSMenuItem.hideTimeOutId = setTimeout(function () {
              self.closeAll($chain);
            }, self.options.hideTimeOut);

            self._items = self._items.add(self._tempChain);
            self._tempChain = null;

            e.preventDefault();
            e.stopPropagation();
          }
        )
        .on(
          'click.HSMegaMenu',
          this.options.classMap.hasMegaMenu + '[data-event="click"] > a, ' +
          this.options.classMap.hasSubMenu + '[data-event="click"] > a',
          function (e) {

            var $this = $(this).parent('[data-event="click"]'),
              HSMenuItem;

            // Lazy initialization
            if (!$this.data('HSMenuItem')) {
              self.initMenuItem($this, self.getType($this));
            }


            self.closeAll($this.add(
              $this.parents(self.itemsSelector)
            ));

            HSMenuItem = $this
              .data('HSMenuItem');

            if (HSMenuItem.isOpened) {
              HSMenuItem.desktopHide();
            }
            else {
              HSMenuItem.desktopShow();
            }


            e.preventDefault();
            e.stopPropagation();

          }
        );
    }
    // Click case
    else {

      this.$element
        .on(
          'click.HSMegaMenu',
          (_isTouch() ?
            this.options.classMap.hasMegaMenu + ' > a, ' +
            this.options.classMap.hasSubMenu + ' > a' :
            this.options.classMap.hasMegaMenu + ':not([data-event="hover"]) > a,' +
            this.options.classMap.hasSubMenu + ':not([data-event="hover"]) > a'),
          function (e) {

            var $this = $(this).parent(),
              HSMenuItem,
              $parents = $this.parents(self.itemsSelector);

            // Lazy initialization
            if (!$this.data('HSMenuItem')) {
              self.initMenuItem($this, self.getType($this));
            }

            self.closeAll($this.add(
              $this.parents(self.itemsSelector)
            ));

            HSMenuItem = $this
              .addClass('hs-event-prevented')
              .data('HSMenuItem');

            if (HSMenuItem.isOpened) {
              HSMenuItem.desktopHide();
            }
            else {
              HSMenuItem.desktopShow();
            }

            e.preventDefault();
            e.stopPropagation();
          }
        );

      if (!_isTouch()) {
        this.$element
          .on(
            'mouseenter.HSMegaMenu',
            this.options.classMap.hasMegaMenu + '[data-event="hover"],' +
            this.options.classMap.hasSubMenu + '[data-event="hover"]',
            function (e) {

              var $this = $(this),
                $parents = $this.parents(self.itemsSelector);

              // Lazy initialization
              if (!$this.data('HSMenuItem')) {
                self.initMenuItem($this, self.getType($this));
              }

              self.closeAll($this.add($parents));

              $parents.add($this).each(function (i, el) {

                var HSMenuItem = $(el).data('HSMenuItem');

                if (HSMenuItem.hideTimeOutId) clearTimeout(HSMenuItem.hideTimeOutId);
                HSMenuItem.desktopShow();

              });

              e.preventDefault();
              e.stopPropagation();
            }
          )
          .on(
            'mouseleave.HSMegaMenu',
            this.options.classMap.hasMegaMenu + '[data-event="hover"],' +
            this.options.classMap.hasSubMenu + '[data-event="hover"]',
            function (e) {

              var $this = $(this),
                HSMenuItem = $this.data('HSMenuItem');

              HSMenuItem.hideTimeOutId = setTimeout(function () {

                self.closeAll(
                  $(e.relatedTarget).parents(self.itemsSelector)
                );

              }, self.options.hideTimeOut);

              e.preventDefault();
              e.stopPropagation();
            }
          )
      }
    }

    this.$element.on('keydown.HSMegaMenu',
      this.options.classMap.hasMegaMenu + ' > a,' +
      this.options.classMap.hasSubMenu + ' > a',
      function (e) {

        var $this = $(this),
          $parent = $this.parent(),
          HSMenuItem;

        // Lazy initialization
        if (!$parent.data('HSMenuItem')) {

          self.initMenuItem($parent, self.getType($parent));

        }

        HSMenuItem = $parent.data('HSMenuItem');

        if ($this.is(':focus')) {

          if (e.keyCode && e.keyCode === 40) {

            e.preventDefault();

            HSMenuItem.desktopShow();

            isMenuOpened = true;

          }

          if (e.keyCode && e.keyCode === 13) {

            if (isMenuOpened === true) {

              HSMenuItem.desktopHide();

              isMenuOpened = false;

            } else {

              HSMenuItem.desktopShow();

              isMenuOpened = true;

            }

          }

        }

        $this.on('focusout', function () {

          isMenuOpened = false;

        });

        HSMenuItem.menu.find('a').on('focusout', function () {

          setTimeout(function () {

            if (!HSMenuItem.menu.find('a').is(':focus')) {

              HSMenuItem.desktopHide();

              isMenuOpened = false;

            }

          });

        });

      })
  };

  /**
   * Initialization of certain menu item.
   *
   * @protected
   */
  MegaMenu.prototype.initMenuItem = function (element, type) {

    var self = this,
      Item = new MenuItem(
        element,
        element.children(
          self.options.classMap[type === 'mega-menu' ? 'megaMenu' : 'subMenu']
        ),
        $.extend(true, {type: type}, self.options, element.data()),
        self.$element
      );

    element.data('HSMenuItem', Item);
    this._items = this._items.add(element);

  };

  /**
   * Destroys of desktop behavior, then makes initialization of mobile behavior.
   *
   * @protected
   */
  MegaMenu.prototype.initMobileBehavior = function () {

    var self = this;

    this.state = 'mobile';

    this.$element
      .off('.HSMegaMenu')
      .addClass(this.options.classMap.mobileState.slice(1))
      .on('click.HSMegaMenu', self.options.classMap.hasSubMenu + ' > a, ' + self.options.classMap.hasMegaMenu + ' > a', function (e) {

        var $this = $(this).parent(),
          MenuItemInstance;

        // Lazy initialization
        if (!$this.data('HSMenuItem')) {
          self.initMenuItem($this, self.getType($this));
        }

        self.closeAll($this.parents(self.itemsSelector).add($this));

        MenuItemInstance = $this
          .data('HSMenuItem');

        if (MenuItemInstance.isOpened) {
          MenuItemInstance.mobileHide();
        }
        else {
          MenuItemInstance.mobileShow();
        }

        e.preventDefault();
        e.stopPropagation();

      })
      .find(this.itemsSelector)
      .not(
        this.options.classMap.hasSubMenuActive + ',' +
        this.options.classMap.hasMegaMenuActive
      )
      .children(
        this.options.classMap.subMenu + ',' +
        this.options.classMap.megaMenu
      )
      .hide();

  };

  /**
   * Destroys of mobile behavior, then makes initialization of desktop behavior.
   *
   * @protected
   */
  MegaMenu.prototype.initDesktopBehavior = function () {

    this.state = 'desktop';

    this.$element
      .removeClass(this.options.classMap.mobileState.slice(1))
      .off('.HSMegaMenu')
      .find(this.itemsSelector)
      .not(
        this.options.classMap.hasSubMenuActive + ',' +
        this.options.classMap.hasMegaMenuActive
      )
      .children(
        this.options.classMap.subMenu + ',' +
        this.options.classMap.megaMenu
      )
      .hide();

    this.bindEvents();

  };

  /**
   * Hides all of opened submenus/megamenus.
   *
   * @param {jQuery} except - collection of elements, which shouldn't be closed.
   * @return {jQuery}
   * @public
   */
  MegaMenu.prototype.closeAll = function (except) {

    var self = this;

    return this._items.not(except && except.length ? except : $()).each(function (i, el) {

      $(el)
        .removeClass('hs-event-prevented')
        .data('HSMenuItem')[self.state === 'mobile' ? 'mobileHide' : 'desktopHide']();

    });

  };

  /**
   * Returns type of sub menu based on specified menu item.
   *
   * @param {jQuery} item
   * @return {String|null}
   * @public
   */
  MegaMenu.prototype.getType = function (item) {

    if (!item || !item.length) return null;

    return item.hasClass(this.options.classMap.hasSubMenu.slice(1)) ? 'sub-menu' :
      (item.hasClass(this.options.classMap.hasMegaMenu.slice(1)) ? 'mega-menu' : null);

  };

  /**
   * Returns current menu state.
   *
   * @return {String}
   * @public
   */
  MegaMenu.prototype.getState = function () {
    return this.state;
  };

  /**
   * Updates bounds of all menu items.
   *
   * @return {jQuery}
   * @public
   */
  MegaMenu.prototype.refresh = function () {

    return this._items.add(this._tempChain).each(function (i, el) {
      $(el).data('HSMenuItem')._updateMenuBounds();
    });

  };


  /**
   * Creates a mega-menu element.
   *
   * @param {jQuery} element
   * @param {jQuery} menu
   * @param {Object} options
   * @param {jQuery} container
   * @constructor
   */
  function MenuItem(element, menu, options, container) {

    var self = this;

    /**
     * Current menu item element.
     *
     * @public
     */
    this.$element = element;

    /**
     * Current mega menu element.
     *
     * @public
     */
    this.menu = menu;

    /**
     * Item options.
     *
     * @public
     */
    this.options = options;


    /**
     * MegaMenu container.
     *
     * @public
     */
    this.$container = container;

    Object.defineProperties(this, {

      /**
       * Contains css class of menu item element.
       *
       * @public
       */
      itemClass: {
        get: function () {
          return self.options.type === 'mega-menu' ?
            self.options.classMap.hasMegaMenu :
            self.options.classMap.hasSubMenu;
        }
      },

      /**
       * Contains css active-class of menu item element.
       *
       * @public
       */
      activeItemClass: {
        get: function () {
          return self.options.type === 'mega-menu' ?
            self.options.classMap.hasMegaMenuActive :
            self.options.classMap.hasSubMenuActive;
        }
      },

      /**
       * Contains css class of menu element.
       *
       * @public
       */
      menuClass: {
        get: function () {
          return self.options.type === 'mega-menu' ?
            self.options.classMap.megaMenu :
            self.options.classMap.subMenu;
        }
      },

      isOpened: {
        get: function () {
          return this.$element.hasClass(this.activeItemClass.slice(1));
        }
      }

    });


    this.menu.addClass('animated').on('click.HSMegaMenu', function (e) {
      self._updateMenuBounds();
    });

    if (this.$element.data('max-width')) this.menu.css('max-width', this.$element.data('max-width'));
    if (this.$element.data('position')) this.menu.addClass('hs-position-' + this.$element.data('position'));


    if (this.options.animationOut) {

      this.menu.on('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function (e) {

        if (self.menu.hasClass(self.options.animationOut)) {
          self.$element.removeClass(self.activeItemClass.slice(1));
          self.options.afterClose.call(self, self.$element, self.menu);
        }

        if (self.menu.hasClass(self.options.animationIn)) {
          self.options.afterOpen.call(self, self.$element, self.menu);
        }

        e.stopPropagation();
        e.preventDefault();
      });
    }

  }

  /**
   * Shows the mega-menu item.
   *
   * @public
   * @return {MenuItem}
   */
  MenuItem.prototype.desktopShow = function () {
    if (!this.menu.length) return this;

    this.$element.addClass(this.activeItemClass.slice(1));

    this._updateMenuBounds();

    this.menu.show();

    if (this.options.direction == 'horizontal') this.smartPosition(this.menu, this.options);

    if (this.options.animationOut) {
      this.menu.removeClass(this.options.animationOut);
    }
    else {
      this.options.afterOpen.call(this, this.$element, this.menu);
    }

    if (this.options.animationIn) {
      this.menu.addClass(this.options.animationIn)
    }

    return this;
  }

  /**
   * Hides the mega-menu item.
   *
   * @public
   * @return {MenuItem}
   */
  MenuItem.prototype.desktopHide = function () {

    var self = this;

    if (!this.menu.length) return this;

    this.$element.removeClass(this.activeItemClass.slice(1));

    this.menu.hide();

    if (this.options.animationIn) {
      this.menu.removeClass(this.options.animationIn);
    }
    if (this.options.animationOut) {
      this.menu.addClass(this.options.animationOut);
    }
    else {
      this.options.afterClose.call(this, this.$element, this.menu);
    }

    return this;
  }

  /**
   * Shows the mega-menu item.
   *
   * @public
   * @return {MenuItem}
   */
  MenuItem.prototype.mobileShow = function () {
    var self = this;

    if (!this.menu.length) return this;


    this.menu
      .removeClass(this.options.animationIn)
      .removeClass(this.options.animationOut)
      .stop()
      .slideDown({
        duration: self.options.mobileSpeed,
        easing: self.options.mobileEasing,
        complete: function () {
          self.options.afterOpen.call(self, self.$element, self.menu);
        }
      });

    this.$element.addClass(this.activeItemClass.slice(1));

    return this;
  };

  /**
   * Hides the mega-menu item.
   *
   * @public
   * @return {MenuItem}
   */
  MenuItem.prototype.mobileHide = function () {
    var self = this;

    if (!this.menu.length) return this;

    this.menu.stop().slideUp({
      duration: self.options.mobileSpeed,
      easing: self.options.mobileEasing,
      complete: function () {
        self.options.afterClose.call(self, self.$element, self.menu);
      }
    });

    this.$element.removeClass(this.activeItemClass.slice(1));

    return this;
  };

  /**
   * Check, if element is in viewport.
   *
   * @param {jQuery} element
   * @param {Object} options
   * @public
   */
  MenuItem.prototype.smartPosition = function (element, options) {

    MenuItem.smartPosition(element, options);

  };

  /**
   * Check, if element is in viewport.
   *
   * @param {jQuery} element
   * @param {Object} options
   * @static
   * @public
   */
  MenuItem.smartPosition = function (element, options) {
    if (!element && !element.length) return;

    var $w = $(window);

    element.removeClass('hs-reversed');

    if (!options.rtl) {

      if (element.offset().left + element.outerWidth() > window.innerWidth) {
        element.addClass('hs-reversed');
      }

    }
    else {

      if (element.offset().left < 0) {
        element.addClass('hs-reversed');
      }

    }
  };

  /**
   * Updates bounds of current opened menu.
   *
   * @private
   */
  MenuItem.prototype._updateMenuBounds = function () {

    var width = 'auto';

    if (this.options.direction == 'vertical' && this.options.type == 'mega-menu') {

      if (this.$container && this.$container.data('HSMegaMenu').getState() == 'desktop') {
        if (!this.options.pageContainer.length) this.options.pageContainer = $('body');
        width = this.options.pageContainer.outerWidth() * (1 - this.options.sideBarRatio);
      }
      else {
        width = 'auto';
      }


      this.menu.css({
        'width': width,
        'height': 'auto'
      });

      if (this.menu.outerHeight() > this.$container.outerHeight()) {
        return;
      }

      this.menu.css('height', '100%');
    }

  };

  /**
   * The jQuery plugin for the MegaMenu.
   *
   * @public
   */

  $.fn.HSMegaMenu = function () {
    var _ = this,
      opt = arguments[0],
      args = Array.prototype.slice.call(arguments, 1),
      l = _.length,
      i,
      ret;
    for (i = 0; i < l; i++) {
      if (typeof opt == 'object' || typeof opt == 'undefined')
        _[i].MegaMenu = new MegaMenu(_[i], opt);
      else
        ret = _[i].MegaMenu[opt].apply(_[i].MegaMenu, args);
      if (typeof ret != 'undefined') return ret;
    }
    return _;
  };

  /**
   * Helper function for detect touch events in the environment.
   *
   * @return {Boolean}
   * @private
   */
  function _isTouch() {
    return ('ontouchstart' in window);
  }

})(jQuery);
