/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.

Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.

You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/

"use strict";

fluid.registerNamespace("fluid.prefs");

/*******************************************************************************
 * Base auxiliary schema grade
 *******************************************************************************/

/**
 * The Auxiliary Schema configuration for a preference's corresponding panel (adjuster) component. Any additional
 * properties, other than those specified below, will be passed along as top level options to the panel component.
 *
 * @typedef {Object} PanelAuxConfig
 * @property {String} type - The grade name for the panel component
 * @property {Selector} container - The CSS selector to find a single DOM element to use as the component's
 *                                  container.
 * @property {URL} template - The URL path to the HTML template used by the component. It may contain string
 *                            templating tokens (%tokenName) which will be expanded with values stored in the
 *                            {AuxiliarySchema} terms Object.
 * @property {URL} message - The URL path to the JSON message bundle used by the component. It may contain string
 *                           templating tokens (%tokenName) which will be expanded with values stored in the
 *                           {AuxiliarySchema} terms Object.
 */

/**
 * The Auxiliary Schema configuration for a preference's corresponding enactor component. Any additional properties,
 * other than those specified below, will be passed along as top level options to the enactor component.
 *
 * @typedef {Object} EnactorAuxConfig
 * @property {String} type - The grade name for the enactor component
 * @property {Selector} [container] - The CSS selector to find a single DOM element to use as the component's
 *                                    container.
 */

/**
 * The Auxiliary Schema configuration for a preference
 *
 * @typedef {Object} PreferenceAuxConfig
 * @property {String} [alias] - An alternative name to use for model values. This is useful to store or access
 *                            model values from specific property. Model values for the preference are also stored
 *                            at a property named after the preferences. Typically by replacing any "." with "_".
 * @property {EnactorAuxConfig} [enactor] - The configuration for the related enactor component
 * @property {PanelAuxConfig} [panel] - The configuration for the related panel (adjuster) component
 */

/**
 * The configuration of the preference panels within a composite panel. The `always` property identifies the
 * the preferences that should always be visible. These always visible preferences can, in turn, be used to
 * identify which other preferences in the composite panel are displayed when it is enabled. To define this, the
 * key {String} is the name of the always on preference, and the value {String[]} is a list of preferences to be
 * displayed when it is enabled.
 *
 * @typedef {Object} CompositePanelPanelsDefinition
 * @property {String[]} always - An array of preferences for whose related panel should always be visible in the
 *                               composite panel.
 */

/**
 * The Auxiliary Schema configuration composing multiple preferences into a single panel. Can also configure a
 * preference to control the visibility of the other preference adjusters contained within the same panel.
 *
 * @typedef {Object} CompositePanelAuxConfig
 * @property {Object} type - The grade name for the composite panel component
 * @property {Selector} container - The CSS selector to find a single DOM element to use as the component's
 *                                  container.
 * @property {URL} template - The URL path to the HTML template used by the component. It may contain string
 *                            templating tokens (%tokenName) which will be expanded with values stored in the
 *                            {AuxiliarySchema} terms Object.
 * @property {URL} message - The URL path to the JSON message bundle used by the component. It may contain string
 *                           templating tokens (%tokenName) which will be expanded with values stored in the
 *                           {AuxiliarySchema} terms Object.
 * @property {CompositePanelPanelsDefinition} panels - The definition of which panels are to be included in the
 */

/**
 * A set of {CompositePanelAuxConfig} defining how to compose groups of preference panels. The key can be any
 * {String} name that appropriately identifies the grouping of preferences.
 *
 * @typedef {Object} CompositePanelsAuxConfig
 */

/**
 * The Auxiliary Schema provides the configuration for a preference editor including the information for which
 * preferences to configure with which panels (adjusters) and enactors. Auxiliary schemas can be merged together
 * allowing for individual auxiliary schemas for each preference; which are combined together for a particular
 * preference editor/enhancer instance. In addition the properties specified below, {PreferenceAuxConfig} should be
 * added, using the preference name as the key, to define which preferences to use and how they are configured.
 *
 * @typedef {Object} AuxiliarySchema
 * @property {String[]} [loaderGrades] - An array of grade names that will be applied to the PrefsEditorLoader component.
 *                                     This can be used to set the type of prefs editor used (e.g. separated panel,
 *                                     full page, full page with preview).
 * @property {Boolean} [generatePanelContainers] - Indicate if the panel containers should be generated. If false
 *                                                 it is expected that the supplied template alread contains all of
 *                                                 the necessary container elements.
 * @property {URL} [template] - The URL path to the HTML template used by the Preference Editor. It may contain string
 *                            templating tokens (%tokenName) which will be expanded with values stored in the
 *                            `terms` property.
 * @property {URL} [message] - The URL path to the JSON message bundle used by the Preference Editor. It may contain
 *                           string templating tokens (%tokenName) which will be expanded with values stored in the
 *                           `terms` property.
 * @property {Object} [terms] - An object containing key/value pairs of tokens/path segments for interpolating into
 *                              the various `template` and `message` URLs.
 * @property {String} [namespace] - A namespace to use for the generated grades.
 * @property {CompositePanelsAuxConfig} [groups] - The configuration defining the composition of groups of preferences
 */

/**
 * A processed, or processing, version of an {AuxiliarySchema}. The defined {PreferenceAuxConfig} preferences
 * remain, but other top level properties are transformed into the various grade definition options.
 *
 * @typedef {Object} AuxSchema
 * @property {Object} templateLoader - Definition for constructing the `templateLoader` component
 * @property {Object} messageLoader - Definition for constructing the `messageLoader` component
 * @property {Object} terms - Definition for constructing a grade containing the `terms` options
 * @property {String} namespace - A namespace to use for the generated grades.
 * @property {Object} [panels] - Definition for constructing the Preference Editor component, including the
 *                               required panels (adjusters).
 * @property {Object} [enactors] - Definition for constructing the UI Enhancer component, including the required
 *                                 enactors.
 * @property {Object} [initialModel] - Definition for constructing a grade containing the default model values of
 *                                     the included preferences.
 * @property {Object} [aliases_prefsEditor] - Definition for constructing a grade containing the model relays from
 *                                            the preference model values to their aliases. Applied to the
 *                                            Preference Editor model.
 * @property {Object} [aliases_enhancer] - Definition for constructing a grade containing the model relays from
 *                                         the preference model values to their aliases. Applied to the UI Enhancer
 *                                         model.
 */

fluid.defaults("fluid.prefs.auxSchema", {
    gradeNames: ["fluid.component"],
    auxiliarySchema: {}
});

fluid.prefs.mergeAtPath = function (root, path, object) {
    var existingObject = fluid.get(root, path);
    fluid.set(root, path, $.extend(true, {}, existingObject, object));

    return root;
};

// only works with top level elements
fluid.prefs.removeKey = function (root, key) {
    var value = root[key];
    delete root[key];
    return value;
};

/**
 * Removes the key/value at the specified `path` from the supplied `root` object. It is returned in a new object
 * either at the same path or at the provided `extractedPath`.
 *
 * @param  {Object} root - The source object to extract from
 * @param  {String|String[]} path - The path to be extracted from the `root` object
 * @param  {String|String[]} [extractedPath] - The path to write the extracted value to in the returned object
 * @return {Object} - An object containing the extracted value
 */
fluid.prefs.extract = function (root, path, extractedPath) {
    extractedPath = fluid.isValue(extractedPath) ? extractedPath : path;
    var result = {};
    var sourceValue = fluid.prefs.removeKey(root, path);
    if (sourceValue) {
        fluid.set(result, extractedPath, sourceValue);
    }
    return result;
};

/**
 * Applies a set of options onto the supplied `root` object at the specified `path`. Values are typically replaced
 * with the exception of `gradeNames` properties, which are concatenated together. The `commonOptions` can also
 * contain string tokens (%tokenName) which will be replaced with the related values from the `templateValues`.
 * @param  {Object} root - Any object, but typically a Component option definition or portion there of.
 * @param  {String|String[]} path - The EL path to property within the `root` to apply the `commonOptions`
 * @param  {Object} commonOptions - The options to apply to the `root` object at the specified `path`. May include
 *                                  string tokens (%tokenName) to be sourced from `templateValues`.
 * @param  {Object.<String.String>} templateValues - A map of token names to the values which should be interpolated
 * @return {Object|undefined} - When successful nothing (Undefined) is returned and the `root` object is modified
 *                              directly. When there is no existing value in the `root` object at the specified
 *                              `path`, the `root` object itself is returned without any modifications.
 */
fluid.prefs.addCommonOptions = function (root, path, commonOptions, templateValues) {
    templateValues = templateValues || {};

    var existingValue = fluid.get(root, path);

    // TODO: Why is the early return on no "existingValue" so important? Why is root returned?
    if (!existingValue) {
        return root;
    }

    var opts = {}, mergePolicy = {};

    fluid.each(commonOptions, function (value, key) {
        // Adds "container" option only for view and renderer components
        if (key === "container") {
            var componentType = fluid.get(root, [path, "type"]);
            var componentOptions = fluid.defaults(componentType);
            // Note that this approach is not completely reliable, although it has been reviewed as "good enough" -
            // a grade which modifies the creation signature of its principal type would cause numerous other problems.
            // We can review this awkward kind of "anticipatory logic" when the new renderer arrives.
            if (fluid.get(componentOptions, ["argumentMap", "container"]) === undefined) {
                return false;
            }
        }
        // Merge grade names defined in aux schema and system default grades
        if (key.indexOf("gradeNames") !== -1) {
            mergePolicy[key] = fluid.arrayConcatPolicy;
        }

        key = fluid.stringTemplate(key, templateValues);
        value = typeof(value) === "string" ? fluid.stringTemplate(value, templateValues) : value;

        fluid.set(opts, key, value);
    });

    fluid.set(root, path, fluid.merge(mergePolicy, existingValue, opts));
};

fluid.prefs.checkPrimarySchema = function (primarySchema, prefKey) {
    if (!primarySchema) {
        fluid.fail("The primary schema for " + prefKey + " is not defined.");
    }
    return !!primarySchema;
};

fluid.prefs.flattenName = function (name) {
    var regexp = new RegExp("\\.", "g");
    return name.replace(regexp,  "_");
};

/**
 * Adds implicit model relay definitions for preference aliases to the `auxSchema`.
 *
 * @param  {AuxSchema} auxSchema - The {AuxiliarySchema} being processed to add the model relay definitions to
 * @param  {String} flattenedPrefKey - The preference to associate with the alias. The preference name must have
 *                                     past through fluid.prefs.flattenName to ensure that is is in a format that
 *                                     is safe to use in IoC expressions.
 * @param  {String|String[]} aliases - One or more model names to use as model properties where the preference's
 *                                     model value will be linked.
 */
fluid.prefs.constructAliases = function (auxSchema, flattenedPrefKey, aliases) {
    aliases = fluid.makeArray(aliases);

    var prefsEditorModel = {};
    var enhancerModel = {};
    fluid.each(aliases, function (alias) {
        prefsEditorModel[alias] = "{that}.model.preferences." + flattenedPrefKey;
        enhancerModel[alias] = "{that}.model." + flattenedPrefKey;
    });

    fluid.prefs.mergeAtPath(auxSchema, ["aliases_prefsEditor", "model", "preferences"], prefsEditorModel);
    fluid.prefs.mergeAtPath(auxSchema, ["aliases_enhancer", "model"], enhancerModel);
};

/**
 * Expands the configuration from the {AuxiliarySchema} related to configuring the panel and enactor components for
 * the various preferences.
 *
 * @param  {AuxSchema} auxSchema - The {AuxiliarySchema} being processed to expand the component definitions into
 * @param  {String} type - Identifies the type of component that is being expanded; either `panels` or `enactors`
 * @param  {String} prefKey - The preference name.
 * @param  {String} alias - A model property to alias the preferences model value to
 * @param  {PanelAuxConfig|EnactorAuxConfig} componentConfig - Component configuration for the preference's enactor
 *                                                             or panel component.
 * @param  {Object} commonOptions - Additional configuration needed to mix into the enactor or panel component.
 * @param  {Object} modelCommonOptions - Configuration for relaying the enactor or panel component's model to the
 *                                       one used by the parent UI Enhancer or Prefs Editor respectively. The
 *                                       configuration can include string tokens for "%internalModelName" and
 *                                       "%externalModelName" which will be interpolated with values from the
 *                                       `preferenceMap` and the flattened preference name.
 * @param  {PrimarySchema} mappedDefaults - A primary schema for the preference
 * @return {AuxSchema} - Returns the supplied `auxSchema` with the additional modifications
 */
fluid.prefs.expandSchemaComponents = function (auxSchema, type, prefKey, alias, componentConfig, commonOptions, modelCommonOptions, mappedDefaults) {
    var componentOptions = fluid.copy(componentConfig) || {};
    var components = {};
    var initialModel = {};

    var componentName = fluid.prefs.removeKey(componentOptions, "type");
    var memberName = fluid.prefs.flattenName(componentName);
    var flattenedPrefKey = fluid.prefs.flattenName(prefKey);

    if (componentName) {

        components[memberName] = {
            type: componentName,
            options: componentOptions
        };

        var selectors = {};
        // only need to generate containers for panels
        if (auxSchema.generatePanelContainers && type === "panels") {
            fluid.prefs.removeKey(componentOptions, "container");
            selectors[memberName] = ".flc-prefsEditor-" + flattenedPrefKey;
        } else {
            selectors = fluid.prefs.extract(componentOptions, "container", memberName);
        }

        var prefToMemberMap = {};
        prefToMemberMap[prefKey] = memberName;

        var templates = fluid.prefs.extract(componentOptions, "template", memberName);
        var messages = fluid.prefs.extract(componentOptions, "message", memberName);

        var preferenceMap = fluid.defaults(componentName).preferenceMap;

        var map = preferenceMap[prefKey];
        var prefSchema = mappedDefaults[prefKey];

        fluid.each(map, function (primaryPath, internalPath) {
            if (fluid.prefs.checkPrimarySchema(prefSchema, prefKey)) {
                var opts = {};
                if (internalPath.indexOf("model.") === 0 && primaryPath === "value") {
                    var internalModelName = internalPath.slice(6);
                    // Set up the binding in "rules" accepted by the modelRelay base grade of every panel
                    fluid.set(opts, "model", fluid.get(opts, "model") || {});
                    fluid.prefs.addCommonOptions(opts, "model", modelCommonOptions, {
                        internalModelName: internalModelName,
                        externalModelName: flattenedPrefKey
                    });
                    fluid.set(initialModel, ["members", "initialModel", "preferences", flattenedPrefKey], prefSchema["default"]);

                    if (alias) {
                        fluid.set(initialModel, ["members", "initialModel", "preferences", alias], prefSchema["default"]);
                    }
                } else {
                    fluid.set(opts, internalPath, prefSchema[primaryPath]);
                }
                $.extend(true, componentOptions, opts);
            }
        });

        fluid.prefs.addCommonOptions(components, memberName, commonOptions, {
            prefKey: memberName
        });

        fluid.prefs.mergeAtPath(auxSchema, [type, "components"], components);
        fluid.prefs.mergeAtPath(auxSchema, [type, "selectors"], selectors);
        fluid.prefs.mergeAtPath(auxSchema, [type, "prefToMemberMap"], prefToMemberMap);
        fluid.prefs.mergeAtPath(auxSchema, ["templateLoader", "resources"], templates);
        fluid.prefs.mergeAtPath(auxSchema, ["messageLoader", "resources"], messages);
        fluid.prefs.mergeAtPath(auxSchema, "initialModel", initialModel);

        fluid.prefs.constructAliases(auxSchema, flattenedPrefKey, alias);
    }

    return auxSchema;
};

/**
 * Expands the configuration from the {AuxiliarySchema} related to configuring composite panels. Composite panels
 * allow for expressing a panel that contains multiple preferences and optionally a preference that controls
 * whether the other preference panels are displayed.
 *
 * @param  {AuxSchema} auxSchema - The {AuxiliarySchema} being processed to expand the composite panel definitions
 * @param  {CompositePanelsAuxConfig} compositePanelList - The groups of preference panels.
 * @param  {Object} panelCommonOptions - Additional configuration needed to mix into the panel component.
 * @param  {Object} subPanelCommonOptions - Additional configuration needed by sub panels.
 * @param  {Object} compositePanelBasedOnSubCommonOptions - [description]
 * @param  {Object} panelModelCommonOptions - Configuration for relaying the panel component's model to the one used
 *                                            by the parent Prefs Editor. The configuration can include string
 *                                            tokens for "%internalModelName" and "%externalModelName" which will be
 *                                            interpolated with values from the `preferenceMap` and the flattened
 *                                            preference name.
 * @param  {PrimarySchema} mappedDefaults - A primary schema for the preference
 * @return {AuxSchema} - Returns the supplied `auxSchema` with the additional modifications
 */
fluid.prefs.expandCompositePanels = function (auxSchema, compositePanelList, panelCommonOptions, subPanelCommonOptions,
    compositePanelBasedOnSubCommonOptions, panelModelCommonOptions, mappedDefaults) {
    var panelsToIgnore = [];

    fluid.each(compositePanelList, function (compositeDetail, compositeKey) {
        var compositePanelOptions = {};
        var components = {};
        var initialModel = {};
        var selectors = {};
        var templates = {};
        var messages = {};
        var selectorsToIgnore = [];

        var thisCompositeOptions = fluid.copy(compositeDetail);
        fluid.set(compositePanelOptions, "type", thisCompositeOptions.type);
        delete thisCompositeOptions.type;

        selectors = fluid.prefs.extract(thisCompositeOptions, "container", compositeKey);
        templates = fluid.prefs.extract(thisCompositeOptions, "template", compositeKey);
        messages = fluid.prefs.extract(thisCompositeOptions, "message", compositeKey);

        var subPanelList = []; // list of subpanels to generate options for
        var subPanels = {};
        var subPanelRenderOn = {};

        // thisCompositeOptions.panels can be in two forms:
        // 1. an array of names of panels that should always be rendered;
        // 2. an object that describes what panels should always be rendered,
        //    and what panels should be rendered when a preference is turned on
        // The loop below is only needed for processing the latter.
        if (fluid.isPlainObject(thisCompositeOptions.panels) && !fluid.isArrayable(thisCompositeOptions.panels)) {
            fluid.each(thisCompositeOptions.panels, function (subpanelArray, pref) {
                subPanelList = subPanelList.concat(subpanelArray);
                if (pref !== "always") {
                    fluid.each(subpanelArray, function (onePanel) {
                        fluid.set(subPanelRenderOn, onePanel, pref);
                    });
                }
            });
        } else {
            subPanelList = thisCompositeOptions.panels;
        }

        fluid.each(subPanelList, function (subPanelID) {
            panelsToIgnore.push(subPanelID);
            var safeSubPanelPrefsKey = fluid.prefs.subPanel.safePrefKey(subPanelID);
            selectorsToIgnore.push(safeSubPanelPrefsKey);

            var subPanelOptions = fluid.copy(fluid.get(auxSchema, [subPanelID, "panel"]));
            var subPanelType = fluid.get(subPanelOptions, "type");

            fluid.set(subPanels, [safeSubPanelPrefsKey, "type"], subPanelType);
            var renderOn = fluid.get(subPanelRenderOn, subPanelID);
            if (renderOn) {
                fluid.set(subPanels, [safeSubPanelPrefsKey, "options", "renderOnPreference"], renderOn);
            }

            // Deal with preferenceMap related options
            var map = fluid.defaults(subPanelType).preferenceMap[subPanelID];
            var prefSchema = mappedDefaults[subPanelID];

            fluid.each(map, function (primaryPath, internalPath) {
                if (fluid.prefs.checkPrimarySchema(prefSchema, subPanelID)) {
                    var opts;
                    if (internalPath.indexOf("model.") === 0 && primaryPath === "value") {
                        // Set up the binding in "rules" accepted by the modelRelay base grade of every panel
                        fluid.set(compositePanelOptions, ["options", "model"], fluid.get(compositePanelOptions, ["options", "model"]) || {});
                        fluid.prefs.addCommonOptions(compositePanelOptions, ["options", "model"], panelModelCommonOptions, {
                            internalModelName: safeSubPanelPrefsKey,
                            externalModelName: safeSubPanelPrefsKey
                        });
                        fluid.set(initialModel, ["members", "initialModel", "preferences", safeSubPanelPrefsKey], prefSchema["default"]);
                    } else {
                        opts = opts || {options: {}};
                        fluid.set(opts, "options." + internalPath, prefSchema[primaryPath]);
                    }
                    $.extend(true, subPanels[safeSubPanelPrefsKey], opts);
                }
            });

            fluid.set(templates, safeSubPanelPrefsKey, fluid.get(subPanelOptions, "template"));
            fluid.set(messages, safeSubPanelPrefsKey, fluid.get(subPanelOptions, "message"));

            fluid.set(compositePanelOptions, ["options", "selectors", safeSubPanelPrefsKey], fluid.get(subPanelOptions, "container"));
            fluid.set(compositePanelOptions, ["options", "members", "resources"], fluid.get(compositePanelOptions, ["options", "members", "resources"]) || {});

            fluid.prefs.addCommonOptions(compositePanelOptions.options, "members.resources", compositePanelBasedOnSubCommonOptions, {
                subPrefKey: safeSubPanelPrefsKey
            });

            // add additional options from the aux schema for subpanels
            delete subPanelOptions.type;
            delete subPanelOptions.template;
            delete subPanelOptions.message;
            delete subPanelOptions.container;
            fluid.set(subPanels, [safeSubPanelPrefsKey, "options"], $.extend(true, {}, fluid.get(subPanels, [safeSubPanelPrefsKey, "options"]), subPanelOptions));

            fluid.prefs.addCommonOptions(subPanels, safeSubPanelPrefsKey, subPanelCommonOptions, {
                compositePanel: compositeKey,
                prefKey: safeSubPanelPrefsKey
            });
        });
        delete thisCompositeOptions.panels;

        // add additional options from the aux schema for the composite panel
        fluid.set(compositePanelOptions, ["options"], $.extend(true, {}, compositePanelOptions.options, thisCompositeOptions));
        fluid.set(compositePanelOptions, ["options", "selectorsToIgnore"], selectorsToIgnore);
        fluid.set(compositePanelOptions, ["options", "components"], subPanels);

        components[compositeKey] = compositePanelOptions;

        fluid.prefs.addCommonOptions(components, compositeKey, panelCommonOptions, {
            prefKey: compositeKey
        });

        // Add onto auxSchema
        fluid.prefs.mergeAtPath(auxSchema, ["panels", "components"], components);
        fluid.prefs.mergeAtPath(auxSchema, ["panels", "selectors"], selectors);
        fluid.prefs.mergeAtPath(auxSchema, ["templateLoader", "resources"], templates);
        fluid.prefs.mergeAtPath(auxSchema, ["messageLoader", "resources"], messages);
        fluid.prefs.mergeAtPath(auxSchema, "initialModel", initialModel);
        $.extend(true, auxSchema, {panelsToIgnore: panelsToIgnore});
    });

    return auxSchema;
};

/**
 * Processes the auxiliary schema to output an object that contains all grade component definitions required for
 * building the preferences editor, uiEnhancer and the settings store. These grade components are: panels, enactors,
 * initialModel, messageLoader, templateLoader, terms, aliases_prefsEditor, and aliases_enhancer. These grades are
 * consumed and integrated by a `fluid.prefs.builder` component.
 *
 * @param  {String[]} [requestedPrefs] - The preferences requested by the builder
 * @param  {AuxiliarySchema} schemaToExpand - The auxiliary schema to process
 * @param  {Object} topCommonOptions - component options to apply to the various grade components.
 * @param  {Object} elementCommonOptions - component options mixed into the various
 * @param  {PrimarySchema} mappedDefaults - A Primary Schema for the requested preferences.
 * @return {AuxSchema} - The processed auxiliary schema
 */
fluid.prefs.expandSchema = function (requestedPrefs, schemaToExpand, topCommonOptions, elementCommonOptions, mappedDefaults) {
    var schemaIndex = fluid.indexDefaults("auxSchemaIndex", {
        gradeNames: "fluid.prefs.auxSchema",
        indexFunc: "fluid.prefs.auxBuilder.defaultSchemaIndexer"
    });
    var auxGradeNames = fluid.prefs.auxBuilder.auxGradesForPrefs(schemaIndex, requestedPrefs);
    var auxGrades = auxGradeNames.map(function (gradeName) {
        return fluid.defaults(gradeName).auxiliarySchema;
    });

    var auxSchema = fluid.extend(true, {}, ...auxGrades, schemaToExpand);

    auxSchema.namespace = auxSchema.namespace || "fluid.prefs.created_" + fluid.allocateGuid();

    var terms = fluid.prefs.removeKey(auxSchema, "terms");
    if (terms) {
        fluid.set(auxSchema, ["terms", "terms"], terms);
    }

    var compositePanelList = fluid.get(auxSchema, "groups");
    if (compositePanelList) {
        fluid.prefs.expandCompositePanels(auxSchema, compositePanelList,
            fluid.get(elementCommonOptions, "panel"), fluid.get(elementCommonOptions, "subPanel"),
            fluid.get(elementCommonOptions, "compositePanelBasedOnSub"), fluid.get(elementCommonOptions, "panelModel"),
            mappedDefaults);
    }

    fluid.each(auxSchema, function (category, prefName) {
        // TODO: Replace this cumbersome scheme with one based on an extensible lookup to handlers
        if (fluid.isValue(category)) {
            var panelSchemaConfig = category.panel;
            // Ignore the subpanels that are only for composing composite panels
            if (panelSchemaConfig && !(auxSchema.panelsToIgnore || []).includes(prefName)) {
                fluid.prefs.expandSchemaComponents(auxSchema, "panels", prefName, category.alias, panelSchemaConfig,
                    fluid.get(elementCommonOptions, "panel"), fluid.get(elementCommonOptions, "panelModel"), mappedDefaults);
            }

            var enactorSchemaConfig = category.enactor;
            if (enactorSchemaConfig) {
                fluid.prefs.expandSchemaComponents(auxSchema, "enactors", prefName, category.alias, enactorSchemaConfig,
                    fluid.get(elementCommonOptions, "enactor"), fluid.get(elementCommonOptions, "enactorModel"), mappedDefaults);
            }

            fluid.each(["template", "message"], function (type) {
                if (prefName === type) {
                    fluid.set(auxSchema, [type + "Loader", "resources", "prefsEditor"], category);
                    delete auxSchema[type];
                }
            });
        }
    });

    // Remove subPanels array. It is to keep track of the panels that are only used as sub-components of composite panels.
    if (auxSchema.panelsToIgnore) {
        delete auxSchema.panelsToIgnore;
    }

    // Add top common options
    fluid.each(topCommonOptions, function (topOptions, type) {
        fluid.prefs.addCommonOptions(auxSchema, type, topOptions);
    });

    return auxSchema;
};

fluid.defaults("fluid.prefs.auxBuilder", {
    gradeNames: ["fluid.component"],
    mergePolicy: {
        elementCommonOptions: "noexpand"
    },
    // A list of all requested preferences, to be supplied by an integrator or concrete grade.
    // Typically provided through the `fluid.prefs.builder` grade.
    // requestedPrefs: [],
    topCommonOptions: {
        panels: {
            gradeNames: ["fluid.prefs.prefsEditor"]
        },
        enactors: {
            gradeNames: ["fluid.uiEnhancer"]
        },
        templateLoader: {
            gradeNames: ["fluid.resourceLoader"]
        },
        messageLoader: {
            gradeNames: ["fluid.resourceLoader"]
        },
        initialModel: {
            gradeNames: ["fluid.prefs.initialModel"]
        },
        terms: {
            gradeNames: ["fluid.component"]
        },
        aliases_prefsEditor: {
            gradeNames: ["fluid.modelComponent"]
        },
        aliases_enhancer: {
            gradeNames: ["fluid.modelComponent"]
        }
    },
    elementCommonOptions: {
        panel: {
            "createOnEvent": "onPrefsEditorMarkupReady",
            "container": "{prefsEditor}.dom.%prefKey",
            "options.gradeNames": "fluid.prefs.prefsEditorConnections",
            "options.members.resources.template": "{templateLoader}.resources.%prefKey",
            "options.messageBase": "{messageLoader}.resources.%prefKey.parsed"
        },
        panelModel: {
            "%internalModelName": "{prefsEditor}.model.preferences.%externalModelName"
        },
        compositePanelBasedOnSub: {
            "%subPrefKey": "{templateLoader}.resources.%subPrefKey"
        },
        subPanel: {
            "container": "{%compositePanel}.dom.%prefKey",
            "options.messageBase": "{messageLoader}.resources.%prefKey.parsed"
        },
        enactor: {
            "container": "{uiEnhancer}.container"
        },
        enactorModel: {
            "%internalModelName": "{uiEnhancer}.model.%externalModelName"
        }
    },
    auxiliarySchema: {
        "loaderGrades": ["fluid.prefs.separatedPanel"],
        "generatePanelContainers": true,
        "template": "%templatePrefix/SeparatedPanelPrefsEditor.html",
        "message": "%messagePrefix/prefsEditor.json",
        "terms": {
            "templatePrefix": "../../framework/preferences/html",
            "messagePrefix": "../../framework/preferences/messages"
        }
    },
    auxSchema: {
        expander: {
            func: "fluid.prefs.expandSchema",
            args: [
                "{that}.options.requestedPrefs",
                "{that}.options.auxiliarySchema",
                "{that}.options.topCommonOptions",
                "{that}.options.elementCommonOptions",
                "{that}.options.schema.properties"
            ]
        }
    }
});

/**
 * An index function that indexes all schema grades based on their
 * preference name.
 * @param {Object} defaults -  Registered defaults for a schema grade.
 * @return {String} - A preference name.
 */
fluid.prefs.auxBuilder.defaultSchemaIndexer = function (defaults) {
    var censoredKeys = ["defaultLocale", "groups", "loaderGrades", "message", "template", "terms"];
    return fluid.keys(fluid.censorKeys(defaults.auxiliarySchema, censoredKeys));
};

/**
 * An invoker method that builds a list of grades that comprise a final version of the primary schema.
 * @param {Object} schemaIndex - A global index of all schema grades registered with the framework.
 * @param {String[]} preferences - A list of the requested preferences.
 * @return {String[]} - A list of aux schema grades.
 */
fluid.prefs.auxBuilder.auxGradesForPrefs = function (schemaIndex, preferences) {
    var auxSchema = [];
    // Lookup all available schema grades from the index that match the requested preference names.
    fluid.each(preferences, function merge(pref) {
        var schemaGrades = schemaIndex[pref];
        if (schemaGrades) {
            auxSchema = auxSchema.concat(schemaGrades);
        }
    });
    return auxSchema;
};
