# Source: core/template.js

    var Montage = require("./core").Montage,
        Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer,
        DocumentPart = require("./document-part").DocumentPart,
        DocumentResources = require("./document-resources").DocumentResources,
        Serialization = require("./serialization/serialization").Serialization,
        MontageLabeler = require("./serialization/serializer/montage-labeler").MontageLabeler,
        Promise = require("./promise").Promise,
        URL = require("./mini-url"),
        logger = require("./logger").logger("template"),
        defaultEventManager = require("./event/event-manager").defaultEventManager,
        defaultApplication;

    /**
     * @class Template
     * @extends Montage
     */
    var Template = Montage.specialize( /** @lends Template# */ {
        _SERIALIZATION_SCRIPT_TYPE: {value: "text/montage-serialization"},
        _ELEMENT_ID_ATTRIBUTE: {value: "data-montage-id"},
        PARAM_ATTRIBUTE: {value: "data-param"},

        _require: {value: null},
        _resources: {value: null},
        _baseUrl: {value: null},
        _instances: {value: null},
        _metadata: {value: null},

        _objectsString: {value: null},
        objectsString: {
            get: function () {
                return this._objectsString;
            },
            set: function (value) {
                this._objectsString = value;
                if (this._serialization) {
                    this._serialization.initWithString(value);
                }
                // Invalidate the deserializer cache since there's a new
                // serialization in town.
                this.__deserializer = null;
            }
        },

        // Deserializer cache
        __deserializer: {value: null},
        _deserializer: {
            get: function () {
                var deserializer = this.__deserializer,
                    metadata,
                    requires;

                if (!deserializer) {
                    metadata = this._metadata;
                    if (metadata) {
                        requires = Object.create(null);

                        /* jshint forin: true */
                        for (var label in metadata) {
                        /* jshint forin: false */
                            requires[label] = metadata[label].require;
                        }
                    }
                    deserializer = new Deserializer().init(this.objectsString,
                        this._require, requires);
                    this.__deserializer = deserializer;
                }

                return deserializer;
            }
        },
        getDeserializer: {
            value: function () {
                return this._deserializer;
            }
        },

        _serialization: {
            value: null
        },
        getSerialization: {
            value: function () {
                var serialiation = this._serialization;

                if (!serialiation) {
                    serialiation = this._serialization = new Serialization();
                    serialiation.initWithString(this.objectsString);
                }

                return serialiation;
            }
        },

        _isDirty: {
            value: false
        },

        isDirty: {
            get: function () {
                return this._isDirty;
            },
            set: function (value) {
                if (this._isDirty !== value) {
                    this._isDirty = value;
                    this.clearTemplateFromElementContentsCache();
                }
            }
        },

        /**
         * Object that knows how to refresh the contents of the template when it's
         * dirty. Expected to implement the refreshTemplate(template) function.
         */
        refresher: {
            value: null
        },

        _document: {
            value: null
        },

        document: {
            get: function () {
                if (this._isDirty) {
                    this.refresh();
                }
                return this._document;
            },
            set: function (value) {
                this._document = value;
            }
        },

        /**
         * Initializes the Template with an empty document.
         *
         * @function
         * @param {require} _require The require function used to load modules when
         *                           a template is instantiated.
         */
        initWithRequire: {
            value: function (_require) {
                this._require = _require;
                this.document = this.createHtmlDocumentWithHtml("");
                this.objectsString = "";

                return this;
            }
        },

        /**
         * Initializes the Template with a document.
         *
         * @function
         * @param {HTMLDocument} _document The document to be used as a template.
         * @param {require} _require The require function used to load modules when
         *                           a template is instantiated.
         * @returns {Promise} A promise for the proper initialization of the
         *                    template.
         */
        initWithDocument: {
            value: function (_document, _require) {
                var self = this;

                this._require = _require;
                this.setDocument(_document);

                return this.getObjectsString(_document)
                .then(function (objectsString) {
                    self.objectsString = objectsString;
                    return self;
                });
            }
        },

        /**
         * Initializes the Template with an HTML string.
         *
         * @function
         * @param {HTMLDocument} html The HTML string to be used as a template.
         * @param {require} _require The require function used to load modules when
         *                           a template is instantiated.
         * @returns {Promise} A promise for the proper initialization of the
         *                    template.
         */
        initWithHtml: {
            value: function (html, _require) {
                var self = this;

                this._require = _require;
                this.document = this.createHtmlDocumentWithHtml(html);

                return this.getObjectsString(this.document)
                .then(function (objectsString) {
                    self.objectsString = objectsString;
                    return self;
                });
            }
        },

        /**
         * Initializes the Template with Objects and a DocumentFragment to be
         * used as the body of the document.
         *
         * @function
         * @param {Object} objects A JSON'able representation of the objects of the
         *                         template.
         * @param {DocumentFragment} html The HTML string to be used as the body.
         * @param {require} _require The require function used to load modules when
         *                           a template is instantiated.
         * @returns {Promise} A promise for the proper initialization of the
         *                    template.
         */
        initWithObjectsAndDocumentFragment: {
            value: function (objects, documentFragment, _require) {
                this._require = _require;
                this.document = this.createHtmlDocumentWithHtml("");
                this.document.body.appendChild(
                    this.document.importNode(documentFragment, true)
                );
                this.setObjects(objects);

                return this;
            }
        },

        /**
         * Initializes the Template with the HTML document at the module id.
         *
         * @function
         * @param {string} moduleId The module id of the HTML page to load.
         * @param {require} _require The require function used to load modules when
         *                           a template is instantiated.
         * @returns {Promise} A promise for the proper initialization of the
         *                    template.
         */
        initWithModuleId: {
            value: function (moduleId, _require) {
                var self = this;

                this._require = _require;

                return this.createHtmlDocumentWithModuleId(moduleId, _require)
                .then(function (_document) {
                    var baseUrl = _require(moduleId).directory;

                    self.document = _document;
                    self.setBaseUrl(baseUrl);

                    return self.getObjectsString(_document)
                    .then(function (objectsString) {
                        self.objectsString = objectsString;

                        return self;
                    });
                });
            }
        },

        clone: {
            value: function () {
                var clonedTemplate = new Template();

                clonedTemplate._require = this._require;
                clonedTemplate._baseUrl = this._baseUrl;
                clonedTemplate.setDocument(this.document);
                clonedTemplate.objectsString = this.objectsString;
                clonedTemplate._instances = Object.clone(this._instances, 1);

                return clonedTemplate;
            }
        },

        instantiate: {
            value: function (targetDocument) {
                return this.instantiateWithInstances(null, targetDocument);
            }
        },

        /**
         * @param instances {Object} The instances to use in the serialization
         *        section of the template, when given they will be used instead of
         *        creating a new object. It's dictionary where the keys are the
         *        labels and the values the instances.
         * @param targetDocument {Document} The document used to create the markup
         *        resultant of the instantiation.
         */
        instantiateWithInstances: {
            value: function (instances, targetDocument) {
                var self = this,
                    fragment,
                    part = new DocumentPart(),
                    templateObjects,
                    templateParameters;

                instances = instances || this._instances;
                fragment = this._createMarkupDocumentFragment(targetDocument);
                templateParameters = this._getParameters(fragment);

                part.initWithTemplateAndFragment(this, fragment);
                part.startActingAsTopComponent();
                part.parameters = templateParameters;

                templateObjects = this._createTemplateObjects(instances);

                return this._instantiateObjects(templateObjects, fragment)
                .then(function (objects) {
                    var resources = self.getResources();

                    if (!resources.resourcesLoaded() && resources.hasResources()) {
                        // Start preloading the resources as soon as possible, no
                        // need to wait for them as the draw cycle will take care
                        // of that when loading the stylesheets into the document.
                        resources.loadResources(targetDocument);
                    }

                    part.objects = objects;
                    self._invokeDelegates(part, instances);
                    part.stopActingAsTopComponent();

                    return part;
                });
            }
        },

        _objectsInstantiationOptimized: {
            value: false
        },
        _optimizeObjectsInstantiationPromise: {
            value: null
        },
        /**
         * @returns {undefined|Promise} A promise if there are objects to optimize,
         *         nothing otherwise.
         */
        _optimizeObjectsInstantiation: {
            value: function () {
                var self = this,
                    promise;

                if (!this._objectsInstantiationOptimized) {
                    if (!this._optimizeObjectsInstantiationPromise) {
                        promise = this._deserializer.preloadModules();

                        if (promise) {
                            this._optimizeObjectsInstantiationPromise = promise
                            .then(function () {
                                self._objectsInstantiationOptimized = true;
                            });
                        } else {
                            this._objectsInstantiationOptimized = true;
                        }
                    }

                    return this._optimizeObjectsInstantiationPromise;
                }
            }
        },

        setBaseUrl: {
            value: function (baseUrl) {
                this._baseUrl = baseUrl;
            }
        },

        getBaseUrl: {
            value: function () {
                return this._baseUrl;
            }
        },

        getResources: {
            value: function () {
                var resources = this._resources;

                if (!resources) {
                    resources = this._resources = new exports.TemplateResources();
                    resources.initWithTemplate(this);
                }

                return resources;
            }
        },

        /**
         * Creates the object instances to be passed to the deserialization.
         * It takes instances and augments it with "application" and "template".
         *
         * @param {Object} instances The instances object.
         * @returns {Object} The object with instances and application and template.
         */
        _createTemplateObjects: {
            value: function (instances) {
                var templateObjects = Object.create(instances || null);

                if (typeof defaultApplication === "undefined") {
                    defaultApplication = require("./application").application;
                }

                templateObjects.application = defaultApplication;
                templateObjects.template = this;

                return templateObjects;
            }
        },

        _instantiateObjects: {
            value: function (instances, fragment) {
                var deserializer = this._deserializer,
                    optimizationPromise;

                if (Promise.is(deserializer)) {
                    return deserializer; // Promise return (error raised while deserializing)
                }

                optimizationPromise = this._optimizeObjectsInstantiation();

                if (optimizationPromise) {
                    return optimizationPromise.then(function () {
                        return deserializer.deserialize(instances, fragment);
                    });
                } else {
                    return deserializer.deserialize(instances, fragment);
                }
            }
        },

        _createMarkupDocumentFragment: {
            value: function (targetDocument) {
                var fragment = targetDocument.createDocumentFragment(),
                    nodes = this.document.body.childNodes;

                for (var i = 0, ii = nodes.length; i < ii; i++) {
                    fragment.appendChild(
                        targetDocument.importNode(nodes[i], true)
                    );
                }

                return fragment;
            }
        },

        getParameterName: {
            value: function (element) {
                return element.getAttribute(this.PARAM_ATTRIBUTE);
            }
        },

        getParameters: {
            value: function () {
                return this._getParameters(this.document.body);
            }
        },

        _getParameters: {
            value: function (rootElement) {
                var elements = rootElement.querySelectorAll("*[" + this.PARAM_ATTRIBUTE + "]"),
                    elementsCount = elements.length,
                    element,
                    parameterName,
                    parameters = {};

                for (var i = 0; i < elementsCount; i++) {
                    element = elements[i];
                    parameterName = this.getParameterName(element);

                    if (parameterName in parameters) {
                        throw new Error('The parameter "' + parameterName + '" is' +
                            ' declared more than once in ' + this.getBaseUrl() +
                            '.');
                    }

                    parameters[parameterName] = element;
                }

                return parameters;
            }
        },

        hasParameters: {
            value: function () {
                return !!this.document.querySelector("*[" + this.PARAM_ATTRIBUTE + "]");
            }
        },

        _invokeDelegates: {
            value: function (documentPart, instances) {
                var objects = documentPart.objects,
                    object,
                    owner = objects.owner || instances && instances.owner,
                    objectOwner,
                    objectLabel;

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

                    // Don't call delegate methods on objects that were passed to
                    // the instantiation.
                    if (instances && label in instances) {
                        continue;
                    }

                    object = objects[label];
                    // getObjectOwner will take into account metadata that might
                    // have been set for this object. Objects in the serialization
                    // of the template might have different owners. This is true
                    // when an object in the serialization is the result of a
                    // data-param that was expanded using arguments from an external
                    // template.
                    objectOwner = this._getObjectOwner(label, owner);
                    objectLabel = this._getObjectLabel(label);

                    if (object) {
                        if (typeof object._deserializedFromTemplate === "function") {
                            object._deserializedFromTemplate(objectOwner, objectLabel, documentPart);
                        }
                        if (typeof object.deserializedFromTemplate === "function") {
                            object.deserializedFromTemplate(objectOwner, objectLabel, documentPart);
                        }
                    }
                }

                if (owner) {
                    var serialization = this.getSerialization();

                    // Don't call delegate methods on external objects
                    if (!serialization.isExternalObject("owner")) {
                        if (typeof owner._templateDidLoad === "function") {
                            owner._templateDidLoad(documentPart);
                        }
                        if (typeof owner.templateDidLoad === "function") {
                            owner.templateDidLoad(documentPart);
                        }
                    }
                }
            }
        },

        /**
         * Sets the instances to use when instantiating the objects of the template.
         * These instances will always be used when instantiating the template
         * unless a different set of instances is passed in
         * instantiateWithInstances().
         *
         * @function
         * @param {Object} instances The objects' instances.
         */
        setInstances: {
            value: function (instances) {
                this._instances = instances;
            }
        },

        getInstances: {
            value: function () {
                return this._instances;
            }
        },

        setObjects: {
            value: function (objects) {
                // TODO: use Serializer.formatSerialization(object|string)
                this.objectsString = JSON.stringify(objects, null, 4);
            }
        },

        /**
         * Add metadata to specific objects of the serialization.
         *
         * @param {string} label The label of the object in the serialization.
         * @param {Require} _require The require function to be used when loading
         *        the module.
         * @param {string} effectiveLabel An alternative label to be given to the
         *        object.
         * @param {Object} owner The owner object to be given to the object.
         */
        setObjectMetadata: {
            value: function (label, _require, effectiveLabel, owner) {
                var metadata = this._metadata;

                if (!metadata) {
                    this._metadata = metadata = Object.create(null);
                }

                metadata[label] = {
                    "require": _require,
                    "label": effectiveLabel,
                    "owner": owner
                };

                // Invalidate the deserializer cache since we need to setup new
                // requires.
                this.__deserializer = null;
            }
        },

        getObjectMetadata: {
            value: function (label) {
                var metadata = this._metadata;

                if (metadata && label in metadata) {
                    return metadata[label];
                } else {
                    return {
                        "require": this._require,
                        "label": label
                    };
                }
            }
        },

        _getObjectOwner: {
            value: function (label, defaultOwner) {
                var objectOwner,
                    metadata = this._metadata;

                if (metadata && label in metadata) {
                    objectOwner = metadata[label].owner;
                } else {
                    objectOwner = defaultOwner;
                }

                return objectOwner;
            }
        },

        _getObjectLabel: {
            value: function (label) {
                var objectLabel,
                    metadata = this._metadata;

                if (metadata && label in metadata) {
                    objectLabel = metadata[label].label;
                } else {
                    objectLabel = label;
                }

                return objectLabel;
            }
        },

        /**
         * Uses the document markup as the base of the template markup.
         *
         * @function
         * @param {HTMLDocument} doc The document.
         * @returns {Promise} A promise for the proper initialization of the
         *                    document.
         */
        setDocument: {
            value: function(_document) {
                this.document = this.cloneHtmlDocument(_document);

                this.clearTemplateFromElementContentsCache();
            }
        },

        /**
         * Searches for objects in the document.
         * The objects string can live as an inline script in the document or as an
         * external resource that needs to be loaded.
         *
         * @function
         * @param {HTMLDocument} doc The document with the objects string.
         * @returns {Promise} A promise for the objects string, null if not
         *                    found.
         */
        getObjectsString: {
            value: function (doc) {
                var objectsString;

                objectsString = this.getInlineObjectsString(doc);

                if (objectsString === null) {
                    return this.getExternalObjectsString(doc);
                } else {
                    return Promise.resolve(objectsString);
                }
            }
        },

        /**
         * Searches for an inline objects string in a document and returns it if
         * found.
         *
         * @function
         * @param {HTMLDocument} doc The document with the objects string.
         * @returns {?String} The objects string or null if not found.
         */
        getInlineObjectsString: {
            value: function (doc) {
                var selector = "script[type='" + this._SERIALIZATION_SCRIPT_TYPE + "']",
                    script = doc.querySelector(selector);

                if (script) {
                    return script.textContent;
                } else {
                    return null;
                }
            }
        },

        /**
         * Searches for an external objects file in a document and returns its
         * contents if found.
         *
         * @function
         * @param {string} doc The document to search.
         * @returns {Promise} A promise to the contents of the objects file or null
         *                    if none found.
         */
        getExternalObjectsString: {
            value: function (doc) {
                var link = doc.querySelector('link[rel="serialization"]'),
                    deferred;

                if (link) {
                    // TODO use core/request
                    deferred = new Promise(function(resolve, reject) {
                        var req = new XMLHttpRequest();
                        var url = link.getAttribute("href");
                        req.open("GET", url);
                        req.addEventListener("load", function(event) {
                            var req = event.target;
                            if (req.status === 200) {
                                resolve(req.responseText);
                            } else {
                                reject(
                                    new Error("Unable to retrive '" + url + "', code status: " + req.status)
                                );
                            }
                        }, false);
                        req.addEventListener("error", function(event) {
                            reject(
                                new Error("Unable to retrive '" + url + "' with error: " + event.error + ".")
                            );
                        }, false);
                        req.send();

                    });

                    return deferred;
                } else {
                    return Promise.resolve(null);
                }
            }
        },

        createHtmlDocumentWithHtml: {
            value: function (html, baseURI) {
                var htmlDocument = document.implementation.createHTMLDocument("");
                if (html) {
                    htmlDocument.documentElement.innerHTML = html;
                    if (baseURI) {
                        this.normalizeRelativeUrls(htmlDocument, baseURI);
                    }
                }
                return htmlDocument;
            }
        },

        cloneHtmlDocument: {
            value: function (htmlDocument) {
                /*
                 * Temporary fix for the function createHtmlDocumentWithHtml.
                 *
                 * Indeed, innerHTML does not allow us to have a document with some invalid HTML code,
                 * that could belong to an iteration's template for example.
                 *
                 * Such as:
                 *  <body><tr></tr></body> --> <body></body>
                 *
                 * Todo Plus, The DOMImplementation.createHTMLDocument() method has a parameter optional `title`
                 * https://dom.spec.whatwg.org/#domimplementation
                 *
                 * Removing it will improve the performance, but it is not optional for IE.
                 *
                 */
                var clonedDocument = document.implementation.createHTMLDocument(""),
                    baseURI = htmlDocument.baseURI || htmlDocument.URL;

                clonedDocument.replaceChild(
                    clonedDocument.importNode(htmlDocument.documentElement, true),
                    clonedDocument.documentElement
                );

                this.normalizeRelativeUrls(clonedDocument, baseURI);

                return clonedDocument;
            }
        },

        createHtmlDocumentWithModuleId: {
            value: function (moduleId, _require) {
                var self = this;

                if (typeof _require !== "function") {
                    return Promise.reject(
                        new Error("Missing 'require' function to load module '" + moduleId + "'.")
                    );
                }

                return _require.async(moduleId).then(function (exports) {
                    return self.createHtmlDocumentWithHtml(exports.content, exports.directory);
                });
            }
        },

        /**
         * Removes all artifacts related to objects string
         */
        _removeObjects: {
            value: function (doc) {
                var selector = "script[type='" + this._SERIALIZATION_SCRIPT_TYPE + "'], link[rel='serialization']";

                Array.prototype.forEach.call(
                    doc.querySelectorAll(selector),
                    function (element) {
                        element.parentNode.removeChild(element);
                    }
                );
            }
        },

        _addObjects: {
            value: function (doc, objectsString) {
                if (objectsString) {
                    var script = doc.createElement("script");

                    script.setAttribute("type", this._SERIALIZATION_SCRIPT_TYPE);
                    script.textContent = JSON.stringify(JSON.parse(objectsString), null, 4);
                    doc.head.appendChild(script);
                }
            }
        },

        _templateFromElementContentsCache: {
            value: null
        },
        clearTemplateFromElementContentsCache: {
            value: function () {
                this._templateFromElementContentsCache = null;
            }
        },

        createTemplateFromElementContents: {
            value: function (elementId) {
                var element,
                    template,
                    range,
                    cache = this._templateFromElementContentsCache;

                if (!cache) {
                    cache = Object.create(null);
                    this._templateFromElementContentsCache = cache;
                }

                if (elementId in cache) {
                    // We always return an extension of the cached object, this
                    // is because the template can be assigned with instances.
                    // An alternate idea would be to clone it but it's much more
                    // expensive.
                    return Object.create(cache[elementId]);
                }

                element = this.getElementById(elementId);

                // Clone the element contents
                range = this.document.createRange();
                range.selectNodeContents(element);

                // Create the new template with the extracted serialization and
                // markup.
                template = this.createTemplateFromRange(range);

                cache[elementId] = template;

                // We always return an extension of the cached object, this
                // is because the template is mutable.
                // An alternate idea would be to clone it but it's much more
                // expensive.
                return Object.create(template);
            }
        },

        createTemplateFromElement: {
            value: function (elementId) {
                var element,
                    range;

                element = this.getElementById(elementId);

                // Clone the element contents
                range = this.document.createRange();
                range.selectNode(element);

                return this.createTemplateFromRange(range);
            }
        },

        createTemplateFromRange: {
            value: function (range) {
                var fragment,
                    elementIds,
                    labels,
                    template,
                    serialization = new Serialization(),
                    extractedSerialization;

                fragment = range.cloneContents();

                // Find all elements of interest to the serialization.
                elementIds = this._getChildrenElementIds(fragment);

                // Create a new serialization with the components found in the
                // element.
                serialization.initWithString(this.objectsString);
                labels = serialization.getSerializationLabelsWithElements(
                    elementIds);
                extractedSerialization = serialization.extractSerialization(
                    labels, ["owner"]);

                // Create the new template with the extracted serialization and
                // markup.
                template = new Template();
                template.initWithObjectsAndDocumentFragment(
                    null, fragment, this._require);
                template.objectsString = extractedSerialization
                    .getSerializationString();
                template._resources = this.getResources();

                return template;
            }
        },

        // TODO: should this be on Serialization?
        _createSerializationWithElementIds: {
            value: function (elementIds) {
                var serialization = new Serialization(),
                    labels,
                    extractedSerialization;

                serialization.initWithString(this.objectsString);
                labels = serialization.getSerializationLabelsWithElements(
                    elementIds);

                extractedSerialization = serialization.extractSerialization(
                    labels, ["owner"]);

                return extractedSerialization;
            }
        },

        /**
         * @param {TemplateArgumentProvider} templateArgumentProvider An object that
         *        implements the interface needed to provide the arguments to the
         *        parameters.
         * @returns {Object} A dictionary with four properties representing the
         *          objects and elements that were imported into the template:
         *          - labels: the labels of the objects added from template
         *                    argument.
         *          - labelsCollisions: a dictionary of label collisions in the form
         *                              of {oldLabel: newLabel}.
         *          - elementIds: the element ids of the markup imported from
         *                        template argument.
         *          - elementIdsCollisions: a dictionary of element id collisions in
         *                                  the form of {oldElementId: newElementId}
         *
         */
        expandParameters: {
            value: function (templateArgumentProvider) {
                var parameterElements,
                    argumentsElementIds = [],
                    collisionTable,
                    argumentElementsCollisionTable = {},
                    objectsCollisionTable,
                    parameterElement,
                    argumentElement,
                    serialization = this.getSerialization(),
                    argumentsSerialization,
                    willMergeObjectWithLabel,
                    result = {};

                parameterElements = this.getParameters();

                // Expand elements.
                for (var parameterName in parameterElements) {
                    if (parameterElements.hasOwnProperty(parameterName)) {
                        parameterElement = parameterElements[parameterName];
                        argumentElement = templateArgumentProvider.getTemplateArgumentElement(
                            parameterName);

                        // Store all element ids of the argument, we need to create
                        // a serialization with the components that point to them.
                        argumentsElementIds.push.apply(argumentsElementIds,
                            this._getElementIds(argumentElement)
                        );

                        // Replace the parameter with the argument and save the
                        // element ids collision table because we need to correct the
                        // serialization that is created from the stored element ids.
                        collisionTable = this.replaceNode(argumentElement, parameterElement);
                        if (collisionTable) {
                            /* jshint forin: true */
                            for (var key in collisionTable) {
                            /* jshint forin: false */
                                argumentElementsCollisionTable[key] = collisionTable[key];
                            }
                        }
                    }
                }
                result.elementIds = argumentsElementIds;
                result.elementIdsCollisions = argumentElementsCollisionTable;

                // Expand objects.
                argumentsSerialization = templateArgumentProvider
                    .getTemplateArgumentSerialization(argumentsElementIds);

                argumentsSerialization.renameElementReferences(
                    argumentElementsCollisionTable);

                // When merging the serializations we need to resolve any template
                // property alias that comes from the arguments, for instance, the
                // argument could be referring to @table:cell in its scope when in
                // this scope (the serialization1) it is aliased to
                // @repetition:iteration. To do this we ask the argument provider
                // to resolve the template property for us.
                // This approach works because the arguments serialization is
                // created assuming that template properties are just like any other
                // label and are considered external objects.
                willMergeObjectWithLabel = function (label) {
                    if (label.indexOf(":") > 0) {
                        return templateArgumentProvider
                            .resolveTemplateArgumentTemplateProperty(label);
                    }
                };

                objectsCollisionTable = serialization.mergeSerialization(
                    argumentsSerialization, {
                        willMergeObjectWithLabel: willMergeObjectWithLabel
                    });
                this.objectsString = serialization.getSerializationString();

                result.labels = argumentsSerialization.getSerializationLabels();
                result.labelsCollisions = objectsCollisionTable;

                return result;
            }
        },

        /**
         * Takes a foreign node and generate new ids for all element ids that
         * already exist in the current template.
         */
        _resolveElementIdCollisions: {
            value: function (node, labeler) {
                var collisionTable,
                    nodeElements,
                    elementId,
                    elementIds,
                    element,
                    newId;

                labeler = labeler || new MontageLabeler();
                // Set up the labeler with the current element ids.
                elementIds = this.getElementIds();
                for (var i = 0; (elementId = elementIds[i]); i++) {
                    labeler.addLabel(elementId);
                }

                // Resolve element ids collisions.
                nodeElements = this._getElements(node);
                for (elementId in nodeElements) {
                    if (this.getElementById(elementId)) {
                        element = nodeElements[elementId];
                        newId = labeler.generateLabel(labeler.getLabelBaseName(elementId));
                        this.setElementId(element, newId);
                        if (!collisionTable) {
                            collisionTable = Object.create(null);
                        }
                        collisionTable[elementId] = newId;
                    }
                }

                return collisionTable;
            }
        },

        replaceNode: {
            value: function (newNode, oldNode, labeler) {
                var collisionTable;

                collisionTable = this._resolveElementIdCollisions(newNode, labeler);
                this.normalizeRelativeUrls(newNode, this.getBaseUrl());
                oldNode.parentNode.replaceChild(newNode, oldNode);

                return collisionTable;
            }
        },

        insertNodeBefore: {
            value: function (node, reference, labeler) {
                var collisionTable;

                collisionTable = this._resolveElementIdCollisions(node, labeler);
                this.normalizeRelativeUrls(node, this.getBaseUrl());
                reference.parentNode.insertBefore(node, reference);

                return collisionTable;
            }
        },

        appendNode: {
            value: function (node, parentNode, labeler) {
                var collisionTable;

                collisionTable = this._resolveElementIdCollisions(node, labeler);
                this.normalizeRelativeUrls(node, this.getBaseUrl());
                parentNode.appendChild(node);

                return collisionTable;
            }
        },

        getElementId: {
            value: function (element) {
                if (element.getAttribute) {
                    return element.getAttribute(this._ELEMENT_ID_ATTRIBUTE);
                }
            }
        },

        setElementId: {
            value: function (element, elementId) {
                element.setAttribute(this._ELEMENT_ID_ATTRIBUTE, elementId);
            }
        },

        getElementIds: {
            value: function () {
                return this._getElementIds(this.document.body);
            }
        },

        _getElements: {
            value: function (rootNode) {
                var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "]",
                    elements,
                    result = {},
                    elementId;

                elements = rootNode.querySelectorAll(selector);

                for (var i = 0, element; (element = elements[i]); i++) {
                    elementId = this.getElementId(element);
                    result[elementId] = element;
                }

                elementId = this.getElementId(rootNode);
                if (elementId) {
                    result[elementId] = rootNode;
                }

                return result;
            }
        },

        _getChildrenElementIds: {
            value: function (rootNode) {
                // XPath might do a better job here...should test.
                var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "]",
                    elements,
                    elementIds = [];

                elements = rootNode.querySelectorAll(selector);

                for (var i = 0, element; (element = elements[i]); i++) {
                    elementIds.push(this.getElementId(element));
                }

                return elementIds;
            }
        },

        _getElementIds: {
            value: function (rootNode) {
                var elementIds = this._getChildrenElementIds(rootNode),
                    elementId;

                elementId = this.getElementId(rootNode);
                if (elementId) {
                    elementIds.push(elementId);
                }

                return elementIds;
            }
        },

        getElementById: {
            value: function (elementId) {
                var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "='" + elementId + "']";

                return this.document.querySelector(selector);
            }
        },

        html: {
            get: function () {
                var _document = this.document;

                this._removeObjects(_document);
                this._addObjects(_document, this.objectsString);

                return this._getDoctypeString(_document.doctype) + "\n" +
                    _document.documentElement.outerHTML;
            }
        },

        _getDoctypeString: {
            value: function (doctype) {
                return "<!DOCTYPE " +
                    doctype.name +
                    (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '') +
                    (!doctype.publicId && doctype.systemId ? ' SYSTEM' : '') +
                    (doctype.systemId ? ' "' + doctype.systemId + '"' : '') +
                    '>';
            }
        },

        normalizeRelativeUrls: {
            value: function (parentNode, baseUrl) {
                // Resolve component's images relative URLs if we have a valid baseUrl
                if (typeof baseUrl === "string" && baseUrl !== "" && baseUrl !== 'about:blank') {
                    // We are only looking for DOM and SVG image elements
                    var XLINK_NS = 'http://www.w3.org/1999/xlink',          // Namespace for SVG's xlink
                        absoluteUrlRegExp = /^[\w\-]+:|^\//,                // Check for "<protocol>:", "/" and "//",
                        nodes = Template._NORMALIZED_TAG_NAMES.indexOf(parentNode.tagName) !== -1 ?
                            [parentNode] : parentNode.querySelectorAll(Template._NORMALIZED_TAG_NAMES_SELECTOR);

                    for (var i = 0, ii = nodes.length; i < ii; i++) {
                        var node = nodes[i],
                            url;

                        if (node.tagName === 'image') {
                            // SVG image
                            url = node.getAttributeNS(XLINK_NS, 'href');
                            if (!absoluteUrlRegExp.test(url)) {
                                node.setAttributeNS(XLINK_NS, 'href', URL.resolve(baseUrl, url));
                            }
                        } else {
                            // DOM image
                            if (node.hasAttribute("src")) {
                                url = node.getAttribute('src');
                                if (url !== "" && !absoluteUrlRegExp.test(url)) {
                                    node.setAttribute('src', URL.resolve(baseUrl, url));
                                }
                            } else if (node.hasAttribute("href")) {
                                // Stylesheets
                                url = node.getAttribute('href');
                                if (url !== "" && !absoluteUrlRegExp.test(url)) {
                                    node.setAttribute('href', URL.resolve(baseUrl, url));
                                }
                            }
                        }
                    }
                }

            }
        },

        replaceContentsWithTemplate: {
            value: function (template) {
                this._require = template._require;
                this._baseUrl = template._baseUrl;
                this._document = template._document;
                this.objectsString = template.objectsString;
                this._instances = template._instances;
                this._templateFromElementContentsCache = template._templateFromElementContentsCache;
                this._metadata = template._metadata;
            }
        },

        /**
         * Refresh the contents of the template when its dirty.
         */
        refresh: {
            value: function () {
                if (this.isDirty) {
                    if (this.refresher &&
                        typeof this.refresher.refreshTemplate === "function") {
                        this.refresher.refreshTemplate(this);
                        this.isDirty = false;
                    } else {
                        console.warn("Not able to refresh without a refresher.refreshTemplate.");
                    }
                }
            }
        }

    }, {

        _templateCache: {
            value: {
                moduleId: Object.create(null)
            }
        },
        _getTemplateCacheKey: {
            value: function (moduleId, _require) {
                // Transforms relative module ids into absolute module ids
                moduleId = _require.resolve(moduleId);
                return _require.location + "#" + moduleId;
            }
        },
        getTemplateWithModuleId: {
            value: function (moduleId, _require) {
                var cacheKey,
                    template;

                cacheKey = this._getTemplateCacheKey(moduleId, _require);
                template = this._templateCache.moduleId[cacheKey];

                if (!template) {
                    template = new Template()
                    .initWithModuleId(moduleId, _require);

                    this._templateCache.moduleId[cacheKey] = template;
                }

                return template;
            }
        },

        _NORMALIZED_TAG_NAMES: {
            value: ["IMG", "image", "IFRAME", "link","script"]
        },

        __NORMALIZED_TAG_NAMES_SELECTOR: {
            value: null
        },

        _NORMALIZED_TAG_NAMES_SELECTOR: {
            get: function () {
                if (!this.__NORMALIZED_TAG_NAMES_SELECTOR) {
                    this.__NORMALIZED_TAG_NAMES_SELECTOR = this._NORMALIZED_TAG_NAMES.join(",");
                }
                return this.__NORMALIZED_TAG_NAMES_SELECTOR;
            }
        }

    });

    /**
     * @class TemplateResources
     * @extends Montage
     */
    var TemplateResources = Montage.specialize( /** @lends TemplateResources# */ {
        _resources: {value: null},
        _resourcesLoaded: {value: false},
        template: {value: null},
        rootUrl: {value: ""},

        constructor: {
            value: function TemplateResources() {
                this._resources = Object.create(null);
            }
        },

        initWithTemplate: {
            value: function (template) {
                this.template = template;
            }
        },

        hasResources: {
            value: function () {
                return this.getStyles().length > 0 || this.getScripts().length > 0;
            }
        },

        resourcesLoaded: {
            value: function () {
                return this._resourcesLoaded;
            }
        },

        loadResources: {
            value: function (targetDocument) {
                this._resourcesLoaded = true;

                return Promise.all([
                    this.loadScripts(targetDocument),
                    this.loadStyles(targetDocument)
                ]);
            }
        },

        getScripts: {
            value: function () {
                var scripts = this._resources.scripts,
                    script,
                    template,
                    templateScripts;

                if (!scripts) {
                    template = this.template;

                    scripts = this._resources.scripts = [];
                    templateScripts = template.document.querySelectorAll("script");

                    for (var i = 0, ii = templateScripts.length; i < ii; i++) {
                        script = templateScripts[i];

                        if (script.type !== this.template._SERIALIZATION_SCRIPT_TYPE) {
                            scripts.push(script);
                        }
                    }
                }

                return scripts;
            }
        },

        loadScripts: {
            value: function (targetDocument) {
                var scripts = this.getScripts(),
                    ii = scripts.length;

                if (ii) {
                    var promises = [];

                    for (var i = 0; i < ii; i++) {
                        promises.push(
                            this.loadScript(scripts[i], targetDocument)
                        );
                    }

                    return Promise.all(promises);
                }

                return Promise.resolve();
            }
        },

        loadScript: {
            value: function (script, targetDocument) {
                var documentResources,
                    newScript;

                documentResources = DocumentResources.getInstanceForDocument(targetDocument);
                // Firefox isn't able to load a script that we reuse, we need to
                // create a new one :(.
                //newScript = targetDocument.importNode(script);
                newScript = this._cloneScriptElement(script, targetDocument);

                return documentResources.addScript(newScript);
            }
        },

        _cloneScriptElement: {
            value: function (scriptTemplate, _document) {
                var script = _document.createElement("script"),
                    attributes = scriptTemplate.attributes,
                    attribute;

                for (var i = 0, ii = attributes.length; i < ii; i++) {
                    attribute = attributes[i];

                    script.setAttribute(attribute.name, attribute.value);
                }
                script.textContent = scriptTemplate.textContent;

                return script;
            }
        },

        getStyles: {
            value: function () {
                var styles = this._resources.styles,
                    template,
                    templateStyles,
                    styleSelector;

                if (!styles) {
                    styleSelector = 'link[rel="stylesheet"], style';
                    template = this.template;

                    templateStyles = template.document.querySelectorAll(styleSelector);

                    styles = Array.prototype.slice.call(templateStyles, 0);
                    this._resources.styles = styles;
                }

                return styles;
            }
        },

        loadStyles: {
            value: function (targetDocument) {
                var promises = [],
                    styles;

                styles = this.getStyles();

                for (var i = 0, ii = styles.length; i < ii; i++) {
                    promises.push(
                        this.loadStyle(styles[i], targetDocument)
                    );
                }

                return Promise.all(promises);
            }
        },

        loadStyle: {
            value: function (element, targetDocument) {
                var url,
                    documentResources;

                url = element.getAttribute("href");

                if (url) {
                    documentResources = DocumentResources.getInstanceForDocument(targetDocument);
                    return documentResources.preloadResource(url);
                } else {
                    return Promise.resolve();
                }
            }
        },

        createStylesForDocument: {
            value: function (targetDocument) {
                var styles = this.getStyles(),
                    newStyle,
                    stylesForDocument = [];

                for (var i = 0, style; (style = styles[i]); i++) {
                    newStyle = targetDocument.importNode(style, true);
                    stylesForDocument.push(newStyle);
                }

                return stylesForDocument;
            }
        }
    });

    // Used to create a DocumentPart from a document without a Template
    function instantiateDocument(_document, _require, instances) {
        var template = new Template(),
            html = _document.documentElement.outerHTML,
            part = new DocumentPart(),
            clonedDocument,
            templateObjects,
            rootElement = _document.documentElement;

        // Setup a template just like we'd do for a document in a template
        clonedDocument = template.createHtmlDocumentWithHtml(html, _document.location.href);

        return template.initWithDocument(clonedDocument, _require)
        .then(function () {
            template.setBaseUrl(_document.location.href);
            // Instantiate it using the document given since we don't want to clone
            // the document markup
            templateObjects = template._createTemplateObjects(instances);
            part.initWithTemplateAndFragment(template);

            return template._instantiateObjects(templateObjects, rootElement)
            .then(function (objects) {
                part.objects = objects;
                template._invokeDelegates(part);

                return part;
            });
        });
    }

    var TemplateArgumentProvider = Montage.specialize({
        /**
         * This function asks the provider to return the element that corresponds
         * to the argument with the same name. This element will be used to replace
         * the corresponding element with data-param of the template being expanded.
         * @param argumentName
         * @private
         */
        getTemplateArgumentElement: {
            value: Function.noop
        },

        /**
         * This function asks the provider to return the serialization components
         * that refer to the given element ids.
         * The serialization returned will be merged with the serialization of the
         * template being expanded.
         * @param elementIds
         * @private
         */
        getTemplateArgumentSerialization: {
            value: Function.noop
        },

        /**
         * This function asks the provider to resolve a template property that was
         * found in the argument serialization. The template property could be an
         * alias that only the provider knows how to resolve because they have
         * access to the template where the argument comes from and where the
         * aliases are defined in the serialization block (e.g: ":cell": {alias:
         * "@repetition:iteration"}).
         * @param templatePropertyLabel
         * @private
         */
        resolveTemplateArgumentTemplateProperty: {
            value: Function.noop
        }
    });

    exports.Template = Template;
    exports.TemplateArgumentProvider = TemplateArgumentProvider;
    exports.TemplateResources = TemplateResources;
    exports.instantiateDocument = instantiateDocument;