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

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

“use strict”;

$.fn.sticky = 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.sticky.settings, parameters),

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

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

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

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

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

      element         = this,
      observer,
      module
    ;

    module      = {

      initialize: function() {
        if(settings.context) {
          $context = $(settings.context);
        }
        else {
          $context = $container;
        }
        if($context.size() === 0) {
          module.error(error.invalidContext, settings.context, $module);
          return;
        }
        module.verbose('Initializing sticky', settings, $container);
        module.save.positions();

        // error conditions
        if( module.is.hidden() ) {
          module.error(error.visible, $module);
        }
        if(module.cache.element.height > module.cache.context.height) {
          module.reset();
          module.error(error.elementSize, $module);
          return;
        }

        $window
          .on('resize' + eventNamespace, module.event.resize)
        ;
        $scroll
          .on('scroll' + eventNamespace, module.event.scroll)
        ;

        module.observeChanges();
        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.reset();
        if(observer) {
          observer.disconnect();
        }
        $window
          .off('resize' + eventNamespace, module.event.resize)
        ;
        $scroll
          .off('scroll' + eventNamespace, module.event.scroll)
        ;
        $module
          .removeData(moduleNamespace)
        ;
      },

      observeChanges: function() {
        var
          context = $context[0]
        ;
        if('MutationObserver' in window) {
          observer = new MutationObserver(function(mutations) {
            clearTimeout(module.timer);
            module.timer = setTimeout(function() {
              module.verbose('DOM tree modified, updating sticky menu');
              module.refresh();
            }, 200);
          });
          observer.observe(element, {
            childList : true,
            subtree   : true
          });
          observer.observe(context, {
            childList : true,
            subtree   : true
          });
          module.debug('Setting up mutation observer', observer);
        }
      },

      event: {
        resize: function() {
          requestAnimationFrame(function() {
            module.refresh();
            module.stick();
          });
        },
        scroll: function() {
          requestAnimationFrame(function() {
            module.stick();
            $.proxy(settings.onScroll, element)();
          });
        }
      },

      refresh: function(hardRefresh) {
        module.reset();
        if(hardRefresh) {
          $container = $module.offsetParent();
        }
        module.save.positions();
        module.stick();
        $.proxy(settings.onReposition, element)();
      },

      supports: {
        sticky: function() {
          var
            $element = $('<div/>'),
            element = $element.get()
          ;
          $element
            .addClass(className.supported)
          ;
          return($element.css('position').match('sticky'));
        }
      },

      save: {
        scroll: function(scroll) {
          module.lastScroll = scroll;
        },
        positions: function() {
          var
            window = {
              height: $window.height()
            },
            element = {
              margin: {
                top    : parseInt($module.css('margin-top'), 10),
                bottom : parseInt($module.css('margin-bottom'), 10),
              },
              offset : $module.offset(),
              width  : $module.outerWidth(),
              height : $module.outerHeight()
            },
            context = {
              offset: $context.offset(),
              height: $context.outerHeight()
            }
          ;
          module.cache = {
            fits : ( element.height < window.height ),
            window: {
              height: window.height
            },
            element: {
              margin : element.margin,
              top    : element.offset.top - element.margin.top,
              left   : element.offset.left,
              width  : element.width,
              height : element.height,
              bottom : element.offset.top + element.height
            },
            context: {
              top    : context.offset.top,
              height : context.height,
              bottom : context.offset.top + context.height
            }
          };
          module.set.containerSize();
          module.set.size();
          module.stick();
          module.debug('Caching element positions', module.cache);
        }
      },

      get: {
        direction: function(scroll) {
          var
            direction = 'down'
          ;
          scroll = scroll || $scroll.scrollTop();
          if(module.lastScroll !== undefined) {
            if(module.lastScroll < scroll) {
              direction = 'down';
            }
            else if(module.lastScroll > scroll) {
              direction = 'up';
            }
          }
          return direction;
        },
        scrollChange: function(scroll) {
          scroll = scroll || $scroll.scrollTop();
          return (module.lastScroll)
            ? (scroll - module.lastScroll)
            : 0
          ;
        },
        currentElementScroll: function() {
          return ( module.is.top() )
            ? Math.abs(parseInt($module.css('top'), 10))    || 0
            : Math.abs(parseInt($module.css('bottom'), 10)) || 0
          ;
        },
        elementScroll: function(scroll) {
          scroll = scroll || $scroll.scrollTop();
          var
            element        = module.cache.element,
            window         = module.cache.window,
            delta          = module.get.scrollChange(scroll),
            maxScroll      = (element.height - window.height + settings.offset),
            currentScroll  = module.get.currentElementScroll(),
            possibleScroll = (currentScroll + delta),
            elementScroll
          ;
          if(module.cache.fits || possibleScroll < 0) {
            elementScroll = 0;
          }
          else if (possibleScroll > maxScroll ) {
            elementScroll = maxScroll;
          }
          else {
            elementScroll = possibleScroll;
          }
          return elementScroll;
        }
      },

      remove: {
        offset: function() {
          $module.css('margin-top', '');
        }
      },

      set: {
        offset: function() {
          module.verbose('Setting offset on element', settings.offset);
          $module.css('margin-top', settings.offset);
        },
        containerSize: function() {
          var
            tagName = $container.get(0).tagName
          ;
          if(tagName === 'HTML' || tagName == 'body') {
            // this can trigger for too many reasons
            //module.error(error.container, tagName, $module);
            $container = $module.offsetParent();
          }
          else {
            module.debug('Settings container size', module.cache.context.height);
            $container.height(module.cache.context.height);
          }
        },
        scroll: function(scroll) {
          module.debug('Setting scroll on element', scroll);
          if( module.is.top() ) {
            $module
              .css('bottom', '')
              .css('top', -scroll)
            ;
          }
          if( module.is.bottom() ) {
            $module
              .css('top', '')
              .css('bottom', scroll)
            ;
          }
        },
        size: function() {
          if(module.cache.element.height !== 0 && module.cache.element.width !== 0) {
            $module
              .css({
                width  : module.cache.element.width,
                height : module.cache.element.height
              })
            ;
          }
        }
      },

      is: {
        top: function() {
          return $module.hasClass(className.top);
        },
        bottom: function() {
          return $module.hasClass(className.bottom);
        },
        initialPosition: function() {
          return (!module.is.fixed() && !module.is.bound());
        },
        hidden: function() {
          return (!$module.is(':visible'));
        },
        bound: function() {
          return $module.hasClass(className.bound);
        },
        fixed: function() {
          return $module.hasClass(className.fixed);
        }
      },

      stick: function() {
        var
          cache          = module.cache,
          fits           = cache.fits,
          element        = cache.element,
          window         = cache.window,
          context        = cache.context,
          offset         = (module.is.bottom() && settings.pushing)
            ? settings.bottomOffset
            : settings.offset,
          scroll         = {
            top    : $scroll.scrollTop() + offset,
            bottom : $scroll.scrollTop() + offset + window.height
          },
          direction      = module.get.direction(scroll.top),
          elementScroll  = module.get.elementScroll(scroll.top),

          // shorthand
          doesntFit      = !fits,
          elementVisible = (element.height !== 0)
        ;

        // save current scroll for next run
        module.save.scroll(scroll.top);

        if(elementVisible) {

          if( module.is.initialPosition() ) {
            if(scroll.top >= element.top) {
              module.debug('Element passed, fixing element to page');
              module.fixTop();
            }
          }
          else if( module.is.fixed() ) {

            // currently fixed top
            if( module.is.top() ) {
              if( scroll.top < element.top ) {
                module.debug('Fixed element reached top of container');
                module.setInitialPosition();
              }
              else if( (element.height + scroll.top - elementScroll) > context.bottom ) {
                module.debug('Fixed element reached bottom of container');
                module.bindBottom();
              }
              // scroll element if larger than screen
              else if(doesntFit) {
                module.set.scroll(elementScroll);
              }
            }

            // currently fixed bottom
            else if(module.is.bottom() ) {

              // top edge
              if( (scroll.bottom - element.height) < element.top) {
                module.debug('Bottom fixed rail has reached top of container');
                module.setInitialPosition();
              }
              // bottom edge
              else if(scroll.bottom > context.bottom) {
                module.debug('Bottom fixed rail has reached bottom of container');
                module.bindBottom();
              }
              // scroll element if larger than screen
              else if(doesntFit) {
                module.set.scroll(elementScroll);
              }

            }
          }
          else if( module.is.bottom() ) {
            if(settings.pushing) {
              if(module.is.bound() && scroll.bottom < context.bottom ) {
                module.debug('Fixing bottom attached element to bottom of browser.');
                module.fixBottom();
              }
            }
            else {
              if(module.is.bound() && (scroll.top < context.bottom - element.height) ) {
                module.debug('Fixing bottom attached element to top of browser.');
                module.fixTop();
              }
            }
          }
        }
      },

      bindTop: function() {
        module.debug('Binding element to top of parent container');
        module.remove.offset();
        $module
          .css('left' , '')
          .css('top' , '')
          .css('bottom' , '')
          .removeClass(className.fixed)
          .removeClass(className.bottom)
          .addClass(className.bound)
          .addClass(className.top)
        ;
        $.proxy(settings.onTop, element)();
        $.proxy(settings.onUnstick, element)();
      },
      bindBottom: function() {
        module.debug('Binding element to bottom of parent container');
        module.remove.offset();
        $module
          .css('left' , '')
          .css('top' , '')
          .css('bottom' , '')
          .removeClass(className.fixed)
          .removeClass(className.top)
          .addClass(className.bound)
          .addClass(className.bottom)
        ;
        $.proxy(settings.onBottom, element)();
        $.proxy(settings.onUnstick, element)();
      },

      setInitialPosition: function() {
        module.unfix();
        module.unbind();
      },

      fixTop: function() {
        module.debug('Fixing element to top of page');
        module.set.offset();
        $module
          .css('left', module.cache.element.left)
          .removeClass(className.bound)
          .removeClass(className.bottom)
          .addClass(className.fixed)
          .addClass(className.top)
        ;
        $.proxy(settings.onStick, element)();
      },

      fixBottom: function() {
        module.debug('Sticking element to bottom of page');
        module.set.offset();
        $module
          .css('left', module.cache.element.left)
          .removeClass(className.bound)
          .removeClass(className.top)
          .addClass(className.fixed)
          .addClass(className.bottom)
        ;
        $.proxy(settings.onStick, element)();
      },

      unbind: function() {
        module.debug('Removing absolute position on element');
        module.remove.offset();
        $module
          .removeClass(className.bound)
          .removeClass(className.top)
          .removeClass(className.bottom)
        ;
      },

      unfix: function() {
        module.debug('Removing fixed position on element');
        module.remove.offset();
        $module
          .removeClass(className.fixed)
          .removeClass(className.top)
          .removeClass(className.bottom)
        ;
        $.proxy(settings.onUnstick, this)();
      },

      reset: function() {
        module.debug('Reseting elements position');
        module.unbind();
        module.unfix();
        module.resetCSS();
      },

      resetCSS: function() {
        $module
          .css({
            top    : '',
            bottom : '',
            width  : '',
            height : ''
          })
        ;
        $container
          .css({
            height: ''
          })
        ;
      },

      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, 0);
        },
        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 {
              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.sticky.settings = {

name          : 'Sticky',
namespace     : 'sticky',

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

pushing       : false,
context       : false,
scrollContext : window,
offset        : 0,
bottomOffset  : 0,

onReposition  : function(){},
onScroll      : function(){},
onStick       : function(){},
onUnstick     : function(){},
onTop         : function(){},
onBottom      : function(){},

error         : {
  container      : 'Sticky element must be inside a relative container',
  visible        : 'Element is hidden, you must call refresh after element becomes visible',
  method         : 'The method you called is not defined.',
  invalidContext : 'Context specified does not exist',
  elementSize    : 'Sticky element is larger than its container, cannot create sticky.'
},

className : {
  bound     : 'bound',
  fixed     : 'fixed',
  supported : 'native',
  top       : 'top',
  bottom    : 'bottom'
}

};

})( jQuery, window , document );