/**
 * jquery.stickOnScroll.js
 * A jQuery plugin for making element fixed on the page.
 *
 * Created by Paul Tavares on 2012-10-19.
 * Copyright 2012 Paul Tavares. All rights reserved.
 * Licensed under the terms of the MIT License
 *
 */

;(function(factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery'], factory);
    } else {
        factory(jQuery);
    }
}(function($){

    "use strict";
    /*jslint plusplus: true */
    /*global $,jQuery */

    var
        // Check if we're working in IE. Will control the animation
        // on the fixed elements.
        isIE = ($.support.optSelected === false ? true : false),

        // List of viewports and associated array of bound sticky elements
        viewports = {},

        // Local variable to hold methods
        fn = {};

    /**
     * Function bound to viewport's scroll event. Loops through
     * the list of elements that needs to be sticked for the
     * given viewport.
     * "this" keyword is assumed to be the viewport.
     *
     * @param {eventObject} jQuery's event object.
     *
     * @return {Object} The viewport (this keyword)
     *
     */
    function processElements(ev) {

        var elements = viewports[$(this).prop("stickOnScroll")],
            i,j;

        // Loop through all elements bound to this viewport.
        for( i=0,j=elements.length; i<j; i++ ){

            // Scope in the variables
            // We call this anonymous funnction with the
            // current array element ( elements[i] )
            (function(o){

                var scrollTop,
                    maxTop,
                    cssPosition,
                    footerTop,
                    eleHeight,
                    yAxis;

                // get this viewport options
                o = elements[i];

                // FIXME: Should the clean up of reference to removed element store the position in the array and delete it later?

                // If element has no parent, then it must have been removed from DOM...
                // Remove reference to it
                if (o !== null) {

                    // jquery contains works on dom object; not jquery selections
                    if (!$.contains(document.documentElement, o.ele[0])) {

                        elements[i] = o = null;

                    }

                }
                if (o !== null) {

                    // Get the scroll top position on the view port
                    scrollTop   = o.viewport.scrollTop();

                    // set the maxTop before we stick the element
                    // to be it's "normal" topPosition minus offset
                    maxTop      = o.getEleMaxTop();

                    // TODO: What about calculating top values with margin's set?
                    // pt.params.footer.css('marginTop').replace(/auto/, 0)

                    // If not using the window object, then stop any IE animation
                    if (o.isWindow === false && isIE) {
                        o.ele.stop();
                    }

                    // If the current scrollTop position is greater
                    // than our maxTop value, then make element stick on the page.
                    if (scrollTop >= maxTop){

                        cssPosition = {
                            position:   "fixed",
                            top:        ( o.topOffset - o.eleTopMargin )
                        };

                        if (o.isWindow === false) {

                            cssPosition = {
                                position:   "absolute",
                                top:        ( ( scrollTop + o.topOffset ) -  o.eleTopMargin )
                            };

                        }

                        o.isStick = true;

                        // ---> HAS FOOTER ELEMENT?
                        // check to see if it we're reaching the footer element,
                        // and if so, scroll the item up with the page
                        if  (o.footerElement.length) {

                            // Calculate the distance from the *bottom* of the fixed
                            // element to the footer element, taking into consideration
                            // the bottomOffset that may have been set by the user.
                            footerTop   = o.getEleTopPosition(o.footerElement);
                            eleHeight   = o.ele.outerHeight();
                            yAxis       = ( cssPosition.top + eleHeight +
                                            o.bottomOffset + o.topOffset );

                            if (o.isWindow === false) {

                                yAxis = (eleHeight + o.bottomOffset + o.topOffset);

                            } else {

                                yAxis = ( cssPosition.top + scrollTop +
                                          eleHeight + o.bottomOffset );

                                footerTop = o.getElementDistanceFromViewport(o.footerElement);

                            }

                            // If the footer element is overstopping the sticky element
                            // position, then adjust it so that we make room for the
                            // fotoer element.
                            if (yAxis > footerTop) {

                                if (o.isWindow === true) {

                                    cssPosition.top = (
                                            footerTop - ( scrollTop + eleHeight + o.bottomOffset )
                                        );

                                // Absolute positioned element
                                } else {

                                    cssPosition.top = (scrollTop - (yAxis - footerTop));

                                }

                            }

                        }

                        // If o.setParentOnStick is true, then set the
                        // height to this node's parent.
                        if (o.setParentOnStick === true) {

                            o.eleParent.css("height", o.eleParent.height());

                        }

                        // If o.setWidthOnStick is true, then set the width on the
                        // element that is about to be Sticky.
                        if (o.setWidthOnStick === true) {

                            o.ele.css("width", o.ele.css("width"));

                        }

                        // If we have additional stick offset, apply it now
                        if (!o.isViewportOffsetParent) {

                            cssPosition.top = (
                                cssPosition.top - o.getElementDistanceFromViewport(o.eleOffsetParent)
                            );

                        }

                        // Stick the element
                        if (isIE && o.isWindow === false) {

                            o.ele
                                .addClass(o.stickClass)
                                .css("position", cssPosition.position)
                                .animate({top: cssPosition.top}, 150);

                        } else {

                            o.ele
                                .css(cssPosition)
                                .addClass(o.stickClass);

                        }

                        // If making element stick now, then trigger
                        // onStick callback if any
                        if (o.wasStickCalled === false) {

                            o.wasStickCalled = true;

                            setTimeout(function(){

                                if (o.isOnStickSet === true) {

                                    o.onStick.call(o.ele, o.ele);

                                }

                                o.ele.trigger("stickOnScroll:onStick", [o.ele]);

                            }, 20);

                        }

                    // ELSE, If the scrollTop of the view port is
                    // less than the maxTop, then throw the element back into the
                    // page normal flow
                    } else if (scrollTop <= maxTop) {

                        if (o.isStick) {

                            // reset element
                            o.ele
                                .css({
                                    position: "",
                                    top: ""
                                })
                                .removeClass(o.stickClass);

                            o.isStick = false;

                            // Reset parent if o.setParentOnStick is true
                            if (o.setParentOnStick === true) {

                                o.eleParent.css("height", "");

                            }

                            // Reset the element's width if o.setWidthOnStick is true
                            if (o.setWidthOnStick === true) {

                                o.ele.css("width", "");

                            }

                            o.wasStickCalled = false;

                            setTimeout(function(){

                                // Execute the onUnStick if defined
                                if (o.isOnUnStickSet) {

                                    o.onUnStick.call( o.ele, o.ele );

                                }

                                o.ele.trigger("stickOnScroll:onUnStick", [o.ele]);

                            }, 20);

                        }
                    }

                    // Recalculate the original top position of the element...
                    // this could have changed from when element was initialized
                    // - ex. elements were inserted into DOM. We re-calculate only
                    // if the we're at the very top of the viewport, so that we can
                    // get a good position.
                    if (scrollTop === 0) {

                        o.setEleTop();

                    }

                }// is element setup null?

            })( elements[i] );

        }//end: for()

        return this;

    }//end: processElements()


    /**
     * Make the selected items stick to the top of the viewport
     * upon reaching a scrolling offset.
     * This method manipulates the following css properties on
     * the element that is to be sticky: top, position.
     * Elements also receive a css class named 'hasStickOnScroll'.
     *
     * @param {Object} options
     * @param {Integer} [options.topOffset=0]
     * @param {Integer} [options.bottomOffset=5]
     * @param {Object|HTMLElement|jQuery} [options.footerElement=null]
     * @param {Object|HTMLElement|jQuery} [options.viewport=window]
     * @param {String} [options.stickClass="stickOnScroll-on"]
     * @param {Boolean} [options.setParentOnStick=false]
     * @param {Boolean} [options.setWidthOnStick=false]
     * @param {Function} [options.onStick=null]
     * @param {Function} [options.onUnStick=null]
     *
     * @return {jQuery} this
     *
     */
    $.fn.stickOnScroll = function(options) {
        return this.each(function(){

            // If element already has stickonscroll, exit.
            if ($(this).hasClass("hasStickOnScroll")) {
                return this;
            }

            // Setup options for tis instance
            var o   = $.extend({}, {
                            topOffset:          0,
                            bottomOffset:       5,
                            footerElement:      null,
                            viewport:           window,
                            stickClass:         'stickOnScroll-on',
                            setParentOnStick:   false,
                            setWidthOnStick:    false,
                            onStick:            null,
                            onUnStick:          null
                        }, options),
                viewportKey,
                setIntID,
                setIntTries = 1800; // 1800 tries * 100 milliseconds = 3 minutes

            o.isStick                   = false;
            o.ele                       = $(this).addClass("hasStickOnScroll");
            o.eleParent                 = o.ele.parent();
            o.eleOffsetParent           = o.ele.offsetParent();
            o.viewport                  = $(o.viewport);
            o.eleTop                    = 0;
            o.eleTopMargin              = parseFloat(
                                            (o.ele.css("margin-top") || 0)
                                        ) || 0;
            o.footerElement             = $(o.footerElement);
            o.isWindow                  = true;
            o.isOnStickSet              = $.isFunction(o.onStick);
            o.isOnUnStickSet            = $.isFunction(o.onUnStick);
            o.wasStickCalled            = false;
            o.isViewportOffsetParent    = true;

            /**
             * Retrieves the element's top position based on the type of viewport
             * and sets on the options object for the instance. This Top position
             * is the element top position relative to the the viewport.
             *
             * @return {Integer}
             */
            o.setEleTop = function(){

                if (o.isStick === false) {

                    if (o.isWindow) {

                        o.eleTop = o.ele.offset().top;

                    } else {

                        o.eleTop = ( o.ele.offset().top - o.viewport.offset().top );

                    }

                }

            }; //end: o.setEleTop()

            /**
             * REturns an elements top position in relation
             * to the viewport's Top Position.
             *
             * @param {jQuery} $ele
             *          This element must be inside the viewport
             *
             * @return {Integer}
             *
             */
            o.getEleTopPosition = function($ele) {

                var pos = 0;

                if (o.isWindow) {

                    pos = $ele.offset().top;

                } else {

                    pos = ( $ele.offset().top - o.viewport.offset().top );

                }

                return pos;

            }; /* o.getEleTopPosition() */

            /**
             * Get's the MAX top position for the element before it
             * is made sticky. In some cases the max could be less
             * than the original position of the element, which means
             * the element would always be sticky... in these instances
             * the max top will be set to the element's top position.
             *
             * @return {Integer}
             */
            o.getEleMaxTop = function() {

                var max = ( ( o.eleTop - o.topOffset ));

                if (!o.isWindow) {

                    max = (max + o.eleTopMargin);

                    // If ele parent is not the viewport, then adjust the max top


                }

                return max;

            }; //end: o.getEleMaxTop()

            /**
             * Gets the distance between the top of the element and the
             * top of the viewport. Basically the offset from the top of
             * the "page" inside the viewport. This distance is alwasy the
             * same even if the viewport is scrolled. The only time it
             * changes is when elements are inserted or removed above the
             * the Element or item above it are hidden/displayed.
             * Methods uses the Position() values until it reaches the
             * viewport
             */
            o.getElementDistanceFromViewport = function($ele) {

                var distance    = $ele.position().top,
                    $parent     = $ele.offsetParent();

                // If the parent element is the root body element, then
                // we've reached the last possible offsetParent(). Exit
                if ($parent.is("body") || $parent.is("html")) {

                    return distance;

                }

                // If the positioned parent of this element is NOT
                // the viewport, then add the distance of that element's
                // top position
                if ($parent[0] !== o.viewport[0] ) {

                    distance = (
                        distance +
                        o.getElementDistanceFromViewport( $parent )
                    );

                // ELSE, this is the viewport... Adjust the elements
                // Distance by adding on the amount of scroll the element
                // currently has
                } else {

                    distance = (distance + o.viewport.scrollTop());

                }

                return distance;

            }; /* end: .getElementDistanceFromViewport() */

            // If setParentOnStick is true, and the parent element
            // is the <body>, then set setParentOnStick to false.
            if (o.setParentOnStick === true && o.eleParent.is("body")){

                o.setParentOnStick = false;

            }

            if (!$.isWindow(o.viewport[0])) {

                o.isWindow  = false;

            }

            /**
             * Adds this sticky element to the list of element for the viewport.
             *
             */
            function addThisEleToViewportList() {

                o.setEleTop();

                viewportKey = o.viewport.prop("stickOnScroll");

                // If the viewport is not the Window element, and the view port is not the
                // stick element's imediate offset parent, then we need to adjust the
                // top-offset so that element are position correctly.
                // See issue #3 on github
                if (!o.isWindow) {

                    o.isViewportOffsetParent    = ( o.eleOffsetParent[0] === o.viewport[0] );

                }

                // If this viewport is not yet defined, set it up now
                if (!viewportKey) {

                    viewportKey = "stickOnScroll" + String(Math.random()).replace(/\D/g,"");
                    o.viewport.prop("stickOnScroll", viewportKey);
                    viewports[viewportKey] = [];
                    o.viewport.on("scroll", processElements);

                }

                // Push this element's data to this view port's array
                viewports[viewportKey].push(o);

                // Trigger a scroll even
                o.viewport.trigger("scroll");

            } /* end: addThisEleToViewportList() */

            // If Element is not visible, then we have to wait until it is
            // in order to set it up. We need to obtain the top position of
            // the element in order to make the right decision when it comes
            // to making the element sticky.
            if (o.ele.is(":visible")) {

                addThisEleToViewportList();

            } else {

                setIntID = setInterval(function(){

                    if (o.ele.is(":visible") || !setIntTries) {

                        clearInterval(setIntID);
                        addThisEleToViewportList();

                    }

                    --setIntTries;

                }, 100);

            }

            return this;

        });//end: each()

    };//end: $.fn.stickOnScroll()

}));