import { Cancellable, Disposable } from "@lincode/promiselikes"
import {
    Object3D,
    AnimationMixer,
    AnimationClip,
    NumberKeyframeTrack,
    AnimationAction,
    LoopRepeat,
    LoopOnce
} from "three"
import { AnimationData } from "../../../api/serializer/types"
import { forceGet } from "@lincode/utils"
import EventLoopItem from "../../../api/core/EventLoopItem"
import { onBeforeRender } from "../../../events/onBeforeRender"
import { dt } from "../../../engine/eventLoop"

const targetMixerMap = new WeakMap<EventLoopItem | Object3D, AnimationMixer>()
const mixerActionMap = new WeakMap<AnimationMixer, [AnimationAction, boolean]>()
const mixerHandleMap = new WeakMap<AnimationMixer, Cancellable>()

export type PlayOptions = {
    crossFade?: number
    repeat?: boolean
    onFinish?: () => void
}

export default class AnimationManager extends Disposable {
    private clip?: AnimationClip
    public name: string
    private mixer: AnimationMixer
    private action?: AnimationAction

    public constructor(
        nameOrClip: string | AnimationClip,
        target: EventLoopItem | Object3D
    ) {
        super()

        this.mixer = forceGet(
            targetMixerMap,
            target,
            () => new AnimationMixer(target as any)
        )

        if (typeof nameOrClip === "string") this.name = nameOrClip
        else {
            this.name = nameOrClip.name
            this.loadClip(nameOrClip)
        }
    }

    public retarget(target: Object3D) {
        const newClip = this.clip!.clone()
        const targetName = target.name + "."
        newClip.tracks = newClip.tracks.filter((track) =>
            track.name.startsWith(targetName)
        )
        return new AnimationManager(newClip, target)
    }

    public override dispose() {
        if (this.done) return this
        super.dispose()
        this.stop()
        return this
    }

    public get duration() {
        return this.clip?.duration ?? 0
    }

    private loadClip(clip: AnimationClip) {
        this.clip = clip
        this.action = this.mixer.clipAction(clip)
    }

    public setTracks(data: AnimationData) {
        const tracks = Object.entries(data).map(
            ([property, frames]) =>
                new NumberKeyframeTrack(
                    "." + property,
                    Object.keys(frames).map((t) => Number(t)),
                    Object.values(frames)
                )
        )
        this.clip && this.mixer.uncacheClip(this.clip)
        this.loadClip(new AnimationClip(this.name, -1, tracks))
    }

    public play({
        crossFade = 0.25,
        repeat = true,
        onFinish
    }: PlayOptions = {}) {
        const [prevAction, prevRepeat] = mixerActionMap.get(this.mixer) ?? []
        if (prevAction?.isRunning() && this.action === prevAction) {
            repeat !== prevRepeat &&
                prevAction.setLoop(repeat ? LoopRepeat : LoopOnce, Infinity)
            return
        }

        mixerHandleMap.get(this.mixer)?.cancel()
        const handle = this.watch(
            onBeforeRender(() => this.mixer.update(dt[0]))
        )
        mixerHandleMap.set(this.mixer, handle)

        const { action } = this
        if (!action) return

        if (prevAction && crossFade) {
            action.time = 0
            action.enabled = true
            action.crossFadeFrom(prevAction, crossFade, true)
        } else this.mixer.stopAllAction()

        mixerActionMap.set(this.mixer, [action, repeat])
        action.setLoop(repeat ? LoopRepeat : LoopOnce, Infinity)
        action.clampWhenFinished = true

        const handleFinish = () => onFinish?.()
        this.mixer.addEventListener("finished", handleFinish)
        handle.then(() =>
            this.mixer.removeEventListener("finished", handleFinish)
        )

        action.paused && action.stop()
        action.play()
    }

    public stop() {
        this.action && (this.action.paused = true)
        mixerHandleMap.get(this.mixer)?.cancel()
    }

    public getPaused() {
        return this.action?.paused
    }
    public setPaused(val: boolean) {
        this.action && (this.action.paused = val)
    }

    public update(seconds: number) {
        this.mixer.time = 0
        this.action && (this.action.time = 0)
        this.mixer.update(seconds)
    }
}
