/*global define*/
define(['Core/createGuid', 'Core/defaultValue', 'Core/defined', 'Core/defineProperties', 'Core/DeveloperError', 'Core/Event', 'Core/JulianDate', 'Core/TimeInterval', 'DynamicScene/createDynamicPropertyDescriptor'], function(
        createGuid,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        Event,
        JulianDate,
        TimeInterval,
        createDynamicPropertyDescriptor) {
    "use strict";

    var reservedPropertyNames = ['cachedAvailabilityDate', 'cachedAvailabilityValue', 'id', 'propertyChanged', //
                                 'propertyNames', 'isAvailable', 'clean', 'merge', 'addProperty', 'removeProperty'];

    /**
     * DynamicObject instances are the primary data store for processed data.
     * They are used primarily by the visualizers to create and maintain graphic
     * primitives that represent the DynamicObject's properties at a specific time.
     * @alias DynamicObject
     * @constructor
     *
     * @param {Object} [id] A unique identifier for this object.  If no id is provided, a GUID is generated.
     *
     * @see Property
     * @see DynamicObjectCollection
     */
    var DynamicObject = function(id) {
        this._cachedAvailabilityDate = undefined;
        this._cachedAvailabilityValue = undefined;

        if (!defined(id)) {
            id = createGuid();
        }

        this._id = id;
        this._availability = undefined;
        this._position = undefined;
        this._orientation = undefined;
        this._billboard = undefined;
        this._cone = undefined;
        this._ellipsoid = undefined;
        this._ellipse = undefined;
        this._label = undefined;
        this._path = undefined;
        this._point = undefined;
        this._polygon = undefined;
        this._polyline = undefined;
        this._pyramid = undefined;
        this._vertexPositions = undefined;
        this._vector = undefined;
        this._viewFrom = undefined;

        this._propertyChanged = new Event();
        this._propertyNames = ['availability', 'position', 'orientation', 'billboard', //
                               'cone', 'ellipsoid', 'ellipse', 'label', 'path', 'point', 'polygon', //
                               'polyline', 'pyramid', 'vertexPositions', 'vector', 'viewFrom'];
    };

    defineProperties(DynamicObject.prototype, {
        /**
         * Gets the event that is raised whenever a new property is assigned.
         * @memberof DynamicObject.prototype
         * @type {Event}
         */
        propertyChanged : {
            get : function() {
                return this._propertyChanged;
            }
        },
        /**
         * Gets the names of all properties registed on this instance.
         * @memberof DynamicObject.prototype
         * @type {Event}
         */
        propertyNames : {
            get : function() {
                return this._propertyNames;
            }
        },
        /**
         * Gets the unique ID associated with this object.
         * @memberof DynamicObject.prototype
         * @type {Object}
         */
        id : {
            get : function() {
                return this._id;
            }
        },
        /**
         * The availability TimeInterval, if any, associated with this object.
         * If availability is undefined, it is assumed that this object's
         * other properties will return valid data for any provided time.
         * If availability exists, the objects other properties will only
         * provide valid data if queried within the given interval.
         * @memberof DynamicObject.prototype
         * @type {TimeInterval}
         */
        availability : createDynamicPropertyDescriptor('availability', '_availability'),
        /**
         * Gets or sets the position.
         * @memberof DynamicObject.prototype
         * @type {PositionProperty}
         */
        position : createDynamicPropertyDescriptor('position', '_position'),
        /**
         * Gets or sets the orientation.
         * @memberof DynamicObject.prototype
         * @type {Property}
         */
        orientation : createDynamicPropertyDescriptor('orientation', '_orientation'),
        /**
         * Gets or sets the suggested initial offset for viewing this object
         * with the camera.  The offset is defined in the east-north-up reference frame.
         * @memberof DynamicObject.prototype
         * @type {Cartesian3}
         */
        viewFrom : createDynamicPropertyDescriptor('viewFrom', '_viewFrom'),
        /**
         * Gets or sets the billboard.
         * @memberof DynamicObject.prototype
         * @type {DynamicBillboard}
         */
        billboard : createDynamicPropertyDescriptor('billboard', '_billboard'),
        /**
         * Gets or sets the cone.
         * @memberof DynamicObject.prototype
         * @type {DynamicCone}
         */
        cone : createDynamicPropertyDescriptor('cone', '_cone'),

        /**
         * Gets or sets the ellipsoid.
         * @memberof DynamicObject.prototype
         * @type {DynamicEllipsoid}
         */
        ellipsoid : createDynamicPropertyDescriptor('ellipsoid', '_ellipsoid'),
        /**
         * Gets or sets the ellipse.
         * @memberof DynamicObject.prototype
         * @type {DynamicEllipse}
         */
        ellipse : createDynamicPropertyDescriptor('ellipse', '_ellipse'),
        /**
         * Gets or sets the label.
         * @memberof DynamicObject.prototype
         * @type {DynamicLabel}
         */
        label : createDynamicPropertyDescriptor('label', '_label'),
        /**
         * Gets or sets the path.
         * @memberof DynamicObject.prototype
         * @type {DynamicPath}
         */
        path : createDynamicPropertyDescriptor('path', '_path'),
        /**
         * Gets or sets the point graphic.
         * @memberof DynamicObject.prototype
         * @type {DynamicPoint}
         */
        point : createDynamicPropertyDescriptor('point', '_point'),
        /**
         * Gets or sets the polygon.
         * @memberof DynamicObject.prototype
         * @type {DynamicPolygon}
         */
        polygon : createDynamicPropertyDescriptor('polygon', '_polygon'),
        /**
         * Gets or sets the polyline.
         * @memberof DynamicObject.prototype
         * @type {DynamicPolyline}
         */
        polyline : createDynamicPropertyDescriptor('polyline', '_polyline'),
        /**
         * Gets or sets the pyramid.
         * @memberof DynamicObject.prototype
         * @type {DynamicPyramid}
         */
        pyramid : createDynamicPropertyDescriptor('pyramid', '_pyramid'),
        /**
         * Gets or sets the vertex positions.
         * @memberof DynamicObject.prototype
         * @type {Property}
         */
        vertexPositions : createDynamicPropertyDescriptor('vertexPositions', '_vertexPositions'),
        /**
         * Gets or sets the vector.
         * @memberof DynamicObject.prototype
         * @type {DynamicVector}
         */
        vector : createDynamicPropertyDescriptor('vector', '_vector')
    });

    /**
     * Given a time, returns true if this object should have data during that time.
     * @memberof DynamicObject
     *
     * @param {JulianDate} time The time to check availability for.
     * @exception {DeveloperError} time is required.
     * @returns true if the object should have data during the provided time, false otherwise.
     */
    DynamicObject.prototype.isAvailable = function(time) {
        if (!defined(time)) {
            throw new DeveloperError('time is required.');
        }

        var availability = this._availability;
        return !defined(availability) || availability.contains(time);
    };

    /**
     * Adds a property to this object.  Once a property is added, it can be
     * observed with {@link DynamicObject.propertyChanged} and composited
     * with {@link CompositeDynamicObjectCollection}
     * @memberof DynamicObject
     *
     * @param propertyName The name of the property to add.
     *
     * @exception {DeveloperError} propertyName is required.
     * @exception {DeveloperError} "propertyName" is a reserved property name.
     * @exception {DeveloperError} "propertyName" is already a registered property.
     */
    DynamicObject.prototype.addProperty = function(propertyName) {
        if (!defined(propertyName)) {
            throw new DeveloperError('propertyName is required.');
        }

        var propertyNames = this._propertyNames;
        if (propertyNames.indexOf(propertyName) !== -1) {
            throw new DeveloperError(propertyName + ' is already a registered property.');
        }
        if (reservedPropertyNames.indexOf(propertyName) !== -1) {
            throw new DeveloperError(propertyName + ' is a reserved property name.');
        }

        propertyNames.push(propertyName);

        Object.defineProperty(this, propertyName, createDynamicPropertyDescriptor(propertyName, '_' + propertyName, true));
    };

    /**
     * Removed a property previously added with addProperty.
     * @memberof DynamicObject
     *
     * @param propertyName The name of the property to remove.
     *
     * @exception {DeveloperError} propertyName is required.
     * @exception {DeveloperError} "propertyName" is a reserved property name.
     * @exception {DeveloperError} "propertyName" is not a registered property.
     */
    DynamicObject.prototype.removeProperty = function(propertyName) {
        if (!defined(propertyName)) {
            throw new DeveloperError('propertyName is required.');
        }

        var propertyNames = this._propertyNames;
        if (reservedPropertyNames.indexOf(propertyName) !== -1) {
            throw new DeveloperError(propertyName + ' is a reserved property name.');
        }
        if (propertyNames.indexOf(propertyName) === -1) {
            throw new DeveloperError(propertyName + ' is not a registered property.');
        }

        this._propertyNames.push(propertyName);
        delete this[propertyName];
    };

    /**
     * Assigns each unassigned property on this object to the value
     * of the same property on the provided source object.
     * @memberof DynamicObject
     *
     * @param {DynamicObject} source The object to be merged into this object.
     * @exception {DeveloperError} source is required.
     */
    DynamicObject.prototype.merge = function(source) {
        if (!defined(source)) {
            throw new DeveloperError('source is required.');
        }
        this.availability = defaultValue(source.availability, this.availability);

        var propertyNames = this._propertyNames;
        var propertyNamesLength = propertyNames.length;
        for ( var i = 0; i < propertyNamesLength; i++) {
            var name = propertyNames[i];
            //TODO Remove this once availability is refactored.
            if (name !== 'availability') {
                var targetProperty = this[name];
                var sourceProperty = source[name];
                if (defined(sourceProperty)) {
                    if (defined(targetProperty)) {
                        if (defined(targetProperty.merge)) {
                            targetProperty.merge(sourceProperty);
                        }
                    } else if (defined(sourceProperty.merge) && defined(sourceProperty.clone)) {
                        this[name] = sourceProperty.clone();
                    } else {
                        this[name] = sourceProperty;
                    }
                }
            }
        }
    };

    return DynamicObject;
});