/*global define*/
define([
        '../Core/createGuid',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/Math',
        './Entity',
        './EntityCollection'
    ], function(
        createGuid,
        defined,
        defineProperties,
        DeveloperError,
        CesiumMath,
        Entity,
        EntityCollection) {
    "use strict";

    var entityOptionsScratch = {
        id : undefined
    };
    var entityIdScratch = new Array(2);

    function clean(entity) {
        var propertyNames = entity.propertyNames;
        var propertyNamesLength = propertyNames.length;
        for (var i = 0; i < propertyNamesLength; i++) {
            entity[propertyNames[i]] = undefined;
        }
    }

    function subscribeToEntity(that, eventHash, collectionId, entity) {
        entityIdScratch[0] = collectionId;
        entityIdScratch[1] = entity.id;
        eventHash[JSON.stringify(entityIdScratch)] = entity.definitionChanged.addEventListener(CompositeEntityCollection.prototype._onDefinitionChanged, that);
    }

    function unsubscribeFromEntity(that, eventHash, collectionId, entity) {
        entityIdScratch[0] = collectionId;
        entityIdScratch[1] = entity.id;
        var id = JSON.stringify(entityIdScratch);
        eventHash[id]();
        eventHash[id] = undefined;
    }

    function recomposite(that) {
        that._shouldRecomposite = true;
        if (that._suspendCount !== 0) {
            return;
        }

        var collections = that._collections;
        var collectionsLength = collections.length;

        var collectionsCopy = that._collectionsCopy;
        var collectionsCopyLength = collectionsCopy.length;

        var i;
        var entity;
        var entities;
        var iEntities;
        var collection;
        var composite = that._composite;
        var newEntities = new EntityCollection(that);
        var eventHash = that._eventHash;
        var collectionId;

        for (i = 0; i < collectionsCopyLength; i++) {
            collection = collectionsCopy[i];
            collection.collectionChanged.removeEventListener(CompositeEntityCollection.prototype._onCollectionChanged, that);
            entities = collection.values;
            collectionId = collection.id;
            for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
                entity = entities[iEntities];
                unsubscribeFromEntity(that, eventHash, collectionId, entity);
            }
        }

        for (i = collectionsLength - 1; i >= 0; i--) {
            collection = collections[i];
            collection.collectionChanged.addEventListener(CompositeEntityCollection.prototype._onCollectionChanged, that);

            //Merge all of the existing entities.
            entities = collection.values;
            collectionId = collection.id;
            for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
                entity = entities[iEntities];
                subscribeToEntity(that, eventHash, collectionId, entity);

                var compositeEntity = newEntities.getById(entity.id);
                if (!defined(compositeEntity)) {
                    compositeEntity = composite.getById(entity.id);
                    if (!defined(compositeEntity)) {
                        entityOptionsScratch.id = entity.id;
                        compositeEntity = new Entity(entityOptionsScratch);
                    } else {
                        clean(compositeEntity);
                    }
                    newEntities.add(compositeEntity);
                }
                compositeEntity.merge(entity);
            }
        }
        that._collectionsCopy = collections.slice(0);

        composite.suspendEvents();
        composite.removeAll();
        var newEntitiesArray = newEntities.values;
        for (i = 0; i < newEntitiesArray.length; i++) {
            composite.add(newEntitiesArray[i]);
        }
        composite.resumeEvents();
    }

    /**
     * Non-destructively composites multiple {@link EntityCollection} instances into a single collection.
     * If a Entity with the same ID exists in multiple collections, it is non-destructively
     * merged into a single new entity instance.  If an entity has the same property in multiple
     * collections, the property of the Entity in the last collection of the list it
     * belongs to is used.  CompositeEntityCollection can be used almost anywhere that a
     * EntityCollection is used.
     *
     * @alias CompositeEntityCollection
     * @constructor
     *
     * @param {EntityCollection[]} [collections] The initial list of EntityCollection instances to merge.
     */
    function CompositeEntityCollection(collections) {
        this._composite = new EntityCollection(this);
        this._suspendCount = 0;
        this._collections = defined(collections) ? collections.slice() : [];
        this._collectionsCopy = [];
        this._id = createGuid();
        this._eventHash = {};
        recomposite(this);
        this._shouldRecomposite = false;
    }

    defineProperties(CompositeEntityCollection.prototype, {
        /**
         * Gets the event that is fired when entities are added or removed from the collection.
         * The generated event is a {@link EntityCollection.collectionChangedEventCallback}.
         * @memberof CompositeEntityCollection.prototype
         * @readonly
         * @type {Event}
         */
        collectionChanged : {
            get : function() {
                return this._composite._collectionChanged;
            }
        },
        /**
         * Gets a globally unique identifier for this collection.
         * @memberof CompositeEntityCollection.prototype
         * @readonly
         * @type {String}
         */
        id : {
            get : function() {
                return this._id;
            }
        },
        /**
         * Gets the array of Entity instances in the collection.
         * This array should not be modified directly.
         * @memberof CompositeEntityCollection.prototype
         * @readonly
         * @type {Entity[]}
         */
        values : {
            get : function() {
                return this._composite.values;
            }
        }
    });

    /**
     * Adds a collection to the composite.
     *
     * @param {EntityCollection} collection the collection to add.
     * @param {Number} [index] the index to add the collection at.  If omitted, the collection will
     *                         added on top of all existing collections.
     *
     * @exception {DeveloperError} index, if supplied, must be greater than or equal to zero and less than or equal to the number of collections.
     */
    CompositeEntityCollection.prototype.addCollection = function(collection, index) {
        var hasIndex = defined(index);
        //>>includeStart('debug', pragmas.debug);
        if (!defined(collection)) {
            throw new DeveloperError('collection is required.');
        }
        if (hasIndex) {
            if (index < 0) {
                throw new DeveloperError('index must be greater than or equal to zero.');
            } else if (index > this._collections.length) {
                throw new DeveloperError('index must be less than or equal to the number of collections.');
            }
        }
        //>>includeEnd('debug');

        if (!hasIndex) {
            index = this._collections.length;
            this._collections.push(collection);
        } else {
            this._collections.splice(index, 0, collection);
        }

        recomposite(this);
    };

    /**
     * Removes a collection from this composite, if present.
     *
     * @param {EntityCollection} collection The collection to remove.
     * @returns {Boolean} true if the collection was in the composite and was removed,
     *                    false if the collection was not in the composite.
     */
    CompositeEntityCollection.prototype.removeCollection = function(collection) {
        var index = this._collections.indexOf(collection);
        if (index !== -1) {
            this._collections.splice(index, 1);
            recomposite(this);
            return true;
        }
        return false;
    };

    /**
     * Removes all collections from this composite.
     */
    CompositeEntityCollection.prototype.removeAllCollections = function() {
        this._collections.length = 0;
        recomposite(this);
    };

    /**
     * Checks to see if the composite contains a given collection.
     *
     * @param {EntityCollection} collection the collection to check for.
     * @returns {Boolean} true if the composite contains the collection, false otherwise.
     */
    CompositeEntityCollection.prototype.containsCollection = function(collection) {
        return this._collections.indexOf(collection) !== -1;
    };

    /**
     * Returns true if the provided entity is in this collection, false otherwise.
     *
     * @param {Entity} entity The entity.
     * @returns {Boolean} true if the provided entity is in this collection, false otherwise.
     */
    CompositeEntityCollection.prototype.contains = function(entity) {
        return this._composite.contains(entity);
    };

    /**
     * Determines the index of a given collection in the composite.
     *
     * @param {EntityCollection} collection The collection to find the index of.
     * @returns {Number} The index of the collection in the composite, or -1 if the collection does not exist in the composite.
     */
    CompositeEntityCollection.prototype.indexOfCollection = function(collection) {
        return this._collections.indexOf(collection);
    };

    /**
     * Gets a collection by index from the composite.
     *
     * @param {Number} index the index to retrieve.
     */
    CompositeEntityCollection.prototype.getCollection = function(index) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(index)) {
            throw new DeveloperError('index is required.', 'index');
        }
        //>>includeEnd('debug');

        return this._collections[index];
    };

    /**
     * Gets the number of collections in this composite.
     */
    CompositeEntityCollection.prototype.getCollectionsLength = function() {
        return this._collections.length;
    };

    function getCollectionIndex(collections, collection) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(collection)) {
            throw new DeveloperError('collection is required.');
        }
        //>>includeEnd('debug');

        var index = collections.indexOf(collection);

        //>>includeStart('debug', pragmas.debug);
        if (index === -1) {
            throw new DeveloperError('collection is not in this composite.');
        }
        //>>includeEnd('debug');

        return index;
    }

    function swapCollections(composite, i, j) {
        var arr = composite._collections;
        i = CesiumMath.clamp(i, 0, arr.length - 1);
        j = CesiumMath.clamp(j, 0, arr.length - 1);

        if (i === j) {
            return;
        }

        var temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;

        recomposite(composite);
    }

    /**
     * Raises a collection up one position in the composite.
     *
     * @param {EntityCollection} collection the collection to move.
     *
     * @exception {DeveloperError} collection is not in this composite.
     */
    CompositeEntityCollection.prototype.raiseCollection = function(collection) {
        var index = getCollectionIndex(this._collections, collection);
        swapCollections(this, index, index + 1);
    };

    /**
     * Lowers a collection down one position in the composite.
     *
     * @param {EntityCollection} collection the collection to move.
     *
     * @exception {DeveloperError} collection is not in this composite.
     */
    CompositeEntityCollection.prototype.lowerCollection = function(collection) {
        var index = getCollectionIndex(this._collections, collection);
        swapCollections(this, index, index - 1);
    };

    /**
     * Raises a collection to the top of the composite.
     *
     * @param {EntityCollection} collection the collection to move.
     *
     * @exception {DeveloperError} collection is not in this composite.
     */
    CompositeEntityCollection.prototype.raiseCollectionToTop = function(collection) {
        var index = getCollectionIndex(this._collections, collection);
        if (index === this._collections.length - 1) {
            return;
        }
        this._collections.splice(index, 1);
        this._collections.push(collection);

        recomposite(this);
    };

    /**
     * Lowers a collection to the bottom of the composite.
     *
     * @param {EntityCollection} collection the collection to move.
     *
     * @exception {DeveloperError} collection is not in this composite.
     */
    CompositeEntityCollection.prototype.lowerCollectionToBottom = function(collection) {
        var index = getCollectionIndex(this._collections, collection);
        if (index === 0) {
            return;
        }
        this._collections.splice(index, 1);
        this._collections.splice(0, 0, collection);

        recomposite(this);
    };

    /**
     * Prevents {@link EntityCollection#collectionChanged} events from being raised
     * until a corresponding call is made to {@link EntityCollection#resumeEvents}, at which
     * point a single event will be raised that covers all suspended operations.
     * This allows for many items to be added and removed efficiently.
     * While events are suspended, recompositing of the collections will
     * also be suspended, as this can be a costly operation.
     * This function can be safely called multiple times as long as there
     * are corresponding calls to {@link EntityCollection#resumeEvents}.
     */
    CompositeEntityCollection.prototype.suspendEvents = function() {
        this._suspendCount++;
        this._composite.suspendEvents();
    };

    /**
     * Resumes raising {@link EntityCollection#collectionChanged} events immediately
     * when an item is added or removed.  Any modifications made while while events were suspended
     * will be triggered as a single event when this function is called.  This function also ensures
     * the collection is recomposited if events are also resumed.
     * This function is reference counted and can safely be called multiple times as long as there
     * are corresponding calls to {@link EntityCollection#resumeEvents}.
     *
     * @exception {DeveloperError} resumeEvents can not be called before suspendEvents.
     */
    CompositeEntityCollection.prototype.resumeEvents = function() {
        //>>includeStart('debug', pragmas.debug);
        if (this._suspendCount === 0) {
            throw new DeveloperError('resumeEvents can not be called before suspendEvents.');
        }
        //>>includeEnd('debug');

        this._suspendCount--;
        // recomposite before triggering events (but only if required for performance) that might depend on a composited collection
        if (this._shouldRecomposite && this._suspendCount === 0) {
            recomposite(this);
            this._shouldRecomposite = false;
        }

        this._composite.resumeEvents();
    };

    /**
     * Computes the maximum availability of the entities in the collection.
     * If the collection contains a mix of infinitely available data and non-infinite data,
     * It will return the interval pertaining to the non-infinite data only.  If all
     * data is infinite, an infinite interval will be returned.
     *
     * @returns {TimeInterval} The availability of entities in the collection.
     */
    CompositeEntityCollection.prototype.computeAvailability = function() {
        return this._composite.computeAvailability();
    };

    /**
     * Gets an entity with the specified id.
     *
     * @param {Object} id The id of the entity to retrieve.
     * @returns {Entity} The entity with the provided id or undefined if the id did not exist in the collection.
     */
    CompositeEntityCollection.prototype.getById = function(id) {
        return this._composite.getById(id);
    };

    CompositeEntityCollection.prototype._onCollectionChanged = function(collection, added, removed) {
        var collections = this._collectionsCopy;
        var collectionsLength = collections.length;
        var composite = this._composite;
        composite.suspendEvents();

        var i;
        var q;
        var entity;
        var compositeEntity;
        var removedLength = removed.length;
        var eventHash = this._eventHash;
        var collectionId = collection.id;
        for (i = 0; i < removedLength; i++) {
            var removedEntity = removed[i];
            unsubscribeFromEntity(this, eventHash, collectionId, removedEntity);

            var removedId = removedEntity.id;
            //Check if the removed entity exists in any of the remaining collections
            //If so, we clean and remerge it.
            for (q = collectionsLength - 1; q >= 0; q--) {
                entity = collections[q].getById(removedId);
                if (defined(entity)) {
                    if (!defined(compositeEntity)) {
                        compositeEntity = composite.getById(removedId);
                        clean(compositeEntity);
                    }
                    compositeEntity.merge(entity);
                }
            }
            //We never retrieved the compositeEntity, which means it no longer
            //exists in any of the collections, remove it from the composite.
            if (!defined(compositeEntity)) {
                composite.removeById(removedId);
            }
            compositeEntity = undefined;
        }

        var addedLength = added.length;
        for (i = 0; i < addedLength; i++) {
            var addedEntity = added[i];
            subscribeToEntity(this, eventHash, collectionId, addedEntity);

            var addedId = addedEntity.id;
            //We know the added entity exists in at least one collection,
            //but we need to check all collections and re-merge in order
            //to maintain the priority of properties.
            for (q = collectionsLength - 1; q >= 0; q--) {
                entity = collections[q].getById(addedId);
                if (defined(entity)) {
                    if (!defined(compositeEntity)) {
                        compositeEntity = composite.getById(addedId);
                        if (!defined(compositeEntity)) {
                            entityOptionsScratch.id = addedId;
                            compositeEntity = new Entity(entityOptionsScratch);
                            composite.add(compositeEntity);
                        } else {
                            clean(compositeEntity);
                        }
                    }
                    compositeEntity.merge(entity);
                }
            }
            compositeEntity = undefined;
        }

        composite.resumeEvents();
    };

    CompositeEntityCollection.prototype._onDefinitionChanged = function(entity, propertyName, newValue, oldValue) {
        var collections = this._collections;
        var composite = this._composite;

        var collectionsLength = collections.length;
        var id = entity.id;
        var compositeEntity = composite.getById(id);
        var compositeProperty = compositeEntity[propertyName];
        var newProperty = !defined(compositeProperty);

        var firstTime = true;
        for (var q = collectionsLength - 1; q >= 0; q--) {
            var innerEntity = collections[q].getById(entity.id);
            if (defined(innerEntity)) {
                var property = innerEntity[propertyName];
                if (defined(property)) {
                    if (firstTime) {
                        firstTime = false;
                        //We only want to clone if the property is also mergeable.
                        //This ensures that leaf properties are referenced and not copied,
                        //which is the entire point of compositing.
                        if (defined(property.merge) && defined(property.clone)) {
                            compositeProperty = property.clone(compositeProperty);
                        } else {
                            compositeProperty = property;
                            break;
                        }
                    }
                    compositeProperty.merge(property);
                }
            }
        }

        if (newProperty && compositeEntity.propertyNames.indexOf(propertyName) === -1) {
            compositeEntity.addProperty(propertyName);
        }

        compositeEntity[propertyName] = compositeProperty;
    };

    return CompositeEntityCollection;
});
