/*global define*/
define([
        '../Core/AssociativeArray',
        '../Core/createGuid',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/Event',
        '../Core/Iso8601',
        '../Core/JulianDate',
        '../Core/RuntimeError',
        '../Core/TimeInterval',
        './Entity'
    ], function(
        AssociativeArray,
        createGuid,
        defined,
        defineProperties,
        DeveloperError,
        Event,
        Iso8601,
        JulianDate,
        RuntimeError,
        TimeInterval,
        Entity) {
    "use strict";

    function fireChangedEvent(collection) {
        if (collection._suspendCount === 0) {
            var added = collection._addedEntities;
            var removed = collection._removedEntities;
            var changed = collection._changedEntities;
            if (changed.length !== 0 || added.length !== 0 || removed.length !== 0) {
                collection._collectionChanged.raiseEvent(collection, added.values, removed.values, changed.values);
                added.removeAll();
                removed.removeAll();
                changed.removeAll();
            }
        }
    }

    /**
     * An observable collection of {@link Entity} instances where each entity has a unique id.
     * @alias EntityCollection
     * @constructor
     */
    var EntityCollection = function() {
        this._entities = new AssociativeArray();
        this._addedEntities = new AssociativeArray();
        this._removedEntities = new AssociativeArray();
        this._changedEntities = new AssociativeArray();
        this._suspendCount = 0;
        this._collectionChanged = new Event();
        this._id = createGuid();
    };

    /**
     * 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.
     * This function can be safely called multiple times as long as there
     * are corresponding calls to {@link EntityCollection#resumeEvents}.
     */
    EntityCollection.prototype.suspendEvents = function() {
        this._suspendCount++;
    };

    /**
     * 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 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.
     */
    EntityCollection.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--;
        fireChangedEvent(this);
    };

    /**
     * The signature of the event generated by {@link EntityCollection#collectionChanged}.
     * @function
     *
     * @param {EntityCollection} collection The collection that triggered the event.
     * @param {Entity[]} added The array of {@link Entity} instances that have been added to the collection.
     * @param {Entity[]} removed The array of {@link Entity} instances that have been removed from the collection.
     * @param {Entity[]} changed The array of {@link Entity} instances that have been modified.
     */
    EntityCollection.collectionChangedEventCallback = undefined;

    defineProperties(EntityCollection.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 EntityCollection.prototype
         * @readonly
         * @type {Event}
         */
        collectionChanged : {
            get : function() {
                return this._collectionChanged;
            }
        },
        /**
         * Gets a globally unique identifier for this collection.
         * @memberof EntityCollection.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 EntityCollection.prototype
         * @readonly
         * @type {Entity[]}
         */
        entities : {
            get : function() {
                return this._entities.values;
            }
        }
    });

    /**
     * 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.
     */
    EntityCollection.prototype.computeAvailability = function() {
        var startTime = Iso8601.MAXIMUM_VALUE;
        var stopTime = Iso8601.MINIMUM_VALUE;
        var entities = this._entities.values;
        for (var i = 0, len = entities.length; i < len; i++) {
            var entity = entities[i];
            var availability = entity.availability;
            if (defined(availability)) {
                var start = availability.start;
                var stop = availability.stop;
                if (JulianDate.lessThan(start, startTime) && !start.equals(Iso8601.MINIMUM_VALUE)) {
                    startTime = start;
                }
                if (JulianDate.greaterThan(stop, stopTime) && !stop.equals(Iso8601.MAXIMUM_VALUE)) {
                    stopTime = stop;
                }
            }
        }

        if (Iso8601.MAXIMUM_VALUE.equals(startTime)) {
            startTime = Iso8601.MINIMUM_VALUE;
        }
        if (Iso8601.MINIMUM_VALUE.equals(stopTime)) {
            stopTime = Iso8601.MAXIMUM_VALUE;
        }
        return new TimeInterval({
            start : startTime,
            stop : stopTime
        });
    };

    /**
     * Add an entity to the collection.
     *
     * @param {Entity} entity The entity to be added.
     * @exception {DeveloperError} An entity with <entity.id> already exists in this collection.
     */
    EntityCollection.prototype.add = function(entity) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(entity)) {
            throw new DeveloperError('entity is required.');
        }
        //>>includeEnd('debug');

        var id = entity.id;
        var entities = this._entities;
        if (entities.contains(id)) {
            throw new RuntimeError('An entity with id ' + id + ' already exists in this collection.');
        }

        entities.set(id, entity);

        var removedEntities = this._removedEntities;
        if (!this._removedEntities.remove(id)) {
            this._addedEntities.set(id, entity);
        }
        entity.definitionChanged.addEventListener(EntityCollection.prototype._onEntityDefinitionChanged, this);

        fireChangedEvent(this);
    };

    /**
     * Removes an entity from the collection.
     *
     * @param {Entity} entity The entity to be added.
     * @returns {Boolean} true if the item was removed, false if it did not exist in the collection.
     */
    EntityCollection.prototype.remove = function(entity) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(entity)) {
            throw new DeveloperError('entity is required');
        }
        //>>includeEnd('debug');

        return this.removeById(entity.id);
    };

    /**
     * Removes an entity with the provided id from the collection.
     *
     * @param {Object} id The id of the entity to remove.
     * @returns {Boolean} true if the item was removed, false if no item with the provided id existed in the collection.
     */
    EntityCollection.prototype.removeById = function(id) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(id)) {
            throw new DeveloperError('id is required.');
        }
        //>>includeEnd('debug');

        var entities = this._entities;
        var entity = entities.get(id);
        if (!this._entities.remove(id)) {
            return false;
        }

        if (!this._addedEntities.remove(id)) {
            this._removedEntities.set(id, entity);
            this._changedEntities.remove(id);
        }
        this._entities.remove(id);
        entity.definitionChanged.removeEventListener(EntityCollection.prototype._onEntityDefinitionChanged, this);
        fireChangedEvent(this);

        return true;
    };

    /**
     * Removes all Entities from the collection.
     */
    EntityCollection.prototype.removeAll = function() {
        //The event should only contain items added before events were suspended
        //and the contents of the collection.
        var entities = this._entities;
        var entitiesLength = entities.length;
        var array = entities.values;

        var addedEntities = this._addedEntities;
        var removed = this._removedEntities;

        for (var i = 0; i < entitiesLength; i++) {
            var existingItem = array[i];
            var existingItemId = existingItem.id;
            var addedItem = addedEntities.get(existingItemId);
            if (!defined(addedItem)) {
                existingItem.definitionChanged.removeEventListener(EntityCollection.prototype._onEntityDefinitionChanged, this);
                removed.set(existingItemId, existingItem);
            }
        }

        entities.removeAll();
        addedEntities.removeAll();
        this._changedEntities.removeAll();
        fireChangedEvent(this);
    };

    /**
     * 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.
     */
    EntityCollection.prototype.getById = function(id) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(id)) {
            throw new DeveloperError('id is required.');
        }
        //>>includeEnd('debug');

        return this._entities.get(id);
    };

    /**
     * Gets an entity with the specified id or creates it and adds it to the collection if it does not exist.
     *
     * @param {Object} id The id of the entity to retrieve or create.
     * @returns {Entity} The new or existing object.
     */
    EntityCollection.prototype.getOrCreateEntity = function(id) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(id)) {
            throw new DeveloperError('id is required.');
        }
        //>>includeEnd('debug');

        var entity = this._entities.get(id);
        if (!defined(entity)) {
            entity = new Entity(id);
            this.add(entity);
        }
        return entity;
    };

    EntityCollection.prototype._onEntityDefinitionChanged = function(entity) {
        var id = entity.id;
        if (!this._addedEntities.contains(id)) {
            this._changedEntities.set(id, entity);
        }
        fireChangedEvent(this);
    };

    return EntityCollection;
});