import { Graphic } from './Graphic';
import { Logger } from '../Util/Log';
import { clamp } from '../Math/util';
import { EventEmitter } from '../EventEmitter';
export var AnimationDirection;
(function (AnimationDirection) {
    /**
     * Animation is playing forwards
     */
    AnimationDirection["Forward"] = "forward";
    /**
     * Animation is playing backwards
     */
    AnimationDirection["Backward"] = "backward";
})(AnimationDirection || (AnimationDirection = {}));
export var AnimationStrategy;
(function (AnimationStrategy) {
    /**
     * Animation ends without displaying anything
     */
    AnimationStrategy["End"] = "end";
    /**
     * Animation loops to the first frame after the last frame
     */
    AnimationStrategy["Loop"] = "loop";
    /**
     * Animation plays to the last frame, then backwards to the first frame, then repeats
     */
    AnimationStrategy["PingPong"] = "pingpong";
    /**
     * Animation ends stopping on the last frame
     */
    AnimationStrategy["Freeze"] = "freeze";
})(AnimationStrategy || (AnimationStrategy = {}));
export const AnimationEvents = {
    Frame: 'frame',
    Loop: 'loop',
    End: 'end'
};
/**
 * Create an Animation given a list of [[Frame|frames]] in [[AnimationOptions]]
 *
 * To create an Animation from a [[SpriteSheet]], use [[Animation.fromSpriteSheet]]
 */
export class Animation extends Graphic {
    constructor(options) {
        var _a, _b, _c;
        super(options);
        this.events = new EventEmitter();
        this.frames = [];
        this.strategy = AnimationStrategy.Loop;
        this.frameDuration = 100;
        this._idempotencyToken = -1;
        this._firstTick = true;
        this._currentFrame = 0;
        this._timeLeftInFrame = 0;
        this._pingPongDirection = 1;
        this._done = false;
        this._playing = true;
        this._speed = 1;
        this._reversed = false;
        this.frames = options.frames;
        this.speed = (_a = options.speed) !== null && _a !== void 0 ? _a : this.speed;
        this.strategy = (_b = options.strategy) !== null && _b !== void 0 ? _b : this.strategy;
        this.frameDuration = options.totalDuration ? options.totalDuration / this.frames.length : (_c = options.frameDuration) !== null && _c !== void 0 ? _c : this.frameDuration;
        if (options.reverse) {
            this.reverse();
        }
        this.goToFrame(0);
    }
    clone() {
        return new Animation({
            frames: this.frames.map((f) => ({ ...f })),
            frameDuration: this.frameDuration,
            speed: this.speed,
            reverse: this._reversed,
            strategy: this.strategy,
            ...this.cloneGraphicOptions()
        });
    }
    get width() {
        const maybeFrame = this.currentFrame;
        if (maybeFrame) {
            return Math.abs(maybeFrame.graphic.width * this.scale.x);
        }
        return 0;
    }
    get height() {
        const maybeFrame = this.currentFrame;
        if (maybeFrame) {
            return Math.abs(maybeFrame.graphic.height * this.scale.y);
        }
        return 0;
    }
    /**
     * Create an Animation from a [[SpriteSheet]], a list of indices into the sprite sheet, a duration per frame
     * and optional [[AnimationStrategy]]
     *
     * Example:
     * ```typescript
     * const spriteSheet = SpriteSheet.fromImageSource({...});
     *
     * const anim = Animation.fromSpriteSheet(spriteSheet, range(0, 5), 200, AnimationStrategy.Loop);
     * ```
     * @param spriteSheet
     * @param frameIndices
     * @param durationPerFrameMs
     * @param strategy
     */
    static fromSpriteSheet(spriteSheet, frameIndices, durationPerFrameMs, strategy = AnimationStrategy.Loop) {
        const maxIndex = spriteSheet.sprites.length - 1;
        const invalidIndices = frameIndices.filter((index) => index < 0 || index > maxIndex);
        if (invalidIndices.length) {
            Animation._LOGGER.warn(`Indices into SpriteSheet were provided that don\'t exist: ${invalidIndices.join(',')} no frame will be shown`);
        }
        return new Animation({
            frames: spriteSheet.sprites
                .filter((_, index) => frameIndices.indexOf(index) > -1)
                .map((f) => ({
                graphic: f,
                duration: durationPerFrameMs
            })),
            strategy: strategy
        });
    }
    /**
     * Create an [[Animation]] from a [[SpriteSheet]] given a list of coordinates
     *
     * Example:
     * ```typescript
     * const spriteSheet = SpriteSheet.fromImageSource({...});
     *
     * const anim = Animation.fromSpriteSheetCoordinates({
     *  spriteSheet,
     *  frameCoordinates: [
     *    {x: 0, y: 5, duration: 100, options { flipHorizontal: true }},
     *    {x: 1, y: 5, duration: 200},
     *    {x: 2, y: 5, duration: 100},
     *    {x: 3, y: 5, duration: 500}
     *  ],
     *  strategy: AnimationStrategy.PingPong
     * });
     * ```
     * @param options
     * @returns Animation
     */
    static fromSpriteSheetCoordinates(options) {
        const { spriteSheet, frameCoordinates, durationPerFrameMs, speed, strategy, reverse } = options;
        const defaultDuration = durationPerFrameMs !== null && durationPerFrameMs !== void 0 ? durationPerFrameMs : 100;
        const frames = [];
        for (const coord of frameCoordinates) {
            const { x, y, duration, options } = coord;
            const sprite = spriteSheet.getSprite(x, y, options);
            if (sprite) {
                frames.push({
                    graphic: sprite,
                    duration: duration !== null && duration !== void 0 ? duration : defaultDuration
                });
            }
            else {
                Animation._LOGGER.warn(`Skipping frame! SpriteSheet does not have coordinate (${x}, ${y}), please check your SpriteSheet to confirm that sprite exists`);
            }
        }
        return new Animation({
            frames,
            strategy,
            speed,
            reverse
        });
    }
    /**
     * Current animation speed
     *
     * 1 meaning normal 1x speed.
     * 2 meaning 2x speed and so on.
     */
    get speed() {
        return this._speed;
    }
    /**
     * Current animation speed
     *
     * 1 meaning normal 1x speed.
     * 2 meaning 2x speed and so on.
     */
    set speed(val) {
        this._speed = clamp(Math.abs(val), 0, Infinity);
    }
    /**
     * Returns the current Frame of the animation
     *
     * Use [[Animation.currentFrameIndex]] to get the frame number and
     * [[Animation.goToFrame]] to set the current frame index
     */
    get currentFrame() {
        if (this._currentFrame >= 0 && this._currentFrame < this.frames.length) {
            return this.frames[this._currentFrame];
        }
        return null;
    }
    /**
     * Returns the current frame index of the animation
     *
     * Use [[Animation.currentFrame]] to grab the current [[Frame]] object
     */
    get currentFrameIndex() {
        return this._currentFrame;
    }
    /**
     * Returns the amount of time in milliseconds left in the current frame
     */
    get currentFrameTimeLeft() {
        return this._timeLeftInFrame;
    }
    /**
     * Returns `true` if the animation is playing
     */
    get isPlaying() {
        return this._playing;
    }
    /**
     * Reverses the play direction of the Animation, this preserves the current frame
     */
    reverse() {
        // Don't mutate with the original frame list, create a copy
        this.frames = this.frames.slice().reverse();
        this._reversed = !this._reversed;
    }
    /**
     * Returns the current play direction of the animation
     */
    get direction() {
        // Keep logically consistent with ping-pong direction
        // If ping-pong is forward = 1 and reversed is true then we are logically reversed
        const reversed = this._reversed && this._pingPongDirection === 1 ? true : false;
        return reversed ? AnimationDirection.Backward : AnimationDirection.Forward;
    }
    /**
     * Plays or resumes the animation from the current frame
     */
    play() {
        this._playing = true;
    }
    /**
     * Pauses the animation on the current frame
     */
    pause() {
        this._playing = false;
        this._firstTick = true; // firstTick must be set to emit the proper frame event
    }
    /**
     * Reset the animation back to the beginning, including if the animation were done
     */
    reset() {
        this._done = false;
        this._firstTick = true;
        this._currentFrame = 0;
        this._timeLeftInFrame = this.frameDuration;
        const maybeFrame = this.frames[this._currentFrame];
        if (maybeFrame) {
            this._timeLeftInFrame = (maybeFrame === null || maybeFrame === void 0 ? void 0 : maybeFrame.duration) || this.frameDuration;
        }
    }
    /**
     * Returns `true` if the animation can end
     */
    get canFinish() {
        switch (this.strategy) {
            case AnimationStrategy.End:
            case AnimationStrategy.Freeze: {
                return true;
            }
            default: {
                return false;
            }
        }
    }
    /**
     * Returns `true` if the animation is done, for looping type animations
     * `ex.AnimationStrategy.PingPong` and `ex.AnimationStrategy.Loop` this will always return `false`
     *
     * See the `ex.Animation.canFinish()` method to know if an animation type can end
     */
    get done() {
        return this._done;
    }
    /**
     * Jump the animation immediately to a specific frame if it exists
     *
     * Optionally specify an override for the duration of the frame, useful for
     * keeping multiple animations in sync with one another.
     * @param frameNumber
     * @param duration
     */
    goToFrame(frameNumber, duration) {
        this._currentFrame = frameNumber;
        this._timeLeftInFrame = duration !== null && duration !== void 0 ? duration : this.frameDuration;
        const maybeFrame = this.frames[this._currentFrame];
        if (maybeFrame && !this._done) {
            this._timeLeftInFrame = duration !== null && duration !== void 0 ? duration : ((maybeFrame === null || maybeFrame === void 0 ? void 0 : maybeFrame.duration) || this.frameDuration);
            this.events.emit('frame', { ...maybeFrame, frameIndex: this.currentFrameIndex });
        }
    }
    _nextFrame() {
        const currentFrame = this._currentFrame;
        if (this._done) {
            return currentFrame;
        }
        let next = -1;
        switch (this.strategy) {
            case AnimationStrategy.Loop: {
                next = (currentFrame + 1) % this.frames.length;
                if (next === 0) {
                    this.events.emit('loop', this);
                }
                break;
            }
            case AnimationStrategy.End: {
                next = currentFrame + 1;
                if (next >= this.frames.length) {
                    this._done = true;
                    this._currentFrame = this.frames.length;
                    this.events.emit('end', this);
                }
                break;
            }
            case AnimationStrategy.Freeze: {
                next = clamp(currentFrame + 1, 0, this.frames.length - 1);
                if (next >= this.frames.length - 1) {
                    this._done = true;
                    this.events.emit('end', this);
                }
                break;
            }
            case AnimationStrategy.PingPong: {
                if (currentFrame + this._pingPongDirection >= this.frames.length) {
                    this._pingPongDirection = -1;
                    this.events.emit('loop', this);
                }
                if (currentFrame + this._pingPongDirection < 0) {
                    this._pingPongDirection = 1;
                    this.events.emit('loop', this);
                }
                next = currentFrame + (this._pingPongDirection % this.frames.length);
                break;
            }
        }
        return next;
    }
    /**
     * Called internally by Excalibur to update the state of the animation potential update the current frame
     * @param elapsedMilliseconds Milliseconds elapsed
     * @param idempotencyToken Prevents double ticking in a frame by passing a unique token to the frame
     */
    tick(elapsedMilliseconds, idempotencyToken = 0) {
        if (this._idempotencyToken === idempotencyToken) {
            return;
        }
        this._idempotencyToken = idempotencyToken;
        if (!this._playing) {
            return;
        }
        // if it's the first frame emit frame event
        if (this._firstTick) {
            this._firstTick = false;
            this.events.emit('frame', { ...this.currentFrame, frameIndex: this.currentFrameIndex });
        }
        this._timeLeftInFrame -= elapsedMilliseconds * this._speed;
        if (this._timeLeftInFrame <= 0) {
            this.goToFrame(this._nextFrame());
        }
    }
    _drawImage(ctx, x, y) {
        if (this.currentFrame) {
            this.currentFrame.graphic.draw(ctx, x, y);
        }
    }
}
Animation._LOGGER = Logger.getInstance();
//# sourceMappingURL=Animation.js.map