# Source: core/serialization/serialization.js

    /**
     * @module montage/serialization/serialization
     * @requires montage/core
     */

    var Montage = require("../core").Montage,
        MontageLabeler = require("./serializer/montage-labeler").MontageLabeler,
        MontageReviver = require("./deserializer/montage-reviver").MontageReviver,
        parse = require("frb/parse"),
        stringify = require("frb/stringify");

    /**
     * @class Serialization
     * @extends Montage
     */
    var Serialization = Montage.specialize( /** @lends Serialization.prototype # */ {
        _serializationString: {value: null},
        _serializationObject: {value: null},
        _serializationLabels: {value: null},

        initWithString: {
            value: function (string) {
                this._serializationString = string;
                this._serializationObject = null;
                this._serializationLabels = null;

                return this;
            }
        },

        initWithObject: {
            value: function (object) {
                this._serializationString = null;
                this._serializationObject = object;
                this._serializationLabels = null;

                return this;
            }
        },

        clone: {
            value: function () {
                var serialization = new Serialization();

                serialization.initWithString(this.getSerializationString());

                return serialization;
            }
        },

        getSerializationObject: {
            value: function () {
                if (!this._serializationObject) {
                    this._serializationObject = JSON.parse(this._serializationString);
                }

                return this._serializationObject;
            }
        },

        getSerializationString: {
            value: function () {
                if (!this._serializationString) {
                    this._serializationString = JSON.stringify(this._serializationObject);
                }

                return this._serializationString;
            }
        },

        getSerializationLabels: {
            value: function () {
                var serializationObject;

                if (!this._serializationLabels) {
                    if ((serializationObject = this.getSerializationObject())) {
                        this._serializationLabels = Object.keys(serializationObject);
                    }
                }

                return this._serializationLabels;
            }
        },

        getExternalObjectLabels: {
            value: function () {
                var serializationObject = this.getSerializationObject(),
                    labels = [];

                for (var label in serializationObject) {
                    if (Object.keys(serializationObject[label]).length === 0) {
                        labels.push(label);
                    }
                }

                return labels;
            }
        },

        hasSerializationLabel: {
            value: function (label) {
                return label in this.getSerializationObject();
            }
        },

        isExternalObject: {
            value: function (label) {
                var serializationObject = this.getSerializationObject();

                if (serializationObject && label in serializationObject) {
                    return Object.keys(serializationObject[label]).length === 0;
                } else {
                    return false;
                }
            }
        },

        isAlias: {
            value: function (label) {
                var serializationObject = this.getSerializationObject();

                if (serializationObject && label in serializationObject) {
                    return "alias" in serializationObject[label];
                } else {
                    return false;
                }
            }
        },

        getElementId: {
            value: function (label) {
                var object = this.getSerializationObject();

                // TODO: much faster than using the visitor, need to make the visitor
                // faster.
                var element = Montage.getPath.call(object, label + ".values.element");

                if (!element) {
                    // .properties deprecated
                    element = Montage.getPath.call(object, label + ".properties.element");
                }

                if (element) {
                    return element["#"];
                }
            }
        },

        getSerializationLabelsWithElements: {
            value: function (elementIds) {
                var inspector = new exports.SerializationInspector(),
                    labels = [];

                inspector.initWithSerialization(this);
                inspector.visitSerialization(function (node) {
                    // Check if this is one of the elements we're looking for
                    if (node.type === "Element" && elementIds.indexOf(node.data) >= 0) {
                        // Check if it's inside a "values" block
                        node = node.parent;
                        // .properties deprecated
                        if (node && (node.name === "values" || node.name === "properties")) {
                            // Check if it's in a montage object
                            node = node.parent;
                            if (node && node.type === "montageObject") {
                                labels.push(node.label);
                            }
                        }
                    }
                });

                return labels;
            }
        },

        renameElementReferences: {
            value: function (elementsTable) {
                var inspector = new exports.SerializationInspector();

                inspector.initWithSerialization(this);
                inspector.visitSerialization(function (node) {
                    if (node.type === "Element" && node.data in elementsTable) {
                        node.data = elementsTable[node.data];
                    }
                });
            }
        },

        renameSerializationLabels: {
            value: function (labelsTable) {
                var inspector = new exports.SerializationInspector();

                inspector.initWithSerialization(this);
                inspector.visitSerialization(function (node) {
                    if (node.label) {
                        var label = node.label;

                        if (label in labelsTable) {
                            node.label = labelsTable[label];
                        }
                    }
                    if (node.type === "reference") {
                        var reference = node.data;

                        if (reference in labelsTable) {
                            node.data = labelsTable[reference];
                        }
                    }
                });
            }
        },

        mergeSerialization: {
            value: function (serialization, delegate) {
                return exports.SerializationMerger.mergeSerializations(this, serialization, delegate);
            }
        },

        extractSerialization: {
            value: function (labels, externalLabels) {
                var extractor = new exports.SerializationExtractor();

                extractor.initWithSerialization(this);
                return extractor.extractSerialization(labels, externalLabels);
            }
        }
    });

    /**
     * @class SerializationMerger
     * @extends Montage
     */
    var SerializationMerger = Montage.specialize(null, /** @lends SerializationMerger.prototype */ {
        /**
         * This delegate method is called when merging an object from serialization2
         * into serialization1. It allows the delegate to change how the object is
         * going to be merged by saying that the object already exists in
         * serialization1 under a different or the same label.
         *
         * When the delegate method doesn't return a string then the default
         * behavior is to add a new object to serialization1. If the object's label
         * collides with another label in serialization1 then a new label is
         * generated and used.
         *
         * By returning a label that exists in serialization1 from the delegate
         * method all references to the object being merged will change to point
         * to the object from serialization1 instead and the object will not be
         * merged.
         * By returning a label that does not exist in both serializations the
         * object will be merged into serialization2 with this new label instead.
         *
         * @callback delegateWillMergeObjectWithLabel
         * @param {string} label The object label.
         * @param {string} newLabel The new label generated by the collision resolver in case of collision.
         * @returns {string|undefined} the new label for this object.
         */

        /**
         * Merges serialization2 into serialization1.
         *
         * @param {Serialization} serialization1 - The serialization to be merged and end result of the merge operation.
         * @param {Serialization} serialization2 - The serialization to be merged.
         * @param {{willMergeObjectWithLabel: delegateWillMergeObjectWithLabel}} delegate
         * - The delegate to override the default behavior.
         *
         * @returns {Object} The collision table with the new labels generated for label clashes
         */
        mergeSerializations: {
            value: function (serialization1, serialization2, delegate) {
                var serializationObject1,
                    serializationObject2,
                    labels1,
                    labels2,
                    collisionTable = {},
                    foundCollisions,
                    hasCollisionTableChanged;

                labels1 = serialization1.getSerializationLabels();
                labels2 = serialization2.getSerializationLabels();

                // Check for name collisions and generate new labels
                foundCollisions = this._createCollisionTable(labels1, labels2, collisionTable, delegate && delegate.labeler);

                if (delegate && delegate.willMergeObjectWithLabel) {
                    hasCollisionTableChanged = this._willMergeObjectWithLabel(delegate, serialization1, serialization2, collisionTable);
                    foundCollisions = foundCollisions || hasCollisionTableChanged;
                }

                // Replace the labels with the new, non-colliding, ones
                if (foundCollisions) {
                    // Clone serialization2 because we don't want to modify it.
                    serialization2 = serialization2.clone();
                    serialization2.renameSerializationLabels(collisionTable);
                    labels2 = serialization2.getSerializationLabels();
                }

                // Merge the two serializations without the fear of name clashing
                serializationObject1 = serialization1.getSerializationObject();
                serializationObject2 = serialization2.getSerializationObject();

                for (var i = 0, label; (label = labels2[i]); i++) {
                    // If this label already exists in serialization1 don't merge
                    // it as it means they are the same object.
                    if (labels1.indexOf(label) === -1) {
                        serializationObject1[label] = serializationObject2[label];
                    }
                }

                serialization1.initWithObject(serializationObject1);

                return collisionTable;
            }
        },

        /**
         * @private
         * @function
         */
        _willMergeObjectWithLabel: {
            value: function (delegate, serialization1, serialization2, collisionTable) {
                var newLabel,
                    collisionLabel,
                    collisionLabels,
                    inDestination,
                    renameLabel,
                    hasCollisionTableChanged = false,
                    labels2 = serialization2.getSerializationLabels();

                if (collisionTable) {
                    collisionLabels = [];
                    Object.keys(collisionTable).forEach(function (label) {
                        collisionLabels.push(collisionTable[label]);
                    });
                }

                for (var i = 0, label; (label = labels2[i]); i++) {
                    collisionLabel = collisionTable && collisionTable[label];
                    newLabel = delegate.willMergeObjectWithLabel(label, collisionLabel);

                    if (typeof newLabel === "string") {
                        // If the delegate returns a new label there are two
                        // possible interpretations:
                        // 1) The label is on the destination serialization so it
                        //    means that we don't need to move this object there
                        //    because it is already there under a different label.
                        //    We just need to update the references to point to the
                        //    right name.
                        // 2) The label doesn't exist anywhere so it means we just
                        //    need to move the object under this new different
                        //    label.
                        inDestination = this._isLabelValidInSerialization(
                            newLabel, serialization1);
                        if (!inDestination) {
                            renameLabel = !this._isLabelValidInSerialization(newLabel, serialization2) &&
                                            collisionLabels.indexOf(newLabel) === -1;
                        }

                        if (inDestination || renameLabel) {
                            hasCollisionTableChanged = true;
                            collisionTable[label] = newLabel;
                        } else {
                            throw new Error("willMergeObjectWithLabel either needs to return a label that exists in the destination serialization to indicate it's the same object or return a completely new label to rename the object being merged. \"" + newLabel + "\"  destination: " + serialization1.getSerializationString() + "\n source: " + serialization2.getSerializationString() + "\n collision table: " + JSON.stringify(collisionTable, null, 4));
                        }
                    }
                }

                return hasCollisionTableChanged;
            }
        },

        /**
         * This function returns true when the label is part of the serialization,
         * or, if a template property, it refers to a component label that is part
         * of the serialization.
         *
         * @private
         */
        _isLabelValidInSerialization: {
            value: function (label, serialization) {
                var componentLabel,
                    ix;

                if (serialization.hasSerializationLabel(label)) {
                    return true;
                } else {
                    ix = label.indexOf(":");
                    // It's a template property, if the component part is
                    // in the serialization then it's a valid label too.
                    if (ix > 0) {
                        componentLabel = label.slice(0, ix);
                        if (serialization.hasSerializationLabel(componentLabel)) {
                            return true;
                        }
                    }
                }

                return false;
            }
        },

        /**
         * This function creates a collision table between labels1 and labels2.
         * The collision table offers renames for the labels in the labels2 array
         * that already exist in the labels1 array.
         *
         * This function knows how to deal with labels that refer to template
         * values. A label for a template property has the following syntax:
         * <component label>:<label>.
         * The collision table guarantees that template values' labels will
         * always be in sync with their corresponding component label.
         *
         * When a collision exist with a label for a template property the
         * collision is solved by creating a new label for the component and
         * adopting that new label: <new component label>:<label>. In this
         * case the component label will also be part of the resulting collision
         * table even if there was no original collision in labels1.
         *
         * @example
         * labels1: ["repetition:iteration"]
         * labels2: ["repetition", "repetition:iteration"]
         * collisionTable: {"repetition:iteration": "object:iteration",
         *                  "repetition": "object"}
         *
         * @private
         */
        _createCollisionTable: {
            value: function (labels1, labels2, collisionTable, labeler) {

                labeler = labeler || new MontageLabeler();
                var foundCollisions = false,
                    componentLabel,
                    labels1Index = Object.create(null),
                    newLabel,
                    label,
                    ix, i;

                for (i = 0; i < labels1.length; i++) {
                    label = labels1[i];

                    // If this label is a property template then we need to register
                    // the component name too, it could be that it's not present
                    // on labels1. We want to avoid the possibility of generating
                    // a label that conflicts with the component part of the template
                    // property.
                    ix = label.indexOf(":");
                    if (ix > 0) {
                        componentLabel = label.slice(0, ix);
                        labeler.addLabel(componentLabel);
                        labels1Index[componentLabel] = 1;
                    }
                    labeler.addLabel(label);
                    labels1Index[label] = 1;
                }

                for (i = 0; (label = labels2[i]); i++) {
                    // If the label is a template property then check to see if
                    // the component label has been renamed already or if the entire
                    // label or component label have a collision to solve.
                    ix = label.indexOf(":");
                    if (ix > 0) {
                        componentLabel = label.slice(0, ix);
                        newLabel = collisionTable[componentLabel];

                        if (newLabel) {
                            collisionTable[label] = newLabel + ":" + label.slice(ix+1);
                            foundCollisions = true;
                        } else if (componentLabel in labels1Index) {
                            // Renaming a label that is a property template is
                            // the same as renaming the component part of the
                            // label.
                            newLabel = labeler.generateLabel(labeler.getLabelBaseName(componentLabel));
                            // Rename the component label too if it exists.
                            if (labels2.indexOf(componentLabel) >= 0) {
                                collisionTable[componentLabel] = newLabel;
                            }
                            collisionTable[label] = newLabel + label.slice(ix);
                            foundCollisions = true;
                        } else {
                            labeler.addLabel(componentLabel);
                        }
                    }
                    // Also check if the label already has a new label, this can
                    // happen if a template property on that component was renamed.
                    else if (label in labels1Index && !(label in collisionTable)) {
                        collisionTable[label] = labeler.generateLabel(labeler.getLabelBaseName(label));
                        foundCollisions = true;
                    } else {
                        labeler.addLabel(label);
                    }
                }

                return foundCollisions;
            }
        }
    });

    /**
     * @class SerializationInspector
     * @extends Montage
     */
    var SerializationInspector = Montage.specialize(/** @lends SerializationInspector.prototype # */ {
        initWithSerialization: {
            value: function (serialization) {
                this._serialization = serialization;
            }
        },

        visitSerialization: {
            value: function (visitor) {
                var serialization = this._serialization.getSerializationObject();

                this._walkRootObjects(visitor, serialization);
                this._serialization.initWithObject(serialization);
            }
        },

        visitSerializationObject: {
            value: function (label, visitor) {
                var serialization = this._serialization.getSerializationObject();

                if (label in serialization) {
                    this._walkRootObject(visitor, serialization, label);
                    this._serialization.initWithObject(serialization);
                } else {
                    throw new Error('Object "' + label + '" does not exist in ' + this._serialization.getSerializationString());
                }
            }
        },

        changeLabel: {
            value: function (oldLabel, newLabel) {
                var serialization = this._serialization.getSerializationObject(),
                    object;

                object = serialization[oldLabel];
                delete serialization[oldLabel];
                serialization[newLabel] = object;
            }
        },

        _walkRootObjects: {
            value: function (visitor, objects) {
                /* jshint forin: true */
                for (var label in objects) {
                /* jshint forin: false */
                    this._walkRootObject(visitor, objects, label);
                }
            }
        },

        _walkRootObject: {
            value: function (visitor, objects, label) {
                var object = objects[label];
                /* jshint forin: true */
                if ("value" in object) {
                /* jshint forin: false */
                    this._walkObject(visitor, object, "value", label);
                } else {
                    this._walkCustomObject(visitor, objects, label);
                }
            }
        },

        /**
         * @private
         * @param parentObject {Object} The parent object of the object to walk
         * @param key {string} The key of the object in the parent object
         * @param label {string} Optional label for when the object has no parent
         * @param parent {Object} The representation of the object's parent
         */
        _walkObject: {
            value: function (visitor, parentObject, key, label, parent) {
                var object = parentObject[key],
                    type = MontageReviver.getTypeOf(object),
                    value;

                // Create the value representing this object in the serialization.
                value = {
                    type: type
                };
                if (label) {
                    value.label = label;
                } else {
                    value.name = key;
                }
                if (parent) {
                    value.parent = parent;
                }

                // Visit the value
                if (type === "number" || type === "string" || type === "null") {
                    value.data = object;
                    visitor(value);
                    parentObject[key] = value.data;

                } else if (type === "regexp") {
                    value.data = object["/"];
                    visitor(value);
                    object["/"] = value.data;

                } else if (type === "reference") {
                    value.data = object["@"];
                    visitor(value);
                    object["@"] = value.data;

                } else if (type === "Element") {
                    value.data = object["#"];
                    visitor(value);
                    object["#"] = value.data;

                } else if (type === "array") {
                    value.data = object;
                    visitor(value);
                    parentObject[key] = object = value.data;

                    for (var i = 0, ii = object.length; i < ii; i++) {
                        this._walkObject(visitor, object, ""+i, null, value);
                    }

                } else if (type === "binding") {
                    this._walkBinding(visitor, parentObject, key, value);

                } else if (type === "object") {
                    value.data = object;
                    visitor(value);
                    parentObject[key] = object = value.data;

                    /* jshint forin: true */
                    for (var prop in object) {
                    /* jshint forin: false */
                        this._walkObject(visitor, object, prop, null, value);
                    }
                }

                if (label !== null && label !== undefined && label !== value && value.label !== label) {
                    this.changeLabel(label, value.label);
                }
            }
        },

        _walkCustomObject: {
            value: function (visitor, objects, label) {
                var object = objects[label],
                    value;

                value = {
                    type: "montageObject",
                    label: label,
                    data: object
                };

                visitor(value);
                objects[label] = object = value.data;
                if (value.label !== label) {
                    this.changeLabel(label, value.label);
                }

                if (object.values) {
                    this._walkObject(visitor, object, "values", null, value);
                } else if (object.properties) { // deprecated
                    this._walkObject(visitor, object, "properties", null, value);
                }
                if (object.bindings) { // deprecated
                    this._walkBindings(visitor, object, null, value);
                }
                if (object.listeners) {
                    this._walkObject(visitor, object, "listeners", null, value);
                }
                if (object.localizations) {
                    this._walkLocalizations(visitor, object, null, value);
                }
            }
        },

        _walkBindings: {
            value: function (visitor, parentObject, parent) {
                var object = parentObject.bindings,
                    value;

                value = {
                    type: "bindings",
                    data: object,
                    parent: parent
                };

                visitor(value);
                parentObject.bindings = object = value.data;

                /* jshint forin: true */
                for (var key in object) {
                /* jshint forin: false */
                    this._walkBinding(visitor, object, key, value);
                }
            }
        },

        _walkBinding: {
            value: function (visitor, parentObject, key, parent) {
                var object = parentObject[key],
                    value;

                value = {
                    type: "binding",
                    name: key,
                    data: object,
                    parent: parent
                };

                visitor(value);
                parentObject[key] = object = value.data;

                this._walkBindingData(visitor, object, value);
            }
        },

        _walkBindingData: {
            value: function (visitor, object, parent) {
                var sourcePath,
                    parseTree,
                    modified = false;

                sourcePath = object["<-"] || object["<->"];
                parseTree = Object.clone(parse(sourcePath));
                this._walksBindingReferences(parseTree, function (syntax) {
                    var value = {
                        type: "reference",
                        data: syntax.label
                    };
                    visitor(value);
                    if (syntax.label !== value.data) {
                        syntax.label = value.data;
                        modified = true;
                    }
                });

                if (modified) {
                    if ("<-" in object) {
                        object["<-"] = stringify(parseTree);
                    } else {
                        object["<->"] = stringify(parseTree);
                    }
                }

                if (object.converter) {
                    this._walkObject(visitor, object, "converter", null, parent);
                }
            }
        },

        _walkLocalizations: {
            value: function (visitor, parentObject, parent) {
                var object = parentObject.localizations,
                    value;

                value = {
                    type: "localizations",
                    data: object,
                    parent: parent
                };

                visitor(value);
                parentObject.localizations = object = value.data;

                /* jshint forin: true */
                for (var key in object) {
                /* jshint forin: false */
                    this._walkLocalization(visitor, object, key, value);
                }
            }
        },

        _walkLocalization: {
            value: function (visitor, parentObject, key, parent) {
                var object = parentObject[key],
                    value,
                    data;

                value = {
                    type: "localization",
                    name: key,
                    data: object,
                    parent: parent
                };

                visitor(value);
                parentObject[key] = object = value.data;

                if (typeof object.key === "object") {
                    this._walkBindingData(visitor, object.key, value);
                }

                if (typeof object.default === "object") {
                    this._walkBindingData(visitor, object.default, value);
                }

                if (typeof object.data === "object") {
                    data = object.data;

                    /* jshint forin: true */
                    for (var prop in data) {
                    /* jshint forin: false */
                        this._walkBindingData(visitor, data[prop], value);
                    }
                }
            }
        },

        /**
         * Visits all object references made in the binding parsing tree
         * @private
         */
        _walksBindingReferences: {
            value: function (parseTree, visitor) {
                var args = parseTree.args;

                if (parseTree.type === "component") {
                    visitor(parseTree);
                }

                if (args) {
                    for (var i = 0, ii = args.length; i < ii; i++) {
                        this._walksBindingReferences(args[i], visitor);
                    }
                }
            }
        }
    });

    /**
     * @class SerializationExtractor
     * @extends Montage
     */
    var SerializationExtractor = Montage.specialize( /** @lends SerializationExtractor.prototype # */ {
        /**
         * @private
         */
        _serialization: {value: null},

        /**
         * @function
         */
        initWithSerialization: {
            value: function (serialization) {
                this._serialization = serialization;
            }
        },

        /**
         * Creates a new serialization with the labels given.
         * @function
         */
        extractSerialization: {
            value: function (labels, externalLabels) {

                var i, label,
                    inspector = new exports.SerializationInspector(),
                    serializationObject,
                    objects = {},
                    references = [];

                serializationObject = this._serialization.getSerializationObject();
                inspector.initWithSerialization(this._serialization);

                for (i = 0, label; (label = labels[i]); i++) {
                    objects[label] = serializationObject[label];

                    inspector.visitSerializationObject(label, function (node) { // jshint ignore:line
                        var label;

                        if (node.type === "reference") {
                            label = node.data;
                            // We don't process template values here, meaning
                            // that if we have "table" and a reference like
                            // "@table:cell" the latter will be considered an
                            // external reference even though the component is in
                            // scope.
                            // We do this on purpose because it allow us to process
                            // all template values of the serialization without
                            // having to walk the entire serialization tree looking
                            // for them.
                            // If for some reason we need to "correct" this behavior
                            // then we also need to change the way we resolve
                            // template values' alias in
                            // Template.expandParameters.
                            // Instead of relying on willMergeObjectWithLabel we
                            // need to walk the serialization looking for these.
                            if (references.indexOf(label) === -1 &&
                                labels.indexOf(label) === -1) {
                                references.push(label);
                            }
                        }
                    });
                }

                if (externalLabels) {
                    for (i = 0, label; (label = externalLabels[i]); i++) {
                        // Make sure we don't add objects that are not part of the
                        // serialization we're extracting from.
                        // If the same label is defined in both labels and
                        // externalLabels then labels takes precedence.
                        if (label in serializationObject && !(label in objects)) {
                            objects[label] = {};
                        }
                    }
                }

                for (i = 0, label; (label = references[i]); i++) {
                    objects[label] = {};
                }

                return new Serialization().initWithObject(objects);
            }
        }
    });

    exports.Serialization = Serialization;
    exports.SerializationMerger = SerializationMerger;
    exports.SerializationInspector = SerializationInspector;
    exports.SerializationExtractor = SerializationExtractor;