"use strict";
/**
 * Copyright (c) Haiku 2016-2018. All rights reserved.
 */
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var Config_1 = require("./Config");
var HaikuBase_1 = require("./HaikuBase");
var HaikuClock_1 = require("./HaikuClock");
var HaikuComponent_1 = require("./HaikuComponent");
var pkg = require('./../package.json');
var VERSION = pkg.version;
/**
 * @class HaikuContext
 * @description Represents the root of a Haiku component tree within an application.
 * A Haiku component tree may contain many components, but there is only one context.
 * The context is where information shared by all components in the tree should go, e.g. clock time.
 */
// tslint:disable:variable-name
var HaikuContext = /** @class */ (function (_super) {
    __extends(HaikuContext, _super);
    function HaikuContext(mount, renderer, platform, bytecode, config) {
        var _this = _super.call(this) || this;
        if (!renderer) {
            throw new Error('Context requires a renderer');
        }
        if (!bytecode) {
            throw new Error('Context requires bytecode');
        }
        _this.PLAYER_VERSION = VERSION; // #LEGACY
        _this.CORE_VERSION = VERSION;
        _this.assignConfig(config || {}, null);
        _this.mount = mount;
        // Make some Haiku internals available on the mount object for hot editing hooks, or for debugging convenience.
        if (_this.mount && !_this.mount.haiku) {
            _this.mount.haiku = {
                context: _this,
            };
        }
        _this.renderer = renderer;
        // Initialize sets up top-level dom listeners so we don't run it if we don't have a mount
        if (_this.mount && _this.renderer.initialize) {
            _this.renderer.initialize();
        }
        _this.platform = platform;
        // List of tickable objects managed by this context. These are invoked on every clock tick.
        // These are removed when context unmounts and re-added in case of re-mount
        _this.tickables = [];
        // Our own tick method is the main driver for animation inside of this context
        _this.tickables.push({ performTick: _this.tick.bind(_this) });
        if (_this.config.frame) {
            _this.tickables.push({ performTick: _this.config.frame });
        }
        _this.clock = new HaikuClock_1.default(_this.tickables, _this.config.clock || {});
        // We need to start the loop even if we aren't autoplaying,
        // because we still need time to be calculated even if we don't 'tick'.
        _this.clock.run();
        _this.container = _this.renderer.createContainer({
            elementName: bytecode,
            attributes: {},
            children: [bytecode.template],
        });
        _this.component = new HaikuComponent_1.default(bytecode, _this, // context
        null, // host
        _this.config, _this.container);
        // If configured, bootstrap the Haiku right-click context menu
        if (_this.mount && _this.renderer.menuize && _this.config.contextMenu !== 'disabled') {
            _this.renderer.menuize(_this.component);
        }
        // By default, Haiku tracks usage by transmitting component metadata to Mixpanel when initialized.
        // Developers can disable this by setting the `mixpanel` option to a falsy value.
        // To transmit metadata to your own Mixpanel account, set the `mixpanel` option to your Mixpanel API token.
        // Don't set up Mixpanel if we're running on localhost since we don't want test data to be tracked
        if (_this.mount &&
            _this.platform &&
            _this.platform.location &&
            _this.platform.location.hostname !== 'localhost' &&
            _this.platform.location.hostname !== '0.0.0.0') {
            // If configured, initialize Mixpanel with the given API token
            if (_this.renderer.mixpanel && _this.config.mixpanel) {
                _this.renderer.mixpanel(_this.config.mixpanel, _this.component);
            }
        }
        // Just a counter for the number of clock ticks that have occurred; used to determine first-frame for mounting
        _this.ticks = 0;
        // Assuming the user wants the app to mount immediately (the default), let's do the mount.
        if (_this.config.automount) {
            // Starting the clock has the effect of doing a render at time 0, a.k.a., mounting!
            _this.component.getClock().start();
        }
        return _this;
    }
    /**
     * @method getRootComponent
     * @description Returns the HaikuComponent managed by this context.
     */
    HaikuContext.prototype.getRootComponent = function () {
        return this.component;
    };
    /**
     * @method getClock
     * @description Returns the HaikuClock instance associated with this context.
     */
    HaikuContext.prototype.getClock = function () {
        return this.clock;
    };
    /**
     * @method contextMount
     * @description Adds this context the global update loop.
     */
    HaikuContext.prototype.contextMount = function () {
        if (this.unmountedTickables) {
            // Gotta remember to _remove_ the tickables so we don't end up with dupes if we re-mount later
            var unmounted = this.unmountedTickables.splice(0);
            for (var i = 0; i < unmounted.length; i++) {
                this.addTickable(unmounted[i]);
            }
        }
    };
    /**
     * @method contextUnmount
     * @description Removes this context from global update loop.
     */
    HaikuContext.prototype.contextUnmount = function () {
        this.unmountedTickables = this.tickables.splice(0);
    };
    HaikuContext.prototype.destroy = function () {
        _super.prototype.destroy.call(this);
        this.component.destroy();
        this.renderer.destroy();
        this.clock.destroy();
    };
    /**
     * @method addTickable
     * @description Add a tickable object to the list of those that will be called on every clock tick.
     * This only adds if the given object isn't already present in the list.
     */
    HaikuContext.prototype.addTickable = function (tickable) {
        var alreadyAdded = false;
        for (var i = 0; i < this.tickables.length; i++) {
            if (tickable === this.tickables[i]) {
                alreadyAdded = true;
                break;
            }
        }
        if (!alreadyAdded) {
            this.tickables.push(tickable);
        }
    };
    /**
     * @method assignConfig
     * @description Updates internal configuration options, assigning those passed in.
     * Also updates the configuration of the clock instance and managed component instance.
     */
    HaikuContext.prototype.assignConfig = function (config, options) {
        this.config = __assign({}, config);
        if (this.clock) { // This method may run before the clock is initialized
            this.clock.assignOptions(this.config.clock);
        }
        if (this.component) { // This method may run before the managed component is initialized
            if (!options || !options.skipComponentAssign) { // Avoid an infinite loop if the managed component is updating us
                this.component.assignConfig(this.config);
            }
        }
    };
    /**
     * @method getContainer
     * @description Returns the container, a virtual-element-like object that provides sizing
     * constraints at the topmost/outermost level from which the descendant layout can be calculated.
     */
    HaikuContext.prototype.getContainer = function (doForceRecalc) {
        if (doForceRecalc === void 0) { doForceRecalc = false; }
        if (doForceRecalc || this.renderer.shouldCreateContainer) {
            this.renderer.createContainer(this.container); // The container is mutated in place
        }
        return this.container;
    };
    /**
     * @method performFullFlushRender
     * @description Updates the entire component tree, flushing updates to the rendering medium.
     */
    HaikuContext.prototype.performFullFlushRender = function () {
        if (!this.mount) {
            return;
        }
        this.component.performFullFlushRenderWithRenderer(this.renderer, this.config);
    };
    /**
     * @method performPatchRender
     * @description Updates the component tree, but only updating properties we know have changed.
     */
    HaikuContext.prototype.performPatchRender = function (skipCache) {
        if (skipCache === void 0) { skipCache = false; }
        if (!this.mount) {
            return;
        }
        this.component.performPatchRenderWithRenderer(this.renderer, this.config, skipCache);
    };
    /**
     * @method updateMountRootStyles
     * @description Reconciles the properties of the rendering medium's mount element with any
     * configuration options that have been passed in, e.g. CSS overflow settings.
     */
    HaikuContext.prototype.updateMountRootStyles = function () {
        if (!this.mount) {
            return;
        }
        // We can assume the mount has only one child since we only mount one component into it (#?)
        var root = this.mount && this.mount.children[0];
        if (root) {
            if (this.config.position && root.style.position !== this.config.position) {
                root.style.position = this.config.position;
            }
            if (this.config.overflow) {
                root.style.overflow = this.config.overflow;
            }
            else {
                if (this.config.overflowX &&
                    root.style.overflowX !== this.config.overflowX) {
                    root.style.overflowX = this.config.overflowX;
                }
                if (this.config.overflowY &&
                    root.style.overflowY !== this.config.overflowY) {
                    root.style.overflowY = this.config.overflowY;
                }
            }
        }
    };
    /**
     * @method tick
     * @description Advances the component animation by one tick. Note that one tick is not necessarily
     * equivalent to one frame. If the animation frame loop is running too fast, the clock may wait before
     * incrementing the frame number. In other words, a tick implies an update but not necessarily a change.
     */
    HaikuContext.prototype.tick = function (skipCache) {
        var _this = this;
        if (skipCache === void 0) { skipCache = false; }
        var flushed = false;
        // Only continue ticking and updating if our root component is still activated and awake;
        // this is mainly a hacky internal hook used during hot editing inside Haiku Desktop
        if (!this.component.isDeactivated && !this.component.isSleeping) {
            // Perform any necessary updates that have to occur in all copmonents in the scene
            this.component.visitGuestHierarchy(function (component) {
                // State transitions are bound to clock time, so we update them on every tick
                component.tickStateTransitions();
                // The top-level component isn't controlled through playback status, so we must skip it
                // otherwise its behavior will not reflect the playback setting specified via options
                if (component === _this.component) {
                    return;
                }
                var timelines = component.getTimelines();
                for (var timelineName in timelines) {
                    // Although a timeline's playback status may not change over time, we still need
                    // to "apply" it, i.e. run the respective procedure to get the playback behavior
                    timelines[timelineName].applyPlaybackStatus();
                }
            });
            // After we've hydrated the tree the first time, we can proceed with patches --
            // unless the component indicates it wants a full flush per its internal settings.
            if (this.component.shouldPerformFullFlush() || this.config.forceFlush || this.ticks < 1) {
                this.performFullFlushRender();
                flushed = true;
            }
            else {
                this.performPatchRender(skipCache);
            }
            // We update the mount root *after* we complete the render pass because configuration
            // from the top level should unset anything that the component set.
            // Specifically important wrt overflow, where the component probably defines
            // overflowX/overflowY: hidden, but our configuration might define them as visible.
            this.updateMountRootStyles();
            // Do any initialization that may need to occur if we happen to be on the very first tick
            if (this.ticks < 1) {
                // If this is the 0th (first) tick, notify anybody listening that we've mounted
                // If we've already flushed, _don't_ request to trigger a re-flush (second arg)
                this.component.callRemount(null, flushed);
            }
            this.ticks++;
        }
    };
    /**
     * @method getGlobalUserState
     * @description Since the core renderer is medium-agnostic, we rely on the renderer to provide data
     * about the current user (the mouse position, for example). This method is just a convenience wrapper.
     */
    HaikuContext.prototype.getGlobalUserState = function () {
        return this.renderer && this.renderer.getUser && this.renderer.getUser();
    };
    /**
     * @function createComponentFactory
     * @description Returns a factory function that can create a HaikuComponent and run it upon a mount.
     * The created instance runs using the passed-in renderer, bytecode, options, and platform.
     */
    HaikuContext.createComponentFactory = function (rendererClass, bytecode, haikuConfigFromFactoryCreator, platform) {
        if (!rendererClass) {
            throw new Error('A runtime renderer class object is required');
        }
        if (!bytecode) {
            throw new Error('A runtime `bytecode` object is required');
        }
        // Note that haiku Config may be passed at this level, or below at the factory invocation level.
        var haikuConfigFromTop = Config_1.default.build({
            // The seed value should remain constant from here on, because it is used for PRNG
            seed: Config_1.default.seed(),
            // The now-value is used to compute a current date with respect to the current time
            timestamp: Date.now(),
        }, 
        // The bytecode itself may contain configuration for playback, etc., but is lower precedence than config passed in
        bytecode && bytecode.options, haikuConfigFromFactoryCreator);
        /**
         * @function HaikuComponentFactory
         * @description Creates a HaikuContext instance, with a component, and returns the component.
         * The (renderer, bytecode) pair are bootstrapped into the given mount element, and played.
         */
        var HaikuComponentFactory = function (mount, haikuConfigFromFactory) {
            // Merge any config received "late" with the config we might have already gotten during bootstrapping
            var haikuConfigMerged = Config_1.default.build(haikuConfigFromTop, haikuConfigFromFactory);
            // Previously these were initialized in the scope above, but I moved them here which seemed to resolve
            // an initialization/mounting issue when running in React.
            var renderer = new rendererClass(mount, haikuConfigMerged);
            var context = new HaikuContext(mount, renderer, platform, bytecode, haikuConfigMerged);
            var component = context.getRootComponent();
            // These properties are added for convenience as hot editing hooks inside Haiku Desktop (and elsewhere?).
            // It's a bit hacky to just expose these in this way, but it proves pretty convenient downstream.
            HaikuComponentFactory.bytecode = bytecode;
            HaikuComponentFactory.renderer = renderer;
            // Note that these ones could theoretically change if this factory was called more than once; use with care
            HaikuComponentFactory.mount = mount;
            HaikuComponentFactory.context = context;
            HaikuComponentFactory.component = component;
            // Finally, return the HaikuComponent instance which can also be used for programmatic behavior
            return component;
        };
        HaikuComponentFactory.PLAYER_VERSION = VERSION; // #LEGACY
        HaikuComponentFactory.CORE_VERSION = VERSION;
        return HaikuComponentFactory;
    };
    // Also expose so we can programatically choose an instance on the page
    HaikuContext.PLAYER_VERSION = VERSION; // #LEGACY
    HaikuContext.CORE_VERSION = VERSION; // #LEGACY
    HaikuContext.__name__ = 'HaikuContext';
    return HaikuContext;
}(HaikuBase_1.default));
exports.default = HaikuContext;
//# sourceMappingURL=HaikuContext.js.map