define(
   ['underscore', 'underscore.string', 'pgadmin', 'jquery'],
function(_, S, pgAdmin, $) {
  'use strict';

  pgAdmin.Browser = pgAdmin.Browser || {};

  // Individual menu-item class
  var MenuItem = pgAdmin.Browser.MenuItem = function(opts) {
    var menu_opts = [
      'name', 'label', 'priority', 'module', 'callback', 'data', 'enable',
      'category', 'target', 'url', 'icon', 'node'
    ],
    defaults = {
      url: '#',
      target: '_self',
      enable: true
    };
    _.extend(this, defaults, _.pick(opts, menu_opts));
  };

  _.extend(pgAdmin.Browser.MenuItem.prototype, {
    /*
     *  Keeps track of the jQuery object representing this menu-item. This will
     *  be used by the update function to enable/disable the individual item.
     */
    $el: null,
    /*
     * Generate the UI for this menu-item. enable/disable, based on the
     * currently selected object.
     */
    generate: function(node, item) {
      this.create_el(node, item);

      this.context = {
        name: this.label,
        icon: this.icon || this.module && (this.module.type),
        disabled: this.is_disabled,
        callback: this.context_menu_callback.bind(this, item)
      };

      return this.$el;
    },

    /*
     * Create the jquery element for the menu-item.
     */
    create_el: function(node, item) {
      var url = $('<a></a>', {
          'id': this.name,
          'href': this.url,
          'target': this.target,
          'data-toggle': 'pg-menu'
        }).data('pgMenu', {
          module: this.module || pgAdmin.Browser,
          cb: this.callback,
          data: this.data
        }).addClass('menu-link');

      if (this.icon) {
        url.append($('<i></i>', {'class': this.icon}));
      }
      url.append($('<span></span>').text('  ' + this.label));

      this.is_disabled = this.disabled(node, item);
      this.$el = $('<li/>')
        .addClass('menu-item' + (this.is_disabled ? ' disabled' : ''))
        .append(url);
    },

    /*
     * Updates the enable/disable state of the menu-item based on the current
     * selection using the disabled function. This also creates a object
     * for this menu, which can be used in the context-menu.
     */
    update: function(node, item) {

      if (this.$el && !this.$el.hasClass('disabled')) {
        this.$el.addClass('disabled');
      }

      this.is_disabled = this.disabled(node, item);
      if (this.$el && !this.is_disabled) {
        this.$el.removeClass('disabled');
      }

      this.context = {
        name: this.label,
        icon: this.icon || (this.module && this.module.type),
        disabled: this.is_disabled,
        callback: this.context_menu_callback.bind(this, item)
      };
    },

    /*
     * This will be called when context-menu is clicked.
     */
    context_menu_callback: function(item) {
      var o = this, cb;

      if (o.module['callbacks'] && (
        o.callback in o.module['callbacks']
      )) {
        cb = o.module['callbacks'][o.callback];
      } else if (o.callback in o.module) {
        cb = o.module[o.callback];
      }
      if (cb) {
        cb.apply(o.module, [o.data, item]);
      } else {
        pgAdmin.Browser.report_error(
          S('Developer Warning: Callback - "%s" not found!').
            sprintf(o.cb).value()
        );
      }
    },

    /*
     * Checks this menu enable/disable state based on the selection.
     */
    disabled: function(node, item) {
      if (this.enable == undefined) {
        return false;
      }

      if (this.node) {
        if (!node) {
          return true;
        }
        if (_.isArray(this.node) ? (
          _.indexOf(this.node, node) == -1
        ) : (this.node != node._type)) {
          return true;
        }
      }

      if (_.isBoolean(this.enable)) return !this.enable;
      if (_.isFunction(this.enable)) return !this.enable.apply(this.module, [node, item, this.data]);
      if (this.module && _.isBoolean(this.module[this.enable])) return !this.module[this.enable];
      if (this.module && _.isFunction(this.module[this.enable])) return !(this.module[this.enable]).apply(this.module, [node, item, this.data]);

      return false;
    }
  });

  /*
   * This a class for creating a menu group, mainly used by the submenu
   * creation logic.
   *
   * Arguments:
   * 1. Options to render the submenu DOM Element.
   *    i.e. label, icon, above (separator), below (separator)
   * 2. List of menu-items comes under this submenu.
   * 3. Did we rendered separator after the menu-item/submenu?
   * 4. A unique-id for this menu-item.
   *
   * Returns a object, similar to the menu-item, which has his own jQuery
   * Element, context menu representing object, etc.
   *
   */

  pgAdmin.Browser.MenuGroup = function(opts, items, prev, ctx) {
    var template = _.template([
          '<% if (above) { %><hr><% } %>',
          '<li class="menu-item dropdown dropdown-submenu">',
          ' <a href="#" class="dropdown-toggle" data-toggle="dropdown">',
          '  <% if (icon) { %><i class="<%= icon %>"></i><% } %>',
          '  <span><%= label %></span>',
          ' </a>',
          ' <ul class="dropdown-menu navbar-inverse">',
          ' </ul>',
          '</li>',
          '<% if (below) { %><hr><% } %>',].join('\n')),
          data = {
            'label': opts.label,
            'icon': opts.icon,
            'above': opts.above && !prev,
            'below': opts.below,
          }, m,
          $el = $(template(data)),
          $menu = $el.find('.dropdown-menu'),
          submenus = {},
          ctxId = 1;

      ctx = _.uniqueId(ctx + '_sub_');

      // Sort by alphanumeric ordered first
      items.sort(function(a, b) {return a.label.localeCompare(b.label);});
      // Sort by priority
      items.sort(function(a, b) {return a.priority - b.priority;});

      for (var idx in items) {
        m = items[idx];
        $menu.append(m.$el);
        if (!m.is_disabled) {
          submenus[ctx + ctxId] = m.context
        }
        ctxId++;
      }

      var is_disabled = (_.size(submenus) == 0);

      return {
        $el: $el,
        priority: opts.priority || 10,
        label: opts.label,
        above: data['above'],
        below: opts.below,
        is_disabled: is_disabled,
        context: {
          name: opts.label,
          icon: opts.icon,
          items: submenus,
          disabled: is_disabled
        }
      };
  };

  /*
   * A function to generate menus (submenus) based on the categories.
   * Attach the current selected browser tree node to each of the generated
   * menu-items.
   *
   * Arguments:
   * 1. jQuery Element on which you may want to created the menus
   * 2. list of menu-items
   * 3. categories - metadata information about the categories, based on which
   *                 the submenu (menu-group) will be created (if any).
   * 4. d - Data object for the selected browser tree item.
   * 5. item - The selected browser tree item
   * 6. menu_items - A empty object on which the context menu for the given
   *                 list of menu-items.
   *
   * Returns if any menu generated for the given input.
   */
  pgAdmin.Browser.MenuCreator = function($mnu, menus, categories, d, item, menu_items) {
    var groups = {'common': []},
        common, idx = 0, j, item,
        ctxId = _.uniqueId('ctx_'),
        update_menuitem = function(m) {
          if (m instanceof MenuItem) {
            if (m.$el) {
              m.$el.remove();
              delete m.$el;
            }
            m.generate(d, item);
            var group = groups[m.category || 'common'] =
              groups[m.category || 'common'] || [];
            group.push(m);
          } else {
            for (var key in m) {
              update_menuitem(m[key]);
            }
          }
        }, ctxIdx = 1;

    for (idx in menus) {
      update_menuitem(menus[idx]);
    }

    // Not all menu creator requires the context menu structure.
    menu_items = menu_items || {};

    common = groups['common'];
    delete groups['common'];

    var prev = true;

    for (name in groups) {
      var g = groups[name],
          c = categories[name] || {'label': name, single: false},
          menu_group = pgAdmin.Browser.MenuGroup(c, g, prev, ctxId);

      if (g.length <= 1 && !c.single) {
        prev = false;
        for (idx in g) {
          common.push(g[idx]);
        }
      } else {
        prev = g.below;
        common.push(menu_group);
      }
    }

    // The menus will be created based on the priority given.
    // Menu with lowest value has the highest priority. If the priority is
    // same, then - it will be ordered by label.
    // Sort by alphanumeric ordered first
    common.sort(function(a, b) {return a.label.localeCompare(b.label);});
    // Sort by priority
    common.sort(function(a, b) {return a.priority - b.priority;});
    var len = _.size(common);

    for (idx in common) {
      item = common[idx];

      item.priority = (item.priority || 10);
      $mnu.append(item.$el);
      var prefix = ctxId + '_' + item.priority + '_' + ctxIdx;

      if (ctxIdx != 1 && item.above && !item.is_disabled) {
        // For creatign the seprator any string will do.
        menu_items[prefix + '_ma'] = '----';
      }

      if (!item.is_disabled) {
        menu_items[prefix + '_ms'] = item.context;
      }

      if (ctxId != len && item.below && !item.is_disabled) {
        menu_items[prefix + '_mz'] = '----';
      }
      ctxIdx++;
    }

    return (len > 0);
  };


  // MENU PUBLIC CLASS DEFINITION
  // ==============================
  var Menu = function (element, options) {
    this.$element  = $(element)
    this.options   = $.extend({}, Menu.DEFAULTS, options)
    this.isLoading = false
  }

  Menu.DEFAULTS = {}

  Menu.prototype.toggle = function (ev) {
    var $parent = this.$element.closest('.menu-item');
    if ($parent.hasClass('disabled')) {
      ev.preventDefault()
      return false;
    }
    var d = this.$element.data('pgMenu');
    if (d.cb) {
      var cb = d.module && d.module['callbacks'] && d.module['callbacks'][d.cb] || d.module && d.module[d.cb];
      if (cb) {
        cb.apply(d.module, [d.data, pgAdmin.Browser.tree.selected()]);
        ev.preventDefault()
      } else {
        pgAdmin.Browser.report_error('Developer Warning: Callback - "' + d.cb + '" not found!');
      }
    }
  }


  // BUTTON PLUGIN DEFINITION
  // ========================

  function Plugin(option, ev) {
    return this.each(function () {
      var $this   = $(this)
      var data  = $this.data('pg.menu')
      var options = typeof option == 'object' && option

      if (!data) $this.data('pg.menu', (data = new Menu(this, options)))

      data.toggle(ev)
    })
  }

  var old = $.fn.button

  $.fn.pgmenu       = Plugin
  $.fn.pgmenu.Constructor = Menu


  // BUTTON NO CONFLICT
  // ==================

  $.fn.pgmenu.noConflict = function () {
    $.fn.pgmenu = old;
    return this;
  }

  // MENU DATA-API
  // =============

  $(document)
    .on('click.pg.menu.data-api', '[data-toggle^="pg-menu"]', function (ev) {
      var $menu = $(ev.target)
      if (!$menu.hasClass('menu-link'))
        $menu = $menu.closest('.menu-link')
      Plugin.call($menu, 'toggle', ev)
    })
    .on('focus.pg.menu.data-api blur.pg.menu.data-api', '[data-toggle^="pg-menu"]',
      function (e) {
        $(e.target).closest('.menu').toggleClass('focus', /^focus(in)?$/.test(e.type))
      });

  return pgAdmin.Browser.MenuItem;
});
