(function ($) {

/**
 * Generate an indented list of links from a nav. Meant for use with panel().
 * @return {jQuery} jQuery object.
 */
$.fn.navList = function () {

  var $this = $(this);
  $a = $this.find('a'),
    b = [];

  $a.each(function () {

    var $this = $(this),
      indent = Math.max(0, $this.parents('li').length - 1),
      href = $this.attr('href'),
      target = $this.attr('target');

    b.push(
      '<a ' +
      'class="link depth-' + indent + '"' +
      ((typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
      ((typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
      '>' +
      '<span class="indent-' + indent + '"></span>' +
      $this.text() +
      '</a>'
    );

  });

  return b.join('');

};

/**
 * Panel-ify an element.
 * @param {object} userConfig User config.
 * @return {jQuery} jQuery object.
 */
$.fn.panel = function (userConfig) {

  // No elements?
  if (this.length == 0)
    return $this;

  // Multiple elements?
  if (this.length > 1) {

    for (var i = 0; i < this.length; i++)
      $(this[i]).panel(userConfig);

    return $this;

  }

  // Vars.
  var $this = $(this),
    $body = $('body'),
    $window = $(window),
    id = $this.attr('id'),
    config;

  // Config.
  config = $.extend({

    // Delay.
    delay: 0,

    // Hide panel on link click.
    hideOnClick: false,

    // Hide panel on escape keypress.
    hideOnEscape: false,

    // Hide panel on swipe.
    hideOnSwipe: false,

    // Reset scroll position on hide.
    resetScroll: false,

    // Reset forms on hide.
    resetForms: false,

    // Side of viewport the panel will appear.
    side: null,

    // Target element for "class".
    target: $this,

    // Class to toggle.
    visibleClass: 'visible'

  }, userConfig);

  // Expand "target" if it's not a jQuery object already.
  if (typeof config.target != 'jQuery')
    config.target = $(config.target);

  // Panel.

  // Methods.
  $this._hide = function (event) {

    // Already hidden? Bail.
    if (!config.target.hasClass(config.visibleClass))
      return;

    // If an event was provided, cancel it.
    if (event) {

      event.preventDefault();
      event.stopPropagation();

    }

    // Hide.
    config.target.removeClass(config.visibleClass);

    // Post-hide stuff.
    window.setTimeout(function () {

      // Reset scroll position.
      if (config.resetScroll)
        $this.scrollTop(0);

      // Reset forms.
      if (config.resetForms)
        $this.find('form').each(function () {
          this.reset();
        });

    }, config.delay);

  };

  // Vendor fixes.
  $this
    .css('-ms-overflow-style', '-ms-autohiding-scrollbar')
    .css('-webkit-overflow-scrolling', 'touch');

  // Hide on click.
  if (config.hideOnClick) {

    $this.find('a')
      .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');

    $this
      .on('click', 'a', function (event) {

        var $a = $(this),
          href = $a.attr('href'),
          target = $a.attr('target');

        if (!href || href == '#' || href == '' || href == '#' + id)
          return;

        // Cancel original event.
        event.preventDefault();
        event.stopPropagation();

        // Hide panel.
        $this._hide();

        // Redirect to href.
        window.setTimeout(function () {

          if (target == '_blank')
            window.open(href);
          else
            window.location.href = href;

        }, config.delay + 10);

      });

  }

  // Event: Touch stuff.
  $this.on('touchstart', function (event) {

    $this.touchPosX = event.originalEvent.touches[0].pageX;
    $this.touchPosY = event.originalEvent.touches[0].pageY;

  })

  $this.on('touchmove', function (event) {

    if ($this.touchPosX === null
      || $this.touchPosY === null)
      return;

    var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
      diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
      th = $this.outerHeight(),
      ts = ($this.get(0).scrollHeight - $this.scrollTop());

    // Hide on swipe?
    if (config.hideOnSwipe) {

      var result = false,
        boundary = 20,
        delta = 50;

      switch (config.side) {

        case 'left':
          result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
          break;

        case 'right':
          result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
          break;

        case 'top':
          result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
          break;

        case 'bottom':
          result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
          break;

        default:
          break;

      }

      if (result) {

        $this.touchPosX = null;
        $this.touchPosY = null;
        $this._hide();

        return false;

      }

    }

    // Prevent vertical scrolling past the top or bottom.
    if (($this.scrollTop() < 0 && diffY < 0)
      || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {

      event.preventDefault();
      event.stopPropagation();

    }

  });

  // Event: Prevent certain events inside the panel from bubbling.
  $this.on('click touchend touchstart touchmove', function (event) {
    event.stopPropagation();
  });

  // Event: Hide panel if a child anchor tag pointing to its ID is clicked.
  $this.on('click', 'a[href="#' + id + '"]', function (event) {

    event.preventDefault();
    event.stopPropagation();

    config.target.removeClass(config.visibleClass);

  });

  // Body.

  // Event: Hide panel on body click/tap.
  $body.on('click touchend', function (event) {
    $this._hide(event);
  });

  // Event: Toggle.
  $body.on('click', 'a[href="#' + id + '"]', function (event) {

    event.preventDefault();
    event.stopPropagation();

    config.target.toggleClass(config.visibleClass);

  });

  // Window.

  // Event: Hide on ESC.
  if (config.hideOnEscape)
    $window.on('keydown', function (event) {

      if (event.keyCode == 27)
        $this._hide(event);

    });

  return $this;

};

/**
 * Apply "placeholder" attribute polyfill to one or more forms.
 * @return {jQuery} jQuery object.
 */
$.fn.placeholder = function () {

  // Browser natively supports placeholders? Bail.
  if (typeof (document.createElement('input')).placeholder != 'undefined')
    return $(this);

  // No elements?
  if (this.length == 0)
    return $this;

  // Multiple elements?
  if (this.length > 1) {

    for (var i = 0; i < this.length; i++)
      $(this[i]).placeholder();

    return $this;

  }

  // Vars.
  var $this = $(this);

  // Text, TextArea.
  $this.find('input[type=text],textarea')
    .each(function () {

      var i = $(this);

      if (i.val() == ''
        || i.val() == i.attr('placeholder'))
        i
          .addClass('polyfill-placeholder')
          .val(i.attr('placeholder'));

    })
    .on('blur', function () {

      var i = $(this);

      if (i.attr('name').match(/-polyfill-field$/))
        return;

      if (i.val() == '')
        i
          .addClass('polyfill-placeholder')
          .val(i.attr('placeholder'));

    })
    .on('focus', function () {

      var i = $(this);

      if (i.attr('name').match(/-polyfill-field$/))
        return;

      if (i.val() == i.attr('placeholder'))
        i
          .removeClass('polyfill-placeholder')
          .val('');

    });

  // Password.
  $this.find('input[type=password]')
    .each(function () {

      var i = $(this);
      var x = $(
        $('<div>')
          .append(i.clone())
          .remove()
          .html()
          .replace(/type="password"/i, 'type="text"')
          .replace(/type=password/i, 'type=text')
      );

      if (i.attr('id') != '')
        x.attr('id', i.attr('id') + '-polyfill-field');

      if (i.attr('name') != '')
        x.attr('name', i.attr('name') + '-polyfill-field');

      x.addClass('polyfill-placeholder')
        .val(x.attr('placeholder')).insertAfter(i);

      if (i.val() == '')
        i.hide();
      else
        x.hide();

      i
        .on('blur', function (event) {

          event.preventDefault();

          var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');

          if (i.val() == '') {

            i.hide();
            x.show();

          }

        });

      x
        .on('focus', function (event) {

          event.preventDefault();

          var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');

          x.hide();

          i
            .show()
            .focus();

        })
        .on('keypress', function (event) {

          event.preventDefault();
          x.val('');

        });

    });

  // Events.
  $this
    .on('submit', function () {

      $this.find('input[type=text],input[type=password],textarea')
        .each(function (event) {

          var i = $(this);

          if (i.attr('name').match(/-polyfill-field$/))
            i.attr('name', '');

          if (i.val() == i.attr('placeholder')) {

            i.removeClass('polyfill-placeholder');
            i.val('');

          }

        });

    })
    .on('reset', function (event) {

      event.preventDefault();

      $this.find('select')
        .val($('option:first').val());

      $this.find('input,textarea')
        .each(function () {

          var i = $(this),
            x;

          i.removeClass('polyfill-placeholder');

          switch (this.type) {

            case 'submit':
            case 'reset':
              break;

            case 'password':
              i.val(i.attr('defaultValue'));

              x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');

              if (i.val() == '') {
                i.hide();
                x.show();
              }
              else {
                i.show();
                x.hide();
              }

              break;

            case 'checkbox':
            case 'radio':
              i.attr('checked', i.attr('defaultValue'));
              break;

            case 'text':
            case 'textarea':
              i.val(i.attr('defaultValue'));

              if (i.val() == '') {
                i.addClass('polyfill-placeholder');
                i.val(i.attr('placeholder'));
              }

              break;

            default:
              i.val(i.attr('defaultValue'));
              break;

          }
        });

    });

  return $this;

};

/**
 * Moves elements to/from the first positions of their respective parents.
 * @param {jQuery} $elements Elements (or selector) to move.
 * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
 */
$.prioritize = function ($elements, condition) {

  var key = '__prioritize';

  // Expand $elements if it's not already a jQuery object.
  if (typeof $elements != 'jQuery')
    $elements = $($elements);

  // Step through elements.
  $elements.each(function () {

    var $e = $(this), $p,
      $parent = $e.parent();

    // No parent? Bail.
    if ($parent.length == 0)
      return;

    // Not moved? Move it.
    if (!$e.data(key)) {

      // Condition is false? Bail.
      if (!condition)
        return;

      // Get placeholder (which will serve as our point of reference for when this element needs to move back).
      $p = $e.prev();

      // Couldn't find anything? Means this element's already at the top, so bail.
      if ($p.length == 0)
        return;

      // Move element to top of parent.
      $e.prependTo($parent);

      // Mark element as moved.
      $e.data(key, $p);

    }

    // Moved already?
    else {

      // Condition is true? Bail.
      if (condition)
        return;

      $p = $e.data(key);

      // Move element back to its original location (using our placeholder).
      $e.insertAfter($p);

      // Unmark element as moved.
      $e.removeData(key);

    }

  });

};

})(jQuery);