"use strict";
/**
 * Copyright (c) Haiku 2016-2018. All rights reserved.
 */
Object.defineProperty(exports, "__esModule", { value: true });
var HaikuHelpers_1 = require("./HaikuHelpers");
var BasicUtils_1 = require("./helpers/BasicUtils");
var consoleErrorOnce_1 = require("./helpers/consoleErrorOnce");
var interactionModes_1 = require("./helpers/interactionModes");
var fallbacks_1 = require("./properties/dom/fallbacks");
var parsers_1 = require("./properties/dom/parsers");
var schema_1 = require("./properties/dom/schema");
var enhance_1 = require("./reflection/enhance");
var Transitions_1 = require("./Transitions");
var assign_1 = require("./vendor/assign");
var FUNCTION = 'function';
var KEYFRAME_ZERO = 0;
var OBJECT = 'object';
var isFunction = function (value) {
    return typeof value === FUNCTION;
};
var INJECTABLES = {};
if (typeof window !== 'undefined') {
    INJECTABLES.$window = {
        schema: {
            width: 'number',
            height: 'number',
            screen: {
                availHeight: 'number',
                availLeft: 'number',
                availWidth: 'number',
                colorDepth: 'number',
                height: 'number',
                pixelDepth: 'number',
                width: 'number',
                orientation: {
                    angle: 'number',
                    type: 'string',
                },
            },
            navigator: {
                userAgent: 'string',
                appCodeName: 'string',
                appName: 'string',
                appVersion: 'string',
                cookieEnabled: 'boolean',
                doNotTrack: 'boolean',
                language: 'string',
                maxTouchPoints: 'number',
                onLine: 'boolean',
                platform: 'string',
                product: 'string',
                vendor: 'string',
            },
            document: {
                charset: 'string',
                compatMode: 'string',
                contentType: 'string',
                cookie: 'string',
                documentURI: 'string',
                fullscreen: 'boolean',
                readyState: 'number',
                referrer: 'string',
                title: 'string',
            },
            location: {
                hash: 'string',
                host: 'string',
                hostname: 'string',
                href: 'string',
                pathname: 'string',
                protocol: 'string',
                search: 'string',
            },
        },
        summon: function (injectees, summonSpec) {
            if (!injectees.$window) {
                injectees.$window = {};
            }
            var out = injectees.$window;
            out.width = window.innerWidth;
            out.height = window.innerHeight;
            if (window.screen) {
                if (!out.screen) {
                    out.screen = {};
                }
                out.screen.availHeight = window.screen.availHeight;
                out.screen.availLeft = window.screen.availLeft;
                out.screen.availWidth = window.screen.availWidth;
                out.screen.colorDepth = window.screen.colorDepth;
                out.screen.height = window.screen.height;
                out.screen.pixelDepth = window.screen.pixelDepth;
                out.screen.width = window.screen.width;
                if (window.screen.orientation) {
                    if (!out.screen.orientation) {
                        out.screen.orientation = {};
                    }
                    out.screen.orientation.angle = window.screen.orientation.angle;
                    out.screen.orientation.type = window.screen.orientation.type;
                }
            }
            if (typeof navigator !== 'undefined') {
                if (!out.navigator) {
                    out.navigator = {};
                }
                out.navigator.userAgent = navigator.userAgent;
                out.navigator.appCodeName = navigator.appCodeName;
                out.navigator.appName = navigator.appName;
                out.navigator.appVersion = navigator.appVersion;
                out.navigator.cookieEnabled = navigator.cookieEnabled;
                out.navigator.doNotTrack = navigator.doNotTrack;
                out.navigator.language = navigator.language;
                out.navigator.maxTouchPoints = navigator.maxTouchPoints;
                out.navigator.onLine = navigator.onLine;
                out.navigator.platform = navigator.platform;
                out.navigator.product = navigator.product;
                out.navigator.userAgent = navigator.userAgent;
                out.navigator.vendor = navigator.vendor;
            }
            if (window.document) {
                if (!out.document) {
                    out.document = {};
                }
                out.document.charset = window.document.charset;
                out.document.compatMode = window.document.compatMode;
                out.document.contenttype = window.document.contentType;
                out.document.cookie = window.document.cookie;
                out.document.documentURI = window.document.documentURI;
                out.document.fullscreen = window.document.fullscreen;
                out.document.readyState = window.document.readyState;
                out.document.referrer = window.document.referrer;
                out.document.title = window.document.title;
            }
            if (window.location) {
                if (!out.location) {
                    out.location = {};
                }
                out.location.hash = window.location.hash;
                out.location.host = window.location.host;
                out.location.hostname = window.location.hostname;
                out.location.href = window.location.href;
                out.location.pathname = window.location.pathname;
                out.location.protocol = window.location.protocol;
                out.location.search = window.location.search;
            }
        },
    };
}
if (typeof global !== 'undefined') {
    INJECTABLES.$global = {
        schema: {
            process: {
                pid: 'number',
                arch: 'string',
                platform: 'string',
                argv: ['string'],
                title: 'string',
                version: 'string',
                env: {},
            },
        },
        summon: function (injectees, summonSpec) {
            if (!injectees.$global) {
                injectees.$global = {};
            }
            var out = injectees.$global;
            if (typeof process !== 'undefined') {
                if (!out.process) {
                    out.process = {};
                }
                out.process.pid = process.pid;
                out.process.arch = process.arch;
                out.process.platform = process.platform;
                out.process.argv = process.argv;
                out.process.title = process.title;
                out.process.version = process.version;
                out.process.env = process.env;
            }
        },
    };
}
INJECTABLES.$core = {
    schema: {
        version: 'string',
        options: {
            seed: 'string',
            loop: 'boolean',
            sizing: 'string',
            preserve3d: 'boolean',
            position: 'string',
            overflowX: 'string',
            overflowY: 'string',
        },
        timeline: {
            name: 'string',
            duration: 'number',
            repeat: 'boolean',
            time: {
                apparent: 'number',
                elapsed: 'number',
                max: 'number',
            },
            frame: {
                apparent: 'number',
                elapsed: 'number',
            },
        },
        clock: {
            frameDuration: 'number',
            frameDelay: 'number',
            time: {
                apparent: 'number',
                elapsed: 'number',
            },
        },
    },
    summon: function (injectees, summonSpec, hostInstance, matchingElement, timelineName) {
        if (!injectees.$core) {
            injectees.$core = {};
        }
        var out = injectees.$core;
        out.version = hostInstance.context.CORE_VERSION || hostInstance.context.PLAYER_VERSION; // Fallback to #LEGACY
        var options = hostInstance.context.config;
        if (options) {
            if (!out.options) {
                out.options = {};
            }
            out.options.seed = options.seed;
            out.options.sizing = options.sizing;
            out.options.preserve3d = options.preserve3d;
            out.options.position = options.position;
            out.options.overflowX = options.overflowX;
            out.options.overflowY = options.overflowY;
        }
        var timelineInstance = hostInstance.getTimeline(timelineName);
        if (timelineInstance) {
            if (!out.timeline) {
                out.timeline = {};
            }
            out.timeline.name = timelineName;
            out.timeline.duration = timelineInstance.getDuration();
            out.timeline.repeat = timelineInstance.getRepeat();
            if (!out.timeline.time) {
                out.timeline.time = {};
            }
            out.timeline.time.apparent = timelineInstance.getTime();
            out.timeline.time.elapsed =
                interactionModes_1.isPreviewMode(hostInstance.config.interactionMode)
                    ? timelineInstance.getElapsedTime()
                    : out.timeline.time.apparent;
            out.timeline.time.max = timelineInstance.getMaxTime();
            if (!out.timeline.frame) {
                out.timeline.frame = {};
            }
            out.timeline.frame.apparent = timelineInstance.getFrame();
            out.timeline.frame.elapsed =
                interactionModes_1.isPreviewMode(hostInstance.config.interactionMode)
                    ? timelineInstance.getUnboundedFrame()
                    : out.timeline.frame.apparent;
        }
        var clockInstance = hostInstance.getClock();
        if (clockInstance) {
            if (!out.clock) {
                out.clock = {};
            }
            out.clock.frameDuration = clockInstance.options.frameDuration;
            out.clock.frameDelay = clockInstance.options.frameDelay;
            if (!out.clock.time) {
                out.clock.time = {};
            }
            out.clock.time.apparent = clockInstance.getExplicitTime();
            out.clock.time.elapsed =
                interactionModes_1.isPreviewMode(hostInstance.config.interactionMode)
                    ? clockInstance.getRunningTime()
                    : out.clock.time.apparent;
        }
    },
};
var EVENT_SCHEMA = {
    mouse: {
        x: 'number',
        y: 'number',
        isDown: 'boolean',
    },
    touches: [{
            x: 'number',
            y: 'number',
        }],
    mouches: [{
            x: 'number',
            y: 'number',
        }],
    keys: [{
            which: 'number',
            code: 'number',
        }],
};
var ELEMENT_SCHEMA = {
    // A function in the schema indicates that schema is dynamic, dependent on some external information
    properties: function (element) {
        var defined = schema_1.default[element.elementName];
        if (!defined) {
            console.warn('[haiku core] element ' + element.elementName + ' has no schema defined');
            return {};
        }
        return defined;
    },
};
var assignElementInjectables = function (obj, key, summonSpec, hostInstance, element) {
    // If for some reason no element, nothing to do
    if (!element) {
        return {};
    }
    // For some reason we get string elements here #FIXME
    if (typeof element === 'string') {
        return {};
    }
    obj[key] = {};
    var out = obj[key];
    out.properties = {
        name: null,
        attributes: null,
    };
    out.properties.name = element.elementName;
    out.properties.attributes = element.attributes;
    if (element.layout.computed) {
        out.properties.matrix = element.layout.computed.matrix;
        out.properties.size = element.layout.computed.size;
    }
    out.properties.align = element.layout.align;
    out.properties.mount = element.layout.mount;
    out.properties.opacity = element.layout.opacity;
    out.properties.origin = element.layout.origin;
    out.properties.rotation = element.layout.rotation;
    out.properties.scale = element.layout.scale;
    out.properties.shown = element.layout.shown;
    out.properties.sizeAbsolute = element.layout.sizeAbsolute;
    out.properties.sizeDifferential = element.layout.sizeDifferential;
    out.properties.sizeMode = element.layout.sizeMode;
    out.properties.sizeProportional = element.layout.sizeProportional;
    out.properties.translation = element.layout.translation;
    // TODO
    // defineProperty so that the calc happens lazily
    // Object.defineProperty(out, 'bbox', {
    //   get: function _get () {
    //     return hostInstance.context._getElementBBox(element)
    //   }
    // })
    // TODO
    // out.events = hostInstance.context.getElementEvents(element)
};
INJECTABLES.$tree = {
    schema: {
        // Unique to $tree
        parent: ELEMENT_SCHEMA,
        children: [ELEMENT_SCHEMA],
        siblings: [ELEMENT_SCHEMA],
        // Aliases for convenience
        component: ELEMENT_SCHEMA,
        root: ELEMENT_SCHEMA,
        element: ELEMENT_SCHEMA,
    },
    summon: function (injectees, summonSpec, hostInstance, matchingElement) {
        if (!injectees.$tree) {
            injectees.$tree = {};
        }
        injectees.$tree.siblings = []; // Provide an array even if no siblings in case user tries to access
        injectees.$tree.parent = null;
        if (matchingElement && matchingElement.__parent) {
            var subspec0 = (typeof summonSpec === 'string') ? summonSpec : (summonSpec.$tree && summonSpec.$tree.parent);
            assignElementInjectables(injectees.$tree, 'parent', subspec0, hostInstance, matchingElement.__parent);
            for (var i = 0; i < matchingElement.__parent.children.length; i++) {
                var sibling = matchingElement.__parent.children[i];
                var subspec1 = (typeof summonSpec === 'string')
                    ? summonSpec
                    : summonSpec.$tree && summonSpec.$tree.siblings && summonSpec.$tree.siblings[i];
                assignElementInjectables(injectees.$tree.siblings, i, subspec1, hostInstance, sibling);
            }
        }
        injectees.$tree.children = []; // Provide an array even if no children in case user tries to access
        if (matchingElement && matchingElement.children) {
            for (var j = 0; j < matchingElement.children.length; j++) {
                var child = matchingElement.children[j];
                var subspec2 = (typeof summonSpec === 'string')
                    ? summonSpec
                    : summonSpec.$tree && summonSpec.$tree.children && summonSpec.$tree.children[j];
                assignElementInjectables(injectees.$tree.children, j, subspec2, hostInstance, child);
            }
        }
        // May as well make use of the implementation we have for the aliases,
        // and avoid recalc if it's already been added:
        if (!injectees.$component) {
            INJECTABLES.$component.summon(injectees, summonSpec, hostInstance, matchingElement);
        }
        injectees.$tree.component = injectees.$component;
        if (!injectees.$root) {
            INJECTABLES.$root.summon(injectees, summonSpec, hostInstance, matchingElement);
        }
        injectees.$tree.root = injectees.$root;
        if (!injectees.$element) {
            INJECTABLES.$element.summon(injectees, summonSpec, hostInstance, matchingElement);
        }
        injectees.$tree.element = injectees.$element;
    },
};
// (top-level Element of a given component, (i.e. tranverse tree upward past groups but
// stop at first component definition))
INJECTABLES.$component = {
    schema: ELEMENT_SCHEMA,
    summon: function (injectees, summonSpec, hostInstance) {
        // Don't double-recalc this if it's already shown to be present in $tree
        if (injectees.$tree && injectees.$tree.component) {
            injectees.$component = injectees.$tree.component;
        }
        else {
            var subspec = (typeof summonSpec === 'string') ? summonSpec : summonSpec.$component;
            assignElementInjectables(injectees, '$component', subspec, hostInstance, hostInstance.getTemplate());
        }
    },
};
// absolute root of component tree (but does not traverse host codebase DOM)
// i.e. if Haiku components are nested. Until we support nested components,
// $root will be the same as $component
INJECTABLES.$root = {
    schema: ELEMENT_SCHEMA,
    summon: function (injectees, summonSpec, hostInstance) {
        // Don't double-recalc this if it's already shown to be present in $tree
        if (injectees.$tree && injectees.$tree.root) {
            injectees.$root = injectees.$tree.root;
        }
        else {
            // Until we support nested components, $root resolves to $component
            var subspec = (typeof summonSpec === 'string') ? summonSpec : summonSpec.$root;
            assignElementInjectables(injectees, '$root', subspec, hostInstance, hostInstance.getTemplate());
        }
    },
};
INJECTABLES.$element = {
    schema: ELEMENT_SCHEMA,
    summon: function (injectees, summonSpec, hostInstance, matchingElement) {
        // Don't double-recalc this if it's already shown to be present in $tree
        if (injectees.$tree && injectees.$tree.element) {
            injectees.$element = injectees.$tree.element;
        }
        else {
            var subspec = (typeof summonSpec === 'string') ? summonSpec : summonSpec.$element;
            assignElementInjectables(injectees, '$element', subspec, hostInstance, matchingElement);
        }
    },
};
INJECTABLES.$user = {
    schema: assign_1.default({}, EVENT_SCHEMA),
    summon: function (injectees, summonSpec, hostInstance, matchingElement) {
        if (interactionModes_1.isPreviewMode(hostInstance.config.interactionMode)) {
            injectees.$user = hostInstance.context.getGlobalUserState();
        }
        else {
            injectees.$user = {
                mouse: {
                    x: 1,
                    y: 1,
                    down: 0,
                    buttons: [0, 0, 0],
                },
                pan: {
                    x: 0,
                    y: 0,
                },
                keys: {},
                touches: [],
                mouches: [],
            };
        }
    },
};
INJECTABLES.$flow = {
    schema: {
        repeat: {
            list: ['any'],
            index: 'number',
            value: 'any',
            data: 'any',
            payload: 'any',
        },
        placeholder: {
            node: 'any',
        },
    },
    summon: function (injectees, summonSpec, hostInstance, matchingElement) {
        // if (!injectees.$flow) injectees.$flow = {}
        // var out = injectees.$flow
    },
};
INJECTABLES.$flow.schema.if = {
    value: 'any',
    data: 'any',
    payload: 'any',
};
INJECTABLES.$flow.schema.yield = {
    value: 'any',
    data: 'any',
    payload: 'any',
};
INJECTABLES.$helpers = {
    schema: {},
    summon: function (injectees, summonSpec, hostInstance, matchingElement) {
        injectees.$helpers = hostInstance.builder.helpers;
    },
};
// List of JavaScript global built-in objects that we want to provide as an injectable.
// In the future, we might end up passing in modified versions of these objects/functions.
var BUILTIN_INJECTABLES = {
    Infinity: Infinity,
    NaN: NaN,
    Object: Object,
    Boolean: Boolean,
    Math: Math,
    Date: Date,
    JSON: JSON,
    Number: Number,
    String: String,
    RegExp: RegExp,
    Array: Array,
    isFinite: isFinite,
    isNaN: isNaN,
    parseFloat: parseFloat,
    parseInt: parseInt,
    decodeURI: decodeURI,
    decodeURIComponent: decodeURIComponent,
    encodeURI: encodeURI,
    encodeURIComponent: encodeURIComponent,
    // escape,
    // unescape,
    Error: Error,
    ReferenceError: ReferenceError,
    SyntaxError: SyntaxError,
    TypeError: TypeError,
    undefined: void (0),
};
var _loop_1 = function (builtinInjectableKey) {
    INJECTABLES[builtinInjectableKey] = {
        builtin: true,
        schema: '*',
        summon: function (injectees) {
            injectees[builtinInjectableKey] = BUILTIN_INJECTABLES[builtinInjectableKey];
        },
    };
};
for (var builtinInjectableKey in BUILTIN_INJECTABLES) {
    _loop_1(builtinInjectableKey);
}
// When editing a component, any of these appearing inside an expression will trigger a warning.
// This is kept in the core so it's easier to compare these to the built-in injectables and
// other special treatment for JavaScript globals. "single source of truth" etc.
var FORBIDDEN_EXPRESSION_TOKENS = {
    // Keywords
    new: true,
    this: true,
    with: true,
    delete: true,
    export: true,
    extends: true,
    super: true,
    class: true,
    abstract: true,
    interface: true,
    static: true,
    label: true,
    goto: true,
    private: true,
    import: true,
    public: true,
    // Future keywords
    do: true,
    native: true,
    package: true,
    transient: true,
    implements: true,
    protected: true,
    throws: true,
    synchronized: true,
    final: true,
    // Common globals
    window: true,
    document: true,
    global: true,
    // Danger
    eval: true,
    uneval: true,
    Function: true,
    EvalError: true,
    // Module stuff to forbid
    require: true,
    module: true,
    exports: true,
    Module: true,
    // Sandbox
    arguments: true,
    callee: true,
    // Identifiers on built-in global objects
    prototpye: true,
    __proto__: true,
    freeze: true,
    setPrototypeOf: true,
    constructor: true,
    defineProperties: true,
    defineProperty: true,
};
/**
 * When evaluating expressions written by the user, don't crash everything.
 * Log the error (but only once, since we're animating) and then return a
 * fairly safe all-purpose value (1).
 */
var safeCall = function (fn, hostInstance, hostStates) {
    try {
        return fn.call(hostInstance, hostStates);
    }
    catch (exception) {
        consoleErrorOnce_1.default(exception);
        return 1;
    }
};
var safeApply = function (fn, hostInstance, summoneesArray) {
    try {
        return fn.apply(hostInstance, summoneesArray);
    }
    catch (exception) {
        consoleErrorOnce_1.default(exception);
        return 1;
    }
};
var areSummoneesDifferent = function (previous, incoming) {
    if (Array.isArray(previous) && Array.isArray(incoming)) {
        if (previous.length !== incoming.length) {
            return true;
        }
        // Do a shallow comparison of elements. We don't go deep because:
        //   - It easily becomes too expensive to do this while rendering
        //   - We can avoid needing to check for recursion
        for (var i = 0; i < previous.length; i++) {
            if (previous[i] !== incoming[i]) {
                return true;
            }
        }
        for (var j = 0; j < previous.length; j++) {
            if (incoming[j] !== previous[j]) {
                return true;
            }
        }
        return false;
    }
    if (typeof previous === OBJECT && typeof incoming === OBJECT) {
        if (previous === null && incoming === null) {
            return false;
        }
        if (previous === null) {
            return true;
        }
        if (incoming === null) {
            return true;
        }
        // Do a shallow comparison of properties. We don't go deep because:
        //   - It easily becomes too expensive to do this while rendering
        //   - We can avoid needing to check for recursion
        for (var pkey in previous) {
            if (previous[pkey] !== incoming[pkey]) {
                return true;
            }
        }
        for (var ikey in incoming) {
            if (incoming[ikey] !== previous[ikey]) {
                return true;
            }
        }
        return false;
    }
    return previous !== incoming;
};
var stringToInt = function (str) {
    var hash = 5381;
    var i = str.length;
    while (i) {
        hash = (hash * 33) ^ str.charCodeAt(--i);
    }
    return hash >>> 0;
};
var MAX_INT = 2147483646;
/* tslint:disable:variable-name */
var ValueBuilder = /** @class */ (function () {
    function ValueBuilder(component) {
        var _this = this;
        this._component = component; // ::HaikuComponent
        this._parsees = {};
        this._changes = {};
        this._summonees = {};
        this._evaluations = {};
        this.helpers = {};
        for (var helperName in HaikuHelpers_1.default.helpers) {
            this.helpers[helperName] = HaikuHelpers_1.default.helpers[helperName];
        }
        this.helpers.now = function () {
            if (interactionModes_1.isPreviewMode(_this._component.config.interactionMode)) {
                return (_this._component.config.timestamp || 1) + (_this._lastTimelineTime || 1);
            }
            return 1;
        };
        this.helpers.rand = function () {
            if (interactionModes_1.isPreviewMode(_this._component.config.interactionMode)) {
                // tslint:disable-next-line
                var scopeKey = _this._lastTimelineName + "|" + _this._lastTimelineTime + "|" + _this._lastPropertyName + "|" + _this._lastFlexId;
                var randKey = _this._component.config.seed + "@" + scopeKey;
                var keyInt = stringToInt(randKey);
                var outFloat = ((keyInt + 1) % MAX_INT) / MAX_INT;
                return outFloat;
            }
            return 1;
        };
        this.helpers.find = function (selector) {
            return _this._component.querySelectorAll(selector);
        };
    }
    ValueBuilder.prototype.clearCaches = function (options) {
        if (options === void 0) { options = {}; }
        this._parsees = {};
        this._changes = {};
        this._summonees = {};
        this._evaluations = {};
        return this;
    };
    ValueBuilder.prototype.clearCachedClusters = function (timelineName, componentId) {
        if (this._parsees[timelineName]) {
            this._parsees[timelineName][componentId] = {};
        }
        return this;
    };
    ValueBuilder.prototype.evaluate = function (fn, timelineName, flexId, matchingElement, propertyName, keyframeMs, keyframeCluster, hostInstance) {
        enhance_1.default(fn, null);
        // We'll store the result of this evaluation in this variable
        // (so we can cache it in case unexpected subsequent calls)
        var evaluation = void 0;
        if (fn.specification === true) {
            // This function is of an unknown kind, so just evaluate it normally without magic dependency injection
            evaluation = safeCall(fn, hostInstance, hostInstance._states);
        }
        else if (!Array.isArray(fn.specification.params)) {
            // If for some reason we got a non-array params, just evaluate
            evaluation = safeCall(fn, hostInstance, hostInstance._states);
        }
        else if (fn.specification.params.length < 1) {
            // If for some reason we got 0 params, just evaluate it
            evaluation = safeCall(fn, hostInstance, hostInstance._states);
        }
        else {
            if (fn.specification.params.length < 1) {
                // If the summon isn't in the destructured object format, just evaluate it
                evaluation = safeCall(fn, hostInstance, hostInstance._states);
            }
            else {
                var summoneesArray = this.summonSummonables(fn.specification.params, timelineName, flexId, matchingElement, propertyName, keyframeMs, keyframeCluster, hostInstance);
                var previousSummoneesArray = this.getPreviousSummonees(timelineName, flexId, propertyName, keyframeMs);
                if (areSummoneesDifferent(previousSummoneesArray, summoneesArray)) {
                    this.cacheSummonees(timelineName, flexId, propertyName, keyframeMs, summoneesArray);
                    evaluation = safeApply(fn, hostInstance, summoneesArray);
                }
                else {
                    // Since nothing is different, return the previous evaluation
                    evaluation = this.getPreviousEvaluation(timelineName, flexId, propertyName, keyframeMs);
                }
            }
        }
        // Store the result so we can return it on the next run without re-eval
        if (fn.specification && fn.specification !== true) {
            this.cacheEvaluation(timelineName, flexId, propertyName, keyframeMs, evaluation);
        }
        return evaluation;
    };
    ValueBuilder.prototype.getPreviousSummonees = function (timelineName, flexId, propertyName, keyframeMs) {
        if (!this._summonees[timelineName]) {
            return;
        }
        if (!this._summonees[timelineName][flexId]) {
            return;
        }
        if (!this._summonees[timelineName][flexId][propertyName]) {
            return;
        }
        return this._summonees[timelineName][flexId][propertyName][keyframeMs];
    };
    ValueBuilder.prototype.cacheSummonees = function (timelineName, flexId, propertyName, keyframeMs, summonees) {
        if (!this._summonees[timelineName]) {
            this._summonees[timelineName] = {};
        }
        if (!this._summonees[timelineName][flexId]) {
            this._summonees[timelineName][flexId] = {};
        }
        if (!this._summonees[timelineName][flexId][propertyName]) {
            this._summonees[timelineName][flexId][propertyName] = {};
        }
        this._summonees[timelineName][flexId][propertyName][keyframeMs] = summonees;
        return summonees;
    };
    ValueBuilder.prototype.getPreviousEvaluation = function (timelineName, flexId, propertyName, keyframeMs) {
        if (!this._evaluations[timelineName]) {
            return;
        }
        if (!this._evaluations[timelineName][flexId]) {
            return;
        }
        if (!this._evaluations[timelineName][flexId][propertyName]) {
            return;
        }
        return this._evaluations[timelineName][flexId][propertyName][keyframeMs];
    };
    ValueBuilder.prototype.cacheEvaluation = function (timelineName, flexId, propertyName, keyframeMs, evaluation) {
        if (!this._evaluations[timelineName]) {
            this._evaluations[timelineName] = {};
        }
        if (!this._evaluations[timelineName][flexId]) {
            this._evaluations[timelineName][flexId] = {};
        }
        if (!this._evaluations[timelineName][flexId][propertyName]) {
            this._evaluations[timelineName][flexId][propertyName] = {};
        }
        this._evaluations[timelineName][flexId][propertyName][keyframeMs] = evaluation;
        return evaluation;
    };
    ValueBuilder.prototype.summonSummonables = function (paramsArray, timelineName, flexId, matchingElement, propertyName, keyframeMs, keyframeCluster, hostInstance) {
        var summonablesArray = [];
        // Temporary storage, just creating one object here to avoid excessive allocations
        var summonStorage = {};
        for (var i = 0; i < paramsArray.length; i++) {
            var summonsEntry = paramsArray[i];
            // We'll store the output of the summons in this var, whether we're dealing with
            // a complex nested summonable or a flat one
            var summonsOutput = void 0;
            // In case of a string, we will treat it as the key for the object to summon
            if (typeof summonsEntry === 'string') {
                // Treat the entry as the key to a known injectable
                if (INJECTABLES[summonsEntry]) {
                    summonStorage[summonsEntry] = undefined; // Clear out the old value before populating with the new one
                    INJECTABLES[summonsEntry].summon(
                    // This arg is populated with the data; it is the var 'out' in the summon function; they summonsKey must be
                    // added.
                    summonStorage, 
                    // The summon function should know how to handle a string and what it signifies.
                    summonsEntry, hostInstance, matchingElement, timelineName);
                    summonsOutput = summonStorage[summonsEntry];
                }
                else {
                    summonsOutput = hostInstance.state[summonsEntry];
                }
            }
            else if (summonsEntry && typeof summonsEntry === 'object') {
                // If dealing with a summon that is an object, the output will be an object.
                summonsOutput = {};
                for (var summonsKey in summonsEntry) {
                    // If the summons structure has a falsy, just skip it;
                    // I don't see why how this could happen, but just in case.
                    if (!summonsEntry[summonsKey]) {
                        continue;
                    }
                    // If a special summonable has been defined, then call its summoner function
                    if (INJECTABLES[summonsKey]) {
                        INJECTABLES[summonsKey].summon(
                        // This arg is populated with the data; it is the var 'out' in the summon function;
                        // they summonsKey must be added.
                        summonsOutput, 
                        // The object specifies the specific fields we want to extract.
                        summonsEntry[summonsKey], hostInstance, matchingElement, timelineName);
                        continue;
                    }
                    // Otherwise, assume the user wants to access one of the states of the component instance
                    // Note that the 'states' defined in the component's bytecode should have been set up upstream by the
                    // core's initialization process. hostInstance is a HaikuCore instance which has a state prop which has
                    // getter/setter props set up corresponding to whatever the 'states' were set to
                    summonsOutput[summonsKey] = hostInstance.state[summonsKey];
                }
            }
            // Whatever the request format was, populate the result in here
            if (summonsOutput !== undefined) {
                summonablesArray[i] = summonsOutput;
            }
        }
        return summonablesArray;
    };
    ValueBuilder.prototype.getSummonablesSchema = function () {
        var summonablesSchema = {};
        for (var key in INJECTABLES) {
            summonablesSchema[key] = INJECTABLES[key].schema;
        }
        return summonablesSchema;
    };
    ValueBuilder.prototype.fetchParsedValueCluster = function (timelineName, flexId, matchingElement, outputName, cluster, hostInstance, isPatchOperation, skipCache) {
        // Establish the cache objects for this properties group within this timeline
        if (!this._parsees[timelineName]) {
            this._parsees[timelineName] = {};
        }
        if (!this._parsees[timelineName][flexId]) {
            this._parsees[timelineName][flexId] = {};
        }
        if (!this._parsees[timelineName][flexId][outputName]) {
            this._parsees[timelineName][flexId][outputName] = {};
        }
        var parsee = this._parsees[timelineName][flexId][outputName];
        for (var ms in cluster) {
            var descriptor = cluster[ms];
            // Important: The ActiveComponent depends on the ability to be able to get fresh values via this option
            if (skipCache) {
                // Easiest way to skip the cache is just to make the destination object falsy
                parsee[ms] = null;
            }
            // In case of a function, we can't cache - we have to recalc, and thus re-parse also
            if (isFunction(descriptor.value)) {
                // We have to recreate this cache object every time due to the need for function recalc
                parsee[ms] = {};
                if (descriptor.curve) {
                    parsee[ms].curve = descriptor.curve;
                }
                // Indicate to the downstream transition cache that this value came from a function and cannot be cached there.
                // See Transitions.js for info on how this gets handled
                parsee[ms].expression = true;
                // Note that evaluate doesn't necessarily call the function - it may itself return a cached value
                var functionReturnValue = this.evaluate(descriptor.value, timelineName, flexId, matchingElement, outputName, ms, cluster, hostInstance);
                // The function's return value is expected to be in the *raw* format - we parse to allow for interpolation
                var parser1 = this.getParser(outputName, matchingElement);
                if (parser1) {
                    parsee[ms].value = parser1(functionReturnValue);
                }
                else {
                    parsee[ms].value = functionReturnValue;
                }
            }
            else {
                // In case of static values, we can cache - no need to re-parse static values if we already parsed them
                if (parsee[ms]) {
                    continue;
                }
                // If nothing in the cache, create the base cache object...
                parsee[ms] = {};
                if (descriptor.curve) {
                    parsee[ms].curve = descriptor.curve;
                }
                var parser2 = this.getParser(outputName, matchingElement);
                if (parser2) {
                    parsee[ms].value = parser2(descriptor.value);
                }
                else {
                    parsee[ms].value = descriptor.value;
                }
            }
        }
        // Return the entire cached object - interpolation is done downstream
        return parsee;
    };
    ValueBuilder.prototype.getParser = function (outputName, virtualElement) {
        if (!virtualElement) {
            return undefined;
        }
        var foundParser = parsers_1.default[virtualElement.elementName] && parsers_1.default[virtualElement.elementName][outputName];
        return foundParser && foundParser.parse;
    };
    ValueBuilder.prototype.getGenerator = function (outputName, virtualElement) {
        if (!virtualElement) {
            return undefined;
        }
        var foundGenerator = parsers_1.default[virtualElement.elementName] && parsers_1.default[virtualElement.elementName][outputName];
        return foundGenerator && foundGenerator.generate;
    };
    ValueBuilder.prototype.generateFinalValueFromParsedValue = function (timelineName, flexId, matchingElement, outputName, computedValue) {
        var generator = this.getGenerator(outputName, matchingElement);
        if (generator) {
            return generator(computedValue);
        }
        return computedValue;
    };
    ValueBuilder.prototype.didChangeValue = function (timelineName, flexId, matchingElement, outputName, outputValue) {
        var answer = false;
        if (!this._changes[timelineName]) {
            this._changes[timelineName] = {};
            answer = true;
        }
        if (!this._changes[timelineName][flexId]) {
            this._changes[timelineName][flexId] = {};
            answer = true;
        }
        if (this._changes[timelineName][flexId][outputName] === undefined ||
            this._changes[timelineName][flexId][outputName] !== outputValue) {
            this._changes[timelineName][flexId][outputName] = outputValue;
            answer = true;
        }
        return answer;
    };
    /**
     * @method build
     * @description Given an 'out' object, accumulate values into that object based on the current timeline, time, and
     * instance state.
     * If we didn't make any changes, we return undefined here. The caller should account for this.
     */
    ValueBuilder.prototype.build = function (out, timelineName, timelineTime, flexId, matchingElement, propertiesGroup, isPatchOperation, haikuComponent, skipCache) {
        if (skipCache === void 0) { skipCache = false; }
        var isAnythingWorthUpdating = false;
        for (var propertyName in propertiesGroup) {
            var finalValue = this.grabValue(timelineName, flexId, matchingElement, propertyName, propertiesGroup, timelineTime, haikuComponent, isPatchOperation, skipCache, null);
            // We use undefined as a signal that it's not worthwhile to put this value in the list of updates.
            // null should be used in the case that we want to explicitly set an empty value
            if (finalValue === undefined) {
                continue;
            }
            // If this is _not_ a patch operation, we have to set the value because downstream, the renderer will strip
            // off old attributes present on the dom nodes.
            if (!isPatchOperation ||
                this.didChangeValue(timelineName, flexId, matchingElement, propertyName, finalValue)) {
                if (out[propertyName] === undefined) {
                    out[propertyName] = finalValue;
                }
                else {
                    out[propertyName] = BasicUtils_1.default.mergeValue(out[propertyName], finalValue);
                }
                isAnythingWorthUpdating = true;
            }
        }
        if (isAnythingWorthUpdating) {
            return out;
        }
        return undefined;
    };
    /**
     * @method grabValue
     * @description Given a timeline and some current state information, return a computed value for the given property
     * name.
     *
     * NOTE: The 'build' method above interprets a return value of 'undefined' to mean "no change" so bear that in mind...
     *
     * @param timelineName {String} Name of the timeline we're using
     * @param flexId {String} Identifier of the matching element
     * @param matchingElement {Object} The matching element
     * @param propertyName {String} Name of the property being grabbed, e.g. position.x
     * @param propertiesGroup {Object} The full timeline properties group, e.g. { position.x: ..., position.y: ... }
     * @param timelineTime {Number} The current time (in ms) that the given timeline is at
     * @param haikuComponent {Object} Instance of HaikuComponent
     * @param isPatchOperation {Boolean} Is this a patch?
     * @param skipCache {Boolean} Skip caching?
     * @param clearSortedKeyframesCache
     */
    ValueBuilder.prototype.grabValue = function (timelineName, flexId, matchingElement, propertyName, propertiesGroup, timelineTime, haikuComponent, isPatchOperation, skipCache, clearSortedKeyframesCache) {
        if (!propertiesGroup) {
            return undefined;
        }
        // Used by $helpers to calculate scope-specific values;
        this._lastTimelineName = timelineName;
        this._lastFlexId = flexId;
        this._lastPropertyName = propertyName;
        this._lastTimelineTime = timelineTime;
        var parsedValueCluster = this.fetchParsedValueCluster(timelineName, flexId, matchingElement, propertyName, propertiesGroup && propertiesGroup[propertyName], haikuComponent, isPatchOperation, skipCache);
        // If there is no property of that name, we would have gotten nothing back, so we can't forward this to Transitions
        // since it expects to receive a populated cluster object
        if (!parsedValueCluster) {
            return undefined;
        }
        if (clearSortedKeyframesCache) {
            delete parsedValueCluster.__sorted;
        }
        var computedValueForTime;
        if (!parsedValueCluster[KEYFRAME_ZERO]) {
            parsedValueCluster[KEYFRAME_ZERO] = {
                value: (matchingElement &&
                    fallbacks_1.default[matchingElement.elementName] &&
                    fallbacks_1.default[matchingElement.elementName][propertyName]) || fallbacks_1.default.div[propertyName],
            };
        }
        // Important: The ActiveComponent depends on the ability to be able to get fresh values via the skipCache option.
        if (isPatchOperation && !skipCache) {
            computedValueForTime = Transitions_1.default.calculateValueAndReturnUndefinedIfNotWorthwhile(parsedValueCluster, timelineTime);
        }
        else {
            computedValueForTime = Transitions_1.default.calculateValue(parsedValueCluster, timelineTime);
        }
        if (computedValueForTime === undefined) {
            return undefined;
        }
        return this.generateFinalValueFromParsedValue(timelineName, flexId, matchingElement, propertyName, computedValueForTime);
    };
    ValueBuilder.INJECTABLES = INJECTABLES;
    ValueBuilder.FORBIDDEN_EXPRESSION_TOKENS = FORBIDDEN_EXPRESSION_TOKENS;
    return ValueBuilder;
}());
exports.default = ValueBuilder;
//# sourceMappingURL=ValueBuilder.js.map