# Source: core/serialization/deserializer/montage-deserializer.js

    var Montage = require("../../core").Montage,
        MontageContext = require("./montage-interpreter").MontageContext,
        MontageReviver = require("./montage-reviver").MontageReviver,
        Map = require("collections/map").Map,
        deprecate = require("../../deprecate");

    var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({

        _serializationString: {
            value: null
        },

        _serialization: {
            value: null
        },

        serialization: {
            value: {
                get: function () {
                    return this._serialization;
                }
            }
        },

        init: {
            value: function (serialization, _require, objectRequires, locationId) {
                if (typeof serialization === "string") {
                    this._serializationString = serialization;
                } else {
                    this._serializationString = JSON.stringify(serialization);
                }
                this._require = _require;
                this._locationId = locationId;
                this._reviver = new MontageReviver().init(_require, objectRequires, this.constructor);

                return this;
            }
        },

        /**
         * @param {Object} instances Map-like object of external user objects to
         * link against the serialization.
         * @param {Element} element The root element to resolve element references
         * against.
         * @return {Promise}
         */
        deserialize: {
            value: function (instances, element) {
                var context = this._locationId && MontageDeserializer.moduleContexts.get(this._locationId);
                if (context) {
                    if (context._objects.root) {
                        return Promise.resolve(context._objects);
                    } else {
                        return Promise.reject(new Error(
                            "Unable to deserialize because a circular dependency was detected. " +
                            "Module \"" + this._locationId + "\" has already been loaded but " +
                            "its root could not be resolved."
                        ));
                    }
                }

                try {
                    var serialization = JSON.parse(this._serializationString);
                    context = new MontageContext()
                        .init(serialization, this._reviver, instances, element, this._require);
                    if (this._locationId) {
                        MontageDeserializer.moduleContexts.set(this._locationId, context);
                    }
                    return context.getObjects();
                } catch (ex) {
                    return this._formatSerializationSyntaxError(this._serializationString);
                }
            }
        },

        deserializeObject: {
            value: function(objects) {
                return this.deserialize(objects).then(function(objects) {
                    return objects.root;
                });
            }
        },

        preloadModules: {
            value: function () {
                var serialization = JSON.parse(this._serializationString),
                    reviver = this._reviver,
                    moduleLoader = reviver.moduleLoader,
                    i,
                    labels,
                    label,
                    object,
                    locationId,
                    locationDesc,
                    module,
                    promises = [];

                if (serialization !== null) {
                    labels = Object.keys(serialization);
                    for (i = 0; (label = labels[i]); ++i) {
                        object = serialization[label];
                        locationId = object.prototype || object.object;

                        if (locationId) {
                            if (typeof locationId !== "string") {
                                throw new Error(
                                    "Property 'object' of the object with the label '" +
                                    label + "' must be a module id"
                                );
                            }
                            locationDesc = MontageReviver.parseObjectLocationId(locationId);
                            module = moduleLoader.getModule(locationDesc.moduleId, label);
                            if (Promise.is(module)) {
                                promises.push(module);
                            }
                        }
                    }
                }

                if (promises.length > 0) {
                    return Promise.all(promises);
                }
            }
        },

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

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

                return labels;
            }
        },

        _formatSerializationSyntaxError: {
            value: function (source) {
                var gutterPadding = "   ",
                    origin = this._origin,
                    message,
                    error,
                    lines,
                    gutterSize,
                    line;

                return require.async("jshint/dist/jshint").then(function (module) {
                    if (!module.JSHINT(source)) {
                        error = module.JSHINT.errors[0];
                        lines = source.split("\n");
                        gutterSize = (gutterPadding + lines.length).length;
                        line = error.line - 1;

                        for (var i = 0, l = lines.length; i < l; i++) {
                            lines[i] = (new Array(gutterSize - (i + 1 + "").length + 1)).join(i === line ? ">" : " ") +
                                (i + 1) + " " + lines[i];
                        }
                        message = "Syntax error at line " + error.line +
                            (origin ? " from " + origin : "") + ":\n" +
                            error.evidence + "\n" + error.reason + "\n" +
                            lines.join("\n");
                    } else {
                        message = "Syntax error in the serialization but not able to find it!\n" + source;
                    }

                    throw new Error(message);
                });
            }
        },

        // Deprecated methods

        initWithObject: {
            value: deprecate.deprecateMethod(void 0, function (serialization, _require, objectRequires, locationId, moduleContexts) {
                return this.init(serialization, _require, objectRequires, locationId, moduleContexts);
            }, "initWithObject", "init")
        },

        initWithObjectAndRequire: {
            value: deprecate.deprecateMethod(void 0, function (serialization, _require, objectRequires) {
                return this.init(serialization, _require, objectRequires);
            }, "initWithObjectAndRequire", "init")
        }

    }, {
        // Adapted from mr/sandbox
        getModuleRequire: {
            value: function (parentRequire, moduleId) {
                var topId = parentRequire.resolve(moduleId);
                var module = parentRequire.getModuleDescriptor(topId);

                while (module.redirect || module.mappingRedirect) {
                    if (module.redirect) {
                        topId = module.redirect;
                    } else {
                        parentRequire = module.mappingRequire;
                        topId = module.mappingRedirect;
                    }
                    module = parentRequire.getModuleDescriptor(topId);
                }

                return module.require;
            }
        },

        _cache: {
            value: null
        },

        moduleContexts: {
            get: function () {
                if (!this._cache) {
                    this._cache = new Map();
                }
                return this._cache;
            }
        }
    });

    MontageDeserializer.defineDeserializationUnit = function (name, funktion) {
        MontageReviver.defineUnitReviver(name, funktion);
    };

    exports.deserialize = function (serializationString, _require) {
        return new MontageDeserializer().init(serializationString, _require).deserializeObject();
    };