/**
 * jQuery Nested v1.03
 *
 * For a (total) gap free, multi column, grid layout experience.
 * http://suprb.com/apps/nested/
 * By Andreas Pihlström and additional brain activity by Jonas Blomdin
 *
 * Licensed under the MIT license.
 */

// Debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
// Copy pasted from http://paulirish.com/2009/throttled-smartresize-jquery-event-handler/

(function ($, sr) {
    var debounce = function (func, threshold, execAsap) {
        var timeout;
        return function debounced() {
            var obj = this,
                args = arguments;

            function delayed() {
                if (!execAsap) func.apply(obj, args);
                timeout = null;
            };
            if (timeout) clearTimeout(timeout);
            else if (execAsap) func.apply(obj, args);

            timeout = setTimeout(delayed, threshold || 150);
        };
    };
    jQuery.fn[sr] = function (fn) {
        return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr);
    };

})(jQuery, 'smartresize');

// Simple count object properties

if (!Object.keys) {
    Object.keys = function (obj) {
        var keys = [],
            k;
        for (k in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, k)) {
                keys.push(k);
            }
        }
        return keys;
    };
}

// The Nested magic

(function ($) {

    $.Nested = function (options, element) {
        this.element = $(element);
        this._init(options);
    };

    $.Nested.settings = {
        selector: '.box',
        minWidth: 50,
        minColumns: 1,
        gutter: 1,
        centered: false,
        resizeToFit: true, // will resize block bigger than the gap
        resizeToFitOptions: {
            resizeAny: true // will resize any block to fit the gap         
        },
        animate: true,
        animationOptions: {
            speed: 20,
            duration: 100,
            queue: true,
            complete: function () {}
        }
    };

    $.Nested.prototype = {

        _init: function (options) {
            var container = this;
            this.box = this.element;
            $(this.box).css('position', 'relative');
            this.options = $.extend(true, {}, $.Nested.settings, options);
            this.elements = [];
            this._isResizing = false;
            this._update = true;
            this.maxy = new Array();

            // add smartresize
            $(window).smartresize(function () {
                container.resize();
            });

            // build box dimensions
            this._setBoxes();
        },

        _setBoxes: function ($els, method) {
            var self = this;
            this.idCounter = 0;
            this.counter = 0;
            this.t = 0;
            this.maxHeight = 0;
            this.currWidth = 0;
            this.total = this.box.find(this.options.selector);
            this.matrix = {};
            this.gridrow = new Object;

            var calcWidth = !this.options.centered ? this.box.innerWidth() : $(window).width();

            this.columns = Math.max(this.options.minColumns, parseInt(calcWidth / (this.options.minWidth + this.options.gutter)) + 1);

            // build columns
            var minWidth = this.options.minWidth;
            var gutter = this.options.gutter;
            var display = "block";

            $els = this.box.find(this.options.selector);

            $.each($els, function () {

                var dim = parseInt($(this).attr('class').replace(/^.*size([0-9]+).*$/, '$1')).toString().split('');
                var x = (dim[0] == "N") ? 1 : parseFloat(dim[0]);
                var y = (dim[1] == "a") ? 1 : parseFloat(dim[1]);

                var currWidth = minWidth * x + gutter * (x - 1);
                var currHeight = minWidth * y + gutter * (y - 1);

                $(this).css({
                    'display': display,
                    'position': 'absolute',
                    'width': currWidth,
                    'height': currHeight,
                    'top': $(this).position().top,
                    'left': $(this).position().left
                }).removeClass('nested-moved').attr('data-box', self.idCounter).attr('data-width', currWidth);

                self.idCounter++;

                // render grid
                self._renderGrid($(this), method);

            });

            // position grid
            if (self.counter == self.total.length) {

                // if option resizeToFit is true
                if (self.options.resizeToFit) {
                    self.elements = self._fillGaps();
                }
                self._renderItems(self.elements);
                // reset elements
                self.elements = [];
            }
        },

        _addMatrixRow: function (y) {
            if (this.matrix[y]) {
                return false;
            } else this.matrix[y] = {};

            for (var c = 0; c < (this.columns - 1); c++) {
                var x = c * (this.options.minWidth + this.options.gutter);
                this.matrix[y][x] = 'false';
            }
        },

        _updateMatrix: function (el) {
            var height = 0;
            var t = parseInt(el['y']);
            var l = parseInt(el['x']);
            for (var h = 0; h < el['height']; h += (this.options.minWidth + this.options.gutter)) {
                for (var w = 0; w < el['width']; w += (this.options.minWidth + this.options.gutter)) {
                    var x = l + w;
                    var y = t + h;
                    if (!this.matrix[y]) {
                        this._addMatrixRow(y);
                    }
                    this.matrix[y][x] = 'true';
                }
            }
        },

        _getObjectSize: function (obj) { // Helper to get size of object, should probably be moved
            var size = 0;
            $.each(obj, function (p, v) {
                size++;
            });
            return size;
        },


        _fillGaps: function () {
            var self = this;
            var box = {};

            $.each(this.elements, function (index, el) {
                self._updateMatrix(el);
            });

            var arr = this.elements;
            arr.sort(function (a, b) {
                return a.y - b.y;
            });
            arr.reverse();

            // Used to keep the highest y value for a box in memory
            var topY = arr[0]['y'];

            // Used for current y with added offset
            var actualY = 0;

            // Current number of rows in matrix
            var rowsLeft = this._getObjectSize(this.matrix);

            $.each(this.matrix, function (y, row) {
                rowsLeft--;
                actualY = parseInt(y); // + parseInt(self.box.offset().top);
                $.each(row, function (x, col) {

                    if (col === 'false' && actualY < topY) {
                        if (!box.y) box.y = y;
                        if (!box.x) box.x = x;
                        if (!box.w) box.w = 0;
                        if (!box.h) box.h = self.options.minWidth;
                        box.w += (box.w) ? (self.options.minWidth + self.options.gutter) : self.options.minWidth;

                        var addonHeight = 0;
                        for (var row = 1; row < rowsLeft; row++) {
                            var z = parseInt(y) + parseInt(row * (self.options.minWidth + self.options.gutter));
                            if (self.matrix[z] && self.matrix[z][x] == 'false') {
                                addonHeight += (self.options.minWidth + self.options.gutter);
                                self.matrix[z][x] = 'true';
                            } else break;
                        }

                        box.h + (parseInt(addonHeight) / (self.options.minWidth + self.options.gutter) == rowsLeft) ? 0 : parseInt(addonHeight);
                        box.ready = true;

                    } else if (box.ready) {

                        $.each(arr, function (i, el) {
                            if (box.y <= arr[i]['y'] && (self.options.resizeToFitOptions.resizeAny || box.w <= arr[i]['width'] && box.h <= arr[i]['height'])) {
                                arr.splice(i, 1);
                                $(el['$el']).addClass('nested-moved');
                                self.elements.push({
                                    $el: $(el['$el']),
                                    x: parseInt(box.x),
                                    y: parseInt(box.y),
                                    col: i,
                                    width: parseInt(box.w),
                                    height: parseInt(box.h)
                                });

                                return false;
                            }
                        });
                        box = {};
                    }
                });

            });

            self.elements = arr;
            return self.elements;

        },

        _renderGrid: function ($box, method) {

            this.counter++;
            var ypos, gridy = ypos = 0;
            var tot = 0;
            var direction = !method ? "append" : "prepend";

            // Width & height
            var width = $box.width();
            var height = $box.height();

            // Calculate row and col
            var col = Math.ceil(width / (this.options.minWidth + this.options.gutter));
            var row = Math.ceil(height / (this.options.minWidth + this.options.gutter));

            // lock widest box to match minColumns
            if (col > this.options.minColumns) {
                this.options.minColumns = col;
            }

            while (true) {

                for (var y = col; y >= 0; y--) {
                    if (this.gridrow[gridy + y]) break;
                    this.gridrow[gridy + y] = new Object;
                    for (var x = 0; x < this.columns; x++) {
                        this.gridrow[gridy + y][x] = false;
                    }
                }

                for (var column = 0; column < (this.columns - col); column++) {

                    // Add default empty matrix, used to calculate and update matrix for each box
                    matrixY = gridy * (this.options.minWidth + this.options.gutter);
                    this._addMatrixRow(matrixY);

                    var fits = true;

                    for (var y = 0; y < row; y++) {
                        for (var x = 0; x < col; x++) {

                            if (!this.gridrow[gridy + y]) {
                                break;
                            }

                            if (this.gridrow[gridy + y][column + x]) {
                                fits = false;
                                break;
                            }
                        }
                        if (!fits) {
                            break;
                        }
                    }
                    if (fits) {
                        // Set as taken
                        for (var y = 0; y < row; y++) {
                            for (var x = 0; x < col; x++) {

                                if (!this.gridrow[gridy + y]) {
                                    break;
                                }
                                this.gridrow[gridy + y][column + x] = true;
                            }
                        }

                        // Push to elements array
                        this._pushItem($box, column * (this.options.minWidth + this.options.gutter), gridy * (this.options.minWidth + this.options.gutter), width, height, col, row, direction);
                        return;
                    }
                }
                gridy++;
            }
        },

        _pushItem: function ($el, x, y, w, h, cols, rows, method) {

            if (method == "prepend") {
                this.elements.unshift({
                    $el: $el,
                    x: x,
                    y: y,
                    width: w,
                    height: h,
                    cols: cols,
                    rows: rows
                });
            } else {
                this.elements.push({
                    $el: $el,
                    x: x,
                    y: y,
                    width: w,
                    height: h,
                    cols: cols,
                    rows: rows
                });
            }
        },

        _setHeight: function ($els) {
            var self = this;
            $.each($els, function (index, value) {
                // set maxHeight
                var colY = (value['y'] + value['height']);
                if (colY > self.maxHeight) {
                    self.maxHeight = colY;
                }
            });
            return self.maxHeight;
        },

        _setWidth: function ($els) {
            var self = this;
            $.each($els, function (index, value) {
                // set maxWidth
                var colX = (value['x'] + value['width']);
                if (colX > self.currWidth) {
                    self.currWidth = colX;
                }
            });
            return self.currWidth;
        },

        _renderItems: function ($els) {
            var self = this;

            // set container height and width
            this.box.css('height', this._setHeight($els));
            if (this.options.centered) {
                this.box.css({'width' : this._setWidth($els), 'margin-left' : 'auto', 'margin-right' : 'auto'});
            }

            $els.reverse();
            var speed = this.options.animationOptions.speed;
            var effect = this.options.animationOptions.effect;
            var duration = this.options.animationOptions.duration;
            var queue = this.options.animationOptions.queue;
            var animate = this.options.animate;
            var complete = this.options.animationOptions.complete;
            var item = this;
            var i = 0;
            var t = 0;

            $.each($els, function (index, value) {

                $currLeft = $(value['$el']).position().left;
                $currTop = $(value['$el']).position().top;
                $currWidth = $(value['$el']).width();
                $currHeight = $(value['$el']).width();

                value['$el'].attr('data-y', $currTop).attr('data-x', $currLeft);
                
                //if animate and queue
                if (animate && queue && ($currLeft != value['x'] || $currTop != value['y'])) {
                    setTimeout(function () {
                        value['$el'].css({
                            'display': 'block',
                            'width': value['width'],
                            'height': value['height']
                        }).animate({
                            'left': value['x'],
                            'top': value['y']
                        }, duration);
                        t++;
                        if (t == i) {
                            complete.call(undefined, $els)
                        }
                    }, i * speed);
                    i++;
                }

                //if animate and no queue
                if (animate && !queue && ($currLeft != value['x'] || $currTop != value['y'])) {
                    setTimeout(function () {
                        value['$el'].css({
                            'display': 'block',
                            'width': value['width'],
                            'height': value['height']
                        }).animate({
                            'left': value['x'],
                            'top': value['y']
                        }, duration);
                        t++;
                        if (t == i) {
                            complete.call(undefined, $els)
                        }
                    }, i);
                    i++;
                }

                //if no animation and no queue
                if (!animate && ($currLeft != value['x'] || $currTop != value['y'])) {
                    value['$el'].css({
                        'display': 'block',
                        'width': value['width'],
                        'height': value['height'],
                        'left': value['x'],
                        'top': value['y']
                    });
                    t++;
                    if (t == i) {
                        complete.call(undefined, $els)
                    }
                }
            });
            if (i == 0) {
                complete.call(undefined, $els)
            }
        },

        append: function ($els) {
            this._isResizing = true;
            this._setBoxes($els, 'append');
            this._isResizing = false;
        },

        prepend: function ($els) {
            this._isResizing = true;
            this._setBoxes($els, 'prepend');
            this._isResizing = false;
        },

        resize: function ($els) {
            if (Object.keys(this.matrix[0]).length % Math.floor(this.element.width() / (this.options.minWidth + this.options.gutter)) > 0) {
                this._isResizing = true;
                this._setBoxes(this.box.find(this.options.selector));
                this._isResizing = false;
            }
        },
      
        refresh: function(options) {
        	
        	options = options || this.options;
        
            this.options = $.extend(true, {}, $.Nested.settings, options);
            this.elements = [];
            this._isResizing = false;

            // build box dimensions
            this._setBoxes();
        },
        
        destroy: function() {
			
			var container = this;

            $(window).unbind("resize", function () {
                container.resize();
            });
	        
	        // unbind the resize event
            $els = this.box.find(this.options.selector);
            $($els).removeClass('nested-moved').removeAttr('style data-box data-width data-x data-y').removeData();
            
            this.box.removeAttr("style").removeData();
        }
        
    }


	var methods =
	{
		refresh: function(options) {
			return this.each(function(){
				var $this=$(this);
				var nested = $this.data('nested');
				
				nested.refresh(options);
			});
		},
		
		destroy: function() {
			return this.each(function(){
				var $this=$(this);
				var nested = $this.data('nested');
				
				nested.destroy();
			});
		}
	};
	
	

    $.fn.nested = function (options, e) {

		if(methods[options]) {
			return methods[options].apply(this, Array.prototype.slice.call(arguments, 1));
		}        
		
		if (typeof options === 'string') {
            this.each(function () {
                var container = $.data(this, 'nested');
                container[options].apply(container, [e]);
            });
        } else {
            this.each(function () {
                $.data(this, 'nested', new $.Nested(options, this));
            });
        }
        return this;
    }

})(jQuery);
