/**
 * Copyright (C) 2014 KO GmbH <copyright@kogmbh.com>
 *
 * @licstart
 * This file is part of WebODF.
 *
 * WebODF is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License (GNU AGPL)
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * WebODF is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 * @licend
 *
 * @source: http://www.webodf.org/
 * @source: https://github.com/kogmbh/WebODF/
 */

/**
 * Namespace of the Wodo.TextEditor
 * @namespace
 */
var Wodo = Wodo || (function () {
    "use strict";

    var installationPath = (function() {
        "use strict";

        /**
         * Sees to get the url of this script on top of the stack trace.
         * @param {!string|undefined} stack
         * @return {!string|undefined}
         */
        function getScriptUrlFromStack(stack) {
            var url, matches;

            if (typeof stack === "string" && stack) {
                matches = stack.match(/((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/);
                url = matches && matches[1];
            }
            if (typeof url === "string" && url) {
                return url;
            }
            return undefined;
        }

        /**
         * Tries by various tricks to get the url of this script.
         * To be called if document.currentScript is not supported
         * @return {!string|undefined}
         */
        function getCurrentScriptElementSrcByTricks() {
            var i,
                pageUrl = window.location.href,
                scriptElements = document.getElementsByTagName("script");

            // if there is only one script, it must be this
            if (scriptElements.length === 1) {
                return scriptElements[0].src;
            }

            // otherwise get it from the stacktrace
            try {
                throw new Error();
            }
            catch (err) {
                return getScriptUrlFromStack(err.stack);
            }
        }

        var path = ".", scriptElementSrc,
            a, pathname, pos;

        if (document.currentScript && document.currentScript.src) {
            scriptElementSrc = document.currentScript.src;
        } else {
            scriptElementSrc = getCurrentScriptElementSrcByTricks();
        }

        if (scriptElementSrc) {
            a = document.createElement('a');
            a.href = scriptElementSrc;
            pathname = a.pathname;

            pos = pathname.lastIndexOf("/");
            if (pos !== -1) {
                path = pathname.substr(0, pos);
            }
        } else {
            alert("Could not estimate installation path of the Wodo.TextEditor.");
        }
        return path;
    }());

    window.dojoConfig = (function() {
        var WebODFEditorDojoLocale = "C";

        if (navigator && navigator.language && navigator.language.match(/^(de)/)) {
            WebODFEditorDojoLocale = navigator.language.substr(0, 2);
        }

        return {
            locale: WebODFEditorDojoLocale,
            paths: {
                "webodf/editor": installationPath,
                "dijit":         installationPath + "/dijit",
                "dojox":         installationPath + "/dojox",
                "dojo":          installationPath + "/dojo",
                "resources":     installationPath + "/resources"
            }
        };
    }());

    var /** @inner @type{!boolean} */
        isInitalized = false,
        /** @inner @type{!Array.<!function():undefined>} */
        pendingInstanceCreationCalls = [],
        /** @inner @type{!number} */
        instanceCounter = 0,
        // TODO: avatar image url needs base-url setting.
        // so far Wodo itself does not have a setup call,
        // but then the avatar is also not used yet here
        defaultUserData = {
            fullName: "",
            color:    "black",
            imageUrl: "avatar-joe.png"
        },
        /** @inner @const
            @type{!Array.<!string>} */
        userDataFieldNames = ["fullName", "color", "imageUrl"],
        /** @inner @const
            @type{!string} */
        memberId = "localuser",
        // constructors
        BorderContainer, ContentPane, FullWindowZoomHelper, EditorSession, Tools,
        /** @inner @const
            @type{!string} */
        EVENT_UNKNOWNERROR = "unknownError",
        /** @inner @const
            @type {!string} */
        EVENT_METADATACHANGED = "metadataChanged";


    /**
     * @return {undefined}
     */
    function initTextEditor() {
        require([
            "dijit/layout/BorderContainer",
            "dijit/layout/ContentPane",
            "webodf/editor/FullWindowZoomHelper",
            "webodf/editor/EditorSession",
            "webodf/editor/Tools",
            "webodf/editor/Translator"],
            function (BC, CP, FWZH, ES, T, Translator) {
                var locale = navigator.language || "en-US",
                    editorBase = dojo.config && dojo.config.paths && dojo.config.paths["webodf/editor"],
                    translationsDir = editorBase + '/translations',
                    t;

                BorderContainer = BC;
                ContentPane = CP;
                FullWindowZoomHelper = FWZH,
                EditorSession = ES;
                Tools = T;

                // TODO: locale cannot be set by the user, also different for different editors
                t = new Translator(translationsDir, locale, function (editorTranslator) {
                    runtime.setTranslator(editorTranslator.translate);
                    // Extend runtime with a convenient translation function
                    runtime.translateContent = function (node) {
                        var i,
                            element,
                            tag,
                            placeholder,
                            translatable = node.querySelectorAll("*[text-i18n]");

                        for (i = 0; i < translatable.length; i += 1) {
                            element = translatable[i];
                            tag = element.localName;
                            placeholder = element.getAttribute('text-i18n');
                            if (tag === "label"
                                    || tag === "span"
                                    || /h\d/i.test(tag)) {
                                element.textContent = runtime.tr(placeholder);
                            }
                        }
                    };

                    defaultUserData.fullName = runtime.tr("Unknown Author");

                    isInitalized = true;
                    pendingInstanceCreationCalls.forEach(function (create) { create(); });
                });
            }
        );
    }

    /**
     * Creates a new record with userdata, and for all official fields
     * copies over the value from the original or, if not present there,
     * sets it to the default value.
     * @param {?Object.<!string,!string>|undefined} original, defaults to {}
     * @return {!Object.<!string,!string>}
     */
    function cloneUserData(original) {
        var result = {};

        if (!original) {
            original = {};
        }

        userDataFieldNames.forEach(function (fieldName) {
            result[fieldName] = original[fieldName] || defaultUserData[fieldName];
        });

        return result;
    }

    /**
     * @name TextEditor
     * @constructor
     * @param {!string} mainContainerElementId
     * @param {!Object.<!string,!*>} editorOptions
     */
    function TextEditor(mainContainerElementId, editorOptions) {
        instanceCounter = instanceCounter + 1;

        /**
        * Returns true if either all features are wanted and this one is not explicitely disabled
        * or if not all features are wanted by default and it is explicitely enabled
        * @param {?boolean|undefined} isFeatureEnabled explicit flag which enables a feature
        * @return {!boolean}
        */
        function isEnabled(isFeatureEnabled) {
            return editorOptions.allFeaturesEnabled ? (isFeatureEnabled !== false) : isFeatureEnabled;
        }

        var self = this,
            userData,
            //
            mainContainerElement = document.getElementById(mainContainerElementId),
            canvasElement,
            canvasContainerElement,
            toolbarElement,
            toolbarContainerElement, // needed because dijit toolbar overwrites direct classList
            editorElement,
            /** @inner @const
                @type{!string} */
            canvasElementId = "webodfeditor-canvas" + instanceCounter,
            /** @inner @const
                @type{!string} */
            canvasContainerElementId = "webodfeditor-canvascontainer" + instanceCounter,
            /** @inner @const
                @type{!string} */
            toolbarElementId = "webodfeditor-toolbar" + instanceCounter,
            /** @inner @const
                @type{!string} */
            editorElementId = "webodfeditor-editor" + instanceCounter,
            //
            fullWindowZoomHelper,
            //
            mainContainer,
            tools,
            odfCanvas,
            //
            editorSession,
            session,
            //
            loadOdtFile = editorOptions.loadCallback,
            saveOdtFile = editorOptions.saveCallback,
            close =       editorOptions.closeCallback,
            //
            directTextStylingEnabled = isEnabled(editorOptions.directTextStylingEnabled),
            directParagraphStylingEnabled = isEnabled(editorOptions.directParagraphStylingEnabled),
            paragraphStyleSelectingEnabled = isEnabled(editorOptions.paragraphStyleSelectingEnabled),
            paragraphStyleEditingEnabled = isEnabled(editorOptions.paragraphStyleEditingEnabled),
            imageEditingEnabled = isEnabled(editorOptions.imageEditingEnabled),
            hyperlinkEditingEnabled = isEnabled(editorOptions.hyperlinkEditingEnabled),
            reviewModeEnabled = Boolean(editorOptions.reviewModeEnabled), // needs to be explicitly enabled
            annotationsEnabled = reviewModeEnabled || isEnabled(editorOptions.annotationsEnabled),
            undoRedoEnabled = isEnabled(editorOptions.undoRedoEnabled),
            zoomingEnabled = isEnabled(editorOptions.zoomingEnabled),
            //
            pendingMemberId,
            pendingEditorReadyCallback,
            //
            eventNotifier = new core.EventNotifier([
                EVENT_UNKNOWNERROR,
                EVENT_METADATACHANGED
            ]);

        runtime.assert(Boolean(mainContainerElement), "No id of an existing element passed to Wodo.createTextEditor(): "+mainContainerElementId);

        /**
         * @param {!Object} changes
         * @return {undefined}
         */
        function relayMetadataSignal(changes) {
            eventNotifier.emit(EVENT_METADATACHANGED, changes);
        }

        /**
         * @return {undefined}
         */
        function createSession() {
            var viewOptions = {
                    editInfoMarkersInitiallyVisible: false,
                    caretAvatarsInitiallyVisible: false,
                    caretBlinksOnRangeSelect: true
                };

            // create session around loaded document
            session = new ops.Session(odfCanvas);
            editorSession = new EditorSession(session, pendingMemberId, {
                viewOptions: viewOptions,
                directTextStylingEnabled: directTextStylingEnabled,
                directParagraphStylingEnabled: directParagraphStylingEnabled,
                paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
                paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
                imageEditingEnabled: imageEditingEnabled,
                hyperlinkEditingEnabled: hyperlinkEditingEnabled,
                annotationsEnabled: annotationsEnabled,
                zoomingEnabled: zoomingEnabled,
                reviewModeEnabled: reviewModeEnabled
            });
            if (undoRedoEnabled) {
                editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
            }

            // Relay any metadata changes to the Editor's consumer as an event
            editorSession.sessionController.getMetadataController().subscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);

            // and report back to caller
            pendingEditorReadyCallback();
            // reset
            pendingEditorReadyCallback = null;
            pendingMemberId = null;
        }

        /**
         * @return {undefined}
         */
        function startEditing() {
            runtime.assert(editorSession, "editorSession should exist here.");

            tools.setEditorSession(editorSession);
            editorSession.sessionController.insertLocalCursor();
            editorSession.sessionController.startEditing();
        }

        /**
         * @return {undefined}
         */
        function endEditing() {
            runtime.assert(editorSession, "editorSession should exist here.");

            tools.setEditorSession(undefined);
            editorSession.sessionController.endEditing();
            editorSession.sessionController.removeLocalCursor();
        }

        /**
         * Loads an ODT document into the editor.
         * @name TextEditor#openDocumentFromUrl
         * @function
         * @param {!string} docUrl url from which the ODT document can be loaded
         * @param {!function(!Error=):undefined} callback Called once the document has been opened, passes an error object in case of error
         * @return {undefined}
         */
        this.openDocumentFromUrl = function(docUrl, editorReadyCallback) {
            runtime.assert(docUrl, "document should be defined here.");
            runtime.assert(!pendingEditorReadyCallback, "pendingEditorReadyCallback should not exist here.");
            runtime.assert(!editorSession, "editorSession should not exist here.");
            runtime.assert(!session, "session should not exist here.");

            pendingMemberId = memberId;
            pendingEditorReadyCallback = function () {
                var op = new ops.OpAddMember();
                op.init({
                    memberid: memberId,
                    setProperties: userData
                });
                session.enqueue([op]);
                startEditing();
                if (editorReadyCallback) {
                    editorReadyCallback();
                }
            };

            odfCanvas.load(docUrl);
        }

        /**
         * Closes the document, and does cleanup.
         * @name TextEditor#closeDocument
         * @function
         * @param {!function(!Error=):undefined} callback  Called once the document has been closed, passes an error object in case of error
         * @return {undefined}
         */
        this.closeDocument = function(callback) {
            runtime.assert(session, "session should exist here.");

            endEditing();

            var op = new ops.OpRemoveMember();
            op.init({
                memberid: memberId
            });
            session.enqueue([op]);

            session.close(function (err) {
                if (err) {
                    callback(err);
                } else {
                    editorSession.sessionController.getMetadataController().unsubscribe(gui.MetadataController.signalMetadataChanged, relayMetadataSignal);
                    editorSession.destroy(function (err) {
                        if (err) {
                            callback(err);
                        } else {
                            editorSession = undefined;
                            session.destroy(function (err) {
                                if (err) {
                                    callback(err);
                                } else {
                                    session = undefined;
                                    callback();
                                }
                            });
                        }
                    });
                }
            });
        }

        /**
         * @name TextEditor#getDocumentAsByteArray
         * @function
         * @param {!function(err:?Error, file:!Uint8Array=):undefined} callback Called with the current document as ODT file as bytearray, passes an error object in case of error
         * @return {undefined}
         */
        this.getDocumentAsByteArray = function(callback) {
            var odfContainer = odfCanvas.odfContainer();

            if (odfContainer) {
                odfContainer.createByteArray(function(ba) {
                    callback(null, ba);
                }, function(errorString) {
                    callback(new Error(errorString ? errorString : "Could not create bytearray from OdfContainer."));
                });
            } else {
                callback(new Error("No odfContainer set!"));
            }
        }

        /**
         * Sets the metadata fields from the given properties map.
         * Avoid setting certain fields since they are automatically set:
         *    dc:creator
         *    dc:date
         *    meta:editing-cycles
         *
         * The following properties are never used and will be removed for semantic
         * consistency from the document:
         *     meta:editing-duration
         *     meta:document-statistic
         *
         * Setting any of the above mentioned fields using this method will have no effect.
         *
         * @name TextEditor#setMetadata
         * @function
         * @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value.
         * @param {?Array.<!string>} removedProperties An array of metadata field names (prefixed).
         * @return {undefined}
         */
        this.setMetadata = function(setProperties, removedProperties) {
            runtime.assert(editorSession, "editorSession should exist here.");

            editorSession.sessionController.getMetadataController().setMetadata(setProperties, removedProperties);
        };

        /**
         * Returns the value of the requested document metadata field.
         * @name TextEditor#getMetadata
         * @function
         * @param {!string} property A namespace-prefixed field name, for example
         * dc:creator
         * @return {?string}
         */
        this.getMetadata = function(property) {
            runtime.assert(editorSession, "editorSession should exist here.");

            return editorSession.sessionController.getMetadataController().getMetadata(property);
        };

        /**
         * Sets the data for the person that is editing the document.
         * The supported fields are:
         *     "fullName": the full name of the editing person
         *     "color": color to use for the user specific UI elements
         * @name TextEditor#setUserData
         * @function
         * @param {?Object.<!string,!string>|undefined} data
         * @return {undefined}
         */
        function setUserData(data) {
            userData = cloneUserData(data);
        }
        this.setUserData = setUserData;

        /**
         * Returns the data set for the person that is editing the document.
         * @name TextEditor#getUserData
         * @function
         * @return {!Object.<!string,!string>}
         */
        this.getUserData = function() {
            return cloneUserData(userData);
        }

        /**
         * @return {undefined}
         */
        function setFocusToOdfCanvas() {
            editorSession.sessionController.getEventManager().focus();
        }

        /**
         * @param {!function(!Error=):undefined} callback passes an error object in case of error
         * @return {undefined}
         */
        function destroyInternal(callback) {
            mainContainerElement.removeChild(editorElement);

            callback();
        }

        /**
         * Destructs the editor object completely.
         * @name TextEditor#destroy
         * @function
         * @param {!function(!Error=):undefined} callback Called once the destruction has been completed, passes an error object in case of error
         * @return {undefined}
         */
        this.destroy = function(callback) {
            var destroyCallbacks = [];

            // TODO: decide if some forced close should be done here instead of enforcing proper API usage
            runtime.assert(!session, "session should not exist here.");

            // TODO: investigate what else needs to be done
            mainContainer.destroyRecursive(true);

            destroyCallbacks = destroyCallbacks.concat([
                fullWindowZoomHelper.destroy,
                tools.destroy,
                odfCanvas.destroy,
                destroyInternal
            ]);

            core.Async.destroyAll(destroyCallbacks, callback);
        }

        // TODO:
        // this.openDocumentFromByteArray = openDocumentFromByteArray; see also https://github.com/kogmbh/WebODF/issues/375
        // setReadOnly: setReadOnly,

        /**
         * Registers a callback which should be called if the given event happens.
         * @name TextEditor#addEventListener
         * @function
         * @param {!string} eventId
         * @param {!Function} callback
         * @return {undefined}
         */
        this.addEventListener = eventNotifier.subscribe;
        /**
         * Unregisters a callback for the given event.
         * @name TextEditor#removeEventListener
         * @function
         * @param {!string} eventId
         * @param {!Function} callback
         * @return {undefined}
         */
        this.removeEventListener = eventNotifier.unsubscribe;


        /**
         * @return {undefined}
         */
        function init() {
            var editorPane,
                /** @inner @const
                    @type{!string} */
                documentns = document.documentElement.namespaceURI;

            /**
             * @param {!string} tagLocalName
             * @param {!string|undefined} id
             * @param {!string} className
             * @return {!Element}
             */
            function createElement(tagLocalName, id, className) {
                var element;
                element = document.createElementNS(documentns, tagLocalName);
                if (id) {
                    element.id = id;
                }
                element.classList.add(className);
                return element;
            }

            // create needed tree structure
            canvasElement = createElement('div', canvasElementId, "webodfeditor-canvas");
            canvasContainerElement = createElement('div', canvasContainerElementId, "webodfeditor-canvascontainer");
            toolbarElement = createElement('span', toolbarElementId, "webodfeditor-toolbar");
            toolbarContainerElement = createElement('span', undefined, "webodfeditor-toolbarcontainer");
            editorElement = createElement('div', editorElementId, "webodfeditor-editor");

            // put into tree
            canvasContainerElement.appendChild(canvasElement);
            toolbarContainerElement.appendChild(toolbarElement);
            editorElement.appendChild(toolbarContainerElement);
            editorElement.appendChild(canvasContainerElement);
            mainContainerElement.appendChild(editorElement);

            // style all elements with Dojo's claro.
            // Not nice to do this on body, but then there is no other way known
            // to style also all dialogs, which are attached directly to body
            document.body.classList.add("claro");

            // create widgets
            mainContainer = new BorderContainer({}, mainContainerElementId);

            editorPane = new ContentPane({
                region: 'center'
            }, editorElementId);
            mainContainer.addChild(editorPane);

            mainContainer.startup();

            tools = new Tools(toolbarElementId, {
                onToolDone: setFocusToOdfCanvas,
                loadOdtFile: loadOdtFile,
                saveOdtFile: saveOdtFile,
                close: close,
                directTextStylingEnabled: directTextStylingEnabled,
                directParagraphStylingEnabled: directParagraphStylingEnabled,
                paragraphStyleSelectingEnabled: paragraphStyleSelectingEnabled,
                paragraphStyleEditingEnabled: paragraphStyleEditingEnabled,
                imageInsertingEnabled: imageEditingEnabled,
                hyperlinkEditingEnabled: hyperlinkEditingEnabled,
                annotationsEnabled: annotationsEnabled,
                undoRedoEnabled: undoRedoEnabled,
                zoomingEnabled: zoomingEnabled,
                aboutEnabled: true
            });

            odfCanvas = new odf.OdfCanvas(canvasElement);
            odfCanvas.enableAnnotations(annotationsEnabled, true);

            odfCanvas.addListener("statereadychange", createSession);

            fullWindowZoomHelper = new FullWindowZoomHelper(toolbarContainerElement, canvasContainerElement);

            setUserData(editorOptions.userData);
        }

        init();
    }

    function loadDojoAndStuff(callback) {
        var head = document.getElementsByTagName("head")[0],
            frag = document.createDocumentFragment(),
            link,
            script;

        // append two link and two script elements to the header
        link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = installationPath + "/app/resources/app.css";
        link.type = "text/css";
        link.async = false;
        frag.appendChild(link);
        link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = installationPath + "/wodotexteditor.css";
        link.type = "text/css";
        link.async = false;
        frag.appendChild(link);
        script = document.createElement("script");
        script.src = installationPath + "/dojo-amalgamation.js";
        script["data-dojo-config"] = "async: true";
        script.charset = "utf-8";
        script.type = "text/javascript";
        script.async = false;
        frag.appendChild(script);
        script = document.createElement("script");
        script.src = installationPath + "/webodf.js";
        script.charset = "utf-8";
        script.type = "text/javascript";
        script.async = false;
        script.onload = callback;
        frag.appendChild(script);
        head.appendChild(frag);
    }

    /**
     * Creates a text editor object and returns it on success in the passed callback.
     * @name Wodo#createTextEditor
     * @function
     * @param {!string} editorContainerElementId id of the existing div element which will contain the editor (should be empty before)
     * @param editorOptions options to configure the features of the editor. All entries are optional
     * @param [editorOptions.loadCallback] parameter-less callback method, adds a "Load" button to the toolbar which triggers this method
     * @param [editorOptions.saveCallback] parameter-less callback method, adds a "Save" button to the toolbar which triggers this method
     * @param [editorOptions.closeCallback] parameter-less callback method, adds a "Save" button to the toolbar which triggers this method
     * @param [editorOptions.allFeaturesEnabled=false] if set to 'true', switches the default for all features from 'false' to 'true'
     * @param [editorOptions.directTextStylingEnabled=false] if set to 'true', enables the direct styling of text (e.g. bold/italic or font)
     * @param [editorOptions.directParagraphStylingEnabled=false] if set to 'true', enables the direct styling of paragraphs (e.g. indentation or alignement)
     * @param [editorOptions.paragraphStyleSelectingEnabled=false] if set to 'true', enables setting of defined paragraph styles to paragraphs
     * @param [editorOptions.paragraphStyleEditingEnabled=false] if set to 'true', enables the editing of defined paragraph styles
     * @param [editorOptions.imageEditingEnabled=false] if set to 'true', enables the insertion of images
     * @param [editorOptions.hyperlinkEditingEnabled=false] if set to 'true', enables the editing of hyperlinks
     * @param [editorOptions.annotationsEnabled=false] if set to 'true', enables the display and the editing of annotations
     * @param [editorOptions.undoRedoEnabled=false] if set to 'true', enables the Undo and Redo of editing actions
     * @param [editorOptions.zoomingEnabled=false] if set to 'true', enables the zooming tool
     * @param [editorOptions.userData] data about the user editing the document
     * @param [editorOptions.userData.fullName] full name of the user, used for annotations and in the metadata of the document
     * @param [editorOptions.userData.color="black"] color to use for any user related indicators like cursor or annotations
     * @param {!function(err:?Error, editor:!TextEditor=):undefined} onEditorCreated
     * @return {undefined}
     */
    function createTextEditor(editorContainerElementId, editorOptions, onEditorCreated) {
        /**
         * @return {undefined}
         */
        function create() {
            var editor = new TextEditor(editorContainerElementId, editorOptions);
            onEditorCreated(null, editor);
        }

        if (!isInitalized) {
            pendingInstanceCreationCalls.push(create);
            // first request?
            if (pendingInstanceCreationCalls.length === 1) {
                if (String(typeof WodoFromSource) === "undefined") {
                    loadDojoAndStuff(initTextEditor);
                } else {
                    initTextEditor();
                }
            }
        } else {
            create();
        }
    }


    /**
     * @lends Wodo#
     */
    return {
        createTextEditor: createTextEditor,
        // flags
        /** Id of event for an unkown error */
        EVENT_UNKNOWNERROR: EVENT_UNKNOWNERROR,
        /** Id of event if metadata changes */
        EVENT_METADATACHANGED: EVENT_METADATACHANGED
    };
}());
