/**
 * FlacFormatDecoder - FLAC implementation of IFormatDecoder.
 *
 * FLAC differs from WAV (raw PCM) and MP3 (self-contained frames): no audio frame
 * is decodable without the STREAMINFO metadata block that opens the stream. So this
 * decoder captures STREAMINFO during header parsing and wrapSegment prepends a minimal
 * valid FLAC header ("fLaC" + STREAMINFO) to every raw audio segment, making each segment
 * independently decodable by the browser's decodeAudioData.
 *
 * Seeking uses the optional SEEKTABLE metadata block when present; absent a table, seek
 * degrades to the start of audio (restart), which is correct behavior with no seek points.
 */

import { FlacSeekData, FormatInfo, IFormatDecoder } from './IFormatDecoder.js';

const FLAC_MAGIC = [0x66, 0x4c, 0x61, 0x43]; // "fLaC"
const STREAMINFO_DATA_LEN = 34;
const SEEK_POINT_SIZE = 18;
const PLACEHOLDER_HI = 0xffffffff; // sample_number placeholder = 0xFFFFFFFFFFFFFFFF
const TWO_POW_32 = 4294967296;

export class FlacFormatDecoder implements IFormatDecoder {
    tryParseHeader(chunks: Uint8Array[], totalSize: number): FormatInfo | null {
        const buf = concat(chunks, totalSize);

        // Need at least the magic to decide anything.
        if (buf.length < 4) return null;
        if (buf[0] !== FLAC_MAGIC[0] || buf[1] !== FLAC_MAGIC[1] ||
            buf[2] !== FLAC_MAGIC[2] || buf[3] !== FLAC_MAGIC[3]) {
            return null; // silently; StreamDecoder will error when MAX_HEADER_SEARCH_BYTES is exhausted
        }

        let sampleRate = 0;
        let channels = 0;
        let bitsPerSample = 0;
        let totalSamples = 0;
        let streamInfoBytes: Uint8Array | null = null;
        let seekPoints: FlacSeekData['points'] = [];

        // Scan metadata blocks starting after the 4-byte magic.
        let offset = 4;
        while (true) {
            // Each block opens with a 4-byte header.
            if (offset + 4 > buf.length) return null;

            const isLast = (buf[offset] & 0x80) !== 0;
            const blockType = buf[offset] & 0x7f;
            const dataLen = (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3];
            const dataStart = offset + 4;

            // Need the full block data before we can advance.
            if (dataStart + dataLen > buf.length) return null;

            if (blockType === 0) {
                // STREAMINFO (mandatory first block). data offsets are relative to dataStart.
                const d = dataStart;
                sampleRate = (buf[d + 10] << 12) | (buf[d + 11] << 4) | (buf[d + 12] >> 4);
                channels = ((buf[d + 12] >> 1) & 0x07) + 1;
                bitsPerSample = (((buf[d + 12] & 0x01) << 4) | (buf[d + 13] >> 4)) + 1;
                totalSamples = ((buf[d + 13] & 0x0f) * TWO_POW_32) + readUint32BE(buf, d + 14);

                // Build the 38-byte synthetic block: header (is_last=1, type=0, len=34) + 34 data bytes.
                streamInfoBytes = new Uint8Array(4 + STREAMINFO_DATA_LEN);
                streamInfoBytes[0] = 0x80; // is_last=1, block_type=0
                streamInfoBytes[1] = 0x00;
                streamInfoBytes[2] = 0x00;
                streamInfoBytes[3] = STREAMINFO_DATA_LEN; // 0x22
                streamInfoBytes.set(buf.subarray(d, d + STREAMINFO_DATA_LEN), 4);
            } else if (blockType === 3) {
                // SEEKTABLE (optional). Each point is 18 bytes.
                const count = Math.floor(dataLen / SEEK_POINT_SIZE);
                const points: FlacSeekData['points'] = [];
                for (let i = 0; i < count; i++) {
                    const p = dataStart + i * SEEK_POINT_SIZE;
                    const sampleHi = readUint32BE(buf, p);
                    const sampleLo = readUint32BE(buf, p + 4);
                    // Placeholder points (sample_number = all 1s) carry no offset — skip.
                    if (sampleHi === PLACEHOLDER_HI && sampleLo === PLACEHOLDER_HI) continue;

                    // sample_number: imprecise beyond 2^53 (~8h at 44100Hz); acceptable for seek nearest.
                    const sampleNumber = sampleHi * TWO_POW_32 + sampleLo;
                    // stream_offset: bytes from start of audio frames; safe to 2^53 for sub-petabyte files.
                    const offsetHi = readUint32BE(buf, p + 8);
                    const offsetLo = readUint32BE(buf, p + 12);
                    const streamOffset = offsetHi * TWO_POW_32 + offsetLo;
                    points.push({ sampleNumber, streamOffset });
                }
                seekPoints = points;
            }

            offset = dataStart + dataLen;
            if (isLast) break;
        }

        if (!streamInfoBytes) {
            console.warn('FlacFormatDecoder: no STREAMINFO block found');
            return null;
        }

        const audioDataOffset = offset; // 4 (magic) + sum of all block header+data sizes
        const totalDuration = sampleRate > 0 && totalSamples > 0
            ? totalSamples / sampleRate : null;

        return {
            sampleRate,
            channels,
            bitsPerSample,
            byteRate: 0,   // FLAC is VBR; seeking uses SEEKTABLE or degrades gracefully.
            blockAlign: 0, // Variable-size FLAC frames; no fixed alignment.
            totalDuration,
            audioDataOffset,
            seekData: {
                kind: 'flac-seektable',
                points: seekPoints,
                streamInfoBytes,
                metadataBlocksSize: audioDataOffset - 4 // metadata bytes, excluding fLaC magic
            }
        };
    }

    getAlignedSegmentSize(
        info: FormatInfo,
        availableBytes: number,
        requestedSize: number,
        streamComplete: boolean,
        rawData?: Uint8Array
    ): number {
        if (availableBytes === 0) return 0;
        const candidate = Math.min(requestedSize, availableBytes);

        if (!rawData || rawData.length === 0) {
            // No scan data — conservative threshold to avoid tiny unusable segments
            if (!streamComplete && availableBytes < 16384) return 0;
            return candidate;
        }

        // Scan backward from the candidate boundary to find the last FLAC frame sync code.
        const boundary = FlacFormatDecoder.findLastFlacFrame(rawData, candidate);
        if (boundary <= 0) {
            if (streamComplete) return candidate; // flush remaining bytes (stream done)
            return 0;                             // wait for more data
        }
        return boundary;
    }

    /**
     * Scan backward from `maxBytes` in `rawData` to find the start of the last valid FLAC
     * audio frame. FLAC frame sync: 0xFF followed by a byte where top 7 bits are 0xF8
     * (i.e. (byte & 0xFE) === 0xF8 — covers both blocking-strategy variants 0xF8 and 0xF9).
     * Returns the byte offset of that sync, or 0 if none is found (causes caller to wait).
     */
    private static findLastFlacFrame(rawData: Uint8Array, maxBytes: number): number {
        const limit = Math.min(maxBytes, rawData.length);
        // Need at least 2 bytes to verify sync pair; skip the very last byte.
        for (let i = limit - 2; i > 0; i--) {
            if (rawData[i] === 0xFF && (rawData[i + 1] & 0xFE) === 0xF8) {
                return i;
            }
        }
        return 0;
    }

    wrapSegment(info: FormatInfo, rawBytes: Uint8Array): Uint8Array {
        const flacData = info.seekData as FlacSeekData | null | undefined;
        const streamInfoBytes = flacData?.streamInfoBytes;
        if (!streamInfoBytes) {
            // Defensive: without STREAMINFO the segment isn't decodable. This path shouldn't
            // occur in practice — tryParseHeader always populates streamInfoBytes on success.
            return rawBytes;
        }

        // Build: fLaC (4) + STREAMINFO block (38) + audio frames.
        const result = new Uint8Array(4 + streamInfoBytes.length + rawBytes.length);
        result[0] = FLAC_MAGIC[0];
        result[1] = FLAC_MAGIC[1];
        result[2] = FLAC_MAGIC[2];
        result[3] = FLAC_MAGIC[3];
        result.set(streamInfoBytes, 4);
        result.set(rawBytes, 4 + streamInfoBytes.length);
        return result;
    }

    calculateByteOffset(info: FormatInfo, positionSeconds: number): number {
        const flacData = info.seekData?.kind === 'flac-seektable'
            ? info.seekData as FlacSeekData : null;

        if (flacData?.points && flacData.points.length > 0 && info.sampleRate > 0) {
            // SEEKTABLE binary search for the nearest point at or before the target sample.
            const targetSample = positionSeconds * info.sampleRate;
            const points = flacData.points;

            let lo = 0, hi = points.length - 1, best = 0;
            while (lo <= hi) {
                const mid = (lo + hi) >> 1;
                if (points[mid].sampleNumber <= targetSample) {
                    best = mid;
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }
            // streamOffset is bytes from start of audio data; add audioDataOffset for file-absolute.
            return info.audioDataOffset + points[best].streamOffset;
        }

        // No SEEKTABLE: degrade to start of audio (seek restarts from beginning).
        return info.audioDataOffset;
    }
}

function concat(chunks: Uint8Array[], totalSize: number): Uint8Array {
    if (chunks.length === 1) return chunks[0];
    const out = new Uint8Array(totalSize);
    let pos = 0;
    for (const c of chunks) {
        out.set(c, pos);
        pos += c.length;
    }
    return out;
}

function readUint32BE(buf: Uint8Array, p: number): number {
    return ((buf[p] << 24) | (buf[p + 1] << 16) | (buf[p + 2] << 8) | buf[p + 3]) >>> 0;
}
