/*
 * Copyright (c) 2011 The Wonderfactory, http://www.thewonderfactory.com

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * @fileOverview Scarecrow adds custom scrolling to any container. It also adds
 *               touch support to scrollable elements.
 * @version 0.1
 * @author Edmond Leung <eleung@thewonderfactory.com> 
 */

/**
 * See (http://jquery.com/).
 * @name jQuery
 * @class 
 * See the jQuery Library  (http://jquery.com/) for full details.  This documents
 * the function and classes that are added to jQuery by this plug-in.
 */

/**
 * See (http://jquery.com/)
 * @name jQuery.fn
 * @class 
 * See the jQuery Library  (http://jquery.com/) for full details.  This documents
 * the function and classes that are added to jQuery by this plug-in.
 * @memberOf jQuery
 */

(function($) {
    // Constants
    var FRICTION = 0.93;
    var FPS = 30.0;
    var TOUCH_DELTA_EPSILON = 0.25;
    var BOUNCE_SPEED = 1.5;
    

    var methods = {
        init: function(options) {
            options = $.extend({}, $.fn.scarecrow.defaults, options);
            
            return this.each(function() {
                var self = $(this);

                var content = self.find('.' + options.contentClass);

                // Store so functions can use these variables
                self.data('scarecrow.options', options);
                self.data('scarecrow.content', content);

                // Call state changed (works if this is a new scarecrow or updating the scarecrow)
                functions.stateChanged.call(self);
                functions.setupInteractions.call(self);
            });
        }
    };
    
    var functions = {
        // This is called whenever something about the scrollable size changes as
        // it updates the variables used to calculate the scrolling.
        stateChanged: function() {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            var content = self.data('scarecrow.content');
            
            var scrollPosition = { left: self.prop('scrollLeft'), top: self.prop('scrollTop') }; // the current scrolling position
            var containerSize = { width: self.width(), height: self.height() }; // size of the actual container
            var contentSize = { width: self.prop('scrollWidth'), height: self.prop('scrollHeight') }; // the size of the content inside the container
            
            var scrollableSize = { 
                width: contentSize.width - containerSize.width,
                height: contentSize.height - containerSize.height
            }; // the scrollable size (so where the scrolling can actually scroll to)
            
            if (scrollableSize.width < 0) { scrollableSize.width = 0; }
            if (scrollableSize.height < 0) { scrollableSize.height = 0; }
            
            // Save these variables for later use
            self.data('scarecrow.scrollPosition', scrollPosition);
            self.data('scarecrow.containerSize', containerSize);
            self.data('scarecrow.contentSize', contentSize);
            self.data('scarecrow.scrollableSize', scrollableSize);
            
            // Update scrollbars when state has changed
            functions.setupScrollbars.call(self);
            
            // Update
            functions.setScrollPosition.call(self, scrollPosition);
        },
        
        // Creates a new scrollbar with the specified class and data name or grabs an old one
        getScrollbar: function(scrollbarClass, name) {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            var scrollbar = self.data(name);
            
            if (scrollbar == null) {
                var options = self.data('scarecrow.options');
                var scrollbar =  $('<div>').addClass(scrollbarClass).html(options.scrollbarHtml).appendTo(self);
                self.data(name, scrollbar);
            }
            
            return scrollbar;
        },
        
        // Creates the scrollbars and works out the max positions they can go
        setupScrollbars: function() {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            var containerSize = self.data('scarecrow.containerSize');
            var contentSize = self.data('scarecrow.contentSize');
            var scrollableSize = self.data('scarecrow.scrollableSize');
            
            // Find the scrollbar
            var verticalScrollbar = functions.getScrollbar.call(self, options.verticalScrollbarClass, 'scarecrow.verticalScrollbar');
            var horizontalScrollbar = functions.getScrollbar.call(self, options.horizontalScrollbarClass, 'scarecrow.horizontalScrollbar');
            
            // Work out how big the scrollbars can go
            var maxScrollbarSizes = { 
                vertical: containerSize.height - parseInt(verticalScrollbar.css('top')) * 2,
                horizontal: containerSize.width - parseInt(horizontalScrollbar.css('left')) * 2
            };
            
            if (scrollableSize.height == 0) { 
                verticalScrollbar.addClass(options.verticalScrollbarDisableClass);
            } else {
                verticalScrollbar.removeClass(options.verticalScrollbarDisableClass);
                maxScrollbarSizes.horizontal -= verticalScrollbar.outerWidth();
            }
            
            if (scrollableSize.width == 0) { 
                horizontalScrollbar.addClass(options.horizontalScrollbarDisableClass);
            } else {
                horizontalScrollbar.removeClass(options.horizontalScrollbarDisableClass);
                maxScrollbarSizes.vertical -= horizontalScrollbar.outerHeight();
            }
            
            // Work out the normal scrollbar sizes
            var normalScrollbarSizes = {
                vertical: (containerSize.height / contentSize.height) * maxScrollbarSizes.vertical,
                horizontal: (containerSize.width / contentSize.width) * maxScrollbarSizes.horizontal
            };
            
            // Max positions for each scrollbar
            var maxScrollbarPositions = {
                vertical: maxScrollbarSizes.vertical - normalScrollbarSizes.vertical,
                horizontal: maxScrollbarSizes.horizontal - normalScrollbarSizes.horizontal                
            }
            
            self.data('scarecrow.maxScrollbarSizes', maxScrollbarSizes);
            self.data('scarecrow.normalScrollbarSizes', normalScrollbarSizes);       
            self.data('scarecrow.maxScrollbarPositions', maxScrollbarPositions);
            
            functions.updateScrollbars.call(self);
        },
        
        // Updates the sizes of the bars, and their positions
        updateScrollbars: function() {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            var containerSize = self.data('scarecrow.containerSize');
            var scrollableSize = self.data('scarecrow.scrollableSize');
            var scrollPosition = self.data('scarecrow.scrollPosition');
            
            var verticalScrollbar = self.data('scarecrow.verticalScrollbar');
            var horizontalScrollbar = self.data('scarecrow.horizontalScrollbar');
            var normalScrollbarSizes = self.data('scarecrow.normalScrollbarSizes');
            var maxScrollbarSizes = self.data('scarecrow.maxScrollbarSizes');
            var maxScrollbarPositions = self.data('scarecrow.maxScrollbarPositions');
            
            // The percentages of where the scrolling is
            var scrollingPercentages = { top: 0, left: 0 };
            if (scrollableSize.height != 0) {
                scrollingPercentages.top = scrollPosition.top / scrollableSize.height;
            }
            if (scrollableSize.width != 0) {
                scrollingPercentages.left = scrollPosition.left / scrollableSize.width;
            }
            
            // Calculate where the scrollbars should be
            var scrollbarPositions = {
                vertical: scrollingPercentages.top * maxScrollbarPositions.vertical,
                horizontal: scrollingPercentages.left * maxScrollbarPositions.horizontal
            };
            
            var clampScrollbarSizes = function() {
                // Clamp to max sure scrollbars don't shrink too much
                if (scrollbarSizes.vertical < options.minimumScrollbarSize) { scrollbarSizes.vertical = options.minimumScrollbarSize };
                if (scrollbarSizes.horizontal < options.minimumScrollbarSize) { scrollbarSizes.horizontal = options.minimumScrollbarSize };
            }
            
            // Clamp sizes if scroll positions goes outside the scrollable area (bouncing)
            var scrollbarSizes = {
                vertical: normalScrollbarSizes.vertical,
                horizontal: normalScrollbarSizes.horizontal
            };
            if (scrollbarPositions.vertical < 0) {
                scrollbarSizes.vertical += scrollbarPositions.vertical;
                scrollbarPositions.vertical = 0;
                
                clampScrollbarSizes();
            } else if (scrollbarPositions.vertical >= maxScrollbarPositions.vertical) {
                scrollbarSizes.vertical += (maxScrollbarPositions.vertical - scrollbarPositions.vertical);
                clampScrollbarSizes();
                scrollbarPositions.vertical = maxScrollbarSizes.vertical - scrollbarSizes.vertical;
            }
            if (scrollbarPositions.horizontal < 0) {
                scrollbarSizes.horizontal += scrollbarPositions.horizontal;
                scrollbarPositions.horizontal = 0;
                
                clampScrollbarSizes();
            } else if (scrollbarPositions.horizontal >= maxScrollbarPositions.horizontal) {
                scrollbarSizes.horizontal += (maxScrollbarPositions.horizontal - scrollbarPositions.horizontal);
                clampScrollbarSizes();
                scrollbarPositions.horizontal = maxScrollbarSizes.horizontal - scrollbarSizes.horizontal;
            }
            
            // Position the scrollbars
            verticalScrollbar.css({
                'margin-top': isNaN(scrollbarPositions.vertical) ? 0 : scrollbarPositions.vertical,
                'height': isNaN(scrollbarSizes.vertical) ? 0 : scrollbarSizes.vertical
            });
            horizontalScrollbar.css({
                'margin-left': isNaN(scrollbarPositions.horizontal) ? 0 : scrollbarPositions.horizontal,
                'width': isNaN(scrollbarSizes.horizontal) ? 0 : scrollbarSizes.horizontal
            });
        },
        
        // Shows a generic scroll bar
        showScrollbar: function(name, visibleClass) {
            var self = $(this);
            var options = self.data('scarecrow.options');
            
            self.data(name).addClass(visibleClass);
        },
        
        // Hides a generic scroll bar
        hideScrollbar: function(name, visibleClass) {
            var self = $(this);
            var options = self.data('scarecrow.options');
            
            self.data(name).removeClass(visibleClass);
        },
        
        // Clamps the scroll position when called (so it can't go over scrollable area)
        clampScrollPosition: function() {
            var self = $(this);
            var horizontalClamped = false, verticalClamped = false;
            
            var scrollPosition = self.data('scarecrow.scrollPosition');
            var scrollableSize = self.data('scarecrow.scrollableSize');
            
            if (scrollPosition.left < 0) {
                scrollPosition.left = 0;
                horizontalClamped = true;
            } else if (scrollPosition.left >= scrollableSize.width) {
                scrollPosition.left = scrollableSize.width;
                horizontalClamped = true;
            }
            
            if (scrollPosition.top < 0) {
                scrollPosition.top = 0;
                verticalClamped = true;
            } else if (scrollPosition.top >= scrollableSize.height) {
                scrollPosition.top = scrollableSize.height;
                verticalClamped = true;
            }
            
            return verticalClamped && horizontalClamped;
        },

        // Clamps the scroll position for touch bouncing
        bounceClampScrollPosition: function() {
            var self = $(this);

            var scrollPosition = self.data('scarecrow.scrollPosition');
            var scrollableSize = self.data('scarecrow.scrollableSize');

            if (scrollableSize.width == 0) {
                if (scrollPosition.left < 0) {
                    scrollPosition.left = 0;
                } else if (scrollPosition.left >= scrollableSize.width) {
                    scrollPosition.left = scrollableSize.width;                
                }
            }

            if (scrollableSize.height == 0) {
                if (scrollPosition.top < 0) {
                    scrollPosition.top = 0;
                } else if (scrollPosition.top >= scrollableSize.height) {
                    scrollPosition.top = scrollableSize.height;  
                }
            }
        },
        
        // Scrolls to the specified position
        setScrollPosition: function(position) {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            var content = self.data('scarecrow.content');
            var scrollableSize = self.data('scarecrow.scrollableSize');
            
            // Store the new position
            self.data('scarecrow.scrollPosition', position);
            
            var cssPosition = { left: -position.left, top: -position.top };
            
            // If outside of scroll area, allow for bounce
            if (position.top < 0) { 
                cssPosition.top = -position.top / 2.0; 
            } else if (position.top >= scrollableSize.height) {
                cssPosition.top = -(scrollableSize.height + ((position.top - scrollableSize.height) / 2.0));
            } 
            if (position.left < 0) { 
                cssPosition.left = -position.left / 2.0;
            } else if (position.left >= scrollableSize.width) {
                cssPosition.left = -(scrollableSize.width + ((position.left - scrollableSize.width) / 2.0));
            }
            
            // Scroll to it
            content.css(cssPosition);
            
            // Update the scrollbar
            functions.updateScrollbars.call(self);
        },
        
        // Sets up the interactions that use the mouse
        setupMouseInteractions: function() {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            
            // Mouse wheel event
            if (self.mousewheel != null) {
                self.mousewheel(function(event, delta, deltaX, deltaY) {
                    // Adjust delta
                    deltaX *= options.mouseScrollSpeed;
                    deltaY *= options.mouseScrollSpeed;
                    
                    var scrollPosition = self.data('scarecrow.scrollPosition');
                    scrollPosition.left += deltaX;
                    scrollPosition.top -= deltaY;
                    
                    // Using mouse means we clamp as there is no inertia bounce
                    var clamped = functions.clampScrollPosition.call(self);
                    functions.setScrollPosition.call(self, scrollPosition);
                    
                    return clamped;
                });   
            }
            
            // Show the scroll bar when mouse is over scarecrow
            self.mouseenter(function() {
                functions.showScrollbar.call(self, 'scarecrow.verticalScrollbar', options.verticalScrollbarVisibleClass);
                functions.showScrollbar.call(self, 'scarecrow.horizontalScrollbar', options.horizontalScrollbarVisibleClass);                
            });
            
            self.mouseleave(function() {
                functions.hideScrollbar.call(self, 'scarecrow.verticalScrollbar', options.verticalScrollbarVisibleClass);
                functions.hideScrollbar.call(self, 'scarecrow.horizontalScrollbar', options.horizontalScrollbarVisibleClass);
            });
        },
        
        // Sets up the interactions that use the mouse
        setupTouchInteractions: function(event) {
            var self = $(this);
            
            var options = self.data('scarecrow.options');
            
            self.bind('touchstart', function(event) {
                if (self.data('scarecrow.touchIdentifier') == null) {
                    // Store identifier so we can find the same touch object later
                    self.data('scarecrow.touchIdentifier', event.originalEvent.changedTouches[0].identifier);

                    // Reset variables
                    self.data('scarecrow.lastTouchPoint', null);
                    self.data('scarecrow.touchDelta', { left: 0, top: 0 });   
                }
            });
            
            self.bind('touchmove', function(event) {
                var touchIdentifier = self.data('scarecrow.touchIdentifier');
                
                if (touchIdentifier != null) {
                    // Search for the touch object with the stored identifier
                    for (var i = 0; i < event.originalEvent.changedTouches.length; i++) {
                        var touch = event.originalEvent.changedTouches[i];
                        
                        if (touch.identifier == touchIdentifier) {
                            // If found, then use it
                            var lastTouchPoint = self.data('scarecrow.lastTouchPoint');
                            var touchPoint = { left: touch.pageX, top: touch.pageY };
                            
                            // If no last, then just make current as last
                            if (lastTouchPoint == null) {
                                lastTouchPoint = touchPoint;
                            }
                            
                            // Calculate the touch delta (how much the touch moved)
                            var touchDelta = { 
                                left: touchPoint.left - lastTouchPoint.left, 
                                top: touchPoint.top - lastTouchPoint.top
                            };
                            
                            // Move the scroll position based on touch delta
                            var scrollPosition = self.data('scarecrow.scrollPosition');
                            scrollPosition.left -= touchDelta.left;
                            scrollPosition.top -= touchDelta.top;
                            
                            functions.bounceClampScrollPosition.call(self);
                            functions.setScrollPosition.call(self, scrollPosition);

                            self.data('scarecrow.lastTouchPoint', touchPoint);                            
                            self.data('scarecrow.touchDelta', touchDelta);
                            
                            // Show scroll bars while touch moving
                            functions.showScrollbar.call(self, 'scarecrow.verticalScrollbar', options.verticalScrollbarVisibleClass);
                            functions.showScrollbar.call(self, 'scarecrow.horizontalScrollbar', options.horizontalScrollbarVisibleClass);
                            
                            return false;
                        }
                    }
                }
            });
            
            self.bind('touchend', function(event) {
                self.data('scarecrow.touchIdentifier', null);
            });
            
            // Inertia updater
            setInterval(function() {
                if (self.data('scarecrow.touchIdentifier') != null) { return; }
                
                // If no touch delta, then quit
                var touchDelta = self.data('scarecrow.touchDelta');
                if (touchDelta == null || (touchDelta.top == 0 && touchDelta.left == 0)) { return; }
                
                var options = self.data('scarecrow.options');
                var scrollPosition = self.data('scarecrow.scrollPosition');
                var scrollableSize = self.data('scarecrow.scrollableSize');
                
                if (scrollPosition.top < 0) {
                    touchDelta.top = (scrollPosition.top / BOUNCE_SPEED);
                } else if (scrollPosition.top >= scrollableSize.height) {
                    var newPosition = scrollableSize.height + (scrollPosition.top - scrollableSize.height) / BOUNCE_SPEED;
                    touchDelta.top = scrollPosition.top - newPosition;
                }
                if (scrollPosition.left < 0) { 
                    touchDelta.left = (scrollPosition.left / BOUNCE_SPEED);
                } else if (scrollPosition.left >= scrollableSize.width) { 
                    var newPosition = scrollableSize.width + (scrollPosition.left - scrollableSize.width) / BOUNCE_SPEED;
                    touchDelta.left = scrollPosition.left - newPosition;
                }
                
                // Calculate left and top frictions
                var friction = { left: FRICTION, top: FRICTION };
                if (Math.abs(touchDelta.left) < TOUCH_DELTA_EPSILON) { friction.left = 0; }
                if (Math.abs(touchDelta.top) < TOUCH_DELTA_EPSILON) { friction.top = 0; }
                
                touchDelta.top *= friction.top;
                touchDelta.left *= friction.left;
                scrollPosition.top -= touchDelta.top;
                scrollPosition.left -= touchDelta.left;
                
                // Show scroll bars if scrolling is moving
                if (touchDelta.top == 0 && touchDelta.left == 0) {
                    functions.hideScrollbar.call(self, 'scarecrow.verticalScrollbar', options.verticalScrollbarVisibleClass);
                    functions.hideScrollbar.call(self, 'scarecrow.horizontalScrollbar', options.horizontalScrollbarVisibleClass);
                } else {
                    functions.showScrollbar.call(self, 'scarecrow.verticalScrollbar', options.verticalScrollbarVisibleClass);
                    functions.showScrollbar.call(self, 'scarecrow.horizontalScrollbar', options.horizontalScrollbarVisibleClass);
                }
                
                functions.setScrollPosition.call(self, scrollPosition);

                // Store the new touch angle difference back in
                self.data('scarecrow.touchDelta', touchDelta);
            }, 1000.0 / FPS);
        },
        
        // Calls all setup interaction related methods
        setupInteractions: function() {
            var self = $(this);
            
            // Only add interactions once (ever!)
            if (!self.data('scarecrow.interactionsSetup')) {
                functions.setupMouseInteractions.call(self);
                functions.setupTouchInteractions.call(self);   
            }
            
            // Notify that this has been 'scarecrowed' before
            self.data('scarecrow.interactionsSetup', true);
        }
    };
    
    /**
     * <p>Remove all elements from the set of matched elements using a a set of 
     * words and options settings. The default is to show return only elements that
     * contain all the specified words.  However, the options can be used to
     * change this to be be any word or the inverse match.</p>
     *
     * The example below will only show paragraphs that contain both the words
     * foo and bar.
     *
     * @example $('.scarecrow').scarecrow(); 
     *
     * @param options An optional options object.
     * @param options.contentClass The class of the content area that will be scrolling.
     * @param options.mouseScrollSpeed The speed of a mouse scroll.
     * @param options.verticalScrollbarClass The class applied to the vertical 
     *                                       scrollbar that will be injected by scarescrow.
     * @param options.horizontalScrollbarClass The class applied to the horizontal 
     *                                         scrollbar that will be injected by scarescrow.
     * @param options.minimumScrollbarSize The minimum size the scrollbar can ever shrink to.
     * @param options.scrollbarHtml The HTML injected into the scrollbar, you can change this
     *                              to customise your scrollbar.
     *
     */
    jQuery.fn.scarecrow = function(method) {
        // Method calling logic
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || ! method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' +  method + ' does not exist');
        }
    };
    
    jQuery.fn.scarecrow.defaults = {
        contentClass: 'scarecrow-content',
        
        mouseScrollSpeed: 1.0,
        
        verticalScrollbarClass: 'scarecrow-vertical-scrollbar',
        horizontalScrollbarClass: 'scarecrow-horizontal-scrollbar',
        verticalScrollbarVisibleClass: 'scarecrow-vertical-scrollbar-visible',
        horizontalScrollbarVisibleClass: 'scarecrow-horizontal-scrollbar-visible',
        verticalScrollbarDisableClass: 'scarecrow-vertical-scrollbar-disabled',
        horizontalScrollbarDisableClass: 'scarecrow-horizontal-scrollbar-disabled',
        minimumScrollbarSize: 6,
        scrollbarHtml: "<div class='scrollbar-top-cap'></div><div class='scrollbar-bottom-cap'></div>"
    };
})(jQuery);

/*
Call $.fn.scarecrow() again to update if any sizes etc. have changed.
Document the jquery.mousewheel requirement (make sure that include it in their projects, use the one in this project as it has been modified)
Document scroll speed and how to adjust
Document that all content inside the scarecrow should be loaded (or at least size is correct) before calling scarecrow
content needs to be position relative
When doing horizontal scrolling (or horizontal+vert) you need to set with width of the scarecrow-content

scrollbar markup is generated by plugin, but you can customise the markup inside the scrollbar and add elements etc.
uses top right position of scrollbar to determine the where the bottom right of the scrollbar is
*/
