/**

*/

;(function($) {

/*
*  internal
*/

var _previousResizeWidth = -1,
    _updateTimeout = -1;

/*
*  _rows
*  utility function returns array of jQuery selections representing each row
*  (as displayed after float wrapping applied by browser)
*/

var _rows = function(elements) {
    var tolerance = 1,
        $elements = $(elements),
        lastTop = null,
        rows = [];

    // group elements by their top position
    $elements.each(function(){
        var $that = $(this),
            top = $that.offset().top - _parse($that.css('margin-top')),
            lastRow = rows.length > 0 ? rows[rows.length - 1] : null;

        if (lastRow === null) {
            // first item on the row, so just push it
            rows.push($that);
        } else {
            // if the row top is the same, add to the row group
            if (Math.floor(Math.abs(lastTop - top)) <= tolerance) {
                rows[rows.length - 1] = lastRow.add($that);
            } else {
                // otherwise start a new row group
                rows.push($that);
            }
        }

        // keep track of the last row top
        lastTop = top;
    });

    return rows;
};

/*
*  _parse
*  value parse utility function
*/

var _parse = function(value) {
    // parse value and convert NaN to 0
    return parseFloat(value) || 0;
};

/*
*  matchHeight
*  plugin definition
*/

var matchHeight = $.fn.matchHeight = function(byRow) {

    // handle matchHeight('remove')
    if (byRow === 'remove') {
        var that = this;

        // remove fixed height from all selected elements
        this.css('height', '');

        // remove selected elements from all groups
        $.each(matchHeight._groups, function(key, group) {
            group.elements = group.elements.not(that);
        });

        // TODO: cleanup empty groups

        return this;
    }

    if (this.length <= 1)
        return this;

    // byRow default to true
    byRow = (typeof byRow !== 'undefined') ? byRow : true;

    // keep track of this group so we can re-apply later on load and resize events
    matchHeight._groups.push({
        elements: this,
        byRow: byRow
    });

    // match each element's height to the tallest element in the selection
    matchHeight._apply(this, byRow);

    return this;
};

/*
*  plugin global options
*/

matchHeight._groups = [];
matchHeight._throttle = 80;
matchHeight._maintainScroll = false;
matchHeight._beforeUpdate = null;
matchHeight._afterUpdate = null;

/*
*  matchHeight._apply
*  apply matchHeight to given elements
*/

matchHeight._apply = function(elements, byRow) {
    var $elements = $(elements),
        rows = [$elements];

    // take note of scroll position
    var scrollTop = $(window).scrollTop(),
        htmlHeight = $('html').outerHeight(true);

    // temporarily must force hidden parents visible
    var $hiddenParents = $elements.parents().filter(':hidden');
    $hiddenParents.css('display', 'block');

    // get rows if using byRow, otherwise assume one row
    if (byRow) {

        // must first force an arbitrary equal height so floating elements break evenly
        $elements.each(function() {
            var $that = $(this),
                display = $that.css('display') === 'inline-block' ? 'inline-block' : 'block';

            // cache the original inline style
            $that.data('style-cache', $that.attr('style'));

            $that.css({
                'display': display,
                'padding-top': '0',
                'padding-bottom': '0',
                'margin-top': '0',
                'margin-bottom': '0',
                'border-top-width': '0',
                'border-bottom-width': '0',
                'height': '100px'
            });
        });

        // get the array of rows (based on element top position)
        rows = _rows($elements);

        // revert original inline styles
        $elements.each(function() {
            var $that = $(this);

            $that.attr('style', $that.data('style-cache') || '')
                 .css('height', '');
        });
    }

    $.each(rows, function(key, row) {
        var $row = $(row),
            maxHeight = 0;

        // skip apply to rows with only one item
        if (byRow && $row.length <= 1)
            return;

        // iterate the row and find the max height
        $row.each(function(){
            var $that = $(this),
                display = $that.css('display') === 'inline-block' ? 'inline-block' : 'block';

            // ensure we get the correct actual height (and not a previously set height value)
            $that.css({ 'display': display, 'height': '' });

            // find the max height (including padding, but not margin)
            if ($that.outerHeight(false) > maxHeight)
                maxHeight = $that.outerHeight(false);

            // revert display block
            $that.css('display', '');
        });

        // iterate the row and apply the height to all elements
        $row.each(function(){
            var $that = $(this),
                verticalPadding = 0;

            // handle padding and border correctly (required when not using border-box)
            if ($that.css('box-sizing') !== 'border-box') {
                verticalPadding += _parse($that.css('border-top-width')) + _parse($that.css('border-bottom-width'));
                verticalPadding += _parse($that.css('padding-top')) + _parse($that.css('padding-bottom'));
            }

            // set the height (accounting for padding and border)
            $that.css('height', maxHeight - verticalPadding);
        });
    });

    // revert hidden parents
    $hiddenParents.css('display', '');

    // restore scroll position if enabled
    if (matchHeight._maintainScroll)
        $(window).scrollTop((scrollTop / htmlHeight) * $('html').outerHeight(true));

    return this;
};

/*
*  matchHeight._applyDataApi
*  applies matchHeight to all elements with a data-match-height attribute
*/

matchHeight._applyDataApi = function() {
    var groups = {};

    // generate groups by their groupId set by elements using data-match-height
    $('[data-match-height], [data-mh]').each(function() {
        var $this = $(this),
            groupId = $this.attr('data-match-height') || $this.attr('data-mh');
        if (groupId in groups) {
            groups[groupId] = groups[groupId].add($this);
        } else {
            groups[groupId] = $this;
        }
    });

    // apply matchHeight to each group
    $.each(groups, function() {
        this.matchHeight(true);
    });
};

/*
*  matchHeight._update
*  updates matchHeight on all current groups with their correct options
*/

var _update = function(event) {
    if (matchHeight._beforeUpdate)
        matchHeight._beforeUpdate(event, matchHeight._groups);

    $.each(matchHeight._groups, function() {
        matchHeight._apply(this.elements, this.byRow);
    });

    if (matchHeight._afterUpdate)
        matchHeight._afterUpdate(event, matchHeight._groups);
};

matchHeight._update = function(throttle, event) {
    // prevent update if fired from a resize event
    // where the viewport width hasn't actually changed
    // fixes an event looping bug in IE8
    if (event && event.type === 'resize') {
        var windowWidth = $(window).width();
        if (windowWidth === _previousResizeWidth)
            return;
        _previousResizeWidth = windowWidth;
    }

    // throttle updates
    if (!throttle) {
        _update(event);
    } else if (_updateTimeout === -1) {
        _updateTimeout = setTimeout(function() {
            _update(event);
            _updateTimeout = -1;
        }, matchHeight._throttle);
    }
};

/*
*  bind events
*/

// apply on DOM ready event
$(matchHeight._applyDataApi);

// update heights on load and resize events
$(window).bind('load', function(event) {
    matchHeight._update(false, event);
});

// throttled update heights on resize events
$(window).bind('resize orientationchange', function(event) {
    matchHeight._update(true, event);
});

})(jQuery);