// TODO threshold
(function($)
{
	/*
	    coordinate pattern
	    -------------------
	    | 0,0 | 1,0 | 2,0 |
	    | 0,1 | 1,1 | 2,1 |
	    | 0,2 | 1,2 | 2,2 |
	    -------------------
	*/
	
	$.fn.sfFindBoundaries = function()
	{
	    // new boundaries
	    var nb = {};
	    this.each(function(i)
	    {
	        var coords = $(this).data('coordinates');
	        if(i>0) {
	            nb.left = Math.min(coords.x, nb.left);
	            nb.right = Math.max(coords.x, nb.right);
	            nb.top = Math.min(coords.y, nb.top);
	            nb.bottom = Math.max(coords.y, nb.bottom);
	        } else {
	            nb.left = nb.right = coords.x;
	            nb.top = nb.bottom = coords.y;
	        }
	    });
	    return nb;
	};

	$.fn.sfGetOffset = function()
	{
		var outerWinOffset = $(this).parents('.scrollf').offset(),
			elOffset = $(this[0]).offset();

		return {
			left: elOffset.left - outerWinOffset.left,
			top: elOffset.top - outerWinOffset.top
		}
	};

	scrollf = function()
	{
	    // configurable variables
	    var options = {
	            SCROLL_HORIZONTAL: true,
	            SCROLL_VERTICAL: true,
	            BOUNDS: {},
	            USE_CONTENT_CACHE: true,
	            //THRESHOLD_MULTIPLIER: 0.2,
	            callback: function(x, y) { return x+','+y; }
	        },
	        panelWidth = 200,
	        panelHeight = 200,
	        panelOuterWidth,
	        panelOuterHeight,
	        // how far the panel must be off the screen before removed
	        // window dimensions multiplied by the THRESHOLD_MULTPLIER to calculate these
	        //verticalThreshold,
	        //horizontalThreshold, 
	        win, // inside container, established in init
	        outerWin, // outside container
	        cache = {}, // content cache
	        cachePending = {}, // requests went, waiting for return
	        sf = this;

	    function establishBoundaries()
	    {    
	        var panels = $('.panel', win),
	            nb = panels.sfFindBoundaries();
	
	        panels.removeClass('left right top bottom');
	    
	        $('.ph'+nb.left, win).addClass('left');
	        $('.ph'+nb.right, win).addClass('right');
	        $('.pv'+nb.top, win).addClass('top');
	        $('.pv'+nb.bottom, win).addClass('bottom');
	    }
	
	    function loadNewPanels(byX, byY)
	    {
	        var newPanels = {
	            top: [],
	            bottom: []
	        };
	
	        function loadPanels(type, secondPass) // TODO this doesn't use the threshold code yet.
	        {
	            this.each(function()
	            {
	                var coords = $(this).data('coordinates'),
	                    x = coords.x,
	                    y = coords.y,
	                    left = parseInt($(this).css('left'), 10),
	                    top = parseInt($(this).css('top'), 10),
	                    panel;
	
	                switch(type) {
	                    case 'left': // moved right
	                        if(left <= 0) {
	                        	//console.log('cancelling left', left, 'window', win.css('left'));
	                        	// cancelling left panels
	                        	return;
	                        }
	
	                        for(var j=1, k=Math.ceil(Math.abs(byX)/panelOuterWidth); j<=k; j++) {
	                            createPanel(x-j, y, left - panelOuterWidth*j, top);
	                        }
	                        break;
	                    case 'right': // moved left
	                        if(left+panelOuterWidth >= win.outerWidth()) {
	                        	//console.log('cancelling right.', left, win.outerWidth());
	                        	// cancelling right panels
	                        	return; // TODO make work with margin/border
	                        }
	
	                        for(var j=1, k=Math.ceil(Math.abs(byX)/panelOuterWidth); j<=k; j++) {
	                            createPanel(x+j, y, left + panelOuterWidth*j, top);
	                        }
	                        break;
	                    case 'top': // moved down
	                        if(top <= 0) {
	                        	//console.log('cancelling top', top, 'window', win.css('left'));
	                        	// cancelling top panels
	                        	return;
	                        }
	
	                        for(var j=1, k=Math.ceil(Math.abs(byY)/panelOuterHeight); j<=k; j++) {
	                            // will return null if out of bounds
	                            panel = createPanel(x, y-j, left, top - panelOuterHeight*j);
	                            if(!secondPass && panel) {
	                                newPanels.top.push(panel[0]);
	                            }
	                        }
	                        break;
	                    case 'bottom': // moved up
	                        if(top+panelOuterHeight >= win.outerHeight()) {
	                        	//console.log('cancelling bottom.', top, win.outerHeight());
	                        	// cancelling bottom panels
	                        	return; // TODO make work with margin/border
	                        }
	
	                        for(var j=1, k=Math.ceil(Math.abs(byY)/panelOuterHeight); j<=k; j++) {
	                            // will return null if out of bounds
	                            panel = createPanel(x, y+j, left, top + panelOuterHeight*j);
	                            if(!secondPass && panel) {
	                                newPanels.bottom.push(panel[0]);
	                            }
	                        }
	                        break;
	                }
	
	                if(!secondPass) {
	                    $(this).removeClass(type);
	                }
	            });
	        }
	
	        // TODO better algorithm to order the elements passed to loadPanels
	        // images barely showing are given the same weight as those fully shown in the middle
	
	        if(byY > 0) {
	            // assign new top
	            loadPanels.call($('.top', win), 'top');
	        }
	        if(byY < 0) {
	            // assign new bottom
	            loadPanels.call($('.bottom', win), 'bottom');
	        }
	        if(byX > 0) {
	            // assign new left
	            loadPanels.call($('.left', win), 'left');
	        }
	        if(byX < 0) {
	            // assign new right
	            loadPanels.call($('.right', win), 'right');
	        }
	
	        // second pass, hit the corners
	        if(byX > 0 && byY > 0) { // already assigned top, left
	            // assign new top left corner
	            var b = $(newPanels.top).sfFindBoundaries();
	            // of the new top panels, pick the leftmost
	            loadPanels.call($(newPanels.top).filter('.ph'+b.left), 'left', true);
	        }
	
	        if(byX > 0 && byY < 0) { // already assigned bottom, left
	            // assign new bottom left corner
	            var b = $(newPanels.bottom).sfFindBoundaries(); 
	            // of the new bottom panels, pick the leftmost
	            loadPanels.call($(newPanels.bottom).filter('.ph'+b.left), 'left', true);
	        }
	 
	        if(byX < 0 && byY > 0) { // already assigned top, right
	            // assign new top right corner
	            var b = $(newPanels.top).sfFindBoundaries();
	            // of the new top panels, pick the rightmost
	            loadPanels.call($(newPanels.top).filter('.ph'+b.right), 'right', true);
	        }
	    
	        if(byX < 0 && byY < 0) { // already assigned bottom, right
	            // assign new bottom right corner
	            var b = $(newPanels.bottom).sfFindBoundaries();
	            // of the new bottom panels, pick the rightmost
	            loadPanels.call($(newPanels.bottom).filter('.ph'+b.right), 'right', true);
	        }
	    }
	
	    function createPanel(x, y, left, top, isTop, isRight, isBottom, isLeft)
	    {
	        // don't create panels if we're out of bounds
	        if(!isNaN(options.BOUNDS.left) && x < options.BOUNDS.left ||
	            !isNaN(options.BOUNDS.right) && x > options.BOUNDS.right ||
	            !isNaN(options.BOUNDS.top) && y < options.BOUNDS.top ||
	            !isNaN(options.BOUNDS.bottom) && y > options.BOUNDS.bottom) {
	            return;
	        }
	
	        var panel = $('<div/>').data('coordinates', {x: x, y: y}).addClass('panel ph'+x+' pv'+y+' p'+x+'_'+y +
	                    (isTop ? ' top' : '') +
	                    (isRight ? ' right' : '') +
	                    (isBottom ? ' bottom' : '') +
	                    (isLeft ? ' left' : '')
	                ).css({
	                    left: left + 'px',
	                    top: top + 'px'
	                }).bind('mouseenter mouseleave', function()
	        		{
	                	$(this).toggleClass('panelHover');
	        		});
	
	        var cached = sf.cacheGet(x, y);
	        if(options.USE_CONTENT_CACHE && cached) {
	            panel.html(cached);
	        } else {
	            var content = options.callback.call(panel[0], x, y);
	            if(content || content === '') {
	                if(options.USE_CONTENT_CACHE) {
	                	sf.cacheAdd(x, y, content); 
	                }
	                panel.html(content);
	            }
	        }
	        win.append(panel);
	        return panel;
	    }
	
	    // fake move will load panels and resize window, but won't actually move the shiz.
	    function movePanels(byX, byY, bFakeMove)
	    {
	        var panels = $('.panel', win);
	
	        var b = panels.sfFindBoundaries(),
	            bPosition,
	            winWidth = outerWin.outerWidth(), // TODO make work with margin/border
	            winHeight = outerWin.outerHeight(); // TODO make work with margin/border
	
	        if(options.SCROLL_HORIZONTAL && !isNaN(options.BOUNDS.left) && b.left === options.BOUNDS.left) {
	            bPosition = $('.ph'+b.left, win).sfGetOffset().left;
	            if(byX > 0 && bPosition >= 0) { // moving left and is boundary
	                byX = -1*bPosition;
	            }
	        }
	        if(options.SCROLL_HORIZONTAL && !isNaN(options.BOUNDS.right) && b.right === options.BOUNDS.right) {
	            bPosition = $('.ph'+b.right, win).sfGetOffset().left + panelOuterWidth;
	            if(byX < 0 && bPosition <= winWidth) { // moving right and is boundary
	                byX = winWidth - bPosition;
	            }
	        }
	        if(options.SCROLL_VERTICAL && !isNaN(options.BOUNDS.top) && b.top === options.BOUNDS.top) {
	            bPosition = $('.pv'+b.top, win).sfGetOffset().top;
	            if(byY > 0 && bPosition >= 0) { // moving up and is boundary
	            	byY = -1*bPosition;
	            }
	        }
	        if(options.SCROLL_VERTICAL && !isNaN(options.BOUNDS.bottom) && b.bottom === options.BOUNDS.bottom) {
	            bPosition = $('.pv'+b.bottom, win).sfGetOffset().top + panelOuterHeight;
	            if(byY < 0 && bPosition <= winHeight) { // moving down and is boundary
	                byY = winHeight - bPosition;
	            }
	        }
	
	        var previousLeft = parseInt($(win).css('left'),10),
		        newX = previousLeft+byX,
		        previousTop = parseInt($(win).css('top'), 10),
		        newY = previousTop+byY,
		        newWidth = outerWin.outerWidth() + Math.abs(newX),
	    		newHeight = outerWin.outerHeight() + Math.abs(newY);
	
	        win.css({
	    		left: bFakeMove ? 0 : newX,
	    		top: bFakeMove ? 0 : newY,
	    		width: newWidth,
				height: newHeight
	        });
	
	
	        // Remove panels that are too far away.
	        panels.each(function()
	        {
	            var offset = $(this).sfGetOffset(),
	                panelLeft = offset.left,
	                panelTop = offset.top;
	
	            // if this panel is off the screen, delete it
	            if(panelLeft + panelOuterWidth < 0 || 		//-1*horizontalThreshold ||
	        		panelLeft > outerWin.outerWidth() || 	//+horizontalThreshold || //TODO make this work with borders/margins
	        		panelTop + panelOuterHeight < 0 || 		// -1*verticalThreshold ||
	                panelTop > outerWin.outerHeight()) { 	// + verticalThreshold) { //TODO make this work with borders/margins
	
	                // remove it (it's off the screen, past the threshold)
	                $(this).remove();
	            }
	        });
	
	        loadNewPanels(byX, byY);     
	        establishBoundaries();
	    }
	
	    
	    var mouseMoveTimeout,
	    	lastDiff = (function()
	    	{
				var d;
	
	    		return {
					get: function()
					{
	    				return d;
					},
					set: function(diff)
					{
						d = diff;
					}
	    		}
	    	})();
	
	    function throttledMovePanels(byX, byY, bFakeMove)
	    {
	    	lastDiff.set({
	            x: byX,
	            y: byY
	        });
	
	    	// throttle movement
	        if(!mouseMoveTimeout) {
	            mouseMoveTimeout = window.setTimeout(function()
	    		{
	            	var d = lastDiff.get();
	            	movePanels(options.SCROLL_HORIZONTAL ? d.x : 0, options.SCROLL_VERTICAL ? d.y : 0, bFakeMove);
	            	window.clearTimeout(mouseMoveTimeout);
	            	mouseMoveTimeout = null;
	    		}, 100);
	        }
	    }

	    $.extend(this, {
	        setBound: function(type, bound)
	        {
	            options.BOUNDS[type] = bound;
	        },
	        getBound: function(type)
	        {
	            return options.BOUNDS[type];
	        },
	        move: function(byX, byY)
	        {
	        	throttledMovePanels(byX, byY);
	        },
	        init: function(element, optionOverrides)
	        {
	        	outerWin = $(element).addClass('scrollf');
	
	    		var p = outerWin.append('<div class="panel"/>').find('.panel');
	    		panelHeight = p.height();
	        	panelWidth = p.width();

	        	// calculate the actual panel width and height (including padding/margin/border
	        	var widthModifiers = ['padding-left', 'padding-right', 'margin-left', 'margin-right', 'border-left-width', 'border-right-width'],
	        		heightModifiers = ['padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'border-top-width', 'border-bottom-width'];

	        	panelOuterWidth = p.width(); // we want the width and height that doesn't include border/padding/margin
	        	for(var j=0, k=widthModifiers.length; j<k; j++) {
	        		panelOuterWidth += (parseInt(p.css(widthModifiers[j]),10) || 0);
	        	}
	        	panelOuterHeight = p.height();
	        	for(var j=0, k=heightModifiers.length; j<k; j++) {
	        		panelOuterHeight += (parseInt(p.css(heightModifiers[j]),10) || 0);
	        	}
	        	p.remove();

	            $.extend(options, optionOverrides);
	
	            outerWin.bind('mousedown.sf', function(event)
	            {
	            	$('> .window', this).addClass('dragging');
	                $(this).data('click', {
	            		x: event.pageX,
	            		y: event.pageY
	            	});
	                return false;
	            }).bind('mousemove.sf', function(event)
	            {
	            	var click = $(this).data('click');
	                if(click) {
	        			throttledMovePanels(event.pageX - click.x, event.pageY - click.y);
	                }
	            }).bind('mouseup.sf', function(event)
	    		{
	            	$('> .window', this).removeClass('dragging');
	        		$(this).removeData('click');
	    		}).bind('selectstart.sf dragstart.sf', function() // prevent IE behavior
	    		{
	    			// cancel content selection when dragging
	    			// so mouseup will fire (if they've dragged on an img)
	            	return false;
	    		});
	
	    		$(window).bind('resize', function()
				{
	    			// on resize, we'll only ever have space on the right and on the bottom
	    			// it's impossible to create extra space on the top or left on resize.
	
	    			var horizontalSpace = outerWin.outerWidth() - $('.top', win).length * panelOuterWidth,
	    				verticalSpace = outerWin.outerHeight() - $('.left', win).length * panelOuterHeight,
	    				move = {
	    					x: -1 * Math.max(horizontalSpace, 0),
	    					y: -1 * Math.max(verticalSpace, 0)
	    				};

	    			if(move.x || move.y) {
	    				throttledMovePanels(move.x, move.y, true);
	    			}
				});
	
	            win = $('<div class="window"/>');
	
	    		//horizontalThreshold = Math.floor(outerWin.outerWidth() * options.THRESHOLD_MULTIPLIER);
	    		//verticalThreshold = Math.floor(outerWin.outerHeight() * options.THRESHOLD_MULTIPLIER);
	
	            outerWin.append(win);
	
	            // Initialize starting panels
	            var numX = Math.ceil(win.outerWidth() / panelOuterWidth), // TODO make work with margin/border
	                numY = Math.ceil(win.outerHeight() / panelOuterHeight); // TODO make work with margin/border
	
	            for(var j=0; j<numX; j++) {
	                for(var k=0; k<numY; k++) {
	                    createPanel(j, k, j*panelOuterWidth, k*panelOuterHeight, k==0, j==numX-1, k==numY-1, j==0);
	                }
	            }
	        },
	        cacheAdd: function(x, y, obj)
	        {
	            if(!cache[x]) {
	                cache[x] = {};
	            }
	            cache[x][y] = obj;
	            sf.cacheSetPending(x, y, false);
	        },
	        cacheGet: function(x, y)
	        {
	            return cache[x] && cache[x][y];
	        },
	        cacheSetPending: function(x, y, value)
	        {
	            if(!cachePending[x]) {
	                cachePending[x] = {};
	            }
	            cachePending[x][y] = !!value;
	        },
	        cacheIsPending: function(x, y)
	        {
	            return cachePending[x] && cachePending[x][y];
	        },
	        setContent: function(x, y, html)
	        {
	        	sf.cacheAdd(x, y, html); 
	            $('.p' + x + '_' + y, win).html(html);
	        }
	    });
	};
})(jQuery);