(function ($) {
  var selectors = [];

  var checkBinded = false;
  var checkLock = false;
  var defaults = {
    interval: 250,
    force_process: false
  };
  var $window = $(window);

  var $priorAppeared = [];

  function isAppeared() {
    return $(this).is(':appeared');
  }

  function isNotTriggered() {
    return !$(this).data('_appear_triggered');
  }

  function process() {
    checkLock = false;

    for (var index = 0, selectorsLength = selectors.length; index < selectorsLength; index++) {
      var $appeared = $(selectors[index]).filter(isAppeared);

      $appeared
        .filter(isNotTriggered)
        .data('_appear_triggered', true)
        .trigger('appear', [$appeared]);

      if ($priorAppeared[index]) {
        var $disappeared = $priorAppeared[index].not($appeared);
        $disappeared
          .data('_appear_triggered', false)
          .trigger('disappear', [$disappeared]);
      }
      $priorAppeared[index] = $appeared;
    }
  }

  function addSelector(selector) {
    selectors.push(selector);
    $priorAppeared.push();
  }

  // ":appeared" custom filter
  $.expr.pseudos.appeared = $.expr.createPseudo(function (_arg) {
    return function (element) {
      var $element = $(element);

      if (!$element.is(':visible')) {
        return false;
      }

      var windowLeft = $window.scrollLeft();
      var windowTop = $window.scrollTop();
      var offset = $element.offset();
      var left = offset.left;
      var top = offset.top;

      if (top + $element.height() >= windowTop &&
          top - ($element.data('appear-top-offset') || 0) <= windowTop + $window.height() &&
          left + $element.width() >= windowLeft &&
          left - ($element.data('appear-left-offset') || 0) <= windowLeft + $window.width()) {
        return true;
      }
      return false;
    };
  });

  $.fn.extend({
    // watching for element's appearance in browser viewport
    appear: function (selector, options) {
      $.appear(this, options);
      return this;
    }
  });

  $.extend({
    appear: function (selector, options) {
      var opts = $.extend({}, defaults, options || {});

      if (!checkBinded) {
        var onCheck = function () {
          if (checkLock) {
            return;
          }
          checkLock = true;

          setTimeout(process, opts.interval);
        };

        $(window).scroll(onCheck).resize(onCheck);
        checkBinded = true;
      }

      if (opts.force_process) {
        setTimeout(process, opts.interval);
      }

      addSelector(selector);
    },
    // force elements's appearance check
    force_appear: function () {
      if (checkBinded) {
        process();
        return true;
      }
      return false;
    }
  });
}(function () {
  if (typeof module !== 'undefined') {
    // Node
    return require('jquery');
  }
  return jQuery;
}()));