/*!
 * Isotope v3.0.6
 *
 * Licensed GPLv3 for open source use
 * or Isotope Commercial License for commercial use
 *
 * https://isotope.metafizzy.co
 * Copyright 2010-2020 Metafizzy
 */

/* eslint-disable max-params */

( function( window, factory ) {
    // universal module definition
    if ( typeof define == 'function' && define.amd ) {
        // AMD
        define( [
                'outlayer/outlayer',
                'get-size/get-size',
                'desandro-matches-selector/matches-selector',
                'fizzy-ui-utils/utils',
                './item',
                './layout-mode',
                // include default layout modes
                './layout-modes/masonry',
                './layout-modes/fit-rows',
                './layout-modes/vertical',
            ],
            function( Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ) {
                return factory( window, Outlayer, getSize, matchesSelector, utils,
                    Item, LayoutMode );
            } );
    } else if ( typeof module == 'object' && module.exports ) {
        // CommonJS
        module.exports = factory(
            window,
            require('outlayer'),
            require('get-size'),
            require('desandro-matches-selector'),
            require('fizzy-ui-utils'),
            require('./item'),
            require('./layout-mode'),
            // include default layout modes
            require('./layout-modes/masonry'),
            require('./layout-modes/fit-rows'),
            require('./layout-modes/vertical')
        );
    } else {
        // browser global
        window.Isotope = factory(
            window,
            window.Outlayer,
            window.getSize,
            window.matchesSelector,
            window.fizzyUIUtils,
            window.Isotope.Item,
            window.Isotope.LayoutMode
        );
    }

}( window, function factory( window, Outlayer, getSize, matchesSelector, utils,
                             Item, LayoutMode ) {

    'use strict';

// -------------------------- vars -------------------------- //

    var jQuery = window.jQuery;

// -------------------------- helpers -------------------------- //

    var trim = String.prototype.trim ?
        function( str ) {
            return str.trim();
        } :
        function( str ) {
            return str.replace( /^\s+|\s+$/g, '' );
        };

// -------------------------- isotopeDefinition -------------------------- //

    // create an Outlayer layout class
    var Isotope = Outlayer.create( 'isotope', {
        layoutMode: 'masonry',
        isJQueryFiltering: true,
        sortAscending: true,
    } );

    Isotope.Item = Item;
    Isotope.LayoutMode = LayoutMode;

    var proto = Isotope.prototype;

    proto._create = function() {
        this.itemGUID = 0;
        // functions that sort items
        this._sorters = {};
        this._getSorters();
        // call super
        Outlayer.prototype._create.call( this );

        // create layout modes
        this.modes = {};
        // start filteredItems with all items
        this.filteredItems = this.items;
        // keep of track of sortBys
        this.sortHistory = [ 'original-order' ];
        // create from registered layout modes
        for ( var name in LayoutMode.modes ) {
            this._initLayoutMode( name );
        }
    };

    proto.reloadItems = function() {
        // reset item ID counter
        this.itemGUID = 0;
        // call super
        Outlayer.prototype.reloadItems.call( this );
    };

    proto._itemize = function() {
        var items = Outlayer.prototype._itemize.apply( this, arguments );
        // assign ID for original-order
        for ( var i = 0; i < items.length; i++ ) {
            var item = items[i];
            item.id = this.itemGUID++;
        }
        this._updateItemsSortData( items );
        return items;
    };

    // -------------------------- layout -------------------------- //

    proto._initLayoutMode = function( name ) {
        var Mode = LayoutMode.modes[ name ];
        // set mode options
        // HACK extend initial options, back-fill in default options
        var initialOpts = this.options[ name ] || {};
        this.options[ name ] = Mode.options ?
            utils.extend( Mode.options, initialOpts ) : initialOpts;
        // init layout mode instance
        this.modes[ name ] = new Mode( this );
    };

    proto.layout = function() {
        // if first time doing layout, do all magic
        if ( !this._isLayoutInited && this._getOption('initLayout') ) {
            this.arrange();
            return;
        }
        this._layout();
    };

    // private method to be used in layout() & magic()
    proto._layout = function() {
        // don't animate first layout
        var isInstant = this._getIsInstant();
        // layout flow
        this._resetLayout();
        this._manageStamps();
        this.layoutItems( this.filteredItems, isInstant );

        // flag for initalized
        this._isLayoutInited = true;
    };

    // filter + sort + layout
    proto.arrange = function( opts ) {
        // set any options pass
        this.option( opts );
        this._getIsInstant();
        // filter, sort, and layout

        // filter
        var filtered = this._filter( this.items );
        this.filteredItems = filtered.matches;

        this._bindArrangeComplete();

        if ( this._isInstant ) {
            this._noTransition( this._hideReveal, [ filtered ] );
        } else {
            this._hideReveal( filtered );
        }

        this._sort();
        this._layout();
    };
    // alias to _init for main plugin method
    proto._init = proto.arrange;

    proto._hideReveal = function( filtered ) {
        this.reveal( filtered.needReveal );
        this.hide( filtered.needHide );
    };

    // HACK
    // Don't animate/transition first layout
    // Or don't animate/transition other layouts
    proto._getIsInstant = function() {
        var isLayoutInstant = this._getOption('layoutInstant');
        var isInstant = isLayoutInstant !== undefined ? isLayoutInstant :
            !this._isLayoutInited;
        this._isInstant = isInstant;
        return isInstant;
    };

    // listen for layoutComplete, hideComplete and revealComplete
    // to trigger arrangeComplete
    proto._bindArrangeComplete = function() {
        // listen for 3 events to trigger arrangeComplete
        var isLayoutComplete, isHideComplete, isRevealComplete;
        var _this = this;
        function arrangeParallelCallback() {
            if ( isLayoutComplete && isHideComplete && isRevealComplete ) {
                _this.dispatchEvent( 'arrangeComplete', null, [ _this.filteredItems ] );
            }
        }
        this.once( 'layoutComplete', function() {
            isLayoutComplete = true;
            arrangeParallelCallback();
        } );
        this.once( 'hideComplete', function() {
            isHideComplete = true;
            arrangeParallelCallback();
        } );
        this.once( 'revealComplete', function() {
            isRevealComplete = true;
            arrangeParallelCallback();
        } );
    };

    // -------------------------- filter -------------------------- //

    proto._filter = function( items ) {
        var filter = this.options.filter;
        filter = filter || '*';
        var matches = [];
        var hiddenMatched = [];
        var visibleUnmatched = [];

        var test = this._getFilterTest( filter );

        // test each item
        for ( var i = 0; i < items.length; i++ ) {
            var item = items[i];
            if ( item.isIgnored ) {
                continue;
            }
            // add item to either matched or unmatched group
            var isMatched = test( item );
            // item.isFilterMatched = isMatched;
            // add to matches if its a match
            if ( isMatched ) {
                matches.push( item );
            }
            // add to additional group if item needs to be hidden or revealed
            if ( isMatched && item.isHidden ) {
                hiddenMatched.push( item );
            } else if ( !isMatched && !item.isHidden ) {
                visibleUnmatched.push( item );
            }
        }

        // return collections of items to be manipulated
        return {
            matches: matches,
            needReveal: hiddenMatched,
            needHide: visibleUnmatched,
        };
    };

    // get a jQuery, function, or a matchesSelector test given the filter
    proto._getFilterTest = function( filter ) {
        if ( jQuery && this.options.isJQueryFiltering ) {
            // use jQuery
            return function( item ) {
                return jQuery( item.element ).is( filter );
            };
        }
        if ( typeof filter == 'function' ) {
            // use filter as function
            return function( item ) {
                return filter( item.element );
            };
        }
        // default, use filter as selector string
        return function( item ) {
            return matchesSelector( item.element, filter );
        };
    };

    // -------------------------- sorting -------------------------- //

    /**
     * @param {Array} elems
     */
    proto.updateSortData = function( elems ) {
        // get items
        var items;
        if ( elems ) {
            elems = utils.makeArray( elems );
            items = this.getItems( elems );
        } else {
            // update all items if no elems provided
            items = this.items;
        }

        this._getSorters();
        this._updateItemsSortData( items );
    };

    // ----- munge sorter ----- //

    // encapsulate this, as we just need mungeSorter
    // other functions in here are just for munging
    var mungeSorter = ( function() {
        // add a magic layer to sorters for convienent shorthands
        // `.foo-bar` will use the text of .foo-bar querySelector
        // `[foo-bar]` will use attribute
        // you can also add parser
        // `.foo-bar parseInt` will parse that as a number
        function mngSorter( sorter ) {
            // if not a string, return function or whatever it is
            if ( typeof sorter != 'string' ) {
                return sorter;
            }
            // parse the sorter string
            var args = trim( sorter ).split(' ');
            var query = args[0];
            // check if query looks like [an-attribute]
            var attrMatch = query.match( /^\[(.+)\]$/ );
            var attr = attrMatch && attrMatch[1];
            var getValue = getValueGetter( attr, query );
            // use second argument as a parser
            var parser = Isotope.sortDataParsers[ args[1] ];
            // parse the value, if there was a parser
            sorter = parser ? function( elem ) {
                    return elem && parser( getValue( elem ) );
                } :
                // otherwise just return value
                function( elem ) {
                    return elem && getValue( elem );
                };

            return sorter;
        }

        // get an attribute getter, or get text of the querySelector
        function getValueGetter( attr, query ) {
            // if query looks like [foo-bar], get attribute
            if ( attr ) {
                return function getAttribute( elem ) {
                    return elem.getAttribute( attr );
                };
            }

            // otherwise, assume its a querySelector, and get its text
            return function getChildText( elem ) {
                var child = elem.querySelector( query );
                return child && child.textContent;
            };
        }

        return mngSorter;
    } )();

    proto._getSorters = function() {
        var getSortData = this.options.getSortData;
        for ( var key in getSortData ) {
            var sorter = getSortData[ key ];
            this._sorters[ key ] = mungeSorter( sorter );
        }
    };

    /**
     * @param {Array} items - of Isotope.Items
     */
    proto._updateItemsSortData = function( items ) {
        // do not update if no items
        var len = items && items.length;
        if ( !len ) {
            return;
        }

        for ( var i = 0; i < len; i++ ) {
            var item = items[i];
            item.updateSortData();
        }
    };

    // parsers used in getSortData shortcut strings
    Isotope.sortDataParsers = {
        parseInt: function( val ) {
            return parseInt( val, 10 );
        },
        parseFloat: function( val ) {
            return parseFloat( val );
        },
    };

    // ----- sort method ----- //

    // sort filteredItem order
    proto._sort = function() {
        if ( !this.options.sortBy ) {
            return;
        }
        // keep track of sortBy History
        var sortBys = utils.makeArray( this.options.sortBy );
        if ( !this._getIsSameSortBy( sortBys ) ) {
            // concat all sortBy and sortHistory, add to front, oldest goes in last
            this.sortHistory = sortBys.concat( this.sortHistory );
        }
        // sort magic
        var itemSorter = getItemSorter( this.sortHistory, this.options.sortAscending );
        this.filteredItems.sort( itemSorter );
    };

    // check if sortBys is same as start of sortHistory
    proto._getIsSameSortBy = function( sortBys ) {
        for ( var i = 0; i < sortBys.length; i++ ) {
            if ( sortBys[i] != this.sortHistory[i] ) {
                return false;
            }
        }
        return true;
    };

    // returns a function used for sorting
    function getItemSorter( sortBys, sortAsc ) {
        return function sorter( itemA, itemB ) {
            // cycle through all sortKeys
            for ( var i = 0; i < sortBys.length; i++ ) {
                var sortBy = sortBys[i];
                var a = itemA.sortData[ sortBy ];
                var b = itemB.sortData[ sortBy ];
                if ( a > b || a < b ) {
                    // if sortAsc is an object, use the value given the sortBy key
                    var isAscending = sortAsc[ sortBy ] !== undefined ? sortAsc[ sortBy ] : sortAsc;
                    var direction = isAscending ? 1 : -1;
                    return ( a > b ? 1 : -1 ) * direction;
                }
            }
            return 0;
        };
    }

    // -------------------------- methods -------------------------- //

    // get layout mode
    proto._mode = function() {
        var layoutMode = this.options.layoutMode;
        var mode = this.modes[ layoutMode ];
        if ( !mode ) {
            // TODO console.error
            throw new Error( 'No layout mode: ' + layoutMode );
        }
        // HACK sync mode's options
        // any options set after init for layout mode need to be synced
        mode.options = this.options[ layoutMode ];
        return mode;
    };

    proto._resetLayout = function() {
        // trigger original reset layout
        Outlayer.prototype._resetLayout.call( this );
        this._mode()._resetLayout();
    };

    proto._getItemLayoutPosition = function( item ) {
        return this._mode()._getItemLayoutPosition( item );
    };

    proto._manageStamp = function( stamp ) {
        this._mode()._manageStamp( stamp );
    };

    proto._getContainerSize = function() {
        return this._mode()._getContainerSize();
    };

    proto.needsResizeLayout = function() {
        return this._mode().needsResizeLayout();
    };

    // -------------------------- adding & removing -------------------------- //

    // HEADS UP overwrites default Outlayer appended
    proto.appended = function( elems ) {
        var items = this.addItems( elems );
        if ( !items.length ) {
            return;
        }
        // filter, layout, reveal new items
        var filteredItems = this._filterRevealAdded( items );
        // add to filteredItems
        this.filteredItems = this.filteredItems.concat( filteredItems );
    };

    // HEADS UP overwrites default Outlayer prepended
    proto.prepended = function( elems ) {
        var items = this._itemize( elems );
        if ( !items.length ) {
            return;
        }
        // start new layout
        this._resetLayout();
        this._manageStamps();
        // filter, layout, reveal new items
        var filteredItems = this._filterRevealAdded( items );
        // layout previous items
        this.layoutItems( this.filteredItems );
        // add to items and filteredItems
        this.filteredItems = filteredItems.concat( this.filteredItems );
        this.items = items.concat( this.items );
    };

    proto._filterRevealAdded = function( items ) {
        var filtered = this._filter( items );
        this.hide( filtered.needHide );
        // reveal all new items
        this.reveal( filtered.matches );
        // layout new items, no transition
        this.layoutItems( filtered.matches, true );
        return filtered.matches;
    };

    /**
     * Filter, sort, and layout newly-appended item elements
     * @param {[Array, NodeList, Element]} elems
     */
    proto.insert = function( elems ) {
        var items = this.addItems( elems );
        if ( !items.length ) {
            return;
        }
        // append item elements
        var i, item;
        var len = items.length;
        for ( i = 0; i < len; i++ ) {
            item = items[i];
            this.element.appendChild( item.element );
        }
        // filter new stuff
        var filteredInsertItems = this._filter( items ).matches;
        // set flag
        for ( i = 0; i < len; i++ ) {
            items[i].isLayoutInstant = true;
        }
        this.arrange();
        // reset flag
        for ( i = 0; i < len; i++ ) {
            delete items[i].isLayoutInstant;
        }
        this.reveal( filteredInsertItems );
    };

    var _remove = proto.remove;
    proto.remove = function( elems ) {
        elems = utils.makeArray( elems );
        var removeItems = this.getItems( elems );
        // do regular thing
        _remove.call( this, elems );
        // bail if no items to remove
        var len = removeItems && removeItems.length;
        // remove elems from filteredItems
        if ( !len ) {
            return;
        }
        for ( var i = 0; i < len; i++ ) {
            var item = removeItems[i];
            // remove item from collection
            utils.removeFrom( this.filteredItems, item );
        }
    };

    proto.shuffle = function() {
        // update random sortData
        for ( var i = 0; i < this.items.length; i++ ) {
            var item = this.items[i];
            item.sortData.random = Math.random();
        }
        this.options.sortBy = 'random';
        this._sort();
        this._layout();
    };

    /**
     * trigger fn without transition
     * kind of hacky to have this in the first place
     * @param {Function} fn
     * @param {Array} args
     * @returns {Object} returnValue
     * @private
     */
    proto._noTransition = function( fn, args ) {
        // save transitionDuration before disabling
        var transitionDuration = this.options.transitionDuration;
        // disable transition
        this.options.transitionDuration = 0;
        // do it
        var returnValue = fn.apply( this, args );
        // re-enable transition for reveal
        this.options.transitionDuration = transitionDuration;
        return returnValue;
    };

    // ----- helper methods ----- //

    /**
     * getter method for getting filtered item elements
     * @returns {Array} elems - collection of item elements
     */
    proto.getFilteredItemElements = function() {
        return this.filteredItems.map( function( item ) {
            return item.element;
        } );
    };

    // -----  ----- //

    return Isotope;

} ) );