"use strict";

App.module("ContextModule", function(ContextModule, App, Backbone, Marionette, $, _, Logger) {

    // Define private variables
    var logger = null;

    ContextModule.addInitializer(function(options){

        logger = Logger.get("ContextModule.Config");
        logger.setLevel(Logger.WARN);

        /**
         * Config embraces common behaviour patterns of an object that can have pending changes –
         * it consists of two attributes: parameters and plannedParameterUpdates. Both are simple
         * Backbone objects (Models) with a list of scalar attributes.
         *
         * The ConfigObject itself is identified by its non-changeable clientId (getClientId())
         * and can be serialized (dumped) and unserialized (restored).
         *
         * It is advised to listen to changes in the Config using "change", "change:parameters"
         * and "change:plannedParameterUpdates" listener.
         * It will be triggered each time when "parameters or "plannedParameterUpdates" attributes have been changed.
         */
        ContextModule.Config = Backbone.Model.extend({

            // Client id prefix (cf123 instead of standard c123)
            cidPrefix: "cf",

            /**
             * @memberOf App.ContextModule.Config
             */
            constructor: function(attributes, options) {
                this._modificationPropagationEnabled = true;
                this._parametersWereModified = false;
                this._plannedParameterUpdatesWereModified = false;
                this._cachedHashForTrimmedParameters = null;
                this._cachedHashForParameters = null;
                this._cachedHashForPlannedParameterUpdates = null;
                this._cachedHashForPermanent = null;
                this._cachedHashForTemp = null;
                this._cachedHash = null;

                var defaultParameters = (_.isSimpleObject(attributes) && _.isSimpleObject(attributes.parameters)) ? attributes.parameters : undefined;
                var defaultPlannedParameterUpdates = (_.isSimpleObject(attributes) && _.isSimpleObject(attributes.plannedParameterUpdates)) ? attributes.plannedParameterUpdates : undefined;

                var realAttributes = {};
                realAttributes.parameters = new Backbone.Model(defaultParameters);
                realAttributes.plannedParameterUpdates = new Backbone.Model(defaultPlannedParameterUpdates);

                this.listenTo(realAttributes.parameters, "change", this._registerModificationOfParameters);
                this.listenTo(realAttributes.plannedParameterUpdates, "change", this._registerModificationOfPlannedParameterUpdates);

                Backbone.Model.apply(this, [realAttributes, options]);

                if (attributes && attributes.clientId) {
                    this.cid = attributes.clientId;
                    _.markUniqueIdAsAlreadyUsed(attributes.clientId);
                }
            },

            getClientId: function() {
                return this.cid;
            },

            getDimension: function() {
                return this.collection ? this.collection.dimension : undefined;
            },
            getConfigGridType: function() {
                return this.collection ? this.collection.configGridType : undefined;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            getParameterValue: function(parameterName) {
                return this.attributes.parameters.attributes[parameterName];
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            getPlannedParameterValue: function(parameterName) {
                var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes;
                if (plannedParameterUpdatesAttributes.hasOwnProperty(parameterName)) {
                    return plannedParameterUpdatesAttributes[parameterName];
                } else {
                    return this.attributes.parameters.attributes[parameterName];
                }
            },

            /**
             * @memberOf App.ContextModule.Config
             *
             * XXX clone parameter values?
             */
            getPlannedParameterValues: function(parameterName) {
                var result = _.clone(this.attributes.parameters.attributes);
                var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes;
                for (var key in plannedParameterUpdatesAttributes) {
                    if (plannedParameterUpdatesAttributes.hasOwnProperty(key)) {
                        if (plannedParameterUpdatesAttributes[key] === undefined) {
                            if (result.hasOwnProperty(key)) {
                                delete result[key];
                            }
                        } else {
                            result[key] = plannedParameterUpdatesAttributes[key];
                        }
                    }
                }
                return result;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            isPlannedToUpdate: function(parameterName) {
                return this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName);
                // var parameterValue = this.getParameterValue(parameterName);
                // var plannedParameterValue = this.getPlannedParameterValue(parameterName);
                // return (parameterValue !== plannedParameterValue);
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            hasPlannedParameterUpdates: function() {
                return _.size(this.attributes.plannedParameterUpdates.attributes) > 0;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            updateParameter: function(parameterName, parameterValue) {
                if (!_.isString(parameterName)) {
                    throw _.str.sprintf("Config::updateParameter called a non-string parameterName: %s", parameterName);
                }
                var prevModificationPropagationEnabled = this._modificationPropagationEnabled;
                this._modificationPropagationEnabled = false;
                this.attributes.plannedParameterUpdates.unset(parameterName);
                if (typeof parameterValue !== "undefined") {
                    this.attributes.parameters.set(parameterName, parameterValue);
                } else {
                    this.attributes.parameters.unset(parameterName);
                }
                if (prevModificationPropagationEnabled) {
                    this._triggerModificationEventsIfNeeded();
                    this._modificationPropagationEnabled = true;
                }
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            planParameterUpdate: function(parameterName, parameterValue) {
                if (!_.isString(parameterName)) {
                    throw _.str.sprintf("Config::planParameterUpdate called a non-string parameterName: %s", parameterName);
                }
                var prevModificationPropagationEnabled = this._modificationPropagationEnabled;

                this._modificationPropagationEnabled = false;
                var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes;
                var parametersAttributes = this.attributes.parameters.attributes;
                if (parameterValue === parametersAttributes[parameterName]) {
                    // special case: backbone won't fire a change event without this hack (due to how _.isEqual works)
                    if (this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName) && this.attributes.plannedParameterUpdates.attributes[parameterName] === undefined) {
                        this.attributes.plannedParameterUpdates.set(parameterName, 42, {silent: true});
                    }
                    this.attributes.plannedParameterUpdates.unset(parameterName);
                } else {
                    // special case: backbone won't fire a change event without this hack (due to how _.isEqual works)
                    if (parameterValue === undefined && this.attributes.parameters.attributes.hasOwnProperty(parameterName) && !this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName)) {
                        this.attributes.plannedParameterUpdates.set(parameterName, 42, {silent: true});
                    }
                    this.attributes.plannedParameterUpdates.set(parameterName, parameterValue);
                }

                if (prevModificationPropagationEnabled) {
                    this._triggerModificationEventsIfNeeded();
                    this._modificationPropagationEnabled = true;
                }
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            cancelPlannedParameterUpdate: function(parameterName) {
                if (!_.isString(parameterName)) {
                    throw _.str.sprintf("Config::cancelPlannedParameterUpdate called a non-string parameterName: %s", parameterName);
                }
                var prevModificationPropagationEnabled = this._modificationPropagationEnabled;
                this._modificationPropagationEnabled = false;

                this.attributes.plannedParameterUpdates.unset(parameterName);
                if (prevModificationPropagationEnabled) {
                    this._triggerModificationEventsIfNeeded();
                    this._modificationPropagationEnabled = true;
                }
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            updateParameters: function(parameters) {
                if (!_.isSimpleObject(parameters)) {
                    throw _.str.sprintf("Config::updateParameters called a wrong argument: %s", parameters);
                }
                this._modificationPropagationEnabled = false;

                for (var parameterName in parameters) {
                    if (parameters.hasOwnProperty(parameterName)) {
                        this.updateParameter(parameterName, parameters[parameterName]);
                    }
                }

                this._triggerModificationEventsIfNeeded();
                this._modificationPropagationEnabled = true;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            planParameterUpdates: function(parameters) {
                if (!_.isSimpleObject(parameters)) {
                    throw _.str.sprintf("Config::planParameterUpdates called with a wrong argument: %s", parameters);
                }
                this._modificationPropagationEnabled = false;

                for (var parameterName in parameters) {
                    if (parameters.hasOwnProperty(parameterName)) {
                        this.planParameterUpdate(parameterName, parameters[parameterName]);
                    }
                }

                this._triggerModificationEventsIfNeeded();
                this._modificationPropagationEnabled = true;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            cancelPlannedParameterUpdates: function(parameterNames) {
                if (_.isArray(parameterNames)) {
                    this._modificationPropagationEnabled = false;

                    for (var i = 0; i < parameterNames.length; i++) {
                        this.cancelPlannedParameterUpdate(parameterNames[i]);
                    }

                    this._triggerModificationEventsIfNeeded();
                    this._modificationPropagationEnabled = true;
                } else if (!_.isUndefined(parameterNames)) {
                    throw _.str.sprintf("Config::planParameterUpdates called a non-string parameters: %s", parameters);
                } else {
                    if (_.keys(this.attributes.plannedParameterUpdates.attributes).length) {
                        this.attributes.plannedParameterUpdates.attributes.fix = 42;
                    }
                    this.attributes.plannedParameterUpdates.clear();
                }
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            applyPlannedParameterUpdates: function() {

                // Combine parameters with planned updates
                var newParameters = _.extend(this.attributes.parameters.toJSON(), this.attributes.plannedParameterUpdates.toJSON());

                // Remove "undefined" from the new parameters
                for (var key in newParameters) {
                    if (!newParameters.hasOwnProperty(key)) continue;

                    if (typeof newParameters[key] === "undefined") {
                        delete newParameters[key];
                    }
                }

                // If there are any existing keys with "undefined" within plannedParameterUpdates,
                // replace "undefined" with some value to make sure change:plannedParameterUpdates is triggered
                // Another part of this hack is in "planParameterUpdate" method
                var attributesInPlannedParameterUpdates = this.attributes.plannedParameterUpdates.attributes;
                for (var key in attributesInPlannedParameterUpdates) {
                    if (attributesInPlannedParameterUpdates[key] === undefined) {
                        attributesInPlannedParameterUpdates[key] = 42;
                        break;
                    }
                }

                // Assign parameters and clear planned updates
                this.unserialize({
                    clientId: this.cid,
                    parameters: newParameters,
                    plannedParameterUpdates: {}
                });
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            serialize: function() {
                var result = {
                        clientId: this.cid,
                        parameters: this.attributes.parameters.toJSON(),
                        plannedParameterUpdates: this.attributes.plannedParameterUpdates.toJSON()
                };

                return result;
            },

            /**
             * @memberOf App.ContextModule.Config
             */
            unserialize: function(serializedAttributes) {
                var fixedSerializedAttributes = serializedAttributes;
                if (!_.isSimpleObject(serializedAttributes)) {
                    logger.warn("Config::unserialize called for not an object: ", serializedAttributes);
                    fixedSerializedAttributes = {};
                }

                if (this.cid != fixedSerializedAttributes.clientId && !_.isUndefined(fixedSerializedAttributes.clientId)) {
                    throw _.str.sprintf("Parameter bag client id (%s) is not equal to the client id of the serialized object (%s).", this.cid, fixedSerializedAttributes.clientId);
                }

                this._parametersWereModified = false;
                this._plannedParameterUpdatesWereModified = false;
                this._modificationPropagationEnabled = false;

                var fixedSerializedPlannedParameterUpdates = fixedSerializedAttributes.plannedParameterUpdates;
                if (!_.isSimpleObject(fixedSerializedPlannedParameterUpdates)) {
                    if (_.isSimpleObject(serializedAttributes)) {
                        logger.warn("Config::unserialize called for object with faulty plannedParameterUpdates: ", fixedSerializedPlannedParameterUpdates);
                    }
                    fixedSerializedPlannedParameterUpdates = {};
                }
                if (!_.isEqual(this.attributes.plannedParameterUpdates.attributes, fixedSerializedPlannedParameterUpdates)) {
                    this.attributes.plannedParameterUpdates
                        .set("fix", 42, {silent: true})
                        .clear()
                        .set(fixedSerializedPlannedParameterUpdates);
                }

                var fixedSerializedParameters = fixedSerializedAttributes.parameters;
                if (!_.isSimpleObject(fixedSerializedParameters)) {
                    if (_.isSimpleObject(serializedAttributes)) {
                        logger.warn("Config::unserialize called for object with faulty parameters: ", fixedSerializedParameters);
                    }
                    fixedSerializedParameters = {};
                }
                if (!_.isEqual(this.attributes.parameters.toJSON(), fixedSerializedParameters)) {
                    this.attributes.parameters
                        .clear()
                        .set(fixedSerializedParameters);
                }

                this._triggerModificationEventsIfNeeded();
                this._modificationPropagationEnabled = true;
            },

            clone: function() {
                var serializedAttributes = this.serialize();
                delete serializedAttributes.clientId;
                return new ContextModule.Config(serializedAttributes);
            },

            getHashForParameters: function() {
                if (this._cachedHashForParameters === null) {
                    this._cachedHashForParameters = JSON.stringify(this.attributes.parameters.attributes);
                }
                return this._cachedHashForParameters;
            },

            getHashForTrimmedParameters: function() {
                if (this._cachedHashForTrimmedParameters === null) {
                    var attributesToHash = _.clone(this.attributes.parameters.attributes);
                    for (var key in attributesToHash){
                        if (attributesToHash.hasOwnProperty(key) && _.isString(attributesToHash[key])) {
                            attributesToHash[key] = _.str.trim(attributesToHash[key]);
                        }
                    }
                    this._cachedHashForTrimmedParameters = JSON.stringify(attributesToHash);
                }
                return this._cachedHashForTrimmedParameters;
            },

            getHashForPlannedParameterUpdates: function() {
                if (this._cachedHashForPlannedParameterUpdates === null) {
                    var attributes = this.attributes.plannedParameterUpdates.attributes;
                    this._cachedHashForPlannedParameterUpdates = JSON.stringify(attributes);
                    // special treatment of undefined is needed here
                    // see http://stackoverflow.com/questions/26540706/json-stringify-removes-hash-keys-with-undefined-values
                    for (var key in attributes) {
                        if (attributes.hasOwnProperty(key) && attributes[key] === undefined) {
                            this._cachedHashForPlannedParameterUpdates += key + "|";
                        }
                    }
                }
                return this._cachedHashForPlannedParameterUpdates;
            },

            getHashForPermanent: function() {
                if (this._cachedHashForPermanent === null) {
                    this._cachedHashForPermanent = this.getHashForParameters() + this.getHashForPlannedParameterUpdates();
                }
                return this._cachedHashForPermanent;
            },

            getHashForTemp: function() {
                if (this._cachedHashForTemp === null) {
                    this._cachedHashForTemp = "";
                }
                return this._cachedHashForTemp;
            },

            getHash: function() {
                if (!this._cachedHash) {
                    this._cachedHash = this.getHashForPermanent() + this.getHashForTemp();
                }
                return this._cachedHash;
            },

            _registerModificationOfParameters: function() {
                this._cachedHashForParameters = null;
                this._cachedHashForTrimmedParameters = null;
                this._cachedHashForPermanent = null;
                this._cachedHash = null;

                this._parametersWereModified = true;
                if (this._modificationPropagationEnabled) {
                    this._triggerModificationEventsIfNeeded();
                };
            },

            _registerModificationOfPlannedParameterUpdates: function() {
                this._cachedHashForPlannedParameterUpdates = null;
                this._cachedHashForPermanent = null;
                this._cachedHash = null;

                this._plannedParameterUpdatesWereModified = true;
                if (this._modificationPropagationEnabled) {
                    this._triggerModificationEventsIfNeeded();
                };
            },

            _triggerModificationEventsIfNeeded: function() {
                this.changed = [true];
                if (this._parametersWereModified) {
                    this.trigger("change:parameters");
                }
                if (this._plannedParameterUpdatesWereModified) {
                    this.trigger("change:plannedParameterUpdates");
                }
                if (this._tempParametersWereModified) {
                    this.trigger("change:tempParameters");
                }

                if (this._parametersWereModified || this._plannedParameterUpdatesWereModified) {
                    this.trigger("change:parametersOrPlannedParameterUpdates");
                    this.trigger("change");
                }
                this._parametersWereModified = false;
                this._plannedParameterUpdatesWereModified = false;
                this._tempParametersWereModified = false;
                this.changed = null;
            },
        });
    });
}, Logger);
