(function($) {
    
    //TODO: Open windows do not survive resize
    
    var $container;
    
    $.fn.boxify = function(options) {
        // build main options before element iteration
        var opts = $.extend({}, $.fn.boxify.defaults, options);
        
        // iterate and reformat each matched element
        return this.each(function() { //ONLY WORKS WITH ONE MATCHED ELEMENT ATM
            $this = $(this);
            var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
            
            //TODO: text resize detector
            
            $container= $this;
            
            create_hidden_boxes($this);
            initial_layout($this);
            
            
            //or box itself might be clickable
            
            // hover if a closer is hovered on an open box or vice versa
            $this.find(".box.resizable.closed .opener").live("click", function() {
                $parent = $(this).closest(".box");
                $.fn.boxify.open_box($parent);
            }).live("mouseover", function() {
                $.fn.boxify.hover_box($(this));
            }).live("mouseout", function() {
                $.fn.boxify.unhover_box($(this));
            });
            
            $this.find(".box.resizable.opened .closer").live("click", function() {
                $parent = $(this).closest(".box");
                $.fn.boxify.close_box($parent);
            }).live("mouseover", function() {
                 $.fn.boxify.hover_box($(this));
            }).live("mouseout", function() {
                $.fn.boxify.unhover_box($(this));
            });

            
            $this.find(".box.resizable.closed.opener").live("click", function() {
                  $.fn.boxify.open_box($(this));
            }).live("mouseover", function() {
                $parent = $(this).closest(".box");
                $.fn.boxify.hover_box($parent);
            }).live("mouseout", function() {
                $parent = $(this).closest(".box");
                $.fn.boxify.unhover_box($parent);
            });
            
            $this.find(".box.resizable.opened.closer").live("click", function() {
                $.fn.boxify.close_box($(this));
            }).live("mouseout", function() {
                $parent = $(this).closest(".box");
                $.fn.boxify.unhover_box($parent);
            }).live("mouseover", function() {
                $parent = $(this).closest(".box");
                 $.fn.boxify.hover_box($parent);
            });
            
            
            //highlight opened box only on hover
            $(".closer").mouseover(function() {
                $parent = $(this).closest(".box");
                 $.fn.boxify.hover_box($parent);
            });
            $(".closer").mouseout(function() {
                $parent = $(this).closest(".box");
                 $.fn.boxify.unhover_box($parent);
            });
            
            
            $(window).resize(function(){
                calculate_layout($container, false);
                move_boxes_into_place(false);
            });
            
        });
    };
    
    
    $.fn.boxify.hover_box = function($this) {
        $this.addClass("hover");
    };
    
    $.fn.boxify.unhover_box = function($this) {
        $this.removeClass("hover");
    };
    
    // open and close
    $.fn.boxify.open_box = function($this, no_scroll) {
        // Scroll to box
        // Fade out old content
        // Fade in new content. When new content has faded in, resize and update layout
        // Update CSS classes.
        
        // if I'm not an openme (no_scroll would be false), add hash to the address
        //if(!no_scroll)
          //  document.location.hash = $this.attr('id');

        // sorry, a little bit hacky - we don't want contact to open. just scroll to it.
        if(!$this.hasClass("keep-closed"))
        {
            $this.css("zIndex", 10000);

            calculate_target_size($this, "open");
            calculate_layout($container, false); //must come after animate
            move_boxes_into_place(true);
            
            resize_box($this, true);
            $this.css("zIndex", 100);
            // as it wasn't working for open anyhow (leaving the mouse on would leave hover on anyway)
            // am taking this removeClass out for the benefit of permalink pages to highlight open elements,
            //$this.removeClass("hover");
        }
        //remove default highlighting when the box is opened
        $this.removeClass("hover");
        /*
        if(!no_scroll){
            scroll_to_div($this);
        }
        */
        //if ($this.hasClass("hash-opened")) scroll_to_div($this);
        //This is too disorientating. Better to keep the width constant, so that the box doesn't have to move.
        
        // add a hash to the url with the element id.
        // the ? has been appended so the page doesn't scroll to that anchor. 

        // taken out at quick fix
        //if(!no_scroll)
        //    document.location.hash = $this.attr('id') + "?";
    };

    $.fn.boxify.close_box = function($this) {
        //Resize and update layout
        //When resize has finished, fade out old, fade in new.

        $this.css("zIndex", 10000);


        calculate_target_size($this, "close");
        calculate_layout($container, false); //must come after animate
        resize_box($this, false);
        move_boxes_into_place(true);
        
        //scroll_to_div($this);
        // get rid of the hash, but stay where you are by adding something that doesn't exist

        // taken out at quick fix
        //document.location.hash = "home";
        
        $this.css("zIndex", 100);
        $this.removeClass("hover");
    };

    var scroll_to_div = function($this) {
        var scrolltop = $container.offset().top + parseInt($this.attr("targettop"), 10) - grid.y.margin - grid.y.inner/2 - 80; // magic number alert!
        // note, the scrollto position must not be negative for ie7i
        if($.scrollTo) $.scrollTo(Math.max(parseInt(scrolltop), 0), 1200, {'offset': {'top': -158 }}); // 158 is height of top section (150) plus margin
        //        if ($.scrollTo) $.scrollTo(Math.max(parseInt(scrolltop),0), 1200);
    };
 
    var calculate_target_size = function($this, type) {
        if (type == "open") {
            var ogw = $this.attr("ogw");
            var ogh = $this.attr("ogh");
        
            if (!ogh) { //measure the height by copying the opened content to a measure box
               ogh = grid.measure_height($this.find(".opened_content"), ogw);
            }
                
            var newsize = grid.dim_to_inner(ogw, ogh, $this);
         
            $this.attr("gw", ogw);
            $this.attr("gh", ogh);
        } else { //closed
            var cgw = $this.attr("cgw");
            var cgh = $this.attr("cgh");
            var newsize = grid.dim_to_inner(cgw, cgh, $this);
    
            $this.attr("gw", cgw);
            $this.attr("gh",cgh);
            
        }
        
        $this.attr("targetwidth", newsize.x);
        $this.attr("targetheight", newsize.y); 
    };

    var resize_box = function($this, change_content_first) {
        if (change_content_first) {
            $this.find(".closed_content").fadeOut(400, function() {
                $this.find(".opened_content").fadeIn(400);
            });
        }
        var tw = parseInt($this.attr("targetwidth"), 10);
        var th = parseInt($this.attr("targetheight"), 10);
        $this.animate({"width": tw, "height": th}, 600, "linear", function() {
            if (change_content_first) {
                $this.addClass("opened");
                $this.removeClass("closed");
            } else {
                $this.addClass("closed");
                $this.removeClass("opened");
                $this.find(".closed_content").fadeIn(400);
                $this.find(".opened_content").fadeOut(400);
            }        
        });
    };
   
    $.fn.infinitescrolling = function(callback) {
        $container.infinitescroll({
            navSelector  : "div.pagination",            
                       // selector for the paged navigation (it will be hidden)
            nextSelector : "div.pagination a.next",    
                       // selector for the NEXT link (to page 2)
            itemSelector : "#boxcontainer div.box", //TODO: hardcoded!
                   // selector for all items you'll retrieve
            bufferPx : 300,
            loadingText : "",
            donetext: "This is the end of the interwebs!",
            loadingImg : "/media/images/chrome/ajax-loader-grey.gif",
            // debug: true,
            localmode : false
          }, function() {
              calculate_layout($container, false);
              move_boxes_into_place(false);
              
              if (callback) {
                callback();
              }
          });
    };
    
    //
    // private functions
    //
    // TODO: make this work with several containers.
    var grid = new Grid2D;
    var bin_heights;
    var startbin;

    function px_to_integer(px_string) {
       return parseInt(px_string.substr(0, px_string.length-2), 10);
    }

    function Vector2D(x, y) {
       this.x = x;
       this.y = y;
    }

    function GridDim() {
        this.inner = 0;
        this.margin=0;
    
        this.outer = function() {
           return this.inner + this.margin;
        }
    
        this.dim_to_inner = function(dim, $div) { //Returns the inner width (in px) that corresponds to a grid dimension (in gridspaces)
            if ($div) {
               offset = this.measure_border($div);
            } else {
               offset = 0;
            }
           return -offset + dim*this.inner + (dim-1) * this.margin;
        }
    
        this.dim_to_outer = function(dim) {
           return dim*this.outer();
        }
        
        this.from_inner_px = function(px) {//inverse of dim_to_inner
            var r = (px + this.margin)/(this.inner + this.margin);
            return Math.ceil(r);
        }
        
    }

    function Grid2D() {
        this.x = new GridDim();
        this.y = new GridDim();

        this.x.measure_border = function($div) {
            return parseInt($div.css("borderLeftWidth"), 10) + parseInt($div.css("borderRightWidth"), 10);        
        }
 
        this.y.measure_border = function($div) {
            return parseInt($div.css("borderBottomWidth"), 10) + parseInt($div.css("borderTopWidth"), 10);        
        }

    
        this.dim_to_inner = function(dimx, dimy, $div) {                        
            return new Vector2D(this.x.dim_to_inner(dimx, $div), this.x.dim_to_inner(dimy, $div));
        }
    
        this.dim_to_outer = function(dimx, dimy) {
            return new Vector2D(this.x.dim_to_outer(dimx), this.x.dim_to_outer(dimy));
        }
        
        this.measure_height = function($content, gridx) {
            var $test = $(".box.test_measure");
            $test.css("height", "auto");
            $test.empty();

            $c2 = $content.clone();
            $c2.css("display", "block"); //fixes a bug where a boxify-mandaded display: none causes the minimum height to be returned.
            //Set the width to the passed width
            //insert the content
            $test.append($c2);
            $test.width(this.x.dim_to_inner(gridx, $test)+"px");
            var h = $test.outerHeight();
            var r = this.y.from_inner_px(h);
            $test.empty();
            return r; 
        }
    }

    var css_units_to_px = function(x, y) { //measure an invisible box sized to the relevant CSS dimensions.
        $test = $(".box.test_measure");
        $test.width(x);
        $test.height(y);
    
        return {
            'x': $test.outerWidth(),
            'y': $test.outerHeight()
        };  
    };


    var css_units_to_grid = function(x, y) {
        var px = css_units_to_px(x+grid.rmargin, y+grid.bmargin);
        return px_to_grid(px); 
    };

    var px_to_grid = function(px) {
        return {
            'x': Math.ceil(px['x']/grid.x.outer()),  //s0.02 add a little bit of rounding tolerance
            'y': Math.ceil(px['y']/grid.y.outer())
        };      
    };


    
    function create_hidden_boxes($div) {
        $div.append('<div class="box reference"/><div class="box test_measure"/>');       
    }
    
    var open_openme_boxes = function($container) {
        if(typeof $container === 'object'){
                $container.find(".box.closed.openme").each(function() {
                $this = $(this);
                
                if($this.hasClass("showcase")) {
                    slideshow("#"+$this.attr("id"));
                }
                
                // taken out at quick fix
                // if($(this).hasClass('hash-opened')){
                //     $.fn.boxify.open_box($this, false);
                $.fn.boxify.open_box($this, true);
                $this.removeClass("openme");
                
                // scroll to the hash
                // taken out at quick fix
                // if($this.attr("id") == $(hash).attr("id"))
                // {
                //     scroll_to_div($this);
                // }
            });
        }
    };
    
    var initial_layout = function($container) {
        //measure the reference box to get width and height and margins in pixels.
        var $ref = $container.find(".box.reference")
        var rmargin = $ref.css("margin-right");
        grid.x.margin = px_to_integer(rmargin);
        var bmargin = $ref.css("margin-bottom");
        grid.y.margin = px_to_integer(bmargin);
        if (!grid.x.inner) { // measure from the 'reference' box
            grid.x.inner = $ref.outerWidth();
            grid.y.inner = $ref.outerHeight();
        }
        //update the layout
        calculate_layout($container, true);
        move_boxes_into_place(true);
    };
    

    var calculate_layout = function($container, force) {
        if (!force) force = false;
        //measure the divs, storing the result in the attributes.
        //empty bins
        var docwidth = $container.width();
        var numbins = Math.floor(docwidth/grid.x.outer());
    
        bin_heights = new Array(numbins); // contains the filled height of each bin
        for (var i=0; i<bin_heights.length; i++) { bin_heights[i]=0; }
        startbin = 0;

        $container.find(".box").not(".reference").not(".test_measure").each(function() {
            measure_div($(this), force);
            //Place the box in the next available slot
            place_in_next_bin($(this));
        });    
    
        $container.height(grid.y.outer()*bin_heights[0]) //TODO: more precise - [0] won't always be the tallest        
    };
    
    var move_boxes_into_place = function(animate) {
        if (!animate) animate = false;
    
        $container.find(".box").not(".reference").not(".test_measure").each(function() {
            var $this = $(this);
            spec = {
               'top': $this.attr("targettop"),
               'left': $this.attr("targetleft")
            }
            if (animate) {
               $this.animate(spec);
            } else {
               $this.animate(spec, 0);
            }
        });    

        
        //fade in all the invisible boxes (ie any that have arrived via ajax)
        $invisibles = $container.find(".box.invisible");
        $invisibles.hide();
        $invisibles.fadeIn(400, function() {
            open_openme_boxes($container);
        });
        $invisibles.removeClass("invisible");            
    };
    
    var find_bins_to_fit = function(width) {
        //Returns the first of consecutive bin_heights that is the lowest that will fit a given width
        //and the maximum of the heights of those bin_heights (which is the minimum of all the bin_heights that will fit a given width)
        var numbin_heights = bin_heights.length;
        var numtests = numbin_heights - width + 1;

        if (startbin > numtests) startbin = 0;
    
        var min_bin_height = 999999999;
        var min_bin;
        for (var i=0; i<numtests; i++) {
            var thebin = i + startbin % numtests;
            var workingheight = bin_heights[thebin];
            for (var j=1; j<width; j++) {
               workingheight = Math.max(workingheight, bin_heights[thebin+j]);
            }
            if (workingheight < min_bin_height) {
                min_bin_height = workingheight;
                min_bin = thebin;
            }
        }
    
        return {"bin":min_bin, "height": min_bin_height};   
    };


    var place_div_in_bin = function($div, bin_spec) {
        var gridwidth = $div.attr("gw");
        var new_height = bin_spec['height'] + parseInt($div.attr("gh"), 10);

        //fill the bin_heights with the div's height
        for(i=0; i< gridwidth; i++) {
           bin_heights[bin_spec['bin']+i] = new_height;
        }
    
        //for followable numbering, fill all bin_heights to the right with the div's top
        for(i=bin_spec['bin']+gridwidth; i< bin_heights.length; i++) {
            if (bin_heights[i]< bin_spec['height']) bin_heights[i] = bin_spec['height'];
        }

        //for followable numbering, raise all the bin_heights to the left to be one small row higher
        for(i=0; i< bin_spec['bin']; i++) {
            if (bin_heights[i] <= bin_spec['height']) {
                bin_heights[i] = bin_spec['height'] + 1;
            } 
        }
    
        var pos = grid.dim_to_outer(bin_spec['bin'], bin_spec['height']);
                
        $div.attr("targettop", pos['y']);//for autoscrolling whilst animating
        $div.attr("targetleft", pos['x']);        
    };

    var place_in_next_bin = function($div) {
        var bin_spec = find_bins_to_fit($div.attr("gw"));
        // console.log("placing", $div, "in", bin_spec);
        place_div_in_bin($div, bin_spec);
    };

    var measure_div = function($div, force) {
        //measure everything in pixels (if font size changes, or window is resized, remeasure).
        //assign gridwidth and gridheight data to divs that have none already defined.
        
        // if height (or openedheight) is omitted, then the height will be big enough for the content, and rounded up to the nearest grid size.
        
        if (!force) force = false;
    
        //only affects block elements
        if ($div.css('display')=="block") {
            if (!$div.attr("gw")||force) { //if we haven't got the measurements yet, or a remeasure is being forced...
            
                //measure current size in pixels (including padding and border)
                var w = $div.outerWidth();
                var h = $div.outerHeight();
                                                
                var current_grid_dims = px_to_grid({'x': w, 'y':h}, true);
                // The initial size of a box is as specified by the box's style.
                // A box is treated as closed, unless it has the class "open".
                
                
                // $div.css("height", grid.y.dim_to_inner(current_grid_dims.y, -measure_border($div)));
 

                if ($div.hasClass("opened")) { //box is currently open. 
                    // If an open box has 'closedwidth' and 'closedheight' measurements, it will be classed 'resizable'.
                    if ($div.attr("closedwidth")) $div.addClass("resizable");
                    if (!$div.attr("openedheight")) $div.attr("openedheight", h+"px");
                    // If an open box is resizable, the 'openwidth' and 'openheight' will be measured from the current size (unless they are manually specified).
                    if ($div.hasClass("resizable")) {
                       if (!$div.attr("openedwidth")) $div.attr("openedwidth", w+"px");
                       if (!$div.attr("openedheight")) $div.attr("openedheight", h+"px");
                    }
                } else { //box is currently closed
                    // If a closed box has 'openedwidth' and 'openedheight' measurements, it will be classed 'resizable'.
                    if ($div.attr("openedwidth")) $div.addClass("resizable");
                    // If a closed box is resizable, the 'closedwidth' and 'closedheight' will be measured from the current size (unless they are manually specified).
                    if ($div.hasClass("resizable")) {
                       if (!$div.attr("closedwidth")) $div.attr("closedwidth", w+"px");
                       if (!$div.attr("closedheight")) $div.attr("closedheight", h+"px");
                    }
                }
            
                //so now we need to calculate open, closed and current widths, in px
            
                if ($div.hasClass("resizable")) {
                    var ow = $div.attr("openedwidth");
                    var oh = $div.attr("openedheight");
                    var ogrid_px = css_units_to_px(ow, oh);
                    var ogrid_dims = px_to_grid(ogrid_px); 
                    var cw = $div.attr("closedwidth");
                    var ch = $div.attr("closedheight");
                    var cgrid_px = css_units_to_px(cw, ch);
                    var cgrid_dims = px_to_grid(cgrid_px);
                 
                    $div.attr("ogw", ogrid_dims['x']);
                    $div.attr("cgw", cgrid_dims['x']);
                    $div.attr("cgh", cgrid_dims['y']);             

                    if(oh) {
                        $div.attr("ogh", ogrid_dims['y']);
                    }
                }

                //set current state
                $div.attr("gw", current_grid_dims['x']);
                $div.attr("gh", current_grid_dims['y']);            
            }
        } else { // the invisibles (set to 0 to leave out of calculations)
            $div.attr("gw", 0);
            $div.attr("gh", 0);        
        }
    };



    $.fn.boxify.defaults = {};

})(jQuery);

