/*!
 * Fluid Infusion v4.8.0
 *
 * Infusion is distributed under the Educational Community License 2.0 and new BSD licenses:
 * http://wiki.fluidproject.org/display/fluid/Fluid+Licensing
 *
 * 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
 */
/*
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

Includes code from Underscore.js 1.4.3
http://underscorejs.org
(c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
Underscore may be freely distributed under the MIT license.
*/

"use strict";

var fluid = fluid || {}; // eslint-disable-line no-redeclare

fluid.version = "Infusion 4.8.0";

// Export this for use in environments like node.js, where it is useful for
// configuring stack trace behaviour
fluid.Error = Error;

fluid.environment = {
    fluid: fluid
};

fluid.global = fluid.global || typeof window !== "undefined" ?
    window : typeof self !== "undefined" ? self : {};

// A standard utility to schedule the invocation of a function after the current
// stack returns. On browsers this defaults to setTimeout(func, 0) but in
// other environments can be customised - e.g. to process.nextTick in node.js
// In future, this could be optimised in the browser to not dispatch into the event queue
// See https://github.com/YuzuJS/setImmediate for a more verbose but very robust replacement
fluid.invokeLater = function (func) {
    return setTimeout(func, 0);
};

// The following flag defeats all logging/tracing activities in the most performance-critical parts of the framework.
// This should really be performed by a build-time step which eliminates calls to pushActivity/popActivity and fluid.log.
fluid.defeatLogging = true;

// This flag enables the accumulating of all "activity" records generated by pushActivity into a running trace, rather
// than removing them from the stack record permanently when receiving popActivity. This trace will be consumed by
// visual debugging tools.
fluid.activityTracing = false;
fluid.activityTrace = [];

var activityParser = /(%\w+)/g;

fluid.renderActivityArgument = function (arg) {
    if (fluid.isComponent(arg)) {
        return fluid.dumpComponentAndPath(arg);
    } else {
        return arg;
    }
};

// Renders a single activity element in a form suitable to be sent to a modern browser's console
// unsupported, non-API function
fluid.renderOneActivity = function (activity, nowhile) {
    var togo = nowhile === true ? [] : ["    while "];
    var message = activity.message;
    var index = activityParser.lastIndex = 0;
    while (true) {
        var match = activityParser.exec(message);
        if (match) {
            var key = match[1].substring(1);
            togo.push(message.substring(index, match.index));
            togo.push(fluid.renderActivityArgument(activity.args[key]));
            index = activityParser.lastIndex;
        }
        else {
            break;
        }
    }
    if (index < message.length) {
        togo.push(message.substring(index));
    }
    return togo;
};

// Renders an activity stack in a form suitable to be sent to a modern browser's console
// unsupported, non-API function
fluid.renderActivity = function (activityStack, renderer) {
    renderer = renderer || fluid.renderOneActivity;
    return fluid.transform(activityStack, renderer);
};

// Definitions for ThreadLocals - lifted here from
// FluidIoC.js so that we can issue calls to fluid.describeActivity for debugging purposes
// in the core framework

// unsupported, non-API function
fluid.singleThreadLocal = function (initFunc) {
    var value = initFunc();
    return function (newValue) {
        return newValue === undefined ? value : value = newValue;
    };
};

// Currently we only support single-threaded environments - ensure that this function
// is not used on startup so it can be successfully monkey-patched
// only remaining uses of threadLocals are for activity reporting and in the renderer utilities
// unsupported, non-API function
fluid.threadLocal = fluid.singleThreadLocal;

// unsupported, non-API function
fluid.globalThreadLocal = fluid.threadLocal(function () {
    return {};
});

// Return an array of objects describing the current activity
// unsupported, non-API function
fluid.getActivityStack = function () {
    var root = fluid.globalThreadLocal();
    if (!root.activityStack) {
        root.activityStack = [];
    }
    return root.activityStack;
};

// Return an array of objects describing the current activity
// unsupported, non-API function
fluid.describeActivity = fluid.getActivityStack;

// Renders either the current activity or the supplied activity to the console
fluid.logActivity = function (activity) {
    activity = activity || fluid.describeActivity();
    var rendered = fluid.renderActivity(activity).reverse();
    if (rendered.length > 0) {
        fluid.log("Current activity: ");
        fluid.each(rendered, function (args) {
            fluid.log.apply(null, args);
        });
    }
};

// Execute the supplied function with the specified activity description pushed onto the stack
// unsupported, non-API function
fluid.pushActivity = function (type, message, args) {
    var record = {type: type, message: message, args: args, time: new Date().getTime()};
    if (fluid.activityTracing) {
        fluid.activityTrace.push(record);
    }
    if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
        fluid.log.apply(null, fluid.renderOneActivity(record, true));
    }
    var activityStack = fluid.getActivityStack();
    activityStack.push(record);
};

// Undo the effect of the most recent pushActivity, or multiple frames if an argument is supplied
fluid.popActivity = function (popframes) {
    popframes = popframes || 1;
    if (fluid.activityTracing) {
        fluid.activityTrace.push({pop: popframes});
    }
    var activityStack = fluid.getActivityStack();
    var popped = activityStack.length - popframes;
    activityStack.length = popped < 0 ? 0 : popped;
};
// "this-ist" style Error so that we can distinguish framework errors whilst still retaining access to platform Error features
// Solution taken from http://stackoverflow.com/questions/8802845/inheriting-from-the-error-object-where-is-the-message-property#answer-17936621
fluid.FluidError = function (/*message*/) {
    var togo = Error.apply(this, arguments);
    this.message = togo.message;
    try { // This technique is necessary on IE11 since otherwise the stack entry is not filled in
        throw togo;
    } catch (togo) {
        this.stack = togo.stack;
    }
    return this;
};
fluid.FluidError.prototype = Object.create(Error.prototype);

// The framework's built-in "log" failure handler - this logs the supplied message as well as any framework activity in progress via fluid.log
fluid.logFailure = function (args, activity) {
    fluid.log.apply(null, [fluid.logLevel.FAIL, "ASSERTION FAILED: "].concat(args));
    fluid.logActivity(activity);
};

fluid.renderLoggingArg = function (arg) {
    return arg === undefined ? "undefined" : fluid.isPrimitive(arg) || !fluid.isPlainObject(arg) ? arg : JSON.stringify(arg);
};

// The framework's built-in "fail" failure handler - this throws an exception of type <code>fluid.FluidError</code>
fluid.builtinFail = function (args /*, activity*/) {
    var message = fluid.transform(args, fluid.renderLoggingArg).join("");
    throw new fluid.FluidError("Assertion failure - check console for more details: " + message);
};

/**
 * Signals an error to the framework. The default behaviour is to log a structured error message and throw an exception. This strategy may be configured
 * by adding and removing suitably namespaced listeners to the special event <code>fluid.failureEvent</code>
 *
 * @param {...String} messages - The error messages to log.
 *
 * All arguments after the first are passed on to (and should be suitable to pass on to) the native console.log
 * function.
 */
fluid.fail = function (...messages) {
    var activity = fluid.makeArray(fluid.describeActivity()); // Take copy since we will destructively modify
    fluid.popActivity(activity.length); // clear any current activity - TODO: the framework currently has no exception handlers, although it will in time
    if (fluid.failureEvent) { // notify any framework failure prior to successfully setting up the failure event below
        fluid.failureEvent.fire(messages, activity);
    } else {
        fluid.logFailure(messages, activity);
        fluid.builtinFail(messages, activity);
    }
};

fluid.notrycatch = false;

// A wrapper for the try/catch/finally language feature, to aid debugging in the QUnit UI by means of exception breakpoints for
// uncaught exceptions, since so many libraries, e.g. jQuery throw junk caught exceptions on startup
fluid.tryCatch = function (tryfun, catchfun, finallyfun) {
    finallyfun = finallyfun || fluid.identity;
    if (fluid.notrycatch) {
        var togo = tryfun();
        finallyfun();
        return togo;
    } else {
        try {
            return tryfun();
        } catch (e) {
            if (catchfun) {
                catchfun(e);
            } else {
                throw (e);
            }
        } finally {
            finallyfun();
        }
    }
};

// TODO: rescued from kettleCouchDB.js - clean up in time
fluid.expect = function (name, target, members) {
    fluid.transform(fluid.makeArray(members), function (key) {
        if (target[key] === undefined) {
            fluid.fail(name + " missing required member " + key);
        }
    });
};

// Logging

/** Returns whether logging is enabled - legacy method
 * @return {Boolean} `true` if the current logging level exceeds `fluid.logLevel.IMPORTANT`
 */
fluid.isLogging = function () {
    return logLevelStack[0].priority > fluid.logLevel.IMPORTANT.priority;
};

/** Determines whether the supplied argument is a valid logLevel marker
 * @param {Any} arg - The value to be tested
 * @return {Boolean} `true` if the supplied argument is a logLevel marker
 */
fluid.isLogLevel = function (arg) {
    return fluid.isMarker(arg) && arg.priority !== undefined;
};

/** Check whether the current framework logging level would cause a message logged with the specified level to be
 * logged. Clients who issue particularly expensive log payload arguments are recommended to guard their logging
 * statements with this function
 * @param {LogLevel} testLogLevel - The logLevel value which the current logging level will be tested against.
 * Accepts one of the members of the <code>fluid.logLevel</code> structure.
 * @return {Boolean} Returns <code>true</code> if a message supplied at that log priority would be accepted at the current logging level.
 */

fluid.passLogLevel = function (testLogLevel) {
    return testLogLevel.priority <= logLevelStack[0].priority;
};

/** Method to allow user to control the current framework logging level. The supplied level will be pushed onto a stack
 * of logging levels which may be popped via `fluid.popLogging`.
 * @param {Boolean|LogLevel} enabled - Either a boolean, for which <code>true</code>
 * represents <code>fluid.logLevel.INFO</code> and <code>false</code> represents <code>fluid.logLevel.IMPORTANT</code> (the default),
 * or else any other member of the structure <code>fluid.logLevel</code>
 * Messages whose priority is strictly less than the current logging level will not be shown by `fluid.log`
 */
fluid.setLogging = function (enabled) {
    var logLevel;
    if (typeof enabled === "boolean") {
        logLevel = fluid.logLevel[enabled ? "INFO" : "IMPORTANT"];
    } else if (fluid.isLogLevel(enabled)) {
        logLevel = enabled;
    } else {
        fluid.fail("Unrecognised fluid logging level ", enabled);
    }
    logLevelStack.unshift(logLevel);
    fluid.defeatLogging = !fluid.isLogging();
};

fluid.setLogLevel = fluid.setLogging;

/** Undo the effect of the most recent "setLogging", returning the logging system to its previous state
 * @return {LogLevel} The logLevel that was just popped
 */
fluid.popLogging = function () {
    var togo = logLevelStack.length === 1 ? logLevelStack[0] : logLevelStack.shift();
    fluid.defeatLogging = !fluid.isLogging();
    return togo;
};

/** Actually do the work of logging <code>args</code> to the environment's console. If the standard "console"
 * stream is available, the message will be sent there.
 * @param {Array} args - The complete array of arguments to be logged
 */
fluid.doBrowserLog = function (args) {
    /* eslint-disable no-console */
    if (typeof(console) !== "undefined") {
        if (console.debug) {
            console.debug.apply(console, args);
        } else if (typeof(console.log) === "function") {
            console.log.apply(console, args);
        }
        /* eslint-enable no-console */
    }
    /* eslint-enable no-console */
};

/* Log a message to a suitable environmental console. If the first argument to fluid.log is
 * one of the members of the <code>fluid.logLevel</code> structure, this will be taken as the priority
 * of the logged message - else if will default to <code>fluid.logLevel.INFO</code>. If the logged message
 * priority does not exceed that set by the most recent call to the <code>fluid.setLogging</code> function,
 * the message will not appear.
 */
fluid.log = function (/* message /*, ... */) {
    var directArgs = fluid.makeArray(arguments);
    var userLogLevel = fluid.logLevel.INFO;
    if (fluid.isLogLevel(directArgs[0])) {
        userLogLevel = directArgs.shift();
    }
    if (fluid.passLogLevel(userLogLevel)) {
        fluid.loggingEvent.fire(directArgs);
    }
};

// Functional programming utilities.

// Type checking functions

/**
 * Check whether the argument is a value other than null or undefined
 *
 * @param {Any} value - The value to be tested
 * @return {Boolean} `true` if the supplied value is other than null or undefined
 */
fluid.isValue = function (value) {
    return value !== undefined && value !== null;
};

/**
 * Check whether the argument is a primitive type
 *
 * @param {Any} value - The value to be tested
 * @return {Boolean} `true` if the supplied value is a JavaScript (ES5) primitive
 */
fluid.isPrimitive = function (value) {
    var valueType = typeof(value);
    return !value || valueType === "string" || valueType === "boolean" || valueType === "number" || valueType === "function";
};

/**
 * Determines whether the supplied object is an jQuery object. The strategy uses optimised inspection of the
 * constructor prototype since jQuery may not actually be loaded
 *
 * @param {Any} totest - The value to be tested
 * @return {Boolean} `true` if the supplied value is a jQuery object
 */
fluid.isJQuery = function (totest) {
    return Boolean(totest && totest.jquery && totest.constructor && totest.constructor.prototype
           && totest.constructor.prototype.jquery);
};

/** Determines whether the supplied object can be treated as an array (primarily, by iterating over numeric keys bounded from 0 to length).
 * The strategy used is an optimised approach taken from an earlier version of jQuery - detecting whether the toString() version
 * of the object agrees with the textual form [object Array], or else whether the object is a
 * jQuery object (the most common source of "fake arrays").
 *
 * @param {Any} totest - The value to be tested
 * @return {Boolean} `true` if the supplied value is an array
 */
// Note: The primary place jQuery->Array conversion is used in the framework is in dynamic components with a jQuery source.
fluid.isArrayable = function (totest) {
    return Boolean(totest) && (Object.prototype.toString.call(totest) === "[object Array]" || fluid.isJQuery(totest));
};

/**
 * Determines whether the supplied object is a plain JSON-forming container - that is, it is either a plain Object
 * or a plain Array. Note that this differs from jQuery's isPlainObject which does not pass Arrays.
 *
 * @param {Any} totest - The object to be tested
 * @param {Boolean} [strict] - (optional) If `true`, plain Arrays will fail the test rather than passing.
 * @return {Boolean} - `true` if `totest` is a plain object, `false` otherwise.
 */
fluid.isPlainObject = function (totest, strict) {
    var string = Object.prototype.toString.call(totest);
    if (string === "[object Array]") {
        return !strict;
    } else if (string !== "[object Object]") {
        return false;
    } // FLUID-5226: This inventive strategy taken from jQuery detects whether the object's prototype is directly Object.prototype by virtue of having an "isPrototypeOf" direct member
    return !totest.constructor || !totest.constructor.prototype || Object.prototype.hasOwnProperty.call(totest.constructor.prototype, "isPrototypeOf");
};

/**
 * Returns a string typeCode representing the type of the supplied value at a coarse level.
 * Returns <code>primitive</code>, <code>array</code> or <code>object</code> depending on whether the supplied object has
 * one of those types, by use of the <code>fluid.isPrimitive</code>, <code>fluid.isPlainObject</code> and <code>fluid.isArrayable</code> utilities
 *
 * @param {Any} totest - The value to be tested
 * @return {String} Either `primitive`, `array` or `object` depending on the type of the supplied value
 */
fluid.typeCode = function (totest) {
    return fluid.isPrimitive(totest) || !fluid.isPlainObject(totest) ? "primitive" :
        fluid.isArrayable(totest) ? "array" : "object";
};

/** Determine whether the supplied value is an IoC reference. The test is passed if the value is a string whose
 * first character is "{" and has closing "}" character somewhere in the string
 * @param {Any} ref - The value to be tested
 * @return {Boolean} `true` if the supplied value is an IoC reference
 */
fluid.isIoCReference = function (ref) {
    return typeof(ref) === "string" && ref.charAt(0) === "{";
};

/** Determine whether the supplied value is a reference or an expander. The test is passed if either fluid.isIoCReference passes
 * or the value has an "expander" member
 * @param {Any} ref - The value to be tested
 * @return {Boolean} `true` if the supplied value is a reference or expander
 */
fluid.isReferenceOrExpander = function (ref) {
    return ref && (fluid.isIoCReference(ref) || ref.expander);
};

fluid.isDOMNode = function (obj) {
    // This could be more sound, but messy:
    // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
    // The real problem is browsers like IE6, 7 and 8 which still do not feature a "constructor" property on DOM nodes
    return obj && typeof(obj.nodeType) === "number";
};

fluid.isComponent = function (obj) {
    return obj && obj.constructor === fluid.componentConstructor;
};

fluid.isUncopyable = function (totest) {
    return fluid.isPrimitive(totest) || !fluid.isPlainObject(totest);
};

fluid.isApplicable = function (totest) {
    return totest.apply && typeof(totest.apply) === "function";
};

/* A basic utility that returns its argument unchanged */
fluid.identity = function (arg) {
    return arg;
};

/** A function which raises a failure if executed */
fluid.notImplemented = function () {
    fluid.fail("This operation is not implemented");
};

/**
 * Returns the first of its arguments if it is not `undefined`, otherwise returns the second.
 *
 * @param {Any} a - The first argument to be tested for being `undefined`
 * @param {Any} b - The fallback argument, to be returned if `a` is `undefined`
 * @return {Any} - `a` if it is not `undefined`, else `b`.
 */
fluid.firstDefined = function (a, b) {
    return a === undefined ? b : a;
};

/* Return an empty container as the same type as the argument (either an array or hash). */
fluid.freshContainer = function (tocopy) {
    return fluid.isArrayable(tocopy) ? [] : {};
};

/**
 * Determine whether the supplied object path exceeds the maximum strategy recursion depth of fluid.strategyRecursionBailout -
 * if it does, fluid.fail will be issued with a diagnostic
 *
 * @param {String} funcName - The name of the function to appear in the diagnostic if issued
 * @param {String[]} segs - The segments of the path that the strategy has reached
 */
fluid.testStrategyRecursion = function (funcName, segs) {
    if (segs.length > fluid.strategyRecursionBailout) {
        fluid.fail("Runaway recursion encountered in " + funcName + " - reached path depth of " + fluid.strategyRecursionBailout + " via path of " + segs.join(".") +
            "this object is probably circularly connected. Either adjust your object structure to remove the circularity or increase fluid.strategyRecursionBailout");
    }
};

fluid.copyRecurse = function (tocopy, segs) {
    fluid.testStrategyRecursion("fluid.copy", segs);
    if (fluid.isUncopyable(tocopy)) {
        return tocopy;
    } else {
        return fluid.transform(tocopy, function (value, key) {
            segs.push(key);
            var togo = fluid.copyRecurse(value, segs);
            segs.pop();
            return togo;
        });
    }
};

/* Performs a deep copy (clone) of its argument. This will guard against cloning a circular object by terminating if it reaches a path depth
 * greater than <code>fluid.strategyRecursionBailout</code>
 */

fluid.copy = function (tocopy) {
    return fluid.copyRecurse(tocopy, []);
};

// TODO: Coming soon - reimplementation of $.extend using strategyRecursionBailout
fluid.extend = $.extend;

/* Corrected version of jQuery makeArray that returns an empty array on undefined rather than crashing.
 * We don't deal with as many pathological cases as jQuery */
fluid.makeArray = function (arg) {
    var togo = [];
    if (arg !== null && arg !== undefined) {
        if (fluid.isPrimitive(arg) || fluid.isPlainObject(arg, true) || typeof(arg.length) !== "number") {
            togo.push(arg);
        }
        else {
            for (var i = 0; i < arg.length; ++i) {
                togo[i] = arg[i];
            }
        }
    }
    return togo;
};

/**
 * Pushes an element or elements onto an array, initialising the array as a member of a holding object if it is
 * not already allocated.
 * @param {Array|Object} holder - The holding object whose member is to receive the pushed element(s).
 * @param {String} member - The member of the <code>holder</code> onto which the element(s) are to be pushed
 * @param {Array|Any} topush - If an array, these elements will be added to the end of the array using Array.push.apply.
 * If a non-array, it will be pushed to the end of the array using Array.push.
 */
fluid.pushArray = function (holder, member, topush) {
    var array = holder[member] ? holder[member] : (holder[member] = []);
    if (Array.isArray(topush)) {
        array.push.apply(array, topush);
    } else {
        array.push(topush);
    }
};

function transformInternal(source, togo, key, transformations) {
    var transit = source[key];
    for (var j = 0; j < transformations.length; ++j) {
        transit = transformations[j](transit, key);
    }
    if (transit !== fluid.NO_VALUE) {
        togo[key] = transit;
    }
}

/**
 * Return an array or hash of objects, transformed by one or more functions. Similar to
 * jQuery.map, only will accept an arbitrary list of transformation functions and also
 * works on non-arrays.
 *
 * @param {Array|Object} source - The initial container of objects to be transformed. If the source is
 * neither an array nor an object, it will be returned untransformed
 * @param {...Function} transformations - An arbitrary number of optional further arguments,
 * all of type Function, accepting the signature (object, index), where object is the
 * structure member to be transformed, and index is its key or index. Each function will be
 * applied in turn to each structure member, which will be replaced by the return value
 * from the function. A function may return fluid.NO_VALUE to indicate that the key should not
 * be assigned any value.
 * @return {Array|Object} - The finally transformed list, where each member has been replaced by the
 * original member acted on by the function or functions.
 */
fluid.transform = function (source, ...transformations) {
    if (fluid.isPrimitive(source)) {
        return source;
    }
    var togo = fluid.freshContainer(source);
    if (fluid.isArrayable(source)) {
        for (var i = 0; i < source.length; ++i) {
            transformInternal(source, togo, i, transformations);
        }
    } else {
        for (var key in source) {
            transformInternal(source, togo, key, transformations);
        }
    }
    return togo;
};

/**
 * Variety of Array.forEach which iterates over an array range
 * @param {Arrayable} array - The array to be iterated over
 * @param {Integer} start - The array index to start iterating at
 * @param {Integer} end - The limit of the array index for the iteration
 * @param {Function} func - A function accepting (value, key) for each iterated
 * object.
 */
fluid.forEachInRange = function (array, start, end, func) {
    for (var i = start; i < end; ++i) {
        func(array[i], i);
    }
};

/**
 * Return the last element of an array. If the array is of length 0, returns `undefined`.
 * @param {Arrayable} array - The array to be peeked into
 * @return {Any} start - The last element of the array
 */
fluid.peek = function (array) {
    return array.length === 0 ? undefined : array[array.length - 1];
};

/**
 * Better jQuery.each which works on hashes as well as having the arguments the right way round.
 * @param {Arrayable|Object} source - The container to be iterated over
 * @param {Function} func - A function accepting (value, key) for each iterated
 * object.
 */
fluid.each = function (source, func) {
    if (fluid.isArrayable(source)) {
        for (var i = 0; i < source.length; ++i) {
            func(source[i], i);
        }
    } else {
        for (var key in source) {
            func(source[key], key);
        }
    }
};

fluid.make_find = function (find_if) {
    var target = find_if ? false : undefined;
    return function (source, func, deffolt) {
        var disp;
        if (fluid.isArrayable(source)) {
            for (var i = 0; i < source.length; ++i) {
                disp = func(source[i], i);
                if (disp !== target) {
                    return find_if ? source[i] : disp;
                }
            }
        } else {
            for (var key in source) {
                disp = func(source[key], key);
                if (disp !== target) {
                    return find_if ? source[key] : disp;
                }
            }
        }
        return deffolt;
    };
};

/** Scan through an array or hash of objects, terminating on the first member which
 * matches a predicate function.
 * @param {Arrayable|Object} source - The array or hash of objects to be searched.
 * @param {Function} func - A predicate function, acting on a member. A predicate which
 * returns any value which is not <code>undefined</code> will terminate
 * the search. The function accepts (object, index).
 * @param {Object} deflt - A value to be returned in the case no predicate function matches
 * a structure member. The default will be the natural value of <code>undefined</code>
 * @return The first return value from the predicate function which is not <code>undefined</code>
 */
fluid.find = fluid.make_find(false);
/* The same signature as fluid.find, only the return value is the actual element for which the
 * predicate returns a value different from <code>false</code>
 */
fluid.find_if = fluid.make_find(true);

/** Scan through an array or hash of objects, removing those which match a predicate. Similar to
 * jQuery.grep, only acts on the list in-place by removal, rather than by creating
 * a new list by inclusion.
 * @param {Array|Object} source - The array or hash of objects to be scanned over. Note that in the case this is an array,
 * the iteration will proceed from the end of the array towards the front.
 * @param {Function} fn - A predicate function determining whether an element should be
 * removed. This accepts the standard signature (object, index) and returns a "truthy"
 * result in order to determine that the supplied object should be removed from the structure.
 * @param {Array|Object} [target] - (optional) A target object of the same type as <code>source</code>, which will
 * receive any objects removed from it.
 * @return {Array|Object} - <code>target</code>, containing the removed elements, if it was supplied, or else <code>source</code>
 * modified by the operation of removing the matched elements.
 */
fluid.remove_if = function (source, fn, target) {
    if (fluid.isArrayable(source)) {
        for (var i = source.length - 1; i >= 0; --i) {
            if (fn(source[i], i)) {
                if (target) {
                    target.unshift(source[i]);
                }
                source.splice(i, 1);
            }
        }
    } else {
        for (var key in source) {
            if (fn(source[key], key)) {
                if (target) {
                    target[key] = source[key];
                }
                delete source[key];
            }
        }
    }
    return target || source;
};

/** Fills an array of given size with copies of a value or result of a function invocation
 * @param {Number} n - The size of the array to be filled
 * @param {Object|Function} generator - Either a value to be replicated or function to be called
 * @param {Boolean} applyFunc - If true, treat the generator value as a function to be invoked with
 * argument equal to the index position
 */

fluid.generate = function (n, generator, applyFunc) {
    var togo = [];
    for (var i = 0; i < n; ++i) {
        togo[i] = applyFunc ? generator(i) : generator;
    }
    return togo;
};

/** Returns an array of size count, filled with increasing integers, starting at 0 or at the index specified by first.
 * @param {Number} count - Size of the filled array to be returned
 * @param {Number} [first] - (optional, defaults to 0) First element to appear in the array
 */

fluid.iota = function (count, first) {
    first = first || 0;
    var togo = [];
    for (var i = 0; i < count; ++i) {
        togo[togo.length] = first++;
    }
    return togo;
};

/** Extracts a particular member from each top-level member of a container, returning a new container of the same type
 * @param {Array|Object} holder - The container to be filtered
 * @param {String|String[]} name - An EL path to be fetched from each top-level member
 * @return {Array|Object} - The desired structure of fetched members
 */
fluid.getMembers = function (holder, name) {
    return fluid.transform(holder, function (member) {
        return fluid.get(member, name);
    });
};

/** Accepts an object to be filtered, and an array of keys. Either all keys not present in
 * the array are removed, or only keys present in the array are returned.
 * @param {Object} toFilter - The object to be filtered - this will be NOT modified by the operation (current implementation
 * passes through $.extend shallow algorithm)
 * @param {String[]} keys - The array of keys to operate with
 * @param {Boolean} exclude - If <code>true</code>, the keys listed are removed rather than included
 * @return {Object} the filtered object (the same object that was supplied as <code>toFilter</code>
 */
fluid.filterKeys = function (toFilter, keys, exclude) {
    return fluid.remove_if($.extend({}, toFilter), function (value, key) {
        return exclude ^ (keys.indexOf(key) === -1);
    });
};

/* A convenience wrapper for <code>fluid.filterKeys</code> with the parameter <code>exclude</code> set to <code>true</code>
 *  Returns the supplied object with listed keys removed */
fluid.censorKeys = function (toCensor, keys) {
    return fluid.filterKeys(toCensor, keys, true);
};

/* Return the keys in the supplied object as an array. Note that this will return keys found in the prototype chain as well as "own properties", unlike Object.keys() */
fluid.keys = function (obj) {
    var togo = [];
    for (var key in obj) {
        togo.push(key);
    }
    return togo;
};

/* Return the values in the supplied object as an array */
fluid.values = function (obj) {
    var togo = [];
    for (var key in obj) {
        togo.push(obj[key]);
    }
    return togo;
};

/**
 * Searches through the supplied object for the first value which matches the one supplied.
 * @param {Object} obj - the Object to be searched through
 * @param {Object} value - the value to be found. This will be compared against the object's
 * member using === equality.
 * @return {String} The first key whose value matches the one supplied
 */
fluid.keyForValue = function (obj, value) {
    return fluid.find(obj, function (thisValue, key) {
        if (value === thisValue) {
            return key;
        }
    });
};

/** Converts an array into an object whose keys are the elements of the array, each with the value "true"
 * @param {String[]} array - The array to be converted to a hash
 * @return {Object} hash An object with value <code>true</code> for each key taken from a member of <code>array</code>
 */
fluid.arrayToHash = function (array) {
    var togo = {};
    fluid.each(array, function (el) {
        togo[el] = true;
    });
    return togo;
};

/** Converts a hash into an array by hoisting out the object's keys into an array element via the supplied String "key", and then transforming via an optional further function, which receives the signature
 * (newElement, oldElement, key) where newElement is the freshly cloned element, oldElement is the original hash's element, and key is the key of the element.
 * If the function is not supplied, the old element is simply deep-cloned onto the new element (same effect as transform fluid.transforms.deindexIntoArrayByKey).
 * The supplied hash will not be modified, unless the supplied function explicitly does so by modifying its 2nd argument.
 * @param {Object} hash - The object to be converted to an array
 * @param {String} [keyName] - (optional) The key name within output array elements that the hash key should be assigned into
 * @param {Function} [func] - (optional) A "replacer" function that accepts signature (new array element, old hash element, key) whose return value will replace (new array element) if it is truthy
 * @return {Array} The hash converted into an array
 */
fluid.hashToArray = function (hash, keyName, func) {
    var togo = [];
    fluid.each(hash, function (el, key) {
        var newEl = el;
        if (keyName !== undefined) {
            newEl = {};
            newEl[keyName] = key;
        }
        if (func) {
            newEl = func(newEl, el, key) || newEl;
        } else if (newEl !== el) {
            $.extend(true, newEl, el);
        }
        togo.push(newEl);
    });
    return togo;
};

/* Converts an array consisting of a mixture of arrays and non-arrays into the concatenation of any inner arrays
 * with the non-array elements
 */
fluid.flatten = function (array) {
    var togo = [];
    fluid.each(array, function (element) {
        if (fluid.isArrayable(element)) {
            togo = togo.concat(element);
        } else {
            togo.push(element);
        }
    });
    return togo;
};

/**
 * Clears an object or array of its contents. For objects, each property is deleted.
 *
 * @param {Object|Array} target - the target to be cleared
 */
fluid.clear = function (target) {
    if (fluid.isArrayable(target)) {
        target.length = 0;
    } else {
        for (var i in target) {
            delete target[i];
        }
    }
};

/**
 * @param {Boolean} ascending -  <code>true</code> if a comparator is to be returned which
 * sorts strings in descending order of length.
 * @return {Function} - A comparison function.
 */
fluid.compareStringLength = function (ascending) {
    return ascending ? function (a, b) {
        return a.length - b.length;
    } : function (a, b) {
        return b.length - a.length;
    };
};

/**
 * Returns the converted integer if the input string can be converted to an integer. Otherwise, return NaN.
 *
 * @param {String} string - A string to be returned in integer form.
 * @return {Number|NaN} - The numeric value if the string can be converted, otherwise, returns NaN.
 */
fluid.parseInteger = function (string) {
    return isFinite(string) && ((string % 1) === 0) ? Number(string) : NaN;
};

/**
 * Derived from Sindre Sorhus's round-to node module ( https://github.com/sindresorhus/round-to ).
 * License: MIT
 *
 * Rounds the supplied number to at most the number of decimal places indicated by the scale, omitting any trailing 0s.
 * There are three possible rounding methods described below: "round", "ceil", "floor"
 * Round: Numbers are rounded away from 0 (i.e 0.5 -> 1, -0.5 -> -1).
 * Ceil: Numbers are rounded up
 * Floor: Numbers are rounded down
 * If the scale is invalid (i.e falsey, not a number, negative value), it is treated as 0.
 * If the scale is a floating point number, it is rounded to an integer.
 *
 * @param {Number} num - the number to be rounded
 * @param {Number} scale - the maximum number of decimal places to round to.
 * @param {String} [method] - (optional) Request a rounding method to use ("round", "ceil", "floor").
 *                          If nothing or an invalid method is provided, it will default to "round".
 * @return {Number} The num value rounded to the specified number of decimal places.
 */
fluid.roundToDecimal = function (num, scale, method) {
    // treat invalid scales as 0
    scale = scale && scale >= 0 ? Math.round(scale) : 0;

    if (method === "ceil" || method === "floor") {
        // The following is derived from https://github.com/sindresorhus/round-to/blob/v2.0.0/index.js#L20
        return Number(Math[method](num + "e" + scale) + "e-" + scale);
    } else {
        // The following is derived from https://github.com/sindresorhus/round-to/blob/v2.0.0/index.js#L17
        var sign = num >= 0 ? 1 : -1; // manually calculating the sign because Math.sign is not supported in IE
        return Number(sign * (Math.round(Math.abs(num) + "e" + scale) + "e-" + scale));
    }
};

/**
 * Copied from Underscore.js 1.4.3 - see licence at head of this file
 *
 * Will execute the passed in function after the specified amount of time since it was last executed.
 *
 * @param {Function} func - the function to execute
 * @param {Number} wait - the number of milliseconds to wait before executing the function
 * @param {Boolean} immediate - Whether to trigger the function at the start (true) or end (false) of
 *                              the wait interval.
 * @return {Function} - A function that can be called as though it were the original function.
 */
fluid.debounce = function (func, wait, immediate) {
    var timeout, result;
    return function () {
        var context = this, args = arguments;
        var later = function () {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
            }
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
        }
        return result;
    };
};


/**
 * Calls Object.freeze at each level of containment of the supplied object.
 *
 * @param {Any} tofreeze - The material to freeze.
 * @param {String[]} [segs] - Implementation-internal - path segments that recursion has reached.
 * @return {Any} - The supplied argument, recursively frozen.
 */
fluid.freezeRecursive = function (tofreeze, segs) {
    segs = segs || [];
    fluid.testStrategyRecursion("fluid.freezeRecursive", segs);
    if (fluid.isPlainObject(tofreeze)) {
        fluid.each(tofreeze, function (value, key) {
            segs.push(key);
            fluid.freezeRecursive(value, segs);
            segs.pop();
        });
        return Object.freeze(tofreeze);
    } else {
        return tofreeze;
    }
};

/*
 * A set of special "marker values" used in signalling in function arguments and return values,
 * to partially compensate for JavaScript's lack of distinguished types. These should never appear
 * in JSON structures or other kinds of static configuration. An API specifically documents if it
 * accepts or returns any of these values, and if so, what its semantic is  - most are of private
 * use internal to the framework
 */

fluid.marker = function () {};

fluid.makeMarker = function (value, extra) {
    var togo = Object.create(fluid.marker.prototype);
    togo.value = value;
    fluid.extend(togo, extra);
    return Object.freeze(togo);
};

/* A special "marker object" representing that no value is present (where
 * signalling using the value "undefined" is not possible - e.g. the return value from a "strategy"). This
 * is intended for "ephemeral use", i.e. returned directly from strategies and transforms and should not be
 * stored in data structures */
fluid.NO_VALUE = fluid.makeMarker("NO_VALUE");

/* A marker indicating that a value requires to be expanded after component construction begins */
fluid.EXPAND = fluid.makeMarker("EXPAND");

/* Determine whether an object is any marker, or a particular marker - omit the
 * 2nd argument to detect any marker
 */
fluid.isMarker = function (totest, type) {
    if (!(totest instanceof fluid.marker)) {
        return false;
    }
    if (!type) {
        return true;
    }
    return totest.value === type.value;
};

fluid.logLevelsSpec = {
    "FATAL":      0,
    "FAIL":       5,
    "WARN":      10,
    "IMPORTANT": 12, // The default logging "off" level - corresponds to the old "false"
    "INFO":      15, // The default logging "on" level - corresponds to the old "true"
    "TRACE":     20
};

/* A structure holding all supported log levels as supplied as a possible first argument to fluid.log
 * Members with a higher value of the "priority" field represent lower priority logging levels */
// Moved down here since it uses fluid.transform and fluid.makeMarker on startup
fluid.logLevel = fluid.transform(fluid.logLevelsSpec, function (value, key) {
    return fluid.makeMarker(key, {priority: value});
});
var logLevelStack = [fluid.logLevel.IMPORTANT]; // The stack of active logging levels, with the current level at index 0


// Model functions
fluid.model = {}; // cannot call registerNamespace yet since it depends on fluid.model

/* Copy a source "model" onto a target */
fluid.model.copyModel = function (target, source) {
    fluid.clear(target);
    $.extend(true, target, source);
};

/** Parse an EL expression separated by periods (.) into its component segments.
 * @param {String} EL - The EL expression to be split
 * @return {String[]} the component path expressions.
 * TODO: This needs to be upgraded to handle (the same) escaping rules (as RSF), so that
 * path segments containing periods and backslashes etc. can be processed, and be harmonised
 * with the more complex implementations in fluid.pathUtil(data binding).
 */
fluid.model.parseEL = function (EL) {
    return EL === "" ? [] : String(EL).split(".");
};

/* Compose an EL expression from two separate EL expressions. The returned
 * expression will be the one that will navigate the first expression, and then
 * the second, from the value reached by the first. Either prefix or suffix may be
 * the empty string */
fluid.model.composePath = function (prefix, suffix) {
    return prefix === "" ? suffix : (suffix === "" ? prefix : prefix + "." + suffix);
};

/* Compose any number of path segments, none of which may be empty */
fluid.model.composeSegments = function () {
    return fluid.makeArray(arguments).join(".");
};

/* Returns the index of the last occurrence of the period character . in the supplied string */
fluid.lastDotIndex = function (path) {
    return path.lastIndexOf(".");
};

/* Returns all of an EL path minus its final segment - if the path consists of just one segment, returns "" -
 * WARNING - this method does not follow escaping rules */
fluid.model.getToTailPath = function (path) {
    var lastdot = fluid.lastDotIndex(path);
    return lastdot === -1 ? "" : path.substring(0, lastdot);
};

/* Returns the very last path component of an EL path
 * WARNING - this method does not follow escaping rules */
fluid.model.getTailPath = function (path) {
    var lastdot = fluid.lastDotIndex(path);
    return path.substring(lastdot + 1);
};

/* Helpful alias for old-style API */
fluid.path = fluid.model.composeSegments;
fluid.composePath = fluid.model.composePath;


// unsupported, NON-API function
fluid.requireDataBinding = function () {
    fluid.fail("Please include DataBinding.js in order to operate complex model accessor configuration");
};

fluid.model.setWithStrategy = fluid.model.getWithStrategy = fluid.requireDataBinding;

// unsupported, NON-API function
fluid.model.resolvePathSegment = function (root, segment, create, origEnv) {
    // TODO: This branch incurs a huge cost that we incur across the whole framework, just to support the DOM binder
    // usage. We need to either do something "schematic" or move to proxies
    // TODO: Most costs are incurred from fluid.compileMergePolicy, some from fluid.model.setChangedPath
    if (!origEnv && root.resolvePathSegment) {
        var togo = root.resolvePathSegment(segment);
        if (togo !== undefined) { // To resolve FLUID-6132
            return togo;
        }
    }
    if (create && root[segment] === undefined) {
        // This optimisation in this heavily used function has a fair effect
        return root[segment] = {};
    }
    return root[segment];
};

// unsupported, NON-API function
fluid.model.parseToSegments = function (EL, parseEL, copy) {
    return typeof(EL) === "number" || typeof(EL) === "string" ? parseEL(EL) : (copy ? fluid.makeArray(EL) : EL);
};

// unsupported, NON-API function
fluid.model.pathToSegments = function (EL, config) {
    var parser = config && config.parser ? config.parser.parse : fluid.model.parseEL;
    return fluid.model.parseToSegments(EL, parser);
};

// Overall strategy skeleton for all implementations of fluid.get/set
fluid.model.accessImpl = function (root, EL, newValue, config, initSegs, returnSegs, traverser) {
    var segs = fluid.model.pathToSegments(EL, config);
    var initPos = 0;
    if (initSegs) {
        initPos = initSegs.length;
        segs = initSegs.concat(segs);
    }
    var uncess = newValue === fluid.NO_VALUE ? 0 : 1;
    root = traverser(root, segs, initPos, config, uncess);
    if (newValue === fluid.NO_VALUE) { // get or custom
        return returnSegs ? {root: root, segs: segs} : root;
    }
    else { // set
        root[fluid.peek(segs)] = newValue;
    }
};

// unsupported, NON-API function
fluid.model.accessSimple = function (root, EL, newValue, environment, initSegs, returnSegs) {
    return fluid.model.accessImpl(root, EL, newValue, environment, initSegs, returnSegs, fluid.model.traverseSimple);
};

// unsupported, NON-API function
fluid.model.traverseSimple = function (root, segs, initPos, environment, uncess) {
    var origEnv = environment;
    var limit = segs.length - uncess;
    for (var i = 0; i < limit; ++i) {
        if (!root) {
            return undefined;
        }
        var segment = segs[i];
        if (environment && environment[segment]) {
            root = environment[segment];
        } else {
            root = fluid.model.resolvePathSegment(root, segment, uncess === 1, origEnv);
        }
        environment = null;
    }
    return root;
};

fluid.model.setSimple = function (root, EL, newValue, environment, initSegs) {
    fluid.model.accessSimple(root, EL, newValue, environment, initSegs, false);
};

/* Optimised version of fluid.get for uncustomised configurations */

fluid.model.getSimple = function (root, EL, environment, initSegs) {
    if (EL === null || EL === undefined || EL.length === 0) {
        return root;
    }
    return fluid.model.accessSimple(root, EL, fluid.NO_VALUE, environment, initSegs, false);
};

/* Even more optimised version which assumes segs are parsed and no configuration */
fluid.getImmediate = function (root, segs, i) {
    var limit = (i === undefined ? segs.length : i + 1);
    for (var j = 0; j < limit; ++j) {
        root = root ? root[segs[j]] : undefined;
    }
    return root;
};

// unsupported, NON-API function
// Returns undefined to signal complex configuration which needs to be farmed out to DataBinding.js
// any other return represents an environment value AND a simple configuration we can handle here
fluid.decodeAccessorArg = function (arg3) {
    return (!arg3 || arg3 === fluid.model.defaultGetConfig || arg3 === fluid.model.defaultSetConfig) ?
        null : (arg3.type === "environment" ? arg3.value : undefined);
};

fluid.set = function (root, EL, newValue, config, initSegs) {
    var env = fluid.decodeAccessorArg(config);
    if (env === undefined) {
        fluid.model.setWithStrategy(root, EL, newValue, config, initSegs);
    } else {
        fluid.model.setSimple(root, EL, newValue, env, initSegs);
    }
};

/** Evaluates an EL expression by fetching a dot-separated list of members
 * recursively from a provided root.
 * @param {Object} root - The root data structure in which the EL expression is to be evaluated
 * @param {String|String[]} EL - The EL expression to be evaluated, or an array of path segments
 * @param {Object} [config] - An optional configuration or environment structure which can customise the fetch operation
 * @return {Any} The fetched data value.
 */

fluid.get = function (root, EL, config, initSegs) {
    var env = fluid.decodeAccessorArg(config);
    return env === undefined ?
        fluid.model.getWithStrategy(root, EL, config, initSegs)
        : fluid.model.accessImpl(root, EL, fluid.NO_VALUE, env, null, false, fluid.model.traverseSimple);
};

/** Returns any value held at a particular global path. This may be an object or a function, depending on what has been stored there.
 * @param {String|String[]} path - The global path from which the value is to be fetched
 * @param {Object} [env] - [optional] An environmental overlay object which will be consulted before any lookups in the global namespace.
 * @return {Any} The value that was stored at the path, or undefined if there is none.
 */

fluid.getGlobalValue = function (path, env) {
    if (path) {
        env = env || fluid.environment;
        return fluid.get(fluid.global, path, {type: "environment", value: env});
    }
};

/**
 * Allows for the binding to a "this-ist" function
 * @param {Object} obj - "this-ist" object to bind to
 * @param {Object} fnName - The name of the function to call.
 * @param {Object} args - Arguments to call the function with.
 * @return {Any} - The return value (if any) of the underlying function.
 */
fluid.bind = function (obj, fnName, args) {
    return obj[fnName].apply(obj, fluid.makeArray(args));
};

// Stub for function in FluidIoC.js
fluid.proxyComponentArgs = fluid.identity;
/* eslint-disable jsdoc/require-returns-check */
/**
 * Allows for the calling of a function from an EL expression "functionPath", with the arguments "args", scoped to an framework version "environment".
 * @param {Object} functionPath - An EL expression
 * @param {Object} args - An array of arguments to be applied to the function, specified in functionPath
 * @param {Object} [environment] - (optional) The object to scope the functionPath to  (typically the framework root for version control)
 * @return {Any} - The return value from the invoked function.
 */
/* eslint-enable jsdoc/require-returns-check */
fluid.invokeGlobalFunction = function (functionPath, args, environment) {
    var func = fluid.getGlobalValue(functionPath, environment);
    if (!func) {
        fluid.fail("Error invoking global function: " + functionPath + " could not be located");
    } else {
        var argsArray = fluid.isArrayable(args) ? args : fluid.makeArray(args);
        fluid.proxyComponentArgs(argsArray);
        return func.apply(null, argsArray);
    }
};

/* Registers a new global function at a given path */

fluid.registerGlobalFunction = function (functionPath, func, env) {
    env = env || fluid.environment;
    fluid.set(fluid.global, functionPath, func, {type: "environment", value: env});
};

fluid.setGlobalValue = fluid.registerGlobalFunction;


/** Ensures that the supplied path has an object allocated in the global Infusion namespace, and retrieves the current value.
 * If no value is stored, a fresh {} will be assigned at the path, and to all currently empty paths leading to the global namespace root.
 * In a browser environment, the global Infusion namespace is rooted in the global `window`.
 * @param {String|String[]} path - The global path at which the namespace is to be allocated.
 * @param {Object} [env] - [optional] An environmental overlay object which will be consulted before any lookups in the global namespace.
 * @return {Any} Any current value held at the supplied path - or a freshly allocated {} to be held at that path if it was previously empty
 */
fluid.registerNamespace = function (path, env) {
    env = env || fluid.environment;
    var existing = fluid.getGlobalValue(path, env);
    if (!existing) {
        existing = {};
        fluid.setGlobalValue(path, existing, env);
    }
    return existing;
};

// stubs for two functions in FluidDebugging.js
fluid.dumpEl = fluid.identity;
fluid.renderTimestamp = fluid.identity;

/*** The Fluid instance id ***/

// unsupported, NON-API function
fluid.generateUniquePrefix = function () {
    return (Math.floor(Math.random() * 1e12)).toString(36) + "-";
};

var fluid_prefix = fluid.generateUniquePrefix();

fluid.fluidInstance = fluid_prefix;

var fluid_guid = 1;

/** Allocate a string value that will be unique within this Infusion instance (frame or process), and
 * globally unique with high probability (50% chance of collision after a million trials)
 * @return {String} A fresh unique id
 */

fluid.allocateGuid = function () {
    return fluid_prefix + (fluid_guid++);
};

/*** The Fluid Event system. ***/

fluid.registerNamespace("fluid.event");

// Fluid priority system for encoding relative positions of, e.g. listeners, transforms, options, in lists

fluid.extremePriority = 4e9; // around 2^32 - allows headroom of 21 fractional bits for sub-priorities
fluid.priorityTypes = {
    first: -1,
    last: 1,
    before: 0,
    after: 0
};
// TODO: This should be properly done with defaults blocks and a much more performant fluid.indexDefaults
fluid.extremalPriorities = {
    // a built-in definition to allow test infrastructure "last" listeners to sort after all impl listeners, and authoring/debugging listeners to sort after those
    // these are "priority intensities", and will be flipped for "first" listeners
    none: 0,
    transaction: 10,
    testing: 20,
    authoring: 30
};

// unsupported, NON-API function
// TODO: Note - no "fixedOnly = true" sites remain in the framework
fluid.parsePriorityConstraint = function (constraint, fixedOnly, site) {
    var segs = constraint.split(":");
    var type = segs[0];
    var lookup = fluid.priorityTypes[type];
    if (lookup === undefined) {
        fluid.fail("Invalid constraint type in priority field " + constraint + ": the only supported values are " + fluid.keys(fluid.priorityTypes).join(", ") + " or numeric");
    }
    if (fixedOnly && lookup === 0) {
        fluid.fail("Constraint type in priority field " + constraint + " is not supported in a " + site + " record - you must use either a numeric value or first, last");
    }
    return {
        type: segs[0],
        target: segs[1]
    };
};

// unsupported, NON-API function
fluid.parsePriority = function (priority, count, fixedOnly, site) {
    priority = priority || 0;
    var togo = {
        count: count || 0,
        fixed: null,
        constraint: null,
        site: site
    };
    if (typeof(priority) === "number") {
        togo.fixed = -priority;
    } else {
        togo.constraint = fluid.parsePriorityConstraint(priority, fixedOnly, site);
    }
    var multiplier = togo.constraint ? fluid.priorityTypes[togo.constraint.type] : 0;
    if (multiplier !== 0) {
        var target = togo.constraint.target || "none";
        var extremal = fluid.extremalPriorities[target];
        if (extremal === undefined) {
            fluid.fail("Unrecognised extremal priority target " + target + ": the currently supported values are " + fluid.keys(fluid.extremalPriorities).join(", ") + ": register your value in fluid.extremalPriorities");
        }
        togo.fixed = multiplier * (fluid.extremePriority + extremal);
    }
    if (togo.fixed !== null) {
        togo.fixed += togo.count / 1024; // use some fractional bits to encode count bias
    }

    return togo;
};

fluid.renderPriority = function (parsed) {
    return parsed.constraint ? (parsed.constraint.target ? parsed.constraint.type + ":" + parsed.constraint.target : parsed.constraint.type ) : Math.floor(parsed.fixed);
};

// unsupported, NON-API function
fluid.compareByPriority = function (recA, recB) {
    if (recA.priority.fixed !== null && recB.priority.fixed !== null) {
        return recA.priority.fixed - recB.priority.fixed;
    } else { // sort constraint records to the end
        // relies on JavaScript boolean coercion rules (ECMA 9.3 toNumber)
        return (recA.priority.fixed === null) - (recB.priority.fixed === null);
    }
};

fluid.honourConstraint = function (array, firstConstraint, c) {
    var constraint = array[c].priority.constraint;
    var matchIndex = fluid.find(array, function (element, index) {
        return element.namespace === constraint.target ? index : undefined;
    }, -1);
    if (matchIndex === -1) { // TODO: We should report an error during firing if this condition persists until then
        return true;
    } else if (matchIndex >= firstConstraint) {
        return false;
    } else {
        var offset = constraint.type === "after" ? 1 : 0;
        var target = matchIndex + offset;
        var temp = array[c];
        for (var shift = c; shift >= target; --shift) {
            array[shift] = array[shift - 1];
        }
        array[target] = temp;
        return true;
    }
};

// unsupported, NON-API function
// Priorities accepted from users have higher numbers representing high priority (sort first) -
fluid.sortByPriority = function (array) {
    array.sort(fluid.compareByPriority);
    // fluid.stableSort(array, fluid.compareByPriority);

    var firstConstraint = fluid.find(array, function (element, index) {
        return element.priority.constraint && fluid.priorityTypes[element.priority.constraint.type] === 0 ? index : undefined;
    }, array.length);

    while (true) {
        if (firstConstraint === array.length) {
            return array;
        }
        var oldFirstConstraint = firstConstraint;
        for (var c = firstConstraint; c < array.length; ++c) {
            var applied = fluid.honourConstraint(array, firstConstraint, c);
            if (applied) {
                ++firstConstraint;
            }
        }
        if (firstConstraint === oldFirstConstraint) {
            var holders = array.slice(firstConstraint);
            fluid.fail("Could not find targets for any constraints in " + holders[0].priority.site + " ", holders, ": none of the targets (" + fluid.getMembers(holders, "priority.constraint.target").join(", ") +
                ") matched any namespaces of the elements in (", array.slice(0, firstConstraint), ") - this is caused by either an invalid or circular reference");
        }
    }
};

/**
 * Parse a hash containing prioritised records (for example, as found in a ContextAwareness record) and return a sorted array of these records in priority order.
 *
 * @param {Object} records - A hash of key names to prioritised records. Each record may contain an member `namespace` - if it does not, the namespace will be taken from the
 * record's key. It may also contain a `String` member `priority` encoding a priority with respect to these namespaces as document at http://docs.fluidproject.org/infusion/development/Priorities.html .
 * @param {String} name - A human-readable name describing the supplied records, which will be incorporated into the message of any error encountered when resolving the priorities
 * @return {Array} An array of the same elements supplied to `records`, sorted into priority order. The supplied argument `records` will not be modified.
 */
fluid.parsePriorityRecords = function (records, name) {
    var array = fluid.hashToArray(records, "namespace", function (newElement, oldElement) {
        $.extend(newElement, oldElement);
        newElement.priority = fluid.parsePriority(oldElement.priority, 0, false, name);
    });
    fluid.sortByPriority(array);
    return array;
};

fluid.event.identifyListener = function (listener, soft) {
    if (typeof(listener) !== "string" && !listener.$$fluid_guid && !soft) {
        listener.$$fluid_guid = fluid.allocateGuid();
    }
    return listener.$$fluid_guid;
};

// unsupported, NON-API function
fluid.event.impersonateListener = function (origListener, newListener) {
    fluid.event.identifyListener(origListener);
    newListener.$$fluid_guid = origListener.$$fluid_guid;
};


// unsupported, NON-API function
fluid.event.sortListeners = function (listeners) {
    var togo = [];
    fluid.each(listeners, function (oneNamespace) {
        var headHard; // notify only the first listener with hard namespace - or else all if all are soft
        for (var i = 0; i < oneNamespace.length; ++i) {
            var thisListener = oneNamespace[i];
            if (!thisListener.softNamespace && !headHard) {
                headHard = thisListener;
            }
        }
        if (headHard) {
            togo.push(headHard);
        } else {
            togo = togo.concat(oneNamespace);
        }
    });
    return fluid.sortByPriority(togo);
};

// unsupported, NON-API function
fluid.event.resolveListener = function (listener) {
    var listenerName = listener.globalName || (typeof(listener) === "string" ? listener : null);
    if (listenerName) {
        var listenerFunc = fluid.getGlobalValue(listenerName);
        if (!listenerFunc) {
            fluid.fail("Unable to look up name " + listenerName + " as a global function");
        } else {
            listener = listenerFunc;
        }
    }
    return listener;
};

/* Generate a name for a component for debugging purposes */
fluid.nameComponent = function (that) {
    return that ? fluid.dumpComponentAndPath(that) : "[unknown component]";
};

fluid.event.nameEvent = function (that, eventName) {
    return eventName + " of " + fluid.nameComponent(that);
};

// A function to tag the type of a Fluid event firer (primarily to mark it uncopyable)
fluid.event.firer = function () {};

/** Construct an "event firer" object which can be used to register and deregister
 * listeners, to which "events" can be fired. These events consist of an arbitrary
 * function signature. General documentation on the Fluid events system is at
 * http://docs.fluidproject.org/infusion/development/InfusionEventSystem.html .
 *
 * @param {Object} options - A structure to configure this event firer. Supported fields:
 *     {String} name - a readable name for this firer to be used in diagnostics and debugging
 *     {Boolean} preventable - If <code>true</code> the return value of each handler will
 * be checked for <code>false</code> in which case further listeners will be shortcircuited, and this
 * will be the return value of fire()
 *     {Boolean} promise - If `true`, the event firer will receive a "thenable" signature allowing
 * it to function as a promise. In this case the event should be fired with only one argument, and
 * not more than once.
 * @return {Object} - The newly-created event firer.
 */
fluid.makeEventFirer = function (options) {
    options = options || {};
    var name = options.name || "<anonymous>";
    var that;

    var lazyInit = function () { // Lazy init function to economise on object references for events which are never listened to
        // The authoritative list of all listeners, a hash indexed by namespace, looking up to a stack (array) of
        // listener records in "burial order"
        that.listeners = {};
        // An index of all listeners by "id" - we should consider removing this since it is only used during removal
        // and because that.listeners is a hash of stacks we can't really accelerate removal by much
        that.byId = {};
        // The "live" list of listeners which will be notified in order on any firing. Recomputed on any use of
        // addListener/removeListener
        that.sortedListeners = [];
        // Very low-level destruction notification scheme primarily intended for FLUID-6445. Will be an array of nullary functions
        that.onDestroy = null;
        // arguments after 3rd are not part of public API
        // listener as Object is used only by ChangeApplier to tunnel path, segs, etc as part of its "spec"
        /** Adds a listener to this event.
         * @param {Function|String} listener - The listener function to be added, or a global name resolving to a function. The signature of the function is arbitrary and matches that sent to event.fire()
         * @param {String} namespace - (Optional) A namespace for this listener. At most one listener with a particular namespace can be active on an event at one time. Removing successively added listeners with a particular
         * namespace will expose previously added ones in a stack idiom
         * @param {String|Number} priority - A priority for the listener relative to others, perhaps expressed with a constraint relative to the namespace of another - see
         * http://docs.fluidproject.org/infusion/development/Priorities.html
         * @param {String} softNamespace - An unsupported internal option that is not part of the public API.
         * @param {String} listenerId - An unsupported internal option that is not part of the public API.
         */
        that.addListener = function (listener, namespace, priority, softNamespace, listenerId) {
            var record;
            if (that.destroyed) {
                fluid.fail("Cannot add listener to destroyed event firer " + that.name);
            }
            if (!listener) {
                return;
            }
            if (fluid.isPlainObject(listener, true) && !fluid.isApplicable(listener)) {
                record = listener;
                listener = record.listener;
                namespace = record.namespace;
                priority = record.priority;
                softNamespace = record.softNamespace;
                listenerId = record.listenerId;
            }
            if (typeof(listener) === "string") {
                listener = {globalName: listener};
            }
            var id = listenerId || fluid.event.identifyListener(listener);
            namespace = namespace || id;
            record = $.extend(record || {}, {
                namespace: namespace,
                listener: listener,
                softNamespace: softNamespace,
                listenerId: listenerId,
                priority: fluid.parsePriority(priority, that.sortedListeners.length, false, "listeners")
            });
            that.byId[id] = record;

            var thisListeners = (that.listeners[namespace] = fluid.makeArray(that.listeners[namespace]));
            thisListeners[softNamespace ? "push" : "unshift"] (record);

            that.sortedListeners = fluid.event.sortListeners(that.listeners);
        };
        that.addListener.apply(null, arguments);
    };
    that = Object.create(fluid.event.firer.prototype);
    fluid.extend(that, {
        eventId: fluid.allocateGuid(),
        name: name,
        ownerId: options.ownerId,
        typeName: "fluid.event.firer",
        destroy: function () {
            that.destroyed = true;
            fluid.each(that.onDestroy, function (func) {
                func();
            });
        },
        addListener: function () {
            lazyInit.apply(null, arguments);
        },
        /**
         * Removes a listener previously registered with this event.
         *
         * @param {Function|String} listener - Either the listener function, the namespace of a listener
         * (in which case a previous listener with that namespace may be uncovered) or an id sent to the
         * undocumented `listenerId` argument of `addListener
         */
        // Can be supplied either listener, namespace, or id (which may match either listener function's guid or original listenerId argument)
        removeListener: function (listener) {
            if (!that.listeners) { return; }
            var namespace, id, record;
            if (typeof(listener) === "string") {
                namespace = listener;
                record = that.listeners[namespace];
                if (!record) { // it was an id and not a namespace - take the namespace from its record later
                    id = namespace;
                    namespace = null;
                }
            }
            else if (typeof(listener) === "function") {
                id = fluid.event.identifyListener(listener, true);
                if (!id) {
                    fluid.fail("Cannot remove unregistered listener function ", listener, " from event " + that.name);
                }
            }
            var rec = that.byId[id];
            var softNamespace = rec && rec.softNamespace;
            namespace = namespace || (rec && rec.namespace) || id;
            delete that.byId[id];
            record = that.listeners[namespace];
            if (record) {
                if (softNamespace) {
                    fluid.remove_if(record, function (thisLis) {
                        return thisLis.listener.$$fluid_guid === id || thisLis.listenerId === id;
                    });
                } else {
                    record.shift();
                }
                if (record.length === 0) {
                    delete that.listeners[namespace];
                }
            }
            that.sortedListeners = fluid.event.sortListeners(that.listeners);
        },
        /* Fires this event to all listeners which are active. They will be notified in order of priority. The signature of this method is free. */
        fire: function () {
            var listeners = that.sortedListeners;
            if (options.promise) {
                that.promisePayload = arguments[0];
            }
            if (!listeners || that.destroyed) { return; }
            for (var i = 0; i < listeners.length; ++i) {
                var lisrec = listeners[i];
                if (typeof(lisrec.listener) !== "function") {
                    lisrec.listener = fluid.event.resolveListener(lisrec.listener);
                }
                var listener = lisrec.listener;
                var ret = listener.apply(null, arguments);
                var value;
                if (options.preventable && ret === false || that.destroyed) {
                    value = false;
                }
                if (value !== undefined) {
                    return value;
                }
            }
        }
    });
    if (options.promise) {
        that.then = function (func) {
            if ("promisePayload" in that) {
                func(that.promisePayload);
            } else {
                that.addListener(func);
            }
        };
    }
    return that;
};

// unsupported, NON-API function
// Supports the framework-internal "onDestroy" list attached to an event which needs to be extremely lightweight
fluid.event.addPrimitiveListener = function (holder, name, func) {
    var existing = holder[name];
    if (!existing) {
        existing = holder[name] = [];
    }
    existing.push(func);
};

// unsupported, NON-API function
// Fires to an event which may not be instantiated (in which case no-op) - primary modern usage is to resolve FLUID-5904
fluid.fireEvent = function (component, eventName, args) {
    var firer = component.events && component.events[eventName];
    if (firer) {
        firer.fire.apply(null, fluid.makeArray(args));
    }
};

// unsupported, NON-API function
fluid.event.addListenerToFirer = function (firer, value, namespace, wrapper) {
    wrapper = wrapper || fluid.identity;
    if (fluid.isArrayable(value)) {
        for (var i = 0; i < value.length; ++i) {
            fluid.event.addListenerToFirer(firer, value[i], namespace, wrapper);
        }
    } else if (typeof(value) === "function" || typeof(value) === "string") {
        wrapper(firer).addListener(value, namespace);
    } else if (value && typeof(value) === "object") {
        wrapper(firer).addListener(value.listener, namespace || value.namespace, value.priority, value.softNamespace, value.listenerId);
    }
};

// unsupported, NON-API function - non-IOC passthrough
fluid.event.resolveListenerRecord = function (records) {
    return { records: records };
};

fluid.expandImmediate = function (material) {
    fluid.fail("fluid.expandImmediate could not be loaded - please include FluidIoC.js in order to operate IoC-driven event with descriptor " + material);
};

// unsupported, NON-API function
fluid.mergeListeners = function (that, events, listeners) {
    fluid.each(listeners, function (value, key) {
        var firer, namespace;
        if (fluid.isIoCReference(key)) {
            firer = fluid.expandImmediate(key, that);
            if (!firer) {
                fluid.fail("Error in listener record: key " + key + " could not be looked up to an event firer - did you miss out \"events.\" when referring to an event firer?");
            }
        } else {
            var keydot = key.indexOf(".");

            if (keydot !== -1) {
                namespace = key.substring(keydot + 1);
                key = key.substring(0, keydot);
            }
            if (!events[key]) {
                fluid.fail("Listener registered for event " + key + " which is not defined for this component");
            }
            firer = events[key];
        }
        var record = fluid.event.resolveListenerRecord(value, that, key, namespace, true);
        fluid.event.addListenerToFirer(firer, record.records, namespace, record.adderWrapper);
    });
};

// unsupported, NON-API function
fluid.eventFromRecord = function (eventSpec, eventKey, that) {
    var isIoCEvent = eventSpec && (typeof(eventSpec) !== "string" || fluid.isIoCReference(eventSpec));
    var event;
    if (isIoCEvent) {
        if (!fluid.event.resolveEvent) {
            fluid.fail("fluid.event.resolveEvent could not be loaded - please include FluidIoC.js in order to operate IoC-driven event with descriptor ",
                eventSpec);
        } else {
            event = fluid.event.resolveEvent(that, eventKey, eventSpec);
        }
    } else {
        event = fluid.makeEventFirer({
            name: fluid.event.nameEvent(that, eventKey),
            preventable: eventSpec === "preventable",
            promise: eventSpec === "promise",
            ownerId: that.id
        });
    }
    return event;
};

// unsupported, NON-API function
fluid.mergeListenerPolicy = function (target, source, key) {
    if (typeof(key) !== "string") {
        fluid.fail("Error in listeners declaration - the keys in this structure must resolve to event names - got " + key + " from ", source);
    }
    // cf. triage in mergeListeners
    var hasNamespace = !fluid.isIoCReference(key) && key.indexOf(".") !== -1;
    return hasNamespace ? (source || target) : fluid.arrayConcatPolicy(target, source);
};

// unsupported, NON-API function
fluid.makeMergeListenersPolicy = function (merger, modelRelay) {
    return function (target, source) {
        target = target || {};
        if (modelRelay && (fluid.isArrayable(source) || "target" in source && (typeof(source.target) === "string" || source.target.segs))) { // This form allowed for modelRelay
            target[""] = merger(target[""], source, "");
        } else {
            fluid.each(source, function (listeners, key) {
                target[key] = merger(target[key], listeners, key);
            });
        }
        return target;
    };
};

fluid.validateListenersImplemented = function (that) {
    var errors = [];
    fluid.each(that.events, function (event, name) {
        fluid.each(event.sortedListeners, function (lisrec) {
            if (lisrec.listener === fluid.notImplemented || lisrec.listener.globalName === "fluid.notImplemented") {
                errors.push({name: name, namespace: lisrec.namespace, componentSource: fluid.model.getSimple(that.options.listeners, [name + "." + lisrec.namespace, 0, "componentSource"])});
            }
        });
    });
    return errors;
};

fluid.arrayConcatPolicy = function (target, source) {
    return fluid.makeArray(target).concat(fluid.makeArray(source));
};

/*** FLUID LOGGING SYSTEM ***/

// This event represents the process of resolving the action of a request to fluid.log. Each listener shares
// access to an array, shallow-copied from the original arguments list to fluid.log, which is assumed writeable
// and which they may splice, transform, etc. before it is dispatched to the listener with namespace "log" which
// actually performs the logging action
fluid.loggingEvent = fluid.makeEventFirer({name: "logging event"});

fluid.addTimestampArg = function (args) {
    var arg0 = fluid.renderTimestamp(new Date()) + ":  ";
    args.unshift(arg0);
};

fluid.loggingEvent.addListener(fluid.doBrowserLog, "log");
// Not intended to be overridden - just a positional placeholder so that the priority of
// actions filtering the log arguments before dispatching may be referred to it
fluid.loggingEvent.addListener(fluid.identity, "filterArgs", "before:log");
fluid.loggingEvent.addListener(fluid.addTimestampArg, "addTimestampArg", "after:filterArgs");

/*** FLUID ERROR SYSTEM ***/

/** Upgrades a promise rejection payload (or Error) by suffixing an additional "while" reason into its "message" field
 * @param {Object|Error} originError - A rejection payload. This should (at least) have the member `isError: true` set, as well as a String `message` holding a rejection reason.
 * @param {String} whileMsg - A message describing the activity which led to this error
 * @return {Object} The rejected payload formed by shallow cloning the supplied argument (if it is not an `Error`) and suffixing its `message` member
 */
fluid.upgradeError = function (originError, whileMsg) {
    var error = originError instanceof Error ? originError :
        fluid.isPrimitive(originError) ? { message: originError} : fluid.extend({}, originError);
    error.message = error.message + whileMsg;
    return error;
};

fluid.failureEvent = fluid.makeEventFirer({name: "failure event"});

fluid.failureEvent.addListener(fluid.builtinFail, "fail");
fluid.failureEvent.addListener(fluid.logFailure, "log", "before:fail");

/*** DEFAULTS AND OPTIONS MERGING SYSTEM ***/

// A function to tag the types of all Fluid components
fluid.componentConstructor = function () {};

// Define the `name` property to be `"fluid.componentConstructor"` as a means to inspect if an Object is actually
// an Infusion component instance; while being agnostic of the Infusion codebase being present. For example this
// technique is used in the jquery.keyboard-a11y plugin for `fluid.thatistBridge`.
Object.defineProperty(fluid.componentConstructor, "name", {
    value: "fluid.componentConstructor"
});

/*
 * Create a "type tag" component with no state but simply a type name and id. The most
 * minimal form of Fluid component
 */
// No longer a publically supported function - we don't abolish this because it is too annoying to prevent
// circularity during the bootup of the IoC system if we try to construct full components before it is complete
// unsupported, non-API function
fluid.typeTag = function (type, id) {
    var that = Object.create(fluid.componentConstructor.prototype);
    that.typeName = type;
    that.id = id || fluid.allocateGuid();
    return that;
};

var gradeTick = 1; // tick counter for managing grade cache invalidation
var gradeTickStore = {};

fluid.defaultsStore = {};

fluid.flattenGradeName = function (gradeName) {
    return typeof(gradeName) === "string" ? gradeName : JSON.stringify(gradeName);
};

// unsupported, NON-API function
// Recursively builds up "gradeStructure" in first argument. 2nd arg receives gradeNames to be resolved, with stronger grades at right (defaults order)
// builds up gradeStructure.gradeChain pushed from strongest to weakest (reverse defaults order)
fluid.resolveGradesImpl = function (gs, gradeNames) {
    gradeNames = fluid.makeArray(gradeNames);
    for (var i = gradeNames.length - 1; i >= 0; --i) { // from stronger to weaker
        var gradeName = gradeNames[i];
        // cf. logic in fluid.accumulateDynamicGrades
        var flatGradeName = fluid.flattenGradeName(gradeName);
        if (gradeName && !gs.gradeHash[flatGradeName]) {
            var isDynamic = fluid.isReferenceOrExpander(gradeName);
            var options = (isDynamic ? null : fluid.rawDefaults(gradeName)) || {};
            var thisTick = gradeTickStore[gradeName] || (gradeTick - 1); // a nonexistent grade is recorded as just previous to current
            gs.lastTick = Math.max(gs.lastTick, thisTick);
            gs.gradeHash[flatGradeName] = true;
            gs.gradeChain.push(gradeName);
            var oGradeNames = fluid.makeArray(options.gradeNames);
            for (var j = oGradeNames.length - 1; j >= 0; --j) { // from stronger to weaker grades
                // TODO: in future, perhaps restore mergedDefaultsCache function of storing resolved gradeNames for bare grades
                fluid.resolveGradesImpl(gs, oGradeNames[j]);
            }
        }
    }
    return gs;
};

// unsupported, NON-API function
fluid.resolveGradeStructure = function (defaultName, gradeNames) {
    var gradeStruct = {
        lastTick: 0,
        gradeChain: [],
        gradeHash: {}
    };
    // stronger grades appear to the right in defaults - dynamic grades are stronger still - FLUID-5085
    // we supply these to resolveGradesImpl with strong grades at the right
    fluid.resolveGradesImpl(gradeStruct, [defaultName].concat(fluid.makeArray(gradeNames)));
    gradeStruct.gradeChain.reverse(); // reverse into defaults order
    return gradeStruct;
};

fluid.hasGrade = function (options, gradeName) {
    return !options || !options.gradeNames ? false : options.gradeNames.includes(gradeName);
};

// unsupported, NON-API function
fluid.resolveGrade = function (defaults, defaultName, gradeNames) {
    var gradeStruct = fluid.resolveGradeStructure(defaultName, gradeNames);
    // TODO: Fault in the merging algorithm does not actually treat arguments as immutable - failure in FLUID-5082 tests
    // due to listeners mergePolicy
    var mergeArgs = fluid.transform(gradeStruct.gradeChain, fluid.rawDefaults, fluid.copy);
    fluid.remove_if(mergeArgs, function (options) {
        return !options;
    });
    var mergePolicy = {};
    for (var i = 0; i < mergeArgs.length; ++i) {
        if (mergeArgs[i] && mergeArgs[i].mergePolicy) {
            mergePolicy = $.extend(true, mergePolicy, mergeArgs[i].mergePolicy);
        }
    }
    mergeArgs = [mergePolicy, {}].concat(mergeArgs);
    var mergedDefaults = fluid.merge.apply(null, mergeArgs);
    mergedDefaults.gradeNames = gradeStruct.gradeChain; // replace these since mergePolicy version is inadequate
    fluid.freezeRecursive(mergedDefaults);
    return {defaults: mergedDefaults, lastTick: gradeStruct.lastTick};
};

fluid.mergedDefaultsCache = {};

// unsupported, NON-API function
fluid.gradeNamesToKey = function (defaultName, gradeNames) {
    return defaultName + "|" + gradeNames.join("|");
};

// unsupported, NON-API function
// The main entry point to acquire the fully merged defaults for a combination of defaults plus mixin grades - from FluidIoC.js as well as recursively within itself
fluid.getMergedDefaults = function (defaultName, gradeNames) {
    gradeNames = fluid.makeArray(gradeNames);
    var key = fluid.gradeNamesToKey(defaultName, gradeNames);
    var mergedDefaults = fluid.mergedDefaultsCache[key];
    if (mergedDefaults) {
        var lastTick = 0; // check if cache should be invalidated through real latest tick being later than the one stored
        var searchGrades = mergedDefaults.defaults.gradeNames;
        for (var i = 0; i < searchGrades.length; ++i) {
            lastTick = Math.max(lastTick, gradeTickStore[searchGrades[i]] || 0);
        }
        if (lastTick > mergedDefaults.lastTick) {
            if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
                fluid.log(fluid.logLevel.TRACE, "Clearing cache for component " + defaultName + " with gradeNames ", searchGrades);
            }
            mergedDefaults = null;
        }
    }
    if (!mergedDefaults) {
        var defaults = fluid.rawDefaults(defaultName);
        if (!defaults) {
            return defaults;
        }
        mergedDefaults = fluid.mergedDefaultsCache[key] = fluid.resolveGrade(defaults, defaultName, gradeNames);
    }
    return mergedDefaults.defaults;
};

fluid.NO_ARGUMENTS = fluid.makeMarker("NO_ARGUMENTS");
// unsupported, NON-API function
/**
 * Upgrades an element of an IoC record which designates a function to prepare for a {func, args} representation.
 *
 * @param {Any} rec - The record to be upgraded. If an object will be returned unchanged. Otherwise it may be a function
 * object or an IoC reference to one.
 * @param {String} key - The key in the returned record to hold the function, this will default to `funcName` if `rec` is a `string` *not*
 * holding an IoC reference, or `func` otherwise
 * @return {Object} The original `rec` if it was not of primitive type, else a record holding { key : rec } if it was of primitive type.
 */
fluid.upgradePrimitiveFunc = function (rec, key) {
    if (rec && fluid.isPrimitive(rec)) {
        var togo = {};
        togo[key || (typeof(rec) === "string" && rec.charAt(0) !== "{" ? "funcName" : "func")] = rec;
        togo.args = fluid.NO_ARGUMENTS;
        return togo;
    } else {
        return rec;
    }
};

// unsupported, NON-API function
// Modify supplied options record to include "componentSource" annotation required by FLUID-5082
// TODO: This function really needs to act recursively in order to catch listeners registered for subcomponents - fix with FLUID-5614
fluid.annotateListeners = function (componentName, options) {
    options.listeners = fluid.transform(options.listeners, function (record) {
        var togo = fluid.makeArray(record);
        return fluid.transform(togo, function (onerec) {
            onerec = fluid.upgradePrimitiveFunc(onerec, "listener");
            onerec.componentSource = componentName;
            return onerec;
        });
    });
    options.invokers = fluid.transform(options.invokers, function (record) {
        record = fluid.upgradePrimitiveFunc(record);
        if (record) {
            record.componentSource = componentName;
        }
        return record;
    });
};

// Key structure: [["local"|"global"], workflowName] to {workflowType, priority, workflowOptions, gradeName, index}===workflowEntry
fluid.workflowCache = {};
// Sorted array of workflowEntry
fluid.workflowCacheSorted = [];

fluid.resortWorkflows = function (workflowType, baseIndex) {
    var thisCache = fluid.workflowCache[workflowType];
    var parsed = fluid.parsePriorityRecords(thisCache, workflowType + " workflows");
    parsed.forEach(function (oneParsed, index) {
        thisCache[oneParsed.namespace].index = index + baseIndex;
    });
    return parsed;
};

fluid.indexOneWorkflows = function (gradeName, workflowType, workflows, baseIndex) {
    fluid.each(workflows, function (oneWorkflow, workflowKey) {
        fluid.model.setSimple(fluid.workflowCache, [workflowType, workflowKey], {
            workflowType: workflowType,
            workflowName: workflowKey,
            priority: oneWorkflow.priority,
            gradeName: gradeName,
            workflowOptions: oneWorkflow
        });
    });
    return fluid.resortWorkflows(workflowType, baseIndex);
};

fluid.clearGradeWorkflows = function (gradeName, workflowType) {
    var cacheForType = fluid.workflowCache[workflowType];
    fluid.each(cacheForType, function (oneWorkflow, workflowKey) {
        if (oneWorkflow.gradeName === gradeName) {
            delete cacheForType[workflowKey];
        }
    });
};

fluid.indexGradeWorkflows = function (gradeName, options) {
    fluid.clearGradeWorkflows(gradeName, "global");
    fluid.clearGradeWorkflows(gradeName, "local");
    var sortedGlobal = fluid.indexOneWorkflows(gradeName, "global", fluid.getImmediate(options, ["workflows", "global"]), 0);
    var globalWorkflowCount = sortedGlobal.length;
    var sortedLocal = fluid.indexOneWorkflows(gradeName, "local", fluid.getImmediate(options, ["workflows", "local"]), globalWorkflowCount);
    fluid.workflowCacheSorted = sortedGlobal.concat(sortedLocal);
};

// unsupported, NON-API function
fluid.rawDefaults = function (componentName) {
    var entry = fluid.defaultsStore[componentName];
    return entry && entry.options;
};

// unsupported, NON-API function
fluid.registerRawDefaults = function (componentName, options) {
    fluid.pushActivity("registerRawDefaults", "registering defaults for grade %componentName with options %options",
        {componentName: componentName, options: options});
    var optionsCopy = fluid.expandCompact ? fluid.expandCompact(options) : fluid.copy(options);
    fluid.annotateListeners(componentName, optionsCopy);
    // TODO: consider moving workflows outside fluid.defaults system entirely since we special-case them so much
    fluid.indexGradeWorkflows(componentName, optionsCopy);
    delete optionsCopy.workflows;
    var callerInfo = fluid.getCallerInfo && fluid.getCallerInfo(6);
    fluid.freezeRecursive(optionsCopy);
    fluid.defaultsStore[componentName] = {
        options: optionsCopy,
        callerInfo: callerInfo
    };
    gradeTickStore[componentName] = gradeTick++;
    fluid.popActivity();
};

// unsupported, NON-API function
fluid.doIndexDefaults = function (defaultName, defaults, index, indexSpec) {
    var requiredGrades = fluid.makeArray(indexSpec.gradeNames);
    for (var i = 0; i < requiredGrades.length; ++i) {
        if (!fluid.hasGrade(defaults, requiredGrades[i])) { return; }
    }
    var indexFunc = typeof(indexSpec.indexFunc) === "function" ? indexSpec.indexFunc : fluid.getGlobalValue(indexSpec.indexFunc);
    var keys = indexFunc(defaults) || [];
    for (var j = 0; j < keys.length; ++j) {
        fluid.pushArray(index, keys[j], defaultName);
    }
};

/**
 * Evaluates an index specification over all the defaults records registered into the system.
 *
 * @param {String} indexName - The name of this index record (currently ignored)
 * @param {Object} indexSpec - Specification of the index to be performed - fields:
 *     gradeNames: {String|String[]} List of grades that must be matched by this indexer
 *     indexFunc:  {String|Function} An index function which accepts a defaults record and returns an array of keys
 * @return {Object} - A structure indexing keys to arrays of matched gradenames
 */
// The expectation is that this function is extremely rarely used with respect to registration of defaults
// in the system, so currently we do not make any attempts to cache the results. The field "indexName" is
// supplied in case a future implementation chooses to implement caching
fluid.indexDefaults = function (indexName, indexSpec) {
    var index = {};
    for (var defaultName in fluid.defaultsStore) {
        var defaults = fluid.getMergedDefaults(defaultName);
        fluid.doIndexDefaults(defaultName, defaults, index, indexSpec);
    }
    return index;
};

/**
 * Retrieves and stores a grade's configuration centrally.
 *
 * @param {String} componentName - The name of the grade whose options are to be read or written
 * @param {Object} [options] - An (optional) object containing the options to be set
 * @return {Object|undefined} - If `options` is omitted, returns the defaults for `componentName`.  Otherwise,
 * creates an instance of the named component with the supplied options.
 */
fluid.defaults = function (componentName, options) {
    if (options === undefined) {
        return fluid.getMergedDefaults(componentName);
    }
    else {
        if (options && options.options) {
            fluid.fail("Probable error in options structure for " + componentName +
                " with option named \"options\" - perhaps you meant to write these options at top level in fluid.defaults? - ", options);
        }
        fluid.registerRawDefaults(componentName, options);
        var gradedDefaults = fluid.getMergedDefaults(componentName);
        if (!fluid.hasGrade(gradedDefaults, "fluid.function")) {
            fluid.makeComponentCreator(componentName);
        }
    }
};

fluid.validateCreatorGrade = function (message, componentName) {
    var defaults = fluid.getMergedDefaults(componentName);
    if (!defaults || !defaults.gradeNames || defaults.gradeNames.length === 0) {
        fluid.fail(message + " type " + componentName + " which does not have any gradeNames defined");
    } else if (!defaults.argumentMap) {
        var blankGrades = [];
        for (var i = 0; i < defaults.gradeNames.length; ++i) {
            var gradeName = defaults.gradeNames[i];
            var rawDefaults = fluid.rawDefaults(gradeName);
            if (!rawDefaults) {
                blankGrades.push(gradeName);
            }
        }
        if (blankGrades.length === 0) {
            fluid.fail(message + " type " + componentName + " which is not derived from fluid.component");
        } else {
            fluid.fail("The grade hierarchy of component with type " + componentName + " is incomplete - it inherits from the following grade(s): " +
             blankGrades.join(", ") + " for which the grade definitions are corrupt or missing. Please check the files which might include these " +
             "grades and ensure they are readable and have been loaded by this instance of Infusion");
        }
    }
};

fluid.makeComponentCreator = function (componentName) {
    var creator = function () {
        fluid.validateCreatorGrade("Cannot make component creator for", componentName);
        return fluid.initFreeComponent(componentName, arguments);
    };
    var existing = fluid.getGlobalValue(componentName);
    if (existing) {
        $.extend(creator, existing);
    }
    fluid.setGlobalValue(componentName, creator);
};

fluid.emptyPolicy = fluid.freezeRecursive({});
/** Dereference an element of a `CompiledMergePolicy` armoured with the `*` key, ensuring to return an object
 * which can be tested for mergePolicy properties such as `replace` without failure. This function always returns
 * an object even if `policy` or `policy.*` is empty.
 * @param {Object} policy - A trunk member of a `CompiledMergePolicy`
 * @return {Object} A leaf object which can be property-tested for a builtin mergePolicy.
 */
// unsupported, NON-API function
fluid.derefMergePolicy = function (policy) {
    return (policy ? policy["*"] : fluid.emptyPolicy) || fluid.emptyPolicy;
};

/** @typedef {Object} CompiledMergePolicyReturn
 * @property {Object} defaultValues - An map of options paths to IoC references holding the path elsewhere within
 *    the options structure from where the default value for them is to be taken
 * @property {CompiledMergePolicy} builtins - A structure isomorphic to the options structure, with node-specific
 *    metadata held in a leaf child named "*". This metadata holds a map of builtin mergePolicies to "*" as well
 *    as possibly a function member named `func`.
 * @property {Boolean} [hasDefaults] - `true` if the `defaultValues` object has any members
 */

/** Accepts a mergePolicy as encoded in a component's options and outputs a "compiled" variant which is more suited
 * to random structure-directed access. This is isomorphic to the options structure itself, with node-specific metadata
 * housed in a leaf child named "*". Function policies will be attached to a leaf member named `func`, and
 * "default value merge policies" (references to other option paths) will be converted into IoC self-references
 * beginning with `{that}.options`.
 * @param {Object} mergePolicy - The `mergePolicy` options area of a component
 * @return {CompiledMergePolicyReturn} - Holds members:
 * A CompiledMergePolicy object housing the "compiled" rendering of the merge policy, in the member `builtins` and
 * any dereferenced default value policies in the member `defaultValues`
 */
// Main entry point is in fluid.mergeComponentOptions
// Note that there is currently no support for other than flat mergePolicies
// unsupported, NON-API function
fluid.compileMergePolicy = function (mergePolicy) {
    var builtins = {}, defaultValues = {};
    var togo = {builtins: builtins, defaultValues: defaultValues};

    if (!mergePolicy) {
        return togo;
    }
    fluid.each(mergePolicy, function (value, key) {
        var parsed = {}, builtin = true;
        if (typeof(value) === "function") {
            parsed.func = value;
        }
        else if (typeof(value) === "object") {
            parsed = value;
        }
        else if (!fluid.isDefaultValueMergePolicy(value)) {
            var split = value.split(/\s*,\s*/);
            for (var i = 0; i < split.length; ++i) {
                parsed[split[i]] = true;
            }
        }
        else {
            // Convert to ginger self-reference - NB, this can only be parsed by IoC
            fluid.set(defaultValues, key, "{that}.options." + value);
            togo.hasDefaults = true;
            builtin = false;
        }
        if (builtin) {
            fluid.set(builtins, fluid.composePath(key, "*"), parsed);
        }
    });
    return togo;
};

// TODO: deprecate this method of detecting default value merge policies before 1.6 in favour of
// explicit typed records a la ModelTransformations
// unsupported, NON-API function
fluid.isDefaultValueMergePolicy = function (policy) {
    return typeof(policy) === "string" &&
        (policy.indexOf(",") === -1 && !/replace|nomerge|noexpand/.test(policy));
};

// unsupported, NON-API function
fluid.mergeOneImpl = function (thisTarget, thisSource, j, sources, newPolicy, newPolicyHolder, i, segs) {
    var togo = thisTarget;

    var primitiveTarget = fluid.isPrimitive(thisTarget);

    if (thisSource !== undefined) {
        if (!newPolicy.func && thisSource !== null && fluid.isPlainObject(thisSource) && !newPolicy.nomerge) {
            if (primitiveTarget) {
                togo = thisTarget = fluid.freshContainer(thisSource);
            }
            // recursion is now external? We can't do it from here since sources are not all known
            // options.recurse(thisTarget, i + 1, segs, sources, newPolicyHolder, options);
        } else {
            sources[j] = undefined;
            if (newPolicy.func) {
                togo = newPolicy.func.call(null, thisTarget, thisSource, newPolicyHolder, segs, i);
            } else {
                togo = thisSource;
            }
        }
    }
    return togo;
};

// NB - same quadratic worry about these as in FluidIoC in the case the RHS trundler is live -
// since at each regeneration step driving the RHS we are discarding the "cursor arguments" these
// would have to be regenerated at each step - although in practice this can only happen once for
// each object for all time, since after first resolution it will be concrete.
//
// TODO: This method is unnecessary and will quadratic inefficiency if RHS block is not concrete.
// The driver should detect "homogeneous uni-strategy trundling" and agree to preserve the extra
// "cursor arguments" which should be advertised somehow (at least their number)
//
// unsupported, NON-API function
fluid.regenerateCursor = function (source, segs, limit, sourceStrategy) {
    for (var i = 0; i < limit; ++i) {
        source = sourceStrategy(source, segs[i], i, fluid.makeArray(segs)); // copy for FLUID-5243
    }
    return source;
};

// unsupported, NON-API function
fluid.regenerateSources = function (sources, segs, limit, sourceStrategies) {
    var togo = [];
    for (var i = 0; i < sources.length; ++i) {
        var thisSource = fluid.regenerateCursor(sources[i], segs, limit, sourceStrategies[i]);
        togo.push(thisSource);
    }
    return togo;
};

// unsupported, NON-API function
fluid.fetchMergeChildren = function (target, i, segs, sources, mergePolicy, options) {
    var thisPolicy = fluid.derefMergePolicy(mergePolicy);
    for (var j = sources.length - 1; j >= 0; --j) { // this direction now irrelevant - control is in the strategy
        var source = sources[j];
        // NB - this detection relies on strategy return being complete objects - which they are
        // although we need to set up the roots separately. We need to START the process of evaluating each
        // object root (sources) COMPLETELY, before we even begin! Even if the effect of this is to cause a
        // dispatch into ourselves almost immediately. We can do this because we can take control over our
        // TARGET objects and construct them early. Even if there is a self-dispatch, it will be fine since it is
        // DIRECTED and so will not trouble our "slow" detection of properties. After all self-dispatches end, control
        // will THEN return to "evaluation of arguments" (expander blocks) and only then FINALLY to this "slow"
        // traversal of concrete properties to do the final merge.
        if (source !== undefined) {
            fluid.each(source, function (newSource, name) {
                var childPolicy = fluid.concreteTrundler(mergePolicy, name);
                // 2nd arm of condition is an Outrageous bodge to fix FLUID-4930 further. See fluid.tests.retrunking in FluidIoCTests.js
                // We make extra use of the old "evaluateFully" flag and ensure to flood any trunk objects again during final "initter" phase of merging.
                // The problem is that a custom mergePolicy may have replaced the system generated trunk with a differently structured object which we must not
                // corrupt. This work should properly be done with a set of dedicated provenance/progress records in a separate structure
                if (!(name in target) || (options.evaluateFully && childPolicy === undefined && !fluid.isPrimitive(target[name]))) { // only request each new target key once -- all sources will be queried per strategy
                    segs[i] = name; // TODO: Why doesn't this corrupt the requestor's segs?
                    options.strategy(target, name, i + 1, segs, sources, mergePolicy);
                }
            });
            if (thisPolicy.replace) { // this branch primarily deals with a policy of replace at the root
                break;
            }
        }
    }
    return target;
};

// A special marker object which will be placed at a current evaluation point in the tree in order
// to protect against circular evaluation
fluid.inEvaluationMarker = Object.freeze({"__CURRENTLY_IN_EVALUATION__": true});

// A path depth above which the core "process strategies" will bail out, assuming that the
// structure has become circularly linked. Helpful in environments such as Firebug which will
// kill the browser process if they happen to be open when a stack overflow occurs. Also provides
// a more helpful diagnostic.
fluid.strategyRecursionBailout = 50;

// unsupported, NON-API function
fluid.makeMergeStrategy = function (options) {
    var strategy = function (target, name, i, segs, sources, policy) {
        if (i > fluid.strategyRecursionBailout) {
            fluid.fail("Overflow/circularity in options merging, current path is ", segs, " at depth " , i, " - please protect components from merging using the \"nomerge\" merge policy");
        }
        if (fluid.isPrimitive(target)) { // For "use strict"
            return undefined; // Review this after FLUID-4925 since the only trigger is in slow component lookahead
        }
        if (fluid.isTracing) {
            fluid.tracing.pathCount.push(fluid.path(segs.slice(0, i)));
        }

        var oldTarget;
        if (name in target || options.fullyEvaluated) { // bail out if our work has already been done
            oldTarget = target[name];
            if (!options.evaluateFully) { // see notes on this hack in "initter" - early attempt to deal with FLUID-4930
                return oldTarget;
            }
        }
        if (sources === undefined) { // recover our state in case this is an external entry point
            segs = fluid.makeArray(segs); // avoid trashing caller's segs
            sources = fluid.regenerateSources(options.sources, segs, i - 1, options.sourceStrategies);
            policy = fluid.regenerateCursor(options.mergePolicy, segs, i - 1, fluid.concreteTrundler);
        }
        var newPolicyHolder = fluid.concreteTrundler(policy, name);
        var newPolicy = fluid.derefMergePolicy(newPolicyHolder);

        var start, limit, mul;
        if (newPolicy.replace) {
            start = 1 - sources.length; limit = 0; mul = -1;
        }
        else {
            start = 0; limit = sources.length - 1; mul = +1;
        }
        var newSources = [];
        var thisTarget;

        for (var j = start; j <= limit; ++j) { // TODO: try to economise on this array and on gaps
            var k = mul * j;
            var thisSource = options.sourceStrategies[k](sources[k], name, i, segs); // Run the RH algorithm in "driving" mode
            if (thisSource !== undefined) {
                if (!fluid.isPrimitive(thisSource)) {
                    newSources[k] = thisSource;
                }
                if (oldTarget === undefined) {
                    if (mul === -1) { // if we are going backwards, it is "replace"
                        thisTarget = target[name] = thisSource;
                        break;
                    }
                    else {
                        // write this in early, since early expansions may generate a trunk object which is written in to by later ones
                        thisTarget = fluid.mergeOneImpl(thisTarget, thisSource, j, newSources, newPolicy, newPolicyHolder, i, segs, options);
                        target[name] = thisTarget;
                    }
                }
            }
        }
        if (oldTarget !== undefined) {
            thisTarget = oldTarget;
        }
        if (newSources.length > 0) {
            if (fluid.isPlainObject(thisTarget)) {
                fluid.fetchMergeChildren(thisTarget, i, segs, newSources, newPolicyHolder, options);
            }
        }
        return thisTarget;
    };
    options.strategy = strategy;
    return strategy;
};

// A simple stand-in for "fluid.get" where the material is covered by a single strategy
fluid.driveStrategy = function (root, pathSegs, strategy) {
    pathSegs = fluid.makeArray(pathSegs);
    for (var i = 0; i < pathSegs.length; ++i) {
        if (!root) {
            return undefined;
        }
        root = strategy(root, pathSegs[i], i + 1, pathSegs);
    }
    return root;
};

// A very simple "new inner trundler" that just performs concrete property access
// Note that every "strategy" is also a "trundler" of this type, considering just the first two arguments
fluid.concreteTrundler = function (source, seg) {
    return !source ? undefined : source[seg];
};

/**
 * Merge a collection of options structures onto a target, following an optional policy.
 * This method is now used only for the purpose of merging "dead" option documents in order to
 * cache graded component defaults. Component option merging is now performed by the
 * fluid.makeMergeOptions pathway which sets up a deferred merging process. This function
 * will not be removed in the Fluid 2.0 release but it is recommended that users not call it
 * directly.
 * The behaviour of this function is explained more fully on
 * the page http://wiki.fluidproject.org/display/fluid/Options+Merging+for+Fluid+Components .
 *
 * @param {Object|String} policy - A "policy object" specifiying the type of merge to be performed.
 * If policy is of type {String} it should take on the value "replace" representing
 * a static policy. If it is an
 * Object, it should contain a mapping of EL paths onto these String values, representing a
 * fine-grained policy. If it is an Object, the values may also themselves be EL paths
 * representing that a default value is to be taken from that path.
 * @param {...Object} sources - An arbitrary list of options structures which are to
 * be merged together. These will not be modified.
 * @return {Object} The merged options
 */
fluid.merge = function (policy, ...sources) {
    var compiled = fluid.compileMergePolicy(policy).builtins;
    var options = fluid.makeMergeOptions(compiled, sources, {});
    options.initter();
    return options.target;
};

/* Construct the core of the `mergeOptions` structure responsible for evaluating merged options.
 * This will eventually be housed in the shadow as `shadow.mergeOptions`.
 * The main entry point is `fluid.mergeComponentOptions` which will add other elements such as `mergeBlocks`
 */
// unsupported, NON-API function
fluid.makeMergeOptions = function (policy, sources, userOptions) {
    // note - we close over the supplied policy as a shared object reference - it will be updated during discovery
    var options = {
        mergePolicy: policy,
        sources: sources
    };
    options = $.extend(options, userOptions);
    options.target = options.target || fluid.freshContainer(options.sources[0]);
    options.sourceStrategies = options.sourceStrategies || fluid.generate(options.sources.length, fluid.concreteTrundler);
    options.initter = function () {
        // This hack is necessary to ensure that the FINAL evaluation doesn't balk when discovering a trunk path which was already
        // visited during self-driving via the expander. This bi-modality is sort of rubbish, but we currently don't have "room"
        // in the strategy API to express when full evaluation is required - and the "flooding API" is not standardised. See FLUID-4930
        options.evaluateFully = true;
        fluid.fetchMergeChildren(options.target, 0, [], options.sources, options.mergePolicy, options);
        options.fullyEvaluated = true;
    };
    fluid.makeMergeStrategy(options);
    return options;
};

// unsupported, NON-API function
fluid.transformOptions = function (options, transRec) {
    fluid.expect("Options transformation record", transRec, ["transformer", "config"]);
    var transFunc = fluid.getGlobalValue(transRec.transformer);
    return transFunc.call(null, options, transRec.config);
};

// unsupported, NON-API function
fluid.findMergeBlocks = function (mergeBlocks, recordType) {
    return fluid.remove_if(fluid.makeArray(mergeBlocks), function (block) { return block.recordType !== recordType; });
};

// unsupported, NON-API function
fluid.transformOptionsBlocks = function (mergeBlocks, transformOptions, recordTypes) {
    fluid.each(recordTypes, function (recordType) {
        var blocks = fluid.findMergeBlocks(mergeBlocks, recordType);
        fluid.each(blocks, function (block) {
            var source = block.source ? "source" : "target"; // TODO: Problem here with irregular presentation of options which consist of a reference in their entirety
            block[block.simple || source === "target" ? "target" : "source"] = fluid.transformOptions(block[source], transformOptions);
        });
    });
};

// unsupported, NON-API function
fluid.dedupeDistributionNamespaces = function (mergeBlocks) { // to implement FLUID-5824
    var byNamespace = {};
    fluid.remove_if(mergeBlocks, function (mergeBlock) {
        var ns = mergeBlock.namespace;
        if (ns) {
            if (byNamespace[ns] && byNamespace[ns] !== mergeBlock.contextThat.id) {  // source check for FLUID-5835
                return true;
            } else {
                byNamespace[ns] = mergeBlock.contextThat.id;
            }
        }
    });
};

// The types of merge record the system supports, with the weakest records first
fluid.mergeRecordTypes = {
    defaults:           1000,
    defaultValueMerge:  900,
    lensedComponents:    800,
    subcomponentRecord: 700,
    user:               600,
    distribution:       100 // and above
};

// Utility used in the framework (primarily with distribution assembly), unconnected with new ChangeApplier
// unsupported, NON-API function
fluid.model.applyChangeRequest = function (model, request) {
    var segs = request.segs;
    if (segs.length === 0) {
        if (request.type === "ADD") {
            $.extend(true, model, request.value);
        } else {
            fluid.clear(model);
        }
    } else if (request.type === "ADD") {
        fluid.model.setSimple(model, request.segs, request.value);
    } else {
        for (var i = 0; i < segs.length - 1; ++i) {
            model = model[segs[i]];
            if (!model) {
                return;
            }
        }
        var last = fluid.peek(segs);
        delete model[last];
    }
};

/**
 * Delete the value in the supplied object held at the specified path
 *
 * @param {Object} target - The object holding the value to be deleted (possibly empty)
 * @param {String[]} segs - the path of the value to be deleted
 */
// unsupported, NON-API function
fluid.destroyValue = function (target, segs) {
    if (target) {
        fluid.model.applyChangeRequest(target, {type: "DELETE", segs: segs});
    }
};

/**
 * Merges the component's declared defaults, as obtained from fluid.defaults(),
 * with the user's specified overrides.
 *
 * @param {Component} that - The instance to attach the options to
 * @param {Potentia} potentia - The potentia record supplied for this construction
 * @param {MergeRecords} lightMerge - A structure as produced from `fluid.lightMergeRecords` performing light pre-merging of
 * options records
 * @return {MergeOptions} The mergeOptions structure ready to be mounted in the component's shadow
 */
// unsupported, NON-API function
fluid.mergeComponentOptions = function (that, potentia, lightMerge) {
    fluid.validateCreatorGrade("Cannot construct component of", lightMerge.type);
    var sharedMergePolicy = {};

    // FROM HERE we notify the instantiator, fabricate destroy, etc.
    var mergeBlocks = fluid.expandComponentOptions(sharedMergePolicy, potentia, lightMerge, that);

    var options = {}; // ultimate target
    var sourceStrategies = [], sources = [];
    var baseMergeOptions = {
        target: options,
        sourceStrategies: sourceStrategies
    };
    // Called both from here and from IoC whenever there is a change of block content or arguments which
    // requires them to be resorted and rebound
    var updateBlocks = function () {
        fluid.each(mergeBlocks, function (block) {
            if (fluid.isPrimitive(block.priority)) {
                block.priority = fluid.parsePriority(block.priority, 0, false, block.recordType);
            }
        });
        fluid.sortByPriority(mergeBlocks);
        fluid.dedupeDistributionNamespaces(mergeBlocks);
        sourceStrategies.length = 0;
        sources.length = 0;
        fluid.each(mergeBlocks, function (block) {
            sourceStrategies.push(block.strategy);
            sources.push(block.target);
        });
    };
    updateBlocks();
    var mergeOptions = fluid.makeMergeOptions(sharedMergePolicy, sources, baseMergeOptions);
    mergeOptions.mergeBlocks = mergeBlocks;
    mergeOptions.updateBlocks = updateBlocks;
    mergeOptions.destroyValue = function (segs) { // This method is a temporary hack to assist FLUID-5091
        for (var i = 0; i < mergeBlocks.length; ++i) {
            if (!mergeBlocks[i].immutableTarget) {
                fluid.destroyValue(mergeBlocks[i].target, segs);
            }
        }
        fluid.destroyValue(baseMergeOptions.target, segs);
    };

    var compiledPolicy;
    var mergePolicy;
    function computeMergePolicy() {
        // Decode the now available mergePolicy
        mergePolicy = fluid.driveStrategy(options, "mergePolicy", mergeOptions.strategy);
        mergePolicy = $.extend({}, fluid.rootMergePolicy, mergePolicy);
        compiledPolicy = fluid.compileMergePolicy(mergePolicy);
        // TODO: expandComponentOptions has already put some builtins here - performance implications of the now huge
        // default mergePolicy material need to be investigated as well as this deep merge
        $.extend(true, sharedMergePolicy, compiledPolicy.builtins); // ensure it gets broadcast to all sharers
    }
    computeMergePolicy();
    mergeOptions.computeMergePolicy = computeMergePolicy;

    if (compiledPolicy.hasDefaults) {
        mergeBlocks.push(fluid.generateExpandBlock({
            options: compiledPolicy.defaultValues,
            recordType: "defaultValueMerge",
            priority: fluid.mergeRecordTypes.defaultValueMerge
        }, that, {}));
        updateBlocks();
    }
    that.options = options;
    fluid.driveStrategy(options, "gradeNames", mergeOptions.strategy);

    fluid.deliverOptionsStrategy(that, options, mergeOptions); // do this early to broadcast and receive "distributeOptions"

    fluid.computeComponentAccessor(that, potentia.localRecord);

    var transformOptions = fluid.driveStrategy(options, "transformOptions", mergeOptions.strategy);
    if (transformOptions) {
        fluid.transformOptionsBlocks(mergeBlocks, transformOptions, ["user", "subcomponentRecord"]);
        updateBlocks(); // because the possibly simple blocks may have changed target
    }

    if (!baseMergeOptions.target.mergePolicy) {
        computeMergePolicy();
    }

    return mergeOptions;
};

// The Fluid Component System proper

// The base system grade definitions

fluid.defaults("fluid.function", {});

/**
 * Invoke a global function by name and named arguments. A courtesy to allow declaratively encoded function calls
 * to use named arguments rather than bare arrays.
 *
 * @param {String} name - A global name which can be resolved to a Function. The defaults for this name must
 * resolve onto a grade including "fluid.function". The defaults record should also contain an entry
 * <code>argumentMap</code>, a hash of argument names onto indexes.
 * @param {Object} spec - A named hash holding the argument values to be sent to the function. These will be looked
 * up in the <code>argumentMap</code> and resolved into a flat list of arguments.
 * @return {Any} The return value from the function
 */
fluid.invokeGradedFunction = function (name, spec) {
    var defaults = fluid.defaults(name);
    if (!defaults || !defaults.argumentMap || !fluid.hasGrade(defaults, "fluid.function")) {
        fluid.fail("Cannot look up name " + name +
            " to a function with registered argumentMap - got defaults ", defaults);
    }
    var args = [];
    fluid.each(defaults.argumentMap, function (value, key) {
        args[value] = spec[key];
    });
    return fluid.invokeGlobalFunction(name, args);
};

fluid.noNamespaceDistributionPrefix = "no-namespace-distribution-";

fluid.mergeOneDistribution = function (target, source, key) {
    var namespace = source.namespace || key || fluid.noNamespaceDistributionPrefix + fluid.allocateGuid();
    source.namespace = namespace;
    target[namespace] = $.extend(true, {}, target[namespace], source);
};

fluid.distributeOptionsPolicy = function (target, source) {
    target = target || {};
    if (fluid.isArrayable(source)) {
        for (var i = 0; i < source.length; ++i) {
            fluid.mergeOneDistribution(target, source[i]);
        }
    } else if (typeof(source.target) === "string") {
        fluid.mergeOneDistribution(target, source);
    } else {
        fluid.each(source, function (oneSource, key) {
            fluid.mergeOneDistribution(target, oneSource, key);
        });
    }
    return target;
};

fluid.mergingArray = function () {};
fluid.mergingArray.prototype = [];

// Defer all evaluation of all nested members to hack FLUID-5668
fluid.deferringMergePolicy = function (target, source, mergePolicyHolder) {
    target = target || {};
    fluid.each(source, function (oneSource, key) {
        if (!target[key]) {
            target[key] = new fluid.mergingArray();
        }
        if (fluid.derefMergePolicy(mergePolicyHolder[key]).replace && oneSource !== undefined) {
            target[key].length = 0;
        }
        if (oneSource instanceof fluid.mergingArray) {
            target[key].push.apply(target[key], oneSource);
        } else if (oneSource !== undefined) {
            target[key].push(oneSource);
        }
    });
    return target;
};

fluid.invokerStrategies = fluid.arrayToHash(["func", "funcName", "listener", "this", "method", "changePath", "value"]);

// Resolve FLUID-5741, FLUID-5184 by ensuring that we avoid mixing incompatible invoker strategies
fluid.invokersMergePolicy = function (target, source) {
    target = target || {};
    fluid.each(source, function (oneInvoker, name) {
        if (!oneInvoker) {
            target[name] = oneInvoker;
            return;
        } else {
            oneInvoker = fluid.upgradePrimitiveFunc(oneInvoker);
        }
        var oneT = target[name];
        if (!oneT) {
            oneT = target[name] = {};
        }
        for (var key in fluid.invokerStrategies) {
            if (key in oneInvoker) {
                for (var key2 in fluid.invokerStrategies) {
                    oneT[key2] = undefined; // can't delete since stupid driveStrategy bug from recordStrategy reinstates them
                }
            }
        }
        $.extend(oneT, oneInvoker);
    });
    return target;
};

fluid.rootMergePolicy = fluid.freezeRecursive({
    gradeNames: fluid.arrayConcatPolicy,
    distributeOptions: fluid.distributeOptionsPolicy,
    members: {
        noexpand: true,
        func: fluid.deferringMergePolicy
    },
    invokers: {
        noexpand: true,
        func: fluid.invokersMergePolicy
    },
    components: {
        noexpand: true,
        // We use this dim mergePolicy since i) there is enough room in the records for provenance information, and
        // ii) noone can try to consume this, e.g. to find type/createOnEvent before it hits a potentia anyway
        func: fluid.deferringMergePolicy
    },
    dynamicComponents: {
        noexpand: true,
        func: fluid.deferringMergePolicy
    },
    transformOptions: "replace",
    listeners: fluid.makeMergeListenersPolicy(fluid.mergeListenerPolicy)
});

fluid.defaults("fluid.component", {
    mergePolicy: fluid.rootMergePolicy,
    argumentMap: {
        options: 0
    },
    workflows: {
        local: {
            concludeComponentObservation: {
                funcName: "fluid.concludeComponentObservation",
                priority: "first"
            },
            concludeComponentInit: {
                funcName: "fluid.concludeComponentInit",
                waitIO: true,
                priority: "last"
            }
        }
    },
    events: { // Three standard lifecycle points common to all components
        onCreate:     "promise",
        onDestroy:    "promise",
        afterDestroy: "promise"
    }
});

/* Compute a "nickname" given a fully qualified typename, by returning the last path
 * segment.
 */
fluid.computeNickName = function (typeName) {
    var segs = fluid.model.parseEL(typeName);
    return fluid.peek(segs);
};

/** Returns <code>true</code> if the supplied reference holds a component which has been destroyed or for which destruction has started
 * @param {Component} that - A reference to a component
 * @param {Boolean} [strict] - If `true`, the test will only check whether the component has been fully destroyed
 * @return {Boolean} `true` if the reference is to a component which has been destroyed
 **/
fluid.isDestroyed = function (that, strict) {
    return that.lifecycleStatus === "destroyed" || (!strict && that.lifecycleStatus === "destroying");
};

// Computes a name for a component appearing at the global root which is globally unique, from its nickName and id
fluid.computeGlobalMemberName = function (type, id) {
    var nickName = fluid.computeNickName(type);
    return nickName + "-" + id;
};

// Adapt a promise rejection indicating a transaction failure back to an exception for clients in orthochronous code
fluid.adaptTransactionFailure = function (transRec) {
    var returned = false;
    // This registration MUST go last otherwise we mask the catch->reject handler in bindDeferredComponent
    transRec.promise.then(null, function (e) {
        if (!returned) {
            throw e;
        } else {
            if (transRec.promise.onReject.length === 0) {
                fluid.fireUnhandledRejection(transRec.promise, e);
            }
        }
    });
    returned = true;
};

// unsupported, NON-API function
// After some error checking, this *is* the component creator function
fluid.initFreeComponent = function (type, initArgs) {
    var id = fluid.allocateGuid();
    // TODO: Perhaps one day we will support a directive which allows the user to select a current component
    // root for free components other than the global root
    var path = [fluid.computeGlobalMemberName(type, id)];
    var userRecord = {
        recordType: "user",
        type: type
    };
    var upDefaults = fluid.defaults(type);
    // TODO: We only support these two signatures now, probably abolish argumentMap if it weren't for "graded functions"
    // Or else write a proper mergePolicy for argumentMaps
    var argMap = fluid.defaults(upDefaults.argumentMap.container !== undefined ?
        "fluid.viewComponent" : "fluid.component").argumentMap;
    fluid.each(argMap, function (index, name) {
        var arg = initArgs[index];
        userRecord[name] = name === "options" ? fluid.expandCompact(arg, true) : arg;
    });
    var potentia = {
        type: "create",
        path: path,
        componentId: id,
        records: [userRecord]
    };
    var transRec = fluid.registerPotentia(potentia);
    var shadow = fluid.commitPotentiae(transRec.transactionId);
    fluid.adaptTransactionFailure(transRec);

    return shadow && shadow.that;
};

// ******* SELECTOR ENGINE *********

// selector regexps copied from jQuery - recent versions correct the range to start C0
// The initial portion of the main character selector: "just add water" to add on extra
// accepted characters, as well as the "\\\\." -> "\." portion necessary for matching
// period characters escaped in selectors
var charStart = "(?:[\\w\\u00c0-\\uFFFF*_-";

fluid.simpleCSSMatcher = {
    regexp: new RegExp("([#.]?)(" + charStart + "]|\\\\.)+)", "g"),
    charToTag: {
        "": "tag",
        "#": "id",
        ".": "clazz"
    }
};

fluid.IoCSSMatcher = {
    regexp: new RegExp("([&#]?)(" + charStart + "]|\\.|\\/)+)", "g"),
    charToTag: {
        "": "context",
        "&": "context",
        "#": "id"
    }
};

var childSeg = new RegExp("\\s*(>)?\\s*", "g");
// var whiteSpace = new RegExp("^\\w*$");

// Parses a selector expression into a data structure holding a list of predicates
// 2nd argument is a "strategy" structure, e.g.  fluid.simpleCSSMatcher or fluid.IoCSSMatcher
// unsupported, non-API function
fluid.parseSelector = function (selstring, strategy) {
    var togo = [];
    selstring = selstring.trim();
    //ws-(ss*)[ws/>]
    var regexp = strategy.regexp;
    regexp.lastIndex = 0;
    var lastIndex = 0;
    while (true) {
        var atNode = []; // a list of predicates at a particular node
        var first = true;
        while (true) {
            var segMatch = regexp.exec(selstring);
            if (!segMatch) {
                break;
            }
            if (segMatch.index !== lastIndex) {
                if (first) {
                    fluid.fail("Error in selector string - cannot match child selector expression starting at " + selstring.substring(lastIndex));
                }
                else {
                    break;
                }
            }
            var thisNode = {};
            var text = segMatch[2];
            var targetTag = strategy.charToTag[segMatch[1]];
            if (targetTag) {
                thisNode[targetTag] = text;
            }
            atNode[atNode.length] = thisNode;
            lastIndex = regexp.lastIndex;
            first = false;
        }
        childSeg.lastIndex = lastIndex;
        var fullAtNode = {predList: atNode};
        var childMatch = childSeg.exec(selstring);
        if (!childMatch || childMatch.index !== lastIndex) {
            fluid.fail("Error in selector string - can not match child selector expression at " + selstring.substring(lastIndex));
        }
        if (childMatch[1] === ">") {
            fullAtNode.child = true;
        }
        togo[togo.length] = fullAtNode;
        // >= test here to compensate for IE bug http://blog.stevenlevithan.com/archives/exec-bugs
        if (childSeg.lastIndex >= selstring.length) {
            break;
        }
        lastIndex = childSeg.lastIndex;
        regexp.lastIndex = childSeg.lastIndex;
    }
    return togo;
};

// Message resolution and templating

/**
 *
 * Take an original object and represent it using top-level sub-elements whose keys are EL Paths.  For example,
 * `originalObject` might look like:
 *
 * ```
 * {
 *   deep: {
 *     path: {
 *       value: "foo",
 *       emptyObject: {},
 *       array: [ "peas", "porridge", "hot"]
 *     }
 *   }
 * }
 * ```
 *
 * Calling `fluid.flattenObjectKeys` on this would result in a new object that looks like:
 *
 * ```
 * {
 *   "deep": "[object Object]",
 *   "deep.path": "[object Object]",
 *   "deep.path.value": "foo",
 *   "deep.path.array": "peas,porridge,hot",
 *   "deep.path.array.0": "peas",
 *   "deep.path.array.1": "porridge",
 *   "deep.path.array.2": "hot"
 * }
 * ```
 *
 * This function preserves the previous functionality of displaying an entire object using its `toString` function,
 * which is why many of the paths above resolve to "[object Object]".
 *
 * This function is an unsupported non-API function that is used in by `fluid.stringTemplate` (see below).
 *
 * @param {Object} originalObject - An object.
 * @return {Object} A representation of the original object that only contains top-level sub-elements whose keys are EL Paths.
 */
// unsupported, non-API function
fluid.flattenObjectPaths = function (originalObject) {
    var flattenedObject = {};
    fluid.each(originalObject, function (value, key) {
        if (value !== null && typeof value === "object") {
            var flattenedSubObject = fluid.flattenObjectPaths(value);
            fluid.each(flattenedSubObject, function (subValue, subKey) {
                flattenedObject[key + "." + subKey] = subValue;
            });
            if (typeof fluid.get(value, "toString") === "function") {
                flattenedObject[key] = value.toString();
            }
        }
        else {
            flattenedObject[key] = value;
        }
    });
    return flattenedObject;
};

/**
 *
 * Simple string template system.  Takes a template string containing tokens in the form of "%value" or
 * "%deep.path.to.value".  Returns a new string with the tokens replaced by the specified values.  Keys and values
 * can be of any data type that can be coerced into a string.
 *
 * @param {String} template - A string that contains placeholders for tokens of the form `%token` embedded into it.
 * @param {Object.<String.String>} values - A map of token names to the values which should be interpolated.
 * @return {String} The text of `template` whose tokens have been interpolated with values.
 */
fluid.stringTemplate = function (template, values) {
    var flattenedValues = fluid.flattenObjectPaths(values);
    var keys = fluid.keys(flattenedValues);
    keys = keys.sort(fluid.compareStringLength());
    for (var i = 0; i < keys.length; ++i) {
        var key = keys[i];
        var templatePlaceholder = "%" + key;
        var replacementValue = flattenedValues[key];

        var indexOfPlaceHolder = -1;
        while ((indexOfPlaceHolder = template.indexOf(templatePlaceholder)) !== -1) {
            template = template.slice(0, indexOfPlaceHolder) + replacementValue + template.slice(indexOfPlaceHolder + templatePlaceholder.length);
        }
    }
    return template;
};
