import Signal from "../../../core/events/signal/Signal.js";
import { min2 } from "../../../core/math/min2.js";

class AnimationTrackPlayback {
    /**
     *
     * @param {AnimationTrack} track
     * @param updateCallback
     * @param {Object} [updateTarget]
     * @constructor
     * @property {AnimationTrack} track
     * @property {number} position Current playback time
     */
    constructor(track, updateCallback, updateTarget) {
        this.track = track;
        this.position = track.timeStart;

        this.updateCallback = updateCallback;
        this.updateTraget = updateTarget;

        this.__lastKeyIndex = 0;
        this.__lastTransitionIndex = 0;

        this.__nextKeyIndex = 0;
        this.__nextKeyTime = 0;

        this.__loop = false;

        this.__valueBuffer = new Float64Array(track.propertyCount);


        this.on = {
            ended: new Signal()
        };

        this.setPosition(0);
    }

    /**
     *
     * @returns {boolean}
     */
    get loop() {
        return this.__loop;
    }

    /**
     *
     * @returns {Promise<void>}
     */
    promiseEnded() {
        if (this.position >= this.track.timeEnd) {
            return Promise.resolve();
        }

        return new Promise((resolve, reject) => {
            this.on.ended.add(resolve);
        });
    }

    /**
     *
     * @param {boolean} v
     */
    setLoop(v) {
        this.__loop = v;
    }

    reset() {
        this.setPosition(this.track.timeStart);
    }

    /**
     *
     * @param {number} time
     */
    setPosition(time) {
        this.position = time;

        const track = this.track;
        const transitionIndex = track.transitionIndexAt(time);

        this.__lastTransitionIndex = transitionIndex;

        if (transitionIndex !== -1) {
            //found a transition, read out start key index
            this.__lastKeyIndex = track.transitionKeys[transitionIndex];
        } else {
            //no transition found, lets find last key at least
            this.__lastKeyIndex = track.keyLowerBoundIndexAt(time);
        }

        //figure out next key time
        const trackKeyCount = track.keyTimes.length;
        if (trackKeyCount > 1) {
            this.__nextKeyIndex = min2(this.__lastKeyIndex + 1, trackKeyCount - 1);
        } else {
            this.__nextKeyIndex = this.__lastKeyIndex;
        }

        this.__nextKeyTime = track.keyTimes[this.__nextKeyIndex];
    }

    /**
     *
     * @returns {boolean} Indicated whenever or not a key was advanced, no advancement is possible past end of the sequence
     */
    advanceKey() {

        const track = this.track;
        const keyCount = track.keyTimes.length;
        if (this.__lastKeyIndex === keyCount - 1) {
            //we were on the last key
            if (this.__loop) {
                //loop back
                this.__lastKeyIndex = -1;
            } else {
                //we're at the end of the track, no sense in using transition as we are directly on a key
                this.__lastTransitionIndex = -1;

                this.on.ended.dispatch();

                //can't advance further
                return false;
            }
        }

        this.__lastKeyIndex = this.__lastKeyIndex + 1;

        this.__nextKeyTime = track.keyTimes[Math.min(this.__lastKeyIndex + 1, keyCount - 1)];

        //fetch transition if one exists
        const transitionKeys = track.transitionKeys;
        const transitionCount = transitionKeys.length;
        for (let i = this.__lastTransitionIndex + 1; i < transitionCount; i++) {
            const startKeyIndex = transitionKeys[i];
            if (startKeyIndex === this.__lastKeyIndex) {
                //we found a match
                this.__lastTransitionIndex = i;
                break;
            } else if (startKeyIndex > this.__lastKeyIndex) {
                //we went too far, it appears there is no transition for current key
                this.__lastTransitionIndex = -1;
                break;
            }
        }

        return true;
    }

    /**
     * Advance animation by given time delta
     * @param {number} timeDelta
     */
    advance(timeDelta) {

        let newPosition = this.position + timeDelta;

        if (timeDelta < 0) {
            //negative delta, going back in time
            this.setPosition(newPosition);
        } else if (newPosition > this.__nextKeyTime) {

            if (newPosition >= this.track.timeEnd) {
                if (this.__loop) {
                    //loop back
                    newPosition %= this.track.timeEnd;
                } else {
                    this.on.ended.dispatch();
                }
            }

            this.setPosition(newPosition);

        } else {

            this.position = newPosition;

        }

        this.update();
    }

    update() {

        this.readCurrentValues(this.__valueBuffer);

        this.updateCallback.apply(this.updateTraget, this.__valueBuffer);

        // console.log("values: ", this.__valueBuffer);
    }

    /**
     *
     * @param {number[]} result
     */
    readCurrentValues(result) {
        const track = this.track;

        const startKeyIndex = this.__lastKeyIndex;

        if (this.__lastTransitionIndex !== -1) {
            //we're in a middle of a transition

            const time1 = this.__nextKeyTime;
            const time0 = track.keyTimes[startKeyIndex];

            const timeDelta = time1 - time0;
            const relativeTime = (this.position - time0);

            const normalizedTime = relativeTime / timeDelta;

            const transitionFunction = track.transitionFunctions[this.__lastTransitionIndex];

            const transitionValue = transitionFunction(normalizedTime);

            const propCount = track.propertyCount;

            const keyValues = track.keyValues;

            const offset0 = startKeyIndex * propCount;
            const offset1 = offset0 + propCount;

            for (let i = 0; i < propCount; i++) {
                const v0 = keyValues[i + offset0];
                const v1 = keyValues[i + offset1];

                const valueDelta = v1 - v0;

                const value = transitionValue * valueDelta + v0;

                result[i] = value;
            }
        } else {
            //we're not in a transition, can re-use old key
            track.readKeyValues(startKeyIndex, result);
        }
    }
}

export default AnimationTrackPlayback;
