const easing = {
    linear: (value) => value,
    "ease-in": (value) => value * value,
    "ease-out": (value) => value * (2 - value),
    "ease-in-out": (value) => value < 0.5 ? 2 * value * value : -1 + (4 - 2 * value) * value
};
export class TimelineClip {
    id;
    name;
    startTime;
    duration;
    assetId;
    clipName;
    easeInDuration;
    easeOutDuration;
    easeIn;
    easeOut;
    speedMultiplier;
    blendMode;
    weight;
    clipInOffset;
    properties;
    enabled = true;
    constructor(config) {
        if (!Number.isFinite(config.startTime) || config.startTime < 0)
            throw new RangeError("Timeline clip startTime must be a finite non-negative number.");
        if (!Number.isFinite(config.duration) || config.duration <= 0)
            throw new RangeError("Timeline clip duration must be a finite positive number.");
        this.id = config.id ?? stableId("clip", config.name);
        this.name = config.name;
        this.startTime = config.startTime;
        this.duration = config.duration;
        this.assetId = config.assetId;
        this.clipName = config.clipName;
        this.easeInDuration = Math.max(0, config.easeInDuration ?? 0);
        this.easeOutDuration = Math.max(0, config.easeOutDuration ?? 0);
        this.easeIn = config.easeIn ?? "linear";
        this.easeOut = config.easeOut ?? "linear";
        this.speedMultiplier = Math.max(0, config.speedMultiplier ?? 1);
        this.blendMode = config.blendMode ?? "replace";
        this.weight = clamp(config.weight ?? 1, 0, 1);
        this.clipInOffset = Math.max(0, config.clipInOffset ?? 0);
        this.properties = { ...(config.properties ?? {}) };
    }
    get endTime() {
        return this.startTime + this.duration;
    }
    contains(time) {
        return this.enabled && time >= this.startTime && time < this.endTime;
    }
    localTime(time) {
        if (!this.contains(time))
            return -1;
        return (time - this.startTime) * this.speedMultiplier;
    }
    assetTime(time) {
        const localTime = this.localTime(time);
        return localTime < 0 ? -1 : this.clipInOffset + localTime;
    }
    normalizedTime(time) {
        const localTime = this.localTime(time);
        return localTime < 0 ? -1 : clamp(localTime / this.duration, 0, 1);
    }
    blendWeight(time) {
        if (!this.contains(time))
            return 0;
        const localTime = time - this.startTime;
        let result = this.weight;
        if (this.easeInDuration > 0 && localTime < this.easeInDuration) {
            result *= easing[this.easeIn](clamp(localTime / this.easeInDuration, 0, 1));
        }
        const timeFromEnd = this.duration - localTime;
        if (this.easeOutDuration > 0 && timeFromEnd < this.easeOutDuration) {
            result *= easing[this.easeOut](clamp(timeFromEnd / this.easeOutDuration, 0, 1));
        }
        return Number(result.toFixed(4));
    }
}
export class TimelineTrack {
    id;
    name;
    type;
    muted;
    locked;
    weight;
    properties;
    clipsInternal = [];
    constructor(config) {
        this.id = config.id ?? stableId("track", config.name);
        this.name = config.name;
        this.type = config.type;
        this.muted = config.muted ?? false;
        this.locked = config.locked ?? false;
        this.weight = clamp(config.weight ?? 1, 0, 1);
        this.properties = { ...(config.properties ?? {}) };
        for (const clip of config.clips ?? []) {
            this.addClip(new TimelineClip(clip), { ignoreLock: true });
        }
    }
    get clips() {
        return this.clipsInternal;
    }
    addClip(clip, options = {}) {
        if (this.locked && !options.ignoreLock)
            throw new Error(`Cannot add clip to locked timeline track: ${this.name}`);
        this.clipsInternal.push(clip);
        this.clipsInternal.sort((left, right) => left.startTime - right.startTime);
        return clip;
    }
    activeClips(time) {
        if (this.muted || this.weight <= 0)
            return [];
        return this.clipsInternal.filter((clip) => clip.contains(time));
    }
    snapshot() {
        return {
            id: this.id,
            name: this.name,
            type: this.type,
            muted: this.muted,
            locked: this.locked,
            weight: this.weight,
            clipCount: this.clipsInternal.length
        };
    }
}
export class TimelineModel {
    id;
    name;
    duration;
    loopMode;
    speed;
    frameRate;
    tracksInternal = [];
    playing = false;
    timeInternal = 0;
    directionInternal = 1;
    emittedSignals = new Set();
    constructor(config = {}) {
        this.id = config.id ?? "editor-timeline";
        this.name = config.name ?? "Editor Timeline";
        this.duration = Math.max(0.01, config.duration ?? 1);
        this.loopMode = config.loopMode ?? "loop";
        this.speed = Math.max(0, config.speed ?? 1);
        this.frameRate = Math.max(0, config.frameRate ?? 0);
        for (const track of config.tracks ?? []) {
            this.addTrack(new TimelineTrack(track));
        }
    }
    get tracks() {
        return this.tracksInternal;
    }
    get currentTime() {
        return this.timeInternal;
    }
    get isPlaying() {
        return this.playing;
    }
    addTrack(track) {
        this.tracksInternal.push(track);
        return track;
    }
    clearTracks() {
        this.tracksInternal.length = 0;
        this.emittedSignals.clear();
    }
    play() {
        this.playing = true;
    }
    pause() {
        this.playing = false;
    }
    stop() {
        this.playing = false;
        this.seek(0);
        this.directionInternal = 1;
    }
    seek(time) {
        this.timeInternal = this.wrapTime(time);
        this.emittedSignals.clear();
    }
    setNormalizedTime(value) {
        this.seek(clamp(value, 0, 1) * this.duration);
    }
    tick(deltaSeconds) {
        if (!this.playing || deltaSeconds <= 0 || this.speed <= 0)
            return;
        this.seek(this.timeInternal + deltaSeconds * this.speed * this.directionInternal);
    }
    snapshot() {
        const activeClips = this.tracksInternal.flatMap((track) => track.activeClips(this.timeInternal).map((clip) => ({
            trackId: track.id,
            trackName: track.name,
            trackType: track.type,
            clipId: clip.id,
            clipName: clip.name,
            localTime: Number(clip.localTime(this.timeInternal).toFixed(4)),
            assetTime: Number(clip.assetTime(this.timeInternal).toFixed(4)),
            normalizedTime: Number(clip.normalizedTime(this.timeInternal).toFixed(4)),
            blendWeight: Number((clip.blendWeight(this.timeInternal) * track.weight).toFixed(4)),
            blendMode: clip.blendMode
        })));
        const signalEvents = this.tracksInternal.flatMap((track) => track.type === "signal"
            ? track.activeClips(this.timeInternal).map((clip) => typeof clip.properties.event === "string" ? clip.properties.event : clip.clipName ?? clip.id)
            : []);
        for (const signal of signalEvents)
            this.emittedSignals.add(signal);
        const clips = this.tracksInternal.flatMap((track) => track.clips);
        return {
            id: this.id,
            name: this.name,
            playback: this.playing ? "playing" : "paused",
            time: Number(this.timeInternal.toFixed(4)),
            normalizedTime: Number((this.timeInternal / this.duration).toFixed(4)),
            duration: this.duration,
            loopMode: this.loopMode,
            speed: this.speed,
            frameRate: this.frameRate,
            direction: this.directionInternal,
            trackCount: this.tracksInternal.length,
            clipCount: clips.length,
            activeClipCount: activeClips.length,
            activeClips,
            tracks: this.tracksInternal.map((track) => track.snapshot()),
            signalEvents: [...this.emittedSignals].sort(),
            evidence: {
                oldCodebasePort: true,
                boundedTimelineAuthoring: true,
                clipEasing: clips.some((clip) => clip.easeIn !== "linear" || clip.easeOut !== "linear" || clip.easeInDuration > 0 || clip.easeOutDuration > 0),
                clipBlending: clips.some((clip) => clip.blendMode !== "replace" || clip.weight < 1),
                muteLockState: this.tracksInternal.some((track) => track.muted || track.locked),
                loopPlayback: this.loopMode !== "none",
                signalMarkers: this.tracksInternal.some((track) => track.type === "signal" && track.clips.length > 0)
            }
        };
    }
    wrapTime(value) {
        if (!Number.isFinite(value))
            return this.timeInternal;
        if (value < 0) {
            if (this.loopMode === "loop")
                return modulo(value, this.duration);
            if (this.loopMode === "pingpong") {
                this.directionInternal = 1;
                return Math.abs(value);
            }
            this.playing = false;
            return 0;
        }
        if (value <= this.duration)
            return value;
        if (this.loopMode === "loop")
            return modulo(value, this.duration);
        if (this.loopMode === "pingpong") {
            this.directionInternal = -1;
            return Math.max(0, this.duration - (value - this.duration));
        }
        this.playing = false;
        return this.duration;
    }
}
function stableId(prefix, name) {
    return `${prefix}-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "item"}`;
}
function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}
function modulo(value, divisor) {
    return ((value % divisor) + divisor) % divisor;
}
//# sourceMappingURL=TimelineModel.js.map