/*

* # Semantic - Visibility
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2014 Contributor
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/

;(function ( $, window, document, undefined ) {

$.fn.visibility = function(parameters) {

var
  $allModules    = $(this),
  moduleSelector = $allModules.selector || '',

  time           = new Date().getTime(),
  performance    = [],

  query          = arguments[0],
  methodInvoked  = (typeof query == 'string'),
  queryArguments = [].slice.call(arguments, 1),
  returnedValue
;

$allModules
  .each(function() {
    var
      settings        = $.extend(true, {}, $.fn.visibility.settings, parameters),

      className       = settings.className,
      namespace       = settings.namespace,
      error           = settings.error,

      eventNamespace  = '.' + namespace,
      moduleNamespace = 'module-' + namespace,

      $window         = $(window),
      $module         = $(this),
      $context        = $(settings.context),
      $container      = $module.offsetParent(),

      selector        = $module.selector || '',
      instance        = $module.data(moduleNamespace),

      requestAnimationFrame = window.requestAnimationFrame
        || window.mozRequestAnimationFrame
        || window.webkitRequestAnimationFrame
        || window.msRequestAnimationFrame
        || function(callback) { setTimeout(callback, 0); },

      element         = this,
      module
    ;

    module      = {

      initialize: function() {
        module.verbose('Initializing visibility', settings);

        module.setup.cache();
        module.save.position();

        if( module.should.trackChanges() ) {
          module.bindEvents();
          if(settings.type == 'image') {
            module.setup.image();
          }
          if(settings.type == 'fixed') {
            module.setup.fixed();
          }
        }
        module.checkVisibility();
        module.instantiate();
      },

      instantiate: function() {
        module.verbose('Storing instance of module', module);
        instance = module;
        $module
          .data(moduleNamespace, module)
        ;
      },

      destroy: function() {
        module.verbose('Destroying previous module');
        $module
          .off(eventNamespace)
          .removeData(moduleNamespace)
        ;
      },

      bindEvents: function() {
        module.verbose('Binding visibility events to scroll and resize');
        $window
          .on('resize' + eventNamespace, module.event.refresh)
        ;
        $context
          .on('scroll' + eventNamespace, module.event.scroll)
        ;
      },

      event: {
        refresh: function() {
          requestAnimationFrame(module.refresh);
        },
        scroll: function() {
          module.verbose('Scroll position changed');
          if(settings.throttle) {
            clearTimeout(module.timer);
            module.timer = setTimeout(module.checkVisibility, settings.throttle);
          }
          else {
            requestAnimationFrame(module.checkVisibility);
          }
        }
      },

      precache: function(images, callback) {
        if (!(images instanceof Array)) {
          images = [images];
        }
        var
          imagesLength  = images.length,
          loadedCounter = 0,
          cache         = [],
          cacheImage    = document.createElement('img'),
          handleLoad    = function() {
            loadedCounter++;
            if (loadedCounter >= images.length) {
              if ($.isFunction(callback)) {
                callback();
              }
            }
          }
        ;
        while (imagesLength--) {
          cacheImage         = document.createElement('img');
          cacheImage.onload  = handleLoad;
          cacheImage.onerror = handleLoad;
          cacheImage.src     = images[imagesLength];
          cache.push(cacheImage);
        }
      },

      should: {

        trackChanges: function() {
          if(methodInvoked && queryArguments.length > 0) {
            module.debug('One time query, no need to bind events');
            return false;
          }
          module.debug('Query is attaching callbacks, watching for changes with scroll');
          return true;
        }

      },

      setup: {
        cache: function() {
          module.cache = {
            occurred : {},
            screen   : {},
            element  : {},
          };
        },
        image: function() {
          var
            src = $module.data('src')
          ;
          if(src) {
            module.verbose('Lazy loading image', src);
            // show when top visible
            module.topVisible(function() {
              module.precache(src, function() {
                module.set.image(src);
                settings.onTopVisible = false;
              });
            });
          }
        },
        fixed: function() {
          module.verbose('Setting up fixed on element pass');
          $module
            .visibility({
              once: false,
              continuous: false,
              onTopPassed: function() {
                $module
                  .addClass(className.fixed)
                  .css({
                    position: 'fixed',
                    top: settings.offset + 'px'
                  })
                ;
                if(settings.animation && $.fn.transition !== undefined) {
                  $module.transition(settings.transition, settings.duration);
                }
              },
              onTopPassedReverse: function() {
                $module
                  .removeClass(className.fixed)
                  .css({
                    position: '',
                    top: ''
                  })
                ;
              }
            })
          ;
        }
      },

      set: {
        image: function(src) {
          var
            offScreen = (module.cache.screen.bottom < module.cache.element.top)
          ;
          $module
            .attr('src', src)
          ;
          if(offScreen) {
            module.verbose('Image outside browser, no show animation');
            $module.show();
          }
          else {
            if(settings.transition && $.fn.transition !== undefined) {
              $module.transition(settings.transition, settings.duration);
            }
            else {
              $module.fadeIn(settings.duration);
            }
          }
        }
      },

      refresh: function() {
        module.debug('Refreshing constants (element width/height)');
        module.reset();
        module.save.position();
        module.checkVisibility();
        $.proxy(settings.onRefresh, element)();
      },

      reset: function() {
        module.verbose('Reseting all cached values');
        if( $.isPlainObject(module.cache) ) {
          module.cache.screen = {};
          module.cache.element = {};
        }
      },

      checkVisibility: function() {
        module.verbose('Checking visibility of element', module.cache.element);
        module.save.calculations();

        // percentage
        module.passed();

        // reverse (must be first)
        module.passingReverse();
        module.topVisibleReverse();
        module.bottomVisibleReverse();
        module.topPassedReverse();
        module.bottomPassedReverse();

        // one time
        module.passing();
        module.topVisible();
        module.bottomVisible();
        module.topPassed();
        module.bottomPassed();
      },

      passed: function(amount, newCallback) {
        var
          calculations   = module.get.elementCalculations(),
          amountInPixels
        ;
        // assign callback
        if(amount !== undefined && newCallback !== undefined) {
          settings.onPassed[amount] = newCallback;
        }
        else if(amount !== undefined) {
          return (module.get.pixelsPassed(amount) > calculations.pixelsPassed);
        }
        else if(calculations.passing) {
          $.each(settings.onPassed, function(amount, callback) {
            if(calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
              module.execute(callback, amount);
            }
            else if(!settings.once) {
              module.remove.occurred(callback);
            }
          });
        }
      },

      passing: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onPassing,
          callbackName = 'passing'
        ;
        if(newCallback) {
          module.debug('Adding callback for passing', newCallback);
          settings.onPassing = newCallback;
        }
        if(calculations.passing) {
          module.execute(callback, callbackName);
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback !== undefined) {
          return calculations.passing;
        }
      },

      topVisible: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onTopVisible,
          callbackName = 'topVisible'
        ;
        if(newCallback) {
          module.debug('Adding callback for top visible', newCallback);
          settings.onTopVisible = newCallback;
        }
        if(calculations.topVisible) {
          module.execute(callback, callbackName);
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return calculations.topVisible;
        }
      },

      bottomVisible: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onBottomVisible,
          callbackName = 'bottomVisible'
        ;
        if(newCallback) {
          module.debug('Adding callback for bottom visible', newCallback);
          settings.onBottomVisible = newCallback;
        }
        if(calculations.bottomVisible) {
          module.execute(callback, callbackName);
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return calculations.bottomVisible;
        }
      },

      topPassed: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onTopPassed,
          callbackName = 'topPassed'
        ;
        if(newCallback) {
          module.debug('Adding callback for top passed', newCallback);
          settings.onTopPassed = newCallback;
        }
        if(calculations.topPassed) {
          module.execute(callback, callbackName);
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return calculations.topPassed;
        }
      },

      bottomPassed: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onBottomPassed,
          callbackName = 'bottomPassed'
        ;
        if(newCallback) {
          module.debug('Adding callback for bottom passed', newCallback);
          settings.onBottomPassed = newCallback;
        }
        if(calculations.bottomPassed) {
          module.execute(callback, callbackName);
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return calculations.bottomPassed;
        }
      },

      passingReverse: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onPassingReverse,
          callbackName = 'passingReverse'
        ;
        if(newCallback) {
          module.debug('Adding callback for passing reverse', newCallback);
          settings.onPassingReverse = newCallback;
        }
        if(!calculations.passing) {
          if(module.get.occurred('passing')) {
            module.execute(callback, callbackName);
          }
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback !== undefined) {
          return !calculations.passing;
        }
      },

      topVisibleReverse: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onTopVisibleReverse,
          callbackName = 'topVisibleReverse'
        ;
        if(newCallback) {
          module.debug('Adding callback for top visible reverse', newCallback);
          settings.onTopVisibleReverse = newCallback;
        }
        if(!calculations.topVisible) {
          if(module.get.occurred('topVisible')) {
            module.execute(callback, callbackName);
          }
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return !calculations.topVisible;
        }
      },

      bottomVisibleReverse: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onBottomVisibleReverse,
          callbackName = 'bottomVisibleReverse'
        ;
        if(newCallback) {
          module.debug('Adding callback for bottom visible reverse', newCallback);
          settings.onBottomVisibleReverse = newCallback;
        }
        if(!calculations.bottomVisible) {
          if(module.get.occurred('bottomVisible')) {
            module.execute(callback, callbackName);
          }
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return !calculations.bottomVisible;
        }
      },

      topPassedReverse: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onTopPassedReverse,
          callbackName = 'topPassedReverse'
        ;
        if(newCallback) {
          module.debug('Adding callback for top passed reverse', newCallback);
          settings.onTopPassedReverse = newCallback;
        }
        if(!calculations.topPassed) {
          if(module.get.occurred('topPassed')) {
            module.execute(callback, callbackName);
          }
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return !calculations.onTopPassed;
        }
      },

      bottomPassedReverse: function(newCallback) {
        var
          calculations = module.get.elementCalculations(),
          callback     = newCallback || settings.onBottomPassedReverse,
          callbackName = 'bottomPassedReverse'
        ;
        if(newCallback) {
          module.debug('Adding callback for bottom passed reverse', newCallback);
          settings.onBottomPassedReverse = newCallback;
        }
        if(!calculations.bottomPassed) {
          if(module.get.occurred('bottomPassed')) {
            module.execute(callback, callbackName);
          }
        }
        else if(!settings.once) {
          module.remove.occurred(callbackName);
        }
        if(newCallback === undefined) {
          return !calculations.bottomPassed;
        }
      },

      execute: function(callback, callbackName) {
        var
          calculations = module.get.elementCalculations(),
          screen       = module.get.screenCalculations()
        ;
        callback     = callback || false;
        if(callback) {
          if(settings.continuous) {
            module.debug('Callback being called continuously', callbackName, calculations);
            $.proxy(callback, element)(calculations, screen);
          }
          else if(!module.get.occurred(callbackName)) {
            module.debug('Conditions met', callbackName, calculations);
            $.proxy(callback, element)(calculations, screen);
          }
        }
        module.save.occurred(callbackName);
      },

      remove: {
        occurred: function(callback) {
          if(callback) {
            if(module.cache.occurred[callback] !== undefined && module.cache.occurred[callback] === true) {
              module.debug('Callback can now be called again', callback);
              module.cache.occurred[callback] = false;
            }
          }
          else {
            module.cache.occurred = {};
          }
        }
      },

      save: {
        calculations: function() {
          module.verbose('Saving all calculations necessary to determine positioning');
          module.save.scroll();
          module.save.direction();
          module.save.screenCalculations();
          module.save.elementCalculations();
        },
        occurred: function(callback) {
          if(callback) {
            if(module.cache.occurred[callback] === undefined || (module.cache.occurred[callback] !== true)) {
              module.verbose('Saving callback occurred', callback);
              module.cache.occurred[callback] = true;
            }
          }
        },
        scroll: function() {
          module.cache.scroll = $context.scrollTop() + settings.offset;
        },
        direction: function() {
          var
            scroll     = module.get.scroll(),
            lastScroll = module.get.lastScroll(),
            direction
          ;
          if(scroll > lastScroll && lastScroll) {
            direction = 'down';
          }
          else if(scroll < lastScroll && lastScroll) {
            direction = 'up';
          }
          else {
            direction = 'static';
          }
          module.cache.direction = direction;
          return module.cache.direction;
        },
        elementPosition: function() {
          var
            screen = module.get.screenSize()
          ;
          module.verbose('Saving element position');
          $.extend(module.cache.element, {
            margin : {
              top    : parseInt($module.css('margin-top'), 10),
              bottom : parseInt($module.css('margin-bottom'), 10)
            },
            fits   : (element.height < screen.height),
            offset : $module.offset(),
            width  : $module.outerWidth(),
            height : $module.outerHeight()
          });
          return module.cache.element;
        },
        elementCalculations: function() {
          var
            screen  = module.get.screenCalculations(),
            element = module.get.elementPosition()
          ;
          // offset
          if(settings.includeMargin) {
            $.extend(module.cache.element, {
              top    : element.offset.top - element.margin.top,
              bottom : element.offset.top + element.height + element.margin.bottom
            });
          }
          else {
            $.extend(module.cache.element, {
              top    : element.offset.top,
              bottom : element.offset.top + element.height
            });
          }
          // visibility
          $.extend(module.cache.element, {
            topVisible       : (screen.bottom >= element.top),
            topPassed        : (screen.top >= element.top),
            bottomVisible    : (screen.bottom >= element.bottom),
            bottomPassed     : (screen.top >= element.bottom),
            pixelsPassed     : 0,
            percentagePassed : 0
          });
          // meta calculations
          $.extend(module.cache.element, {
            visible : (module.cache.element.topVisible || module.cache.element.bottomVisible),
            passing : (module.cache.element.topPassed && !module.cache.element.bottomPassed),
            hidden  : (!module.cache.element.topVisible && !module.cache.element.bottomVisible)
          });
          if(module.cache.element.passing) {
            module.cache.element.pixelsPassed = (screen.top - element.top);
            module.cache.element.percentagePassed = (screen.top - element.top) / element.height;
          }
          module.verbose('Updated element calculations', module.cache.element);
        },
        screenCalculations: function() {
          var
            scroll = $context.scrollTop() + settings.offset
          ;
          if(module.cache.scroll === undefined) {
            module.cache.scroll = $context.scrollTop() + settings.offset;
          }
          module.save.direction();
          $.extend(module.cache.screen, {
            top    : scroll,
            bottom : scroll + module.cache.screen.height
          });
          return module.cache.screen;
        },
        screenSize: function() {
          module.verbose('Saving window position');
          module.cache.screen = {
            height: $context.height()
          };
        },
        position: function() {
          module.save.screenSize();
          module.save.elementPosition();
        }
      },

      get: {
        pixelsPassed: function(amount) {
          var
            element = module.get.elementCalculations()
          ;
          if(amount.search('%') > -1) {
            return ( element.height * (parseInt(amount, 10) / 100) );
          }
          return parseInt(amount, 10);
        },
        occurred: function(callback) {
          return (module.cache.occurred !== undefined)
            ? module.cache.occurred[callback] || false
            : false
          ;
        },
        direction: function() {
          if(module.cache.direction === undefined) {
            module.save.direction();
          }
          return module.cache.direction;
        },
        elementPosition: function() {
          if(module.cache.element === undefined) {
            module.save.elementPosition();
          }
          return module.cache.element;
        },
        elementCalculations: function() {
          if(module.cache.element === undefined) {
            module.save.elementCalculations();
          }
          return module.cache.element;
        },
        screenCalculations: function() {
          if(module.cache.screen === undefined) {
            module.save.screenCalculations();
          }
          return module.cache.screen;
        },
        screenSize: function() {
          if(module.cache.screen === undefined) {
            module.save.screenSize();
          }
          return module.cache.screen;
        },
        scroll: function() {
          if(module.cache.scroll === undefined) {
            module.save.scroll();
          }
          return module.cache.scroll;
        },
        lastScroll: function() {
          if(module.cache.screen === undefined) {
            module.debug('First scroll event, no last scroll could be found');
            return false;
          }
          return module.cache.screen.top;
        }
      },

      setting: function(name, value) {
        if( $.isPlainObject(name) ) {
          $.extend(true, settings, name);
        }
        else if(value !== undefined) {
          settings[name] = value;
        }
        else {
          return settings[name];
        }
      },
      internal: function(name, value) {
        if( $.isPlainObject(name) ) {
          $.extend(true, module, name);
        }
        else if(value !== undefined) {
          module[name] = value;
        }
        else {
          return module[name];
        }
      },
      debug: function() {
        if(settings.debug) {
          if(settings.performance) {
            module.performance.log(arguments);
          }
          else {
            module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
            module.debug.apply(console, arguments);
          }
        }
      },
      verbose: function() {
        if(settings.verbose && settings.debug) {
          if(settings.performance) {
            module.performance.log(arguments);
          }
          else {
            module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
            module.verbose.apply(console, arguments);
          }
        }
      },
      error: function() {
        module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
        module.error.apply(console, arguments);
      },
      performance: {
        log: function(message) {
          var
            currentTime,
            executionTime,
            previousTime
          ;
          if(settings.performance) {
            currentTime   = new Date().getTime();
            previousTime  = time || currentTime;
            executionTime = currentTime - previousTime;
            time          = currentTime;
            performance.push({
              'Name'           : message[0],
              'Arguments'      : [].slice.call(message, 1) || '',
              'Element'        : element,
              'Execution Time' : executionTime
            });
          }
          clearTimeout(module.performance.timer);
          module.performance.timer = setTimeout(module.performance.display, 100);
        },
        display: function() {
          var
            title = settings.name + ':',
            totalTime = 0
          ;
          time = false;
          clearTimeout(module.performance.timer);
          $.each(performance, function(index, data) {
            totalTime += data['Execution Time'];
          });
          title += ' ' + totalTime + 'ms';
          if(moduleSelector) {
            title += ' \'' + moduleSelector + '\'';
          }
          if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
            console.groupCollapsed(title);
            if(console.table) {
              console.table(performance);
            }
            else {
              $.each(performance, function(index, data) {
                console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
              });
            }
            console.groupEnd();
          }
          performance = [];
        }
      },
      invoke: function(query, passedArguments, context) {
        var
          object = instance,
          maxDepth,
          found,
          response
        ;
        passedArguments = passedArguments || queryArguments;
        context         = element         || context;
        if(typeof query == 'string' && object !== undefined) {
          query    = query.split(/[\. ]/);
          maxDepth = query.length - 1;
          $.each(query, function(depth, value) {
            var camelCaseValue = (depth != maxDepth)
              ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
              : query
            ;
            if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
              object = object[camelCaseValue];
            }
            else if( object[camelCaseValue] !== undefined ) {
              found = object[camelCaseValue];
              return false;
            }
            else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
              object = object[value];
            }
            else if( object[value] !== undefined ) {
              found = object[value];
              return false;
            }
            else {
              module.error(error.method, query);
              return false;
            }
          });
        }
        if ( $.isFunction( found ) ) {
          response = found.apply(context, passedArguments);
        }
        else if(found !== undefined) {
          response = found;
        }
        if($.isArray(returnedValue)) {
          returnedValue.push(response);
        }
        else if(returnedValue !== undefined) {
          returnedValue = [returnedValue, response];
        }
        else if(response !== undefined) {
          returnedValue = response;
        }
        return found;
      }
    };

    if(methodInvoked) {
      if(instance === undefined) {
        module.initialize();
      }
      module.invoke(query);
    }
    else {
      if(instance !== undefined) {
        module.destroy();
      }
      module.initialize();
    }
  })
;

return (returnedValue !== undefined)
  ? returnedValue
  : this
;

};

$.fn.visibility.settings = {

name                   : 'Visibility',
namespace              : 'visibility',

className: {
  fixed: 'fixed'
},

debug                  : false,
verbose                : false,
performance            : true,

offset                 : 0,
includeMargin          : false,

context                : window,

// visibility check delay in ms (defaults to animationFrame)
throttle               : false,

// special visibility type (image, fixed)
type                   : false,

// image only animation settings
transition             : false,
duration               : 500,

// array of callbacks for percentage
onPassed               : {},

// standard callbacks
onPassing              : false,
onTopVisible           : false,
onBottomVisible        : false,
onTopPassed            : false,
onBottomPassed         : false,

// reverse callbacks
onPassingReverse       : false,
onTopVisibleReverse    : false,
onBottomVisibleReverse : false,
onTopPassedReverse     : false,
onBottomPassedReverse  : false,

once                   : true,
continuous             : false,

// utility callbacks
onRefresh              : function(){},
onScroll               : function(){},

error : {
  method : 'The method you called is not defined.'
}

};

})( jQuery, window , document );