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

    /*global console, Proxy */
    var Montage = require("../../core").Montage,
        ValuesDeserializer = require("./values-deserializer").ValuesDeserializer,
        SelfDeserializer = require("./self-deserializer").SelfDeserializer,
        UnitDeserializer = require("./unit-deserializer").UnitDeserializer,
        ModuleReference = require("../../module-reference").ModuleReference,
        Alias = require("../alias").Alias, Bindings = require("../bindings"),
        Promise = require("../../promise").Promise,
        deprecate = require("../../deprecate"),
        camelCaseConverter = require('../../converter/camel-case-converter').singleton,
        kebabCaseConverter = require('../../converter/kebab-case-converter').singleton,
        ONE_ASSIGNMENT = "=",
        ONE_WAY = "<-",
        TWO_WAY = "<->";

    require("../../shim/string");

    var PROXY_ELEMENT_MAP = new WeakMap();
    var DATA_ATTRIBUTES_MAP = new Map();

    var ModuleLoader = Montage.specialize({

        _require: {
            value: null
        },

        _objectRequires: {
            value: null
        },

        init: {
            value: function (_require, objectRequires) {
                if (typeof _require !== "function") {
                    throw new Error("Function 'require' missing.");
                }
                if (typeof _require.location !== "string") {
                    throw new Error("Function 'require' location is missing");
                }
                if (typeof objectRequires !== "object" &&
                    typeof objectRequires !== "undefined") {
                    throw new Error("Parameter 'objectRequires' should be an object.");
                }

                this._require = _require;
                this._objectRequires = objectRequires;

                return this;
            }
        },

        getExports: {
            value: function (_require, moduleId) {
                var module = _require.getModuleDescriptor(_require.resolve(moduleId));

                while (module.redirect !== void 0) {
                    module = _require.getModuleDescriptor(module.redirect);
                }

                if (module.mappingRedirect !== void 0) {
                    return this.getExports(module.mappingRequire, module.mappingRedirect);
                }

                return module.exports;
            }
        },

        getModule: {
            value: function (moduleId, label) {
                var objectRequires = this._objectRequires,
                    _require,
                    module;

                if (objectRequires && label in objectRequires) {
                    _require = objectRequires[label];
                } else {
                    _require = this._require;
                }

                module = this.getExports(_require, moduleId);

                if (!module) {
                    module = _require.async(moduleId);
                }

                return module;
            }
        }
    });

    /**
     * @class MontageReviver
     */
    var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends MontageReviver# */ {

        moduleLoader: {
            value: null
        },

        /**
         * @param {Require} _require The require object to load modules
         * @param {Object} objectRequires A dictionary indexed by object label with
         *        the require object to use for a specific object of the
         *        serialization.
         * @param {Function} deserializerConstructor Function to create a new
         *        deserializer. Useful for linking to an external module
         *        that also needs to be deserialized.
         */
        init: {
            value: function (_require, objectRequires, deserializerConstructor) {
                this.moduleLoader = new ModuleLoader().init(_require, objectRequires);
                this._require = _require;
                this._deserializerConstructor = deserializerConstructor;
                return this;
            }
        },

        getTypeOf: {
            value: function (value) {
                var typeOf = typeof value;

                if (value === null) {
                    return "null";
                } else if (Array.isArray(value)) {
                    return "array";
                } else if (typeOf === "object" && Object.keys(value).length === 1) {
                    if ("@" in value) {
                        return "reference";
                    } else if ("/" in value) {
                        return "regexp";
                    } else if ("#" in value) {
                        return "Element";
                    } else if ("%" in value) {
                        return "Module";
                    } else if (ONE_WAY in value || TWO_WAY in value || ONE_ASSIGNMENT in value) {
                        return "binding";
                    } // else return typeOf -> object
                }

                return typeOf;
            }
        },

        _checkLabel: {
            value: function (label, isTemplateProperty) {
                if (isTemplateProperty && label[0] !== ":") {
                    return new Error("Aliases can only be defined in template values (start with a colon (:)), \"" + label + "\".");
                } else if (!isTemplateProperty && label[0] === ":") {
                    return new Error("Only aliases are allowed as template values (start with a colon (:), \"" + label + "\".");
                }
            }
        },

        setProxyForDatasetOnElement: {
            value: function (element, montageObjectDesc) {
                var originalDataset = element.dataset;

                if (Object.getPrototypeOf(originalDataset) !== null) {
                    var datasetAttributes = Object.keys(originalDataset),
                        targetObject = Object.create(null), self = this,
                        datasetAttribute, propertyNames;

                    if (Proxy.prototype) { // The native Proxy has no prototype property.
                        // Workaround for Proxy polyfill https://github.com/GoogleChrome/proxy-polyfill
                        // the properties of a proxy must be known at creation time.
                        // TODO: remove when we drop the support of IE11.
                        if (montageObjectDesc.values) {
                            propertyNames = Object.keys(montageObjectDesc.values);
                        } else { // deprecated
                            propertyNames = Object.keys(montageObjectDesc.properties)
                                .concat(Object.keys(montageObjectDesc.bindings));
                        }

                        datasetAttributes = datasetAttributes.concat(
                            propertyNames.filter(function (propertyName) {
                                return propertyName.startsWith("dataset.");
                            })
                        );

                        for (var i = 0, length = datasetAttributes.length; i < length; i++) {
                            datasetAttribute = datasetAttributes[i];
                            if (originalDataset[datasetAttribute]) {
                                targetObject[datasetAttribute] =
                                    originalDataset[datasetAttribute];
                            } else {
                                targetObject[datasetAttribute.replace(/^dataset\./, '')] = void 0;
                            }
                        }
                    }

                    Object.defineProperty(element, "dataset", {
                        value: new Proxy(targetObject, {
                            set: function (target, propertyName, value) {
                                target[propertyName] = value;
                                originalDataset[propertyName] = value;
                                element.nativeSetAttribute(
                                    DATA_ATTRIBUTES_MAP.get(propertyName) ||
                                    (DATA_ATTRIBUTES_MAP.set(
                                        propertyName,
                                        'data-' +
                                        kebabCaseConverter.convert(propertyName)
                                    )).get(propertyName),
                                    value
                                );
                                return true;
                            },
                            get: function (target, propertyName) {
                                return target[propertyName];
                            }
                        })
                    });
                }
            }
        },

        setProxyOnElement: {
            value: function (element, montageObjectDesc) {
                if (!PROXY_ELEMENT_MAP.has(element)) {
                    var targetObject = Object.create(null);

                    if (Proxy.prototype) { // The native Proxy has no prototype property.
                        // Workaround for Proxy polyfill https://github.com/GoogleChrome/proxy-polyfill
                        // the properties of a proxy must be known at creation time.
                        // TODO: remove when we drop the support of IE11.
                        var propertyNames, propertyName;

                        for (propertyName in element) {
                            if (element.hasOwnProperty(propertyName)) {
                                targetObject[propertyName] = void 0;
                            }
                        }

                        if (montageObjectDesc.values) {
                            propertyNames = Object.keys(montageObjectDesc.values);
                        } else { // deprecated
                            propertyNames = Object.keys(montageObjectDesc.properties)
                                .concat(Object.keys(montageObjectDesc.bindings));
                        }

                        for (var i = 0, length = propertyNames.length; i < length; i++) {
                            propertyName = propertyNames[i];
                            if (!(propertyName in element) && propertyName.indexOf('.') === -1) {
                                targetObject[propertyName] = void 0;
                            }
                        }
                    }

                    PROXY_ELEMENT_MAP.set(element, new Proxy(targetObject, {
                        set: function (target, propertyName, value) {
                            if (!(propertyName in Object.getPrototypeOf(element))) {
                                if (Object.getOwnPropertyDescriptor(element, propertyName) === void 0) {
                                    Object.defineProperty(element, propertyName, {
                                        set: function (value) {
                                            target[propertyName] = value;

                                            if (value === null || value === void 0) {
                                                element.removeAttribute(propertyName);
                                            } else {
                                                element.nativeSetAttribute(propertyName, value);
                                            }
                                        },
                                        get: function () {
                                            return target[propertyName];
                                        }
                                    });
                                }
                            }

                            if (target[propertyName] !== value) {
                                element[propertyName] = value;
                            }

                            return true;
                        },
                        get: function (target, propertyName) {
                            return target[propertyName] || element[propertyName];
                        }
                    }));
                }

                return PROXY_ELEMENT_MAP.get(element);
            }
        },

        wrapSetAttributeForElement: {
            value: function (element) {
                if (element.setAttribute === element.nativeSetAttribute) {
                    var proxyElement = PROXY_ELEMENT_MAP.get(element),
                        self = this;

                    element.setAttribute = function (key, value) {
                        var propertyName;
                        if (key.startsWith('data-')) {
                            propertyName = camelCaseConverter.convert(key.replace('data-', ''));
                            proxyElement.dataset[propertyName] = value;
                        } else {
                            propertyName = camelCaseConverter.convert(key);
                            proxyElement[propertyName] = value;
                        }
                        element.nativeSetAttribute(key, value);
                    };
                }
            }
        },

        reviveRootObject: {
            value: function (value, context, label) {
                var error,
                    object,
                    isAlias = "alias" in value;

                // Only aliases are allowed as template values, everything else
                // should be rejected as an error.
                error = this._checkLabel(label, isAlias);
                if (error) {
                    return Promise.reject(error);
                }

                // Check if the optional "debugger" unit is set for this object
                // and stop the execution. This is intended to provide a certain
                // level of debugging in the serialization.
                if (value.debugger) {
                    debugger; // jshint ignore:line
                }

                if ("value" in value) {
                    // it's overriden by a user object
                    if (context.hasUserObject(label)) {
                        object = context.getUserObject(label);
                        context.setObjectLabel(object, label);
                        return object;
                    }

                    var revivedValue = this.reviveValue(value.value, context, label),
                        valueType = this.getTypeOf(value.value);

                    if (valueType === "Element") {
                        if (!Promise.is(revivedValue)) {
                            var proxyElement = this.setProxyOnElement(revivedValue, value);
                            this.setProxyForDatasetOnElement(revivedValue, value);
                            this.wrapSetAttributeForElement(revivedValue);
                            context.setBindingsToDeserialize(proxyElement, value);
                            this.deserializeMontageObjectValues(
                                proxyElement,
                                value.values || value.properties, //deprecated
                                context
                            );
                        }
                    } else if (valueType === "object") {
                        context.setBindingsToDeserialize(revivedValue, value);
                        this.deserializeMontageObjectValues(
                            revivedValue,
                            value.values || value.properties, //deprecated
                            context
                        );
                        context.setUnitsToDeserialize(revivedValue, value, MontageReviver._unitNames);
                    }

                    return revivedValue;

                } else if (Object.keys(value).length === 0) {
                    // it's an external object
                    if (context.hasUserObject(label)) {
                        object = context.getUserObject(label);
                        context.setObjectLabel(object, label);
                        return object;
                    }

                    return this.reviveExternalObject(value, context, label);
                } else if ("alias" in value) {
                    return this.reviveAlias(value, context, label);
                } else {
                    return this.reviveMontageObject(value, context, label);
                }
            }
        },

        reviveElement: {
            value: function (value, context, label) {
                var elementId = value["#"],
                    element = context.getElementById(elementId);

                if (element) {
                    if (label) {
                        context.setObjectLabel(element, label);
                    }
                    return element;
                } else {
                    return Promise.reject(new Error("Element with id '" + elementId + "' was not found."));
                }
            }
        },

        reviveModule: {
            value: function (value, context, label) {
                var moduleId = value["%"],
                    _require = context.getRequire();

                moduleId = _require.resolve(moduleId);
                var module = _require.getModuleDescriptor(moduleId);

                return new ModuleReference().initWithIdAndRequire(module.id, module.require);
            }
        },

        reviveAlias: {
            value: function (value, context, label) {
                var alias = new Alias();
                alias.value = value.alias;

                context.setObjectLabel(alias, label);
                return alias;
            }
        },

        reviveMontageObject: {
            value: function (value, context, label) {
                var self = this,
                    module,
                    locationDesc,
                    locationId = value.prototype || value.object,
                    objectName;

                if (locationId) {
                    locationDesc = MontageReviver.parseObjectLocationId(locationId);
                    module = this.moduleLoader.getModule(locationDesc.moduleId,
                        label);
                    objectName = locationDesc.objectName;
                }

                if (Promise.is(module)) {
                    return module.then(function (exports) {
                        return self.instantiateObject(exports, locationDesc, value, objectName, context, label);
                    }, function (error) {
                        if (error.stack) {
                            console.error(error.stack);
                        }
                        throw new Error('Error deserializing "' + label +
                            '" when loading module "' + locationDesc.moduleId +
                            "' from '" + value.prototype + "' cause: " + error.message);
                    });
                } else {
                    return this.instantiateObject(module, locationDesc, value, objectName, context, label);
                }
            }
        },

        instantiateObject: {
            value: function (module, locationDesc, value, objectName, context, label) {
                var self = this,
                    moduleId = value.prototype || value.object,
                    object;

                if (moduleId && (moduleId.endsWith(".mjson") || moduleId.endsWith(".meta"))) {
                    return this.getMjsonObject(value, module, moduleId, context)
                        .then(function (object) {
                            context.setObjectLabel(object, label);
                            return self.instantiateMjsonObject(value, object, objectName, context, label);
                        });
                } else {
                    object = this.getMontageObject(value, module, objectName, context, label);
                    context.setObjectLabel(object, label);
                    return this.instantiateMontageObject(value, object, objectName, context, label);
                }
            }
        },

        getMjsonObject: {
            value: function (serialization, json, moduleId, context) {
                var self = this,
                    deserializer = new this._deserializerConstructor().init(
                        json,
                        this._deserializerConstructor.getModuleRequire(this._require, moduleId),
                        void 0,
                        moduleId
                    );
                return Promise.resolve(deserializer)
                    .then(function (deserializer) {
                        return deserializer.deserializeObject();
                    })
                    .then(function (object) {
                        if ("prototype" in serialization) {
                            return Object.create(object);
                        } else {
                            return object;
                        }
                    });
            }
        },

        getMontageObject: {
            value: function (value, module, objectName, context, label) {
                var object;

                if (context.hasUserObject(label)) {

                    return context.getUserObject(label);

                } else if ("prototype" in value) {

                    if (!(objectName in module)) {
                        throw new Error('Error deserializing "' + label +
                            '": object named "' + objectName + '"' +
                            ' was not found in "' + value.prototype + '".' +
                            " Available objects are: " + Object.keys(module) + ".");
                    }
                    // TODO: For now we need this because we need to set
                    // isDeserilizing before calling didCreate.
                    object = module[objectName];
                    object = (typeof object === "function") ? new object() : Object.create(object);
                    object.isDeserializing = true;
                    return object;
                } else if ("object" in value) {
                    if (value.object.endsWith(".json")) {
                        return module;
                    }

                    if (!(objectName in module)) {
                        throw new Error('Error deserializing "' + label +
                            '": object named "' + object +
                            "' was not found given '" + value.object + "'");
                    }
                    return module[objectName];

                } else {
                    throw new Error("Error deserializing " + JSON.stringify(value) + ", might need \"prototype\" or \"object\" on label " + JSON.stringify(label));
                }
            }
        },

        instantiateMjsonObject: {
            value: function (serialization, object, objectName, context, label) {
                var self = this,
                    montageObjectDesc;

                if (object !== null && object !== void 0) {
                    object.isDeserializing = true;
                }

                context.setBindingsToDeserialize(object, serialization);
                montageObjectDesc = this.reviveObjectLiteral(serialization, context);

                if (Promise.is(montageObjectDesc)) {
                    return montageObjectDesc.then(function(montageObjectDesc) {
                        return self.deserializeMontageObject(montageObjectDesc, object, context, label);
                    });
                } else {
                    return this.deserializeMontageObject(montageObjectDesc, object, context, label);
                }
            }
        },

        instantiateMontageObject: {
            value: function (serialization, object, objectName, context, label) {
                var self = this,
                    montageObjectDesc;

                if (object !== null && object !== void 0) {
                    object.isDeserializing = true;
                }

                if (serialization.bindings) {
                    deprecate.deprecationWarningOnce(
                        "'bindings' block is deprecated, use 'values' instead"
                    );
                }

                if (serialization.properties) {
                    deprecate.deprecationWarningOnce(
                        "'properties' block is deprecated, use 'values' instead"
                    );
                }

                context.setBindingsToDeserialize(object, serialization);
                montageObjectDesc = this.reviveObjectLiteral(serialization, context);

                if (Promise.is(montageObjectDesc)) {
                    return montageObjectDesc.then(function(montageObjectDesc) {
                        if (typeof object.deserializeSelf === "function") {
                            return self.deserializeCustomMontageObject(object, montageObjectDesc, context, label);
                        } else {
                            return self.deserializeMontageObject(montageObjectDesc, object, context, label);
                        }
                    });
                } else {
                    if (typeof object.deserializeSelf === "function") {
                        return this.deserializeCustomMontageObject(object, montageObjectDesc, context, label);
                    } else {
                        return this.deserializeMontageObject(montageObjectDesc, object, context, label);
                    }
                }
            }
        },

        deserializeMontageObject: {
            value: function (montageObjectDesc, object, context, label) {
                var values;

                // Units are deserialized after all objects have been revived.
                // This happens at didReviveObjects.
                context.setUnitsToDeserialize(object, montageObjectDesc, MontageReviver._unitNames);
                values = this.deserializeMontageObjectValues(
                    object,
                    montageObjectDesc.values || montageObjectDesc.properties, //deprecated
                    context
                );

                return object;
            }
        },

        deserializeMontageObjectProperties: {
            value: deprecate.deprecateMethod(void 0, function (object, values, context) {
                return this.deserializeMontageObjectValues(object, values, context);
            }, "deserializeMontageObjectProperties", "deserializeMontageObjectValues")
        },

        deserializeMontageObjectValues: {
            value: function (object, values, context) {
                var value;

                if (typeof object.deserializeProperties === "function" || typeof object.deserializeValues === "function") {
                    var valuesDeserializer = new ValuesDeserializer()
                        .initWithReviverAndObjects(this, context);
                    if (object.deserializeValues) {
                        value = object.deserializeValues(valuesDeserializer);
                    } else { // deprecated
                        value = object.deserializeProperties(valuesDeserializer);
                    }
                } else {
                    /* jshint forin: true */
                    for (var key in values) {
                    /* jshint forin: false */
                        object[key] = values[key];
                    }
                }

                return value;
            }
        },

        deserializeCustomMontageObject: {
            value: function (object, objectDesc, context, label) {
                var substituteObject;

                var selfDeserializer = new SelfDeserializer()
                    .initWithObjectAndObjectDescriptorAndContextAndUnitNames(object, objectDesc, context, MontageReviver._unitNames);
                substituteObject = object.deserializeSelf(selfDeserializer);

                if (Promise.is(substituteObject)) {
                    return substituteObject.then(function(substituteObject) {
                        context.setObjectLabel(substituteObject, label);
                        return substituteObject;
                    });
                } else if (typeof substituteObject !== "undefined") {
                    context.setObjectLabel(substituteObject, label);
                    return substituteObject;
                } else {
                    return object;
                }
            }
        },

        didReviveObjects: {
            value: function (objects, context) {
                var self = this;

                return Promise.all([
                    this._deserializeBindings(context),
                    this._deserializeUnits(context)
                ]).then(function () {
                    self._invokeDeserializedFromSerialization(objects, context);
                });
            }
        },

        // TODO: can deserializeSelf make deserializedFromSerialization irrelevant?
        _invokeDeserializedFromSerialization: {
            value: function (objects, context) {
                var object;

                /* jshint forin: true */
                for (var label in objects) {
                /* jshint forin: false */

                    object = objects[label];

                    if (object !== null && object !== void 0) {
                        delete object.isDeserializing;
                    }

                    if (!context.hasUserObject(label)) {
                        // TODO: merge deserializedFromSerialization with
                        //       deserializedFromTemplate?
                        if (object && typeof object.deserializedFromSerialization === "function") {
                            object.deserializedFromSerialization(label);
                        }
                    }
                }
            }
        },

        _deserializeBindings: {
            value: function (context) {
                var bindingsToDeserialize = context.getBindingsToDeserialize(),
                    unitDeserializer = new UnitDeserializer(),
                    bindingsToDeserializeDesc;

                if (bindingsToDeserialize) {
                    try {
                        for (var i = 0, length = bindingsToDeserialize.length; i < length; i++) {
                            bindingsToDeserializeDesc = bindingsToDeserialize[i];
                            Bindings.deserializeObjectBindings(
                                unitDeserializer.initWithContext(context),
                                bindingsToDeserializeDesc.object,
                                bindingsToDeserializeDesc.bindings
                            );
                        }
                    } catch (ex) {
                        return Promise.reject(ex);
                    }
                }
            }
        },

        _deserializeUnits: {
            value: function (context) {
                var unitsToDeserialize = context.getUnitsToDeserialize(),
                    unitDeserializer = new UnitDeserializer(),
                    unitNames;

                try {
                    for (var i = 0, unitsDesc; (unitsDesc = unitsToDeserialize[i]); i++) {
                        unitNames = unitsDesc.unitNames;

                        for (var j = 0, unitName; (unitName = unitNames[j]); j++) {
                            if (unitName in unitsDesc.objectDesc) {
                                unitDeserializer.initWithContext(context);
                                MontageReviver._unitRevivers.get(unitName)(unitDeserializer, unitsDesc.object, unitsDesc.objectDesc[unitName]);
                            }
                        }
                    }
                } catch (ex) {
                    return Promise.reject(ex);
                }
            }
        },

        _createAssignValueFunction: {
            value: function(object, propertyName) {
                return function(value) {
                    object[propertyName] = value;
                };
            }
        },

        getCustomObjectTypeOf: {
            writable: true,
            value: function() {}
        },

        reviveValue: {
            value: function(value, context, label) {
                var type = this.getTypeOf(value);

                if (type === "string" || type === "number" || type === "boolean" || type === "null" || type === "undefined") {
                    return this.reviveNativeValue(value, context, label);
                } else if (type === "regexp") {
                    return this.reviveRegExp(value, context, label);
                } else if (type === "reference") {
                    return this.reviveObjectReference(value, context, label);
                } else if (type === "array") {
                    return this.reviveArray(value, context, label);
                } else if (type === "object") {
                    return this.reviveObjectLiteral(value, context, label);
                } else if (type === "Element") {
                    return this.reviveElement(value, context, label);
                } else if (type === "binding") {
                    return value;
                } else {
                    return this._callReviveMethod("revive" + type, value, context, label);
                }
            }
        },

        reviveNativeValue: {
            value: function(value, context, label) {
                if (label) {
                    context.setObjectLabel(value, label);
                }

                return value;
            }
        },

        reviveObjectLiteral: {
            value: function(value, context, label) {
                var item,
                    promises = [];

                if (label) {
                    context.setObjectLabel(value, label);
                }

                for (var propertyName in value) {
                    if (value.hasOwnProperty(propertyName)) {
                        if (value[propertyName] === value) {
                            // catch object property that point to its parent
                            return value;
                        }

                        item = this.reviveValue(value[propertyName], context);

                        if (Promise.is(item)) {
                            promises.push(
                                item.then(this._createAssignValueFunction(
                                        value, propertyName)
                                )
                            );
                        } else {
                            value[propertyName] = item;
                        }
                    }
                }

                if (promises.length === 0) {
                    return value;
                } else {
                    return Promise.all(promises).then(function() {
                        return value;
                    });
                }
            }
        },

        reviveRegExp: {
            value: function(value, context, label) {

                var valuePath = value["/"],
                    regexp = new RegExp(valuePath.source, valuePath.flags);

                if (label) {
                    context.setObjectLabel(regexp, label);
                }

                return regexp;
            }
        },

        reviveObjectReference: {
            value: function(value, context, label) {
                var valuePath = value["@"],
                    object = context.getObject(valuePath);

                return object;
            }
        },

        reviveArray: {
            value: function(value, context, label) {
                var item,
                    promises = [];

                if (label) {
                    context.setObjectLabel(value, label);
                }

                for (var i = 0, ii = value.length; i < ii; i++) {
                    item = this.reviveValue(value[i], context);

                    if (Promise.is(item)) {
                        promises.push(
                            item.then(this._createAssignValueFunction(value, i))
                        );
                    } else {
                        value[i] = item;
                    }
                }

                if (promises.length === 0) {
                    return value;
                } else {
                    return Promise.all(promises).then(function() {
                        return value;
                    });
                }
            }
        },

        reviveExternalObject: {
            value: function(value, context, label) {
                return Promise.reject(
                    new Error("External object '" + label + "' not found in user objects.")
                );
            }
        },

        _callReviveMethod: {
            value: function(methodName, value, context, label) {
                return this[methodName](value, context, label);
            }
        }

    }, /** @lends MontageReviver. */ {
        _unitRevivers: {value: new Map()},
        _unitNames: {value: []},

        _findObjectNameRegExp: {
            value: /([^\/]+?)(\.reel)?$/
        },
        _toCamelCaseRegExp: {
            value: /(?:^|-)([^-])/g
        },
        _replaceToCamelCase: {
            value: function (_, g1) { return g1.toUpperCase(); }
        },
        // Cache of location descriptors indexed by locationId
        _locationDescCache: {value: new Map()},

        customObjectRevivers: {value: new Map()},

        /**
         * Location Id is in the form of <moduleId>[<objectName>] where
         * [<objectName>] is optional. When objectName is missing it is derived
         * from the last path component of moduleId transformed into CamelCase.
         *
         * @example "event/event-manager" has a default objectName of EventManager.
         *
         * When the last path component ends with ".reel" it is removed before
         * creating the default objectName.
         *
         * @example "matte/ui/input-range.reel" has a default objectName of
         *          InputRange.
         *
         * @returns {moduleId, objectName}
         */
        parseObjectLocationId: {
            value: function (locationId) {
                return this._locationDescCache.get(locationId) || this.createObjectLocationDesc(locationId);
            }
        },

        createObjectLocationDesc: {
            value: function (locationId) {
                var moduleId,
                    objectName,
                    bracketIndex = locationId.indexOf("[");

                if (bracketIndex > 0) {
                    moduleId = locationId.substr(0, bracketIndex);
                    objectName = locationId.slice(bracketIndex + 1, -1);
                } else {
                    moduleId = locationId;
                    this._findObjectNameRegExp.test(locationId);
                    objectName = RegExp.$1.replace(
                        this._toCamelCaseRegExp,
                        this._replaceToCamelCase
                    );
                }

                var locationDesc = {
                    moduleId: moduleId,
                    objectName: objectName
                };
                this._locationDescCache.set(locationId, locationDesc);
                return locationDesc;
            }
        },

        defineUnitReviver: {
            value: function (name, funktion) {
                this._unitRevivers.set(name, funktion);
                this._unitNames.push(name);
            }
        },

        getTypeOf: {
            value: function (value) {
                return this.prototype.getTypeOf.call(this, value);
            }
        },

        addCustomObjectReviver: {
            value: function(reviver) {
                var customObjectRevivers = this.customObjectRevivers;

                /* jshint forin: true */
                for (var methodName in reviver) {
                /* jshint forin: false */

                    if (methodName === "getTypeOf") {
                        continue;
                    }

                    if (
                        typeof reviver[methodName] === "function" &&
                            methodName.substr(0, 5) === "revive"
                    ) {
                        if (typeof (customObjectRevivers.get(methodName)) === "undefined") {
                            customObjectRevivers.set(methodName, reviver[methodName].bind(reviver));
                        } else {
                            return new Error("Reviver '" + methodName + "' is already registered.");
                        }
                    }
                }

                this.prototype.getCustomObjectTypeOf = this.makeGetCustomObjectTypeOf(reviver.getTypeOf);
            }
        },

        resetCustomObjectRevivers: {
            value: function() {
                this.customObjectRevivers.clear();
                this.prototype.getCustomObjectTypeOf = function() {};
            }
        },

        makeGetCustomObjectTypeOf:{
            value: function (getCustomObjectTypeOf) {
                var previousGetCustomObjectTypeOf = this.prototype.getCustomObjectTypeOf;

                return function(value) {
                    return getCustomObjectTypeOf(value) || previousGetCustomObjectTypeOf(value);
                };
            }
        }

    });

    MontageReviver.findProxyForElement = function (element) {
        return PROXY_ELEMENT_MAP.get(element);
    };

    if (typeof exports !== "undefined") {

        exports.MontageReviver = MontageReviver;
    }