import { Renderer } from './Renderer.js';
import { Compositor } from './Compositor.js';
import { Timeline, FIXED, REALTIME } from './Timeline.js';
import { createLayer } from './Layer.js';
import { hashFrame } from './hash.js';

/** Zeroed features, so the engine runs before any audio is loaded. */
export const NULL_FEATURES = Object.freeze({
    loudness: 0, rms: 0,
    bandSub: 0, bandLow: 0, bandMid: 0, bandHigh: 0, bandAir: 0,
    flux: 0, centroid: 0.5, flatness: 0, width: 0.5,
    beat: 0, beatPhase: 0, barPhase: 0, phrasePhase: 0,
    sectionProgress: 0, sectionEnergy: 0, buildSlope: 0,
});

/**
 * Ties the clock, the renderer and the layer stack together. Deliberately the
 * only place that knows about all three, and deliberately unaware of the DOM
 * beyond its canvas — the exporter and the check harness drive the exact same
 * object the preview does, which is what keeps them from diverging.
 */
export class Engine {
    constructor({ width = 1280, height = 720, canvas = null, fps = 60 } = {}) {
        this.renderer = new Renderer({ width, height, canvas });
        this.compositor = new Compositor(this.renderer, { width, height });
        this.timeline = new Timeline({ fps, mode: REALTIME });
        this.featureProvider = null;
        this.ownedLayers = [];
        this.lastFrameRendered = -1;
    }

    get width() { return this.renderer.width; }
    get height() { return this.renderer.height; }

    setSize(width, height) {
        this.renderer.setSize(width, height);
        this.compositor.setSize(width, height);
    }

    setFeatureProvider(provider) {
        this.featureProvider = provider;
        return this;
    }

    featuresAt(frame) {
        if (!this.featureProvider) return NULL_FEATURES;
        return this.featureProvider.at(frame) || NULL_FEATURES;
    }

    /**
     * Replace the stack from specs. `specs` are { module, params, seed, opacity,
     * blend }. Layers built this way are owned by the Engine and disposed with
     * it; layers supplied directly by the arc driver are owned by the driver.
     */
    setLayerSpecs(specs) {
        this.ownedLayers.forEach((l) => l.dispose());
        const layers = specs.map((s) => {
            const layer = createLayer(s.module, s);
            if (s.palette) layer.setPalette(s.palette);
            if (s.personality) layer.setPersonality(s.personality);
            return layer;
        });
        this.ownedLayers = layers;
        this.compositor.setLayers(layers);
        return layers;
    }

    setPalette(colors) {
        this.compositor.layers.forEach((l) => l.setPalette(colors));
    }

    /**
     * Compile every shader and discard a warm frame, so the next frame rendered
     * is correct. Required before any frame-exact use (export, hashing).
     */
    prime(frame = 0) {
        this.timeline.seek(frame);
        this.compositor.prime({ timeline: this.timeline, features: this.featuresAt(frame) });
        return this;
    }

    /** Render exactly one frame at the timeline's current position. */
    renderCurrent() {
        const features = this.featuresAt(this.timeline.frame);
        const target = this.compositor.render({ timeline: this.timeline, features });
        this.lastFrameRendered = this.timeline.frame;
        return target;
    }

    /** Render a specific frame without warm-up. Used by checks and by export. */
    renderFrame(frameIndex) {
        this.timeline.seek(frameIndex);
        return this.renderCurrent();
    }

    present(target) {
        this.compositor.present(target);
    }

    readPixels(target) {
        return this.renderer.readPixels(target);
    }

    hashCurrent(target) {
        return hashFrame(this.renderer.readPixels(target));
    }

    /**
     * Render frames [start, start+count) sequentially from a clean state and
     * return a hash per frame. Sequential and reset-first, so the result depends
     * only on the inputs — this is the primitive every determinism check uses.
     */
    hashRun(start, count, { reset = true, prime = true } = {}) {
        if (prime) this.prime(start);
        if (reset) this.compositor.reset();
        const hashes = [];
        for (let i = 0; i < count; i++) {
            const target = this.renderFrame(start + i);
            hashes.push(hashFrame(this.renderer.readPixels(target)));
        }
        return hashes;
    }

    /**
     * Advance stateful layers up to `frame` without presenting, so a seek lands
     * on converged feedback state. Section-boundary seeks pass warmup: 0,
     * because layer state is re-seeded there and is exact by construction.
     */
    warmUp(frame, warmupFrames = 120) {
        const start = Math.max(0, frame - warmupFrames);
        this.compositor.reset();
        for (let f = start; f < frame; f++) {
            this.timeline.seek(f);
            const features = this.featuresAt(f);
            this.compositor.render({ timeline: this.timeline, features });
        }
        this.timeline.seek(frame);
    }

    dispose() {
        this.ownedLayers.forEach((l) => l.dispose());
        this.ownedLayers = [];
        this.compositor.dispose();
        this.renderer.dispose();
    }
}

export { FIXED, REALTIME };
