// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

/**
 * The main helper class, implemented as a global singleton. You can use this singleton to store and retrieve static app
 * configuration as well as text snippets. Furthermore it provides a simple interface for registering extra fields for
 * existing ExtJS models as well as for assembling shopware backend URLs.
 */
Ext.define('Shopware.apps.ViisonCommonApp', {

    singleton: true,
    alternateClassName: 'ViisonCommonApp',
    /**
     * An object containing all app config that was added via 'addConfig()'. The config is separated by namespace.
     */
    configNamespaces: {},
    /**
     * An object containing all snippets that were added via 'addSnippetNamespaces()'. The snippets are separated by
     * namespace.
     */
    snippetNamespaces: {},
    /**
     * An object containing all custom fields added via 'registerExtraModelFields()', grouped by model.
     */
    modelFieldExtensions: {},

    /**
     * Adds new config values for the given namespace. The given values are 'applied' to any existing config for the
     * same namespace. That is, any existing keys are overridden with new values and new keys are appended.
     *
     * @param String namespace
     * @param Object config
     */
    addConfig: function (namespace, config) {
        this.configNamespaces[namespace] = this.configNamespaces[namespace] || {};
        Ext.apply(this.configNamespaces[namespace], config);
    },

    /**
     * Returns a single config value for the given namespace and key. If no such value exists, the optionally
     * provided defaultValue is returned.
     *
     * @param String key
     * @param String namespace
     * @param Mixed defaultValue
     * @return Mixed
     */
    getConfig: function (key, namespace, defaultValue) {
        var config = this.configNamespaces[namespace] || {};

        return (key in config) ? config[key] : defaultValue;
    },

    /**
     * Adds new snippet namespaces to the container. The given namespaces parameter must be an object containing the
     * namespace as key an object containing name/snippet pairs as value. The given values are merged into any
     * existing snippets. That is, snippets of existing namespaces are appended and, for conflicting snippet names,
     * updated using the provided values.
     *
     * @param Object namespaces
     */
    addSnippetNamespaces: function (namespaces) {
        Ext.Object.merge(this.snippetNamespaces, namespaces);
    },

    /**
     * Returns a single snippet identified by the given namespace and key. If no such snippet exists, an empty
     * string is returned instead.
     *
     * @param String key
     * @param String namespace
     * @return string
     */
    getSnippet: function (key, namespace) {
        var snippets = this.snippetNamespaces[namespace] || {};

        return (key in snippets) ? snippets[key] : '';
    },

    /**
     * Adds the given fields to the internal collection of extra fields for the model with the given modelName. Any
     * elements contained in fields that is not already an instance of 'Ext.data.Field', is converted to instances of
     * that class.
     *
     * @param String modelName
     * @param Object[] fields
     */
    registerExtraModelFields: function (modelName, fields) {
        this.modelFieldExtensions[modelName] = this.modelFieldExtensions[modelName] || [];
        fields.forEach(
            function (field) {
                this.modelFieldExtensions[modelName].push((field.isField) ? field : new Ext.data.Field(field));
            },
            this
        );
    },

    /**
     * Determines whether any custom fields are registered for the class/model with the given className and, if they
     * are, adds those fields to the fields contained in the given classData.
     *
     * Note: You must not call this method directly, since the field injection is done in the custom postprocessor.
     *
     * @param String className
     * @param Object classData
     */
    applyExtraModelFields: function (className, classData) {
        if (className in this.modelFieldExtensions && classData.fields) {
            this.modelFieldExtensions[className].forEach(function (fieldToAdd) {
                // Only add fields that do not exist in the model yet.
                var fieldExists = classData.fields.items.some(function (existingField) {
                    return existingField.name === fieldToAdd.name;
                });
                if (!fieldExists) {
                    classData.fields.add(fieldToAdd);
                }
            }, this);
        }
    },

    /**
     * Gets the base URL of the current shopware environment from Ext.Loader and appends the path, if given. If an
     * object is passed as the second, optional argument 'params', they are converted to a query string and appended
     * to the constructed URL. Finally the URL is returned.
     *
     * @param String path (optional)
     * @param Object params (optional)
     * @return String
     */
    assembleBackendUrl: function (path, params) {
        // Assemble URL, starting with the loader's base URL
        var url = Ext.Loader.getPath('', 'Shopware.apps')[0].replace(/\/+$/, '');
        if (Ext.isString(path) && path.length > 0) {
            url += '/' + path.replace(/^\/+/, '');
        }
        if (Ext.isObject(params)) {
            var paramString = Ext.Object.toQueryString(params);
            url = Ext.String.urlAppend(url, paramString);
        }

        return url;
    },

}, function () {
    // Register a pre processor that applies any extra model fields to the class, if necessary
    Ext.ClassManager.defaultPostprocessors.push(function (className, cls, classData) {
        ViisonCommonApp.applyExtraModelFields(className, classData);
    });
});

Ext.onReady(function () {
    var viisonNamespace = 'Shopware.apps.Viison';
    var viisonNamespaceLength = viisonNamespace.length;
    var dependencyProperties = [
        'extend',
        'mixins',
        'requires',
    ];

    /*
     * Modify the global Ext.ClassManager to prevent Viison classes/overrides from being defined or loaded more than
     * once. Otherwise, if files are loaded by the ViisonCommonJSLoader and more than one sub app depends on the same
     * module, that dependency's code may be included more than once in the response.
     * The class manager is modified in three different ways:
     *
     *      1. By adding a new default postprocessor that blocks the class loading, if the class is part of the
     *         protected namespace and already loaded.
     *      2. By overriding the 'createOverride()' method to block the override loading, if the override is part of the
     *         protected namespace and already loaded.
     *      3. By overriding the 'singleton' postprocessor to prevent it from instantiating the class, if the class is
     *         part of the protected namespace and already loaded. This is important, because the default 'singleton'
     *         postprocessor always creates a new instance of the loaded class without checking whether an instance
     *         already exists (this looks like a bug in ExtJS).
     *
     * Remark: We restrict the modifications to class/override definitions in our own 'namespace'. That is, we check
     *         whether the name of the loading class has the prefix 'Shopware.apps.Viison', which should be used by all
     *         our classes.
     */

    // Closure to check if the given className belongs to an existing VIISON class
    var isRegisteredViisonClass = function (className) {
        return typeof className === 'string' && className.indexOf(viisonNamespace) === 0 && Ext.ClassManager.isCreated(className);
    };

    // Prevent classes from being defined more than once
    Ext.ClassManager.defaultPostprocessors.push(function (className) {
        return !isRegisteredViisonClass(className);
    });

    // Prevent overrides from being applied more than once
    var originalCreateOverride = Ext.ClassManager.createOverride;
    Ext.ClassManager.createOverride = function (className) {
        if (isRegisteredViisonClass(className)) {
            return this;
        }

        return originalCreateOverride.apply(this, arguments);
    };

    // Prevent singletons from being instantiated more than once
    var originalSingletonPostProcessor = Ext.ClassManager.postprocessors.singleton.fn;
    Ext.ClassManager.postprocessors.singleton.fn = function (className) {
        if (isRegisteredViisonClass(className)) {
            return false;
        }

        return originalSingletonPostProcessor.apply(this, arguments);
    };

    /*
     * Modify how classes are loaded, if they have dependencies, e.g. 'extends', that are part of the same namespace as
     * the class that is about to be loaded. Originally these dependencies are loaded by Shopware's class loaded by
     * sending a request to load their code. However, modules that are loaded by our JS loader do neither require nor
     * support these loading requests. Therefore we have to apply three overrides:
     *
     *      1. Override the 'loader' preprocessor of Ext.Class to be able to detect dependencies, e.g. 'extends', that
     *         are part of the same namespace as the class that is about to be loaded. These dependencies are added to
     *         a list of known dependencies that is stored in Ext.Loader.
     *      2. Override 'Ext.Loader.require', which is overridden by Shopware to apply their backend request based
     *         loading, to intercept any loading requests that should be handled locally. For more info on this, read
     *         the info in ./base/loader.js.
     *      3. Add a new 'onCreated' callback to Ext.ClassManager that checks for any classes whose loading was deferred
     *         until after the class, that was just created, is available. If any deferred classes are found, their
     *         respective loading callbacks are executed to continue their loading process.
     *
     * Remark: We restrict the modifications to class/override definitions in our own 'namespace'. That is, we check
     *         whether the name of the loading class has the prefix 'Shopware.apps.Viison', which should be used by all
     *         our classes.
     */

    // Override the 'loader' preprocessor of Ext.Class to be able to detect dependencies
    var originalLoaderPreProcessor = Ext.Class.preprocessors.loader.fn;
    Ext.Class.preprocessors.loader.fn = function (cls, data) {
        // Only apply the override for VIISON apps
        var className = Ext.ClassManager.getName(data);
        if (className.indexOf(viisonNamespace) === -1) {
            return originalLoaderPreProcessor.apply(this, arguments);
        }

        // Collect the names of all dependencies of the class that is about to be loaded
        var dependencyNames = [];
        Ext.Array.forEach(dependencyProperties, function (propertyName) {
            if (!Object.prototype.hasOwnProperty.call(data, propertyName)) {
                return;
            }

            var propertyValue = data[propertyName];
            if (Ext.isString(propertyValue)) {
                dependencyNames.push(propertyValue);
            } else if (Ext.isArray(propertyValue)) {
                dependencyNames = dependencyNames.concat(propertyValue);
            }
        });

        // Save all dependencies that are part of the same namespace as the current class in the class loader's list
        var nextSeparatorIndex = className.indexOf('.', viisonNamespaceLength);
        var appNamespace = (nextSeparatorIndex > 0) ? className.substr(0, nextSeparatorIndex) : className;
        Ext.Array.forEach(dependencyNames, function (dependencyName) {
            if (dependencyName.indexOf(appNamespace) === 0) {
                Ext.Loader.viisonDependencies[dependencyName] = Ext.Loader.viisonDependencies[dependencyName] || [];
            }
        });

        return originalLoaderPreProcessor.apply(this, arguments);
    };

    // Add a new callback to the class manager to finish any deferred class loading
    Ext.ClassManager.onCreated(function (className) {
        if (Ext.isArray(Ext.Loader.viisonDependencies[className])) {
            Ext.Array.forEach(Ext.Loader.viisonDependencies[className], function (callback) {
                callback.fn.call(callback.scope);
            });
            Ext.Loader.viisonDependencies[className] = [];
        }
    });
});

/**
 * Note: It is not possible to apply mixins when defining an 'override' in ExtJS 4. Therefore you must access the
 *       ViisonCommonApp singleton directly when overriding components.
 *
 * A mixin the provides super easy access to any config values and text snippets stored in the global ViisonCommonApp
 * singleton. Since most classes belong to a certain sub application and use only snippets of a single namespace, the
 * methods provided by this mixin don't expect a specific namespace as a parameter, but fallback to the namespace
 * configured in the component. To use this mixin and its automatic namespacing, add the following to your component:
 *
 *      // Activate the mixin
 *      mixins: [
 *          'Shopware.apps.ViisonCommonApp.Mixin',
 *      ],
 *      // Select a default config namespace
 *      viisonConfigNamespace: 'MyAppName',
 *      // Select a default snippet namespace
 *      viisonSnippetNamespace: 'backend/my_app_name/main',
 *
 * These extensions allow you to simply call 'this.getViisonConfig(<key>)' and 'this.getViisonSnippet(<key>)' to get a
 * config value or text snippet, respectively. In case you must access configuration or snippets from different
 * namespaces the ones configured, you can add the desired namespace as a second parameter to these function calls.
 */
Ext.define('Shopware.apps.ViisonCommonApp.Mixin', {

    /**
     * Returns the config value for the given key. If the namespace parameter is provided, the config value is
     * retrieved from that namespace. Otherwise the 'viisonConfigNamespace' of the calling component is used.
     *
     * @param String key
     * @param String namespace (optional)
     * @return Mixed
     */
    getViisonConfig: function (key, namespace) {
        var configNamespace;
        if (namespace) {
            configNamespace = namespace;
        } else if (Ext.isString(this.viisonConfigNamespace) && this.viisonConfigNamespace.length > 0) {
            configNamespace = this.viisonConfigNamespace;
        } else {
            return undefined;
        }

        return ViisonCommonApp.getConfig(key, configNamespace);
    },

    /**
     * Returns the text snippet for the given key. If the namespace parameter is provided, the snippet is retrieved
     * from that namespace. Otherwise the 'viisonSnippetNamespace' of the calling component is used.
     *
     * @param String key
     * @param String namespace (optional)
     * @return String
     */
    getViisonSnippet: function (key, namespace) {
        var snippetNamespace;
        if (namespace) {
            snippetNamespace = namespace;
        } else if (Ext.isString(this.viisonSnippetNamespace) && this.viisonSnippetNamespace.length > 0) {
            snippetNamespace = this.viisonSnippetNamespace;
        } else {
            return '';
        }

        return ViisonCommonApp.getSnippet(key, snippetNamespace);
    },

});
