import { __assign, __read, __values } from "tslib";
import { noop, createFrameGenerator, EasingFunctions, } from "@intelligentgraphics/ig.utilities";
import { State } from "./State";
var DEFAULT_PERIOD = 1;
var DEFAULT_STAGE = 1;
var DURATION_PER_PERIOD = 1000;
/**
 * Displays an animation in the specified direction
 *
 * @param {boolean} forward
 * @param {boolean} [invertAnimationAfter=false]
 * @returns {PlaybackControl}
 */
export var showAnimation = function (componentIds, animationParams, animatorOptions) { return new Animator(componentIds, animatorOptions).showAnimation(animationParams); };
/**
 * Sets the animation position of all components and their interactors
 *
 * @param {("Start" | "End")} position
 */
export var setPosition = function (components, position) {
    return new Animator(components).setPosition(position);
};
/**
 * Sets the current progress using the specified percentage
 *
 * (Between 0 and 1)
 *
 * @param {number} percent
 */
export var setProgress = function (componentIds, progress) {
    return new Animator(componentIds).setProgress(progress);
};
/**
 * convenience function: return if there are components with animatable interactors
 */
export function hasAnimatableComponents(componentId) {
    var animator = new Animator([componentId]);
    return animator.hasAnimatableComponents();
}
var Animator = /** @class */ (function () {
    function Animator(componentIds, options) {
        var e_1, _a;
        this.api = State.API;
        this.components = new Map();
        this.maxPeriods = 0;
        this.currentProgress = 0;
        this.options = __assign({ disableSnapping: false, durationPerPeriod: DURATION_PER_PERIOD, applyEasing: EasingFunctions.linear, maxFPS: 1000, onProgressChange: noop }, options);
        this.findAnimatableComponents(componentIds);
        try {
            for (var _b = __values(this.components.values()), _c = _b.next(); !_c.done; _c = _b.next()) {
                var componentData = _c.value;
                var componentPeriodAmount = 0;
                for (var index = 0; index < componentData.stages.length; index++) {
                    var stage = componentData.stages[index];
                    componentPeriodAmount +=
                        stage === undefined ? DEFAULT_PERIOD : stage.periods;
                }
                componentData.totalPeriods = componentPeriodAmount;
                this.maxPeriods = Math.max(componentPeriodAmount, this.maxPeriods);
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
            }
            finally { if (e_1) throw e_1.error; }
        }
    }
    /**
     * return if there are components with animatable interactors
     */
    Animator.prototype.hasAnimatableComponents = function () {
        var result = false;
        this.components.forEach(function (animatorComponent) {
            if (animatorComponent.stages.length > 0) {
                result = true;
            }
        });
        return result;
    };
    /**
     * Displays an animation in the specified direction
     *
     * @param {AnimationParams} {
     * 		forward,
     * 		invertAnimationAfter = false,
     * 		startFromProgress = forward ? 0 : 1,
     * 	}
     * @returns {PlaybackControl}
     */
    Animator.prototype.showAnimation = function (_a) {
        var _this = this;
        var forward = _a.forward, _b = _a.invertAnimationAfter, invertAnimationAfter = _b === void 0 ? false : _b, _c = _a.startFromProgress, startFromProgress = _c === void 0 ? forward ? 0 : 1 : _c;
        var createAnimation = function (remaining) {
            if (remaining === void 0) { remaining = 1; }
            var start = 1 - remaining;
            var fullLengthTime = _this.options.totalDuration !== undefined
                ? _this.options.totalDuration
                : _this.maxPeriods * _this.options.durationPerPeriod;
            var remainingTime = remaining * fullLengthTime;
            // we need to initialize the time value here, so
            // animations can be paused before the first animation tick
            _this.currentTimeProgress = start;
            return createFrameGenerator(remainingTime, _this.options.maxFPS, function (percent) {
                _this.currentTimeProgress = percent =
                    start + remaining * percent;
                var easedPercent = _this.options.applyEasing(percent);
                var directionPercent = forward
                    ? easedPercent
                    : 1 - easedPercent;
                _this.stepTo(directionPercent);
            });
        };
        var playbackControl = {
            stop: noop,
            pause: noop,
            continue: noop,
            done: undefined,
        };
        playbackControl.done = new Promise(function (resolve, reject) {
            var animation;
            var status;
            var onDone = function () {
                if (!invertAnimationAfter) {
                    status = "done";
                    return resolve();
                }
                _this.currentTimeProgress = 0;
                var reverseAnimation = _this.showAnimation({
                    forward: !forward,
                    invertAnimationAfter: false,
                });
                playbackControl.continue = reverseAnimation.continue;
                playbackControl.pause = reverseAnimation.pause;
                playbackControl.stop = reverseAnimation.stop;
                reverseAnimation.done.then(function () {
                    status = "done";
                    resolve();
                });
            };
            playbackControl.stop = function () {
                if (status === "running" || status === "paused") {
                    status = "stopped";
                    animation.cancel();
                    _this.currentTimeProgress = 0;
                    reject();
                }
            };
            playbackControl.pause = function () {
                if (status === "running") {
                    status = "paused";
                    animation.cancel();
                }
            };
            playbackControl.continue = function () {
                if (status === "paused") {
                    if (_this.currentTimeProgress === undefined) {
                        return reject(new Error("The animation time progress has been cleared. Refusing to continue the animation"));
                    }
                    status = "running";
                    animation = createAnimation(1 - _this.currentTimeProgress);
                    animation.done = animation.done.then(onDone).catch(noop);
                }
            };
            status = "running";
            var revertedEasing = _this.options.applyEasing.reverse(startFromProgress);
            animation = createAnimation(forward ? 1 - revertedEasing : revertedEasing);
            animation.done = animation.done.then(onDone).catch(noop);
        });
        playbackControl.done
            .then(function () {
            var newPosition = forward || (!forward && invertAnimationAfter)
                ? "End"
                : "Start";
            _this.currentTimeProgress = undefined;
            _this.saveComponentAnimationPosition(newPosition);
        })
            // prevent uncaught promise errors when this animation is being stopped
            .catch(noop);
        return playbackControl;
    };
    /**
     * Sets the animation position of all components and their interactors
     *
     * @param {("Start" | "End")} position
     */
    Animator.prototype.setPosition = function (position) {
        this.stepTo(position === "Start" ? 0 : 1);
        this.saveComponentAnimationPosition(position);
        this.api.RedrawScene();
    };
    /**
     * Retrieves the current progress percentage
     *
     * (Between 0 and 1)
     *
     * @returns
     */
    Animator.prototype.getProgress = function () {
        return this.currentProgress;
    };
    /**
     * Sets the current progress using the specified percentage
     *
     * (Between 0 and 1)
     *
     * @param {number} percent
     */
    Animator.prototype.setProgress = function (percent) {
        if (percent > 1) {
            throw new Error("Can not set animator progress to a number larger than 1");
        }
        if (percent < 0) {
            throw new Error("Can not set animator progress to a number smaller than 0");
        }
        if (percent === 0 || percent === 1) {
            this.setPosition(percent === 0 ? "Start" : "End");
            return;
        }
        this.currentTimeProgress = 0;
        this.stepTo(percent);
        this.api.RedrawScene();
    };
    Animator.prototype.stepTo = function (percent) {
        var e_2, _a, e_3, _b, e_4, _c, e_5, _d, e_6, _e;
        this.currentProgress = percent;
        this.options.onProgressChange(percent);
        try {
            for (var _f = __values(this.components.values()), _g = _f.next(); !_g.done; _g = _f.next()) {
                var component = _g.value;
                // the current period of this component relative to the global current period
                var componentPeriod = component.totalPeriods * percent;
                var periodCounter = 0;
                var componentStageIndex = void 0;
                var stage = void 0;
                for (componentStageIndex = 0; componentStageIndex < component.stages.length; componentStageIndex++) {
                    stage = component.stages[componentStageIndex];
                    periodCounter +=
                        stage === undefined ? DEFAULT_PERIOD : stage.periods;
                    if (periodCounter > componentPeriod) {
                        break;
                    }
                }
                for (var index = 0; index < component.stages.length; index++) {
                    if (index === componentStageIndex) {
                        continue;
                    }
                    var stage_1 = component.stages[index];
                    if (stage_1 !== undefined) {
                        try {
                            for (var _h = (e_3 = void 0, __values(stage_1.interactors)), _j = _h.next(); !_j.done; _j = _h.next()) {
                                var interactor = _j.value;
                                try {
                                    interactor.SetStagePosition(index + 1, index < componentStageIndex ? 1 : 0);
                                }
                                catch (e) {
                                    console.log("Exception in SetStagePosition: ", e);
                                }
                            }
                        }
                        catch (e_3_1) { e_3 = { error: e_3_1 }; }
                        finally {
                            try {
                                if (_j && !_j.done && (_b = _h.return)) _b.call(_h);
                            }
                            finally { if (e_3) throw e_3.error; }
                        }
                    }
                }
                // stages might be empty
                if (stage === undefined) {
                    continue;
                }
                var stageTotalPercent = (1 / component.totalPeriods) * stage.periods;
                var stageStart = periodCounter - stage.periods;
                var stageStartPercent = (1 / component.totalPeriods) * stageStart;
                var stagePosition = (1 / stageTotalPercent) * (percent - stageStartPercent);
                try {
                    // console.log({
                    // 	componentId: component.componentId,
                    // 	totalPeriods: component.totalPeriods,
                    // 	componentPeriod,
                    // 	componentStageIndex,
                    // 	stageTotalPercent,
                    // 	stageStart,
                    // 	stageStartPercent,
                    // 	stagePosition
                    // });
                    for (var _k = (e_4 = void 0, __values(stage.interactors)), _l = _k.next(); !_l.done; _l = _k.next()) {
                        var interactor = _l.value;
                        try {
                            interactor.SetStagePosition(componentStageIndex + 1, stagePosition);
                        }
                        catch (e) {
                            console.log("Exception in SetStagePosition: ", e);
                        }
                    }
                }
                catch (e_4_1) { e_4 = { error: e_4_1 }; }
                finally {
                    try {
                        if (_l && !_l.done && (_c = _k.return)) _c.call(_k);
                    }
                    finally { if (e_4) throw e_4.error; }
                }
            }
        }
        catch (e_2_1) { e_2 = { error: e_2_1 }; }
        finally {
            try {
                if (_g && !_g.done && (_a = _f.return)) _a.call(_f);
            }
            finally { if (e_2) throw e_2.error; }
        }
        try {
            for (var _m = __values(this.components), _o = _m.next(); !_o.done; _o = _m.next()) {
                var _p = __read(_o.value, 1), componentId = _p[0];
                this.api.SetComponentData(componentId, "animationProgress", percent);
                this.api.Hooks.afterAnimationProgressChange.emit(componentId, percent);
            }
        }
        catch (e_5_1) { e_5 = { error: e_5_1 }; }
        finally {
            try {
                if (_o && !_o.done && (_d = _m.return)) _d.call(_m);
            }
            finally { if (e_5) throw e_5.error; }
        }
        try {
            for (var _q = __values(State.cameraViews), _r = _q.next(); !_r.done; _r = _q.next()) {
                var view = _r.value;
                view.enterPostprocessing();
            }
        }
        catch (e_6_1) { e_6 = { error: e_6_1 }; }
        finally {
            try {
                if (_r && !_r.done && (_e = _q.return)) _e.call(_q);
            }
            finally { if (e_6) throw e_6.error; }
        }
        this.api.RedrawScene();
    };
    Animator.prototype.saveComponentAnimationPosition = function (position) {
        var e_7, _a;
        try {
            for (var _b = __values(this.components), _c = _b.next(); !_c.done; _c = _b.next()) {
                var _d = __read(_c.value, 1), componentId = _d[0];
                this.api.SetComponentData(componentId, "animationPosition", position);
            }
        }
        catch (e_7_1) { e_7 = { error: e_7_1 }; }
        finally {
            try {
                if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
            }
            finally { if (e_7) throw e_7.error; }
        }
    };
    /**
     * Gets the stage of the interactor
     *
     * @private
     */
    Animator.prototype.getInteractorStage = function (interactor) {
        if (interactor !== undefined && interactor.parameters !== undefined) {
            var stage = interactor.parameters.Stage;
            if (typeof stage === "number") {
                return stage;
            }
        }
        return DEFAULT_STAGE;
    };
    /**
     * Get the interactor periodcount
     *
     * @private
     */
    Animator.prototype.getInteractorPeriod = function (interactor) {
        if (interactor !== undefined && interactor.parameters !== undefined) {
            var period = interactor.parameters.Period;
            if (typeof period === "number") {
                return period;
            }
        }
        return DEFAULT_PERIOD;
    };
    /**
     * Finds all interactors which can be animated
     *
     * @private
     * @param {string[]} componentIds
     */
    Animator.prototype.findAnimatableComponents = function (componentIds) {
        var _this = this;
        var findAnimatableChildren = function (parentComponentId, rootId) {
            var e_8, _a;
            var children = _this.api.GetChildren(parentComponentId);
            if (!children) {
                return;
            }
            try {
                for (var children_1 = __values(children), children_1_1 = children_1.next(); !children_1_1.done; children_1_1 = children_1.next()) {
                    var componentId = children_1_1.value;
                    var userData = _this.api.GetComponentData(componentId);
                    if (!userData ||
                        userData.geometryStatus === "preview" ||
                        !userData.interactor ||
                        (userData.interactorParameters &&
                            (userData.interactorParameters.Active === "false" ||
                                userData.interactorParameters.Active === false))) {
                        findAnimatableChildren(componentId, rootId);
                        continue;
                    }
                    var interactorParameters = userData.interactorParameters || {};
                    if (_this.options.disableSnapping) {
                        interactorParameters.Snap = 0;
                    }
                    var interactorName = userData.interactor;
                    var interactor = _this.api.CreateInteractor(interactorName, interactorParameters);
                    interactor.SetComponentId(componentId);
                    var stage = _this.getInteractorStage(interactor) - 1;
                    if (stage < 0) {
                        stage = -1;
                    }
                    var periods = _this.getInteractorPeriod(interactor);
                    var componentData = _this.components.get(rootId);
                    var stageData = componentData.stages[stage];
                    if (!stageData) {
                        stageData = componentData.stages[stage] = {
                            interactors: [],
                            periods: 0,
                            stage: stage,
                        };
                    }
                    stageData.periods = Math.max(stageData.periods, periods);
                    stageData.interactors.push(interactor);
                    findAnimatableChildren(componentId, rootId);
                }
            }
            catch (e_8_1) { e_8 = { error: e_8_1 }; }
            finally {
                try {
                    if (children_1_1 && !children_1_1.done && (_a = children_1.return)) _a.call(children_1);
                }
                finally { if (e_8) throw e_8.error; }
            }
        };
        componentIds.forEach(function (componentId) {
            _this.components.set(componentId, {
                componentId: componentId,
                stages: [],
                totalPeriods: 0,
            });
            findAnimatableChildren(componentId, componentId);
        });
    };
    return Animator;
}());
export { Animator };
