import { analyzeBuffer } from './analyze.js';
import { detectTempo, buildBeatTracks } from './tempo.js';
import { segment } from './segment.js';

/**
 * The frame-indexed feature table. Everything visual reads from here and from
 * nowhere else — no live AnalyserNode exists in this project.
 *
 * Realtime preview maps audio.currentTime to a frame index and reads row n.
 * The exporter counts frames and reads row n. Same rows, same visuals, which is
 * the whole basis of preview/export parity.
 */
export class FeatureTrack {
    constructor(data) {
        Object.assign(this, data);
        // Reused row object: at() is called 60 times a second and there is no
        // reason to allocate for it.
        this._row = {};
    }

    /**
     * The feature row for a frame, clamped to range.
     *
     * WARNING: the returned object is REUSED between calls — at() is called every
     * frame and allocating for it is pointless. The consequence is that you must
     * never call at() again while still holding a previous result, and in
     * particular never inside a render pass that is using one. Index the typed
     * arrays in `raw`/`tracks` directly for incidental lookups.
     */
    at(frame) {
        const f = Math.max(0, Math.min(this.frameCount - 1, frame | 0));
        const row = this._row;
        const raw = this.raw;

        row.rms = raw.rms[f];
        row.loudness = raw.loudness[f];
        row.bandSub = raw.bandSub[f];
        row.bandLow = raw.bandLow[f];
        row.bandMid = raw.bandMid[f];
        row.bandHigh = raw.bandHigh[f];
        row.bandAir = raw.bandAir[f];
        row.flux = raw.flux[f];
        row.centroid = raw.centroid[f];
        row.flatness = raw.flatness[f];
        row.width = raw.width[f];

        row.beat = this.tracks.beat[f];
        row.beatPhase = this.tracks.beatPhase[f];
        row.barPhase = this.tracks.barPhase[f];
        row.phrasePhase = this.tracks.phrasePhase[f];

        row.sectionProgress = this.tracks.sectionProgress[f];
        row.sectionEnergy = this.tracks.sectionEnergy[f];
        row.buildSlope = this.tracks.buildSlope[f];

        return row;
    }

    sectionIndexAt(frame) {
        return this.tracks.sectionIndex[Math.max(0, Math.min(this.frameCount - 1, frame | 0))];
    }

    sectionAt(frame) {
        return this.sections[this.sectionIndexAt(frame)] || this.sections[0];
    }

    /** Nearest section boundary frame in a direction. Powers the transport buttons. */
    boundaryFrame(frame, direction) {
        const bounds = this.sections.map((s) => s.startFrame).concat([this.frameCount - 1]);
        if (direction < 0) {
            for (let i = bounds.length - 1; i >= 0; i--) if (bounds[i] < frame - 2) return bounds[i];
            return 0;
        }
        for (let i = 0; i < bounds.length; i++) if (bounds[i] > frame + 2) return bounds[i];
        return this.frameCount - 1;
    }

    /** Feature series decimated for plotting in the debug scope. */
    series(name, points = 600) {
        const src = this.raw[name] || this.tracks[name];
        if (!src) return null;
        const out = new Float32Array(points);
        const step = this.frameCount / points;
        for (let i = 0; i < points; i++) {
            const start = Math.floor(i * step);
            const end = Math.min(this.frameCount, Math.floor((i + 1) * step));
            let peak = 0;
            for (let f = start; f < end; f++) if (src[f] > peak) peak = src[f];
            out[i] = peak;
        }
        return out;
    }

    /**
     * Build the whole table from decoded audio. Synchronous and CPU-bound;
     * callers should run it off the main thread or accept a short freeze.
     */
    static fromAudioBuffer(audioBuffer, { fps = 60, onProgress = null } = {}) {
        const report = (stage, p) => onProgress && onProgress(stage, p);

        report('spectrum', 0);
        const analysis = analyzeBuffer(audioBuffer, {
            fps,
            onProgress: (p) => report('spectrum', p * 0.7),
        });

        report('tempo', 0.7);
        const tempo = detectTempo(analysis.onsetEnvelope, fps);
        const beatTracks = buildBeatTracks(tempo, analysis.frameCount, fps, analysis.raw.loudness);

        report('structure', 0.85);
        const sections = segment(analysis.raw, analysis.frameCount, fps, tempo);

        report('lookahead', 0.95);
        const tracks = {
            ...beatTracks,
            ...buildSectionTracks(sections, analysis.frameCount, fps, tempo),
        };

        report('done', 1);

        return new FeatureTrack({
            frameCount: analysis.frameCount,
            fps,
            duration: analysis.duration,
            sampleRate: analysis.sampleRate,
            raw: analysis.raw,
            summary: { ...analysis.summary, bpm: tempo.bpm, tempoConfidence: tempo.confidence },
            normalization: analysis.normalization,
            onsetEnvelope: analysis.onsetEnvelope,
            tempo,
            sections,
            tracks,
        });
    }
}

/**
 * Per-frame section tracks, including the lookahead fields.
 *
 * `buildSlope` is the anticipation signal and the main payoff of analysing
 * offline: it rises through the bars leading into a HIGHER-energy section, so a
 * build can ramp its visuals into the drop instead of reacting once the drop has
 * already landed. A causal analyser cannot produce this at all.
 */
function buildSectionTracks(sections, frameCount, fps, tempo) {
    const sectionIndex = new Int32Array(frameCount);
    const sectionProgress = new Float32Array(frameCount);
    const sectionEnergy = new Float32Array(frameCount);
    const buildSlope = new Float32Array(frameCount);
    const timeToNextSection = new Float32Array(frameCount);

    const barSeconds = (tempo.period * tempo.beatsPerBar) / fps;
    const maxEnergy = Math.max(1e-6, ...sections.map((s) => s.energy));

    for (let si = 0; si < sections.length; si++) {
        const s = sections[si];
        const next = sections[si + 1] || null;
        const n = Math.max(1, s.endFrame - s.startFrame);

        // Anticipation window: eight bars, or a third of the section if shorter.
        const windowSeconds = Math.min(barSeconds * 8, s.duration / 3);
        const windowFrames = Math.max(1, Math.round(windowSeconds * fps));

        const rises = next ? next.energy > s.energy * 1.08 : false;
        const rise = next ? Math.min(1, (next.energy - s.energy) / Math.max(1e-6, maxEnergy * 0.5)) : 0;

        for (let f = s.startFrame; f < s.endFrame && f < frameCount; f++) {
            sectionIndex[f] = si;
            sectionProgress[f] = (f - s.startFrame) / n;
            sectionEnergy[f] = s.energy / maxEnergy;
            const toNext = (s.endFrame - f) / fps;
            timeToNextSection[f] = toNext;

            if (rises && s.endFrame - f <= windowFrames) {
                const t = 1 - (s.endFrame - f) / windowFrames;
                buildSlope[f] = t * t * rise;      // eased, so the ramp starts gently
            }
        }
    }

    // Frames past the last section boundary (rounding slack at the tail).
    for (let f = 0; f < frameCount; f++) {
        if (sectionIndex[f] === 0 && sections.length && f >= sections[0].endFrame) {
            sectionIndex[f] = sections.length - 1;
        }
    }

    return { sectionIndex, sectionProgress, sectionEnergy, buildSlope, timeToNextSection };
}

/** Feature provider interface the Engine expects. */
export function featureProviderFor(featureTrack) {
    return {
        at: (frame) => featureTrack.at(frame),
        frameCount: featureTrack.frameCount,
    };
}
