/**
 * Spz4Transcoder - SPZ v4 (NGSP / ZSTD 六流) → 旧版 v3 (gzip 单流) 纯前端转码器
 *
 * 背景：
 *   SPZ v4 文件头为 32 字节明文（NGSP magic），数据部分改为 6 条独立的
 *   ZSTD 压缩流（位置/透明度/颜色/缩放/旋转/球谐），SparkJS 等基于旧版
 *   gzip 单流的加载器无法直接读取（会报 Unsupported SPZ version: 4）。
 *
 * 原理：
 *   v4 各属性的逐点编码与 v3 完全一致（24bit 定点坐标、8bit log 缩放、
 *   smallest-three 10bit 四元数、8bit alpha/颜色/球谐），因此无需逐点解码，
 *   只需把 6 条流 ZSTD 解压后按旧版顺序重新拼接，套上 16 字节 v3 头再
 *   gzip，即可被任何旧版 spz 加载器读取。
 *
 * 用法：
 *   import { probeSpzFormat, transcodeSpz4ToV3 } from "./Spz4Transcoder.js";
 *   const format = probeSpzFormat(headBytes);          // "v4" | "legacy" | "unknown"
 *   const v3Bytes = await transcodeSpz4ToV3(fileBytes); // gzip 后的旧版 .spz
 */

import { decompress as zstdDecompress } from "./js/fzstd.module.js";

export const SPZ_MAGIC = 0x5053474e; // "NGSP"
const GZIP_MAGIC_0 = 0x1f;
const GZIP_MAGIC_1 = 0x8b;

// v4 六条流的官方固定顺序（spz 源码 kAllSplatAttributes）
const V4_STREAM_ORDER = ["positions", "alphas", "colors", "scales", "rotations", "sh"];
// 旧版 gzip 单流内的属性拼接顺序
const LEGACY_PAYLOAD_ORDER = ["positions", "scales", "rotations", "alphas", "colors", "sh"];

/** 每个 SH 阶数对应的系数维度（每通道） */
const dimForDegree = (d) => [0, 3, 8, 15, 24][d] ?? 0;

/** v4 各属性每点字节数 */
function bytesPerPoint(attr, shDegree) {
    switch (attr) {
        case "positions": return 9;              // 3 × 24bit 定点
        case "alphas":    return 1;
        case "colors":    return 3;
        case "scales":    return 3;
        case "rotations": return 4;              // smallest-three 10bit
        case "sh":        return dimForDegree(shDegree) * 3;
        default:          return 0;
    }
}

/**
 * 探测 spz 文件格式（只需前 4 字节）
 * @param {Uint8Array} head - 文件头部字节（至少 4 字节）
 * @returns {"v4"|"legacy"|"unknown"}
 */
export function probeSpzFormat(head) {
    if (!head || head.length < 4) return "unknown";
    // "NGSP" 明文 magic → v4
    if (head[0] === 0x4e && head[1] === 0x47 && head[2] === 0x53 && head[3] === 0x50) {
        return "v4";
    }
    // gzip magic → 旧版 v1/v2/v3
    if (head[0] === GZIP_MAGIC_0 && head[1] === GZIP_MAGIC_1) {
        return "legacy";
    }
    return "unknown";
}

/**
 * 解析 v4 的 32 字节明文头
 * @param {Uint8Array} bytes - 至少包含前 32 字节的文件数据
 */
export function parseSpz4Header(bytes) {
    if (bytes.length < 32) throw new Error("[Spz4] 文件太小，不足 32 字节头");
    const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const magic = dv.getUint32(0, true);
    if (magic !== SPZ_MAGIC) throw new Error("[Spz4] magic 不匹配，不是 NGSP 文件");
    const header = {
        magic,
        version: dv.getUint32(4, true),
        numPoints: dv.getUint32(8, true),
        shDegree: dv.getUint8(12),
        fractionalBits: dv.getUint8(13),
        flags: dv.getUint8(14),
        numStreams: dv.getUint8(15),
        tocByteOffset: dv.getUint32(16, true),
    };
    header.antialiased = (header.flags & 0x1) !== 0;
    header.hasExtensions = (header.flags & 0x2) !== 0;
    if (header.version !== 4) throw new Error(`[Spz4] 不支持的版本: ${header.version}`);
    if (header.shDegree > 4) throw new Error(`[Spz4] 非法 SH 阶数: ${header.shDegree}`);
    return header;
}

/**
 * 把 v4 文件转码为旧版 v3（gzip 单流）字节
 * @param {Uint8Array} bytes - 完整 v4 文件内容
 * @param {Object} [options]
 * @param {Function} [options.onStage] - 阶段回调 (stage: string, ratio: 0~1)
 * @returns {Promise<Uint8Array>} gzip 压缩后的旧版 .spz 字节
 */
export async function transcodeSpz4ToV3(bytes, options = {}) {
    const { onStage = null } = options;
    const header = parseSpz4Header(bytes);
    const { numPoints, numStreams, tocByteOffset } = header;
    let { shDegree } = header;

    if (header.hasExtensions) {
        console.warn("[Spz4] 文件含厂商扩展（如相机边界元数据），转码后将丢弃");
    }
    console.log(
        `[Spz4] 头信息: ${numPoints} 点, SH${shDegree}, fractionalBits=${header.fractionalBits}, ` +
        `${numStreams} 条流, TOC@${tocByteOffset}`
    );

    // ── 1. 读 TOC：每条流 [compressedSize u64, uncompressedSize u64] ──
    const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const toc = [];
    for (let i = 0; i < numStreams; i++) {
        const off = tocByteOffset + i * 16;
        toc.push({
            compressed: Number(dv.getBigUint64(off, true)),
            uncompressed: Number(dv.getBigUint64(off + 8, true)),
        });
    }

    // ── 2. 逐条 ZSTD 解压 ──
    let streamStart = tocByteOffset + numStreams * 16;
    const decoded = {}; // attr -> Uint8Array
    for (let i = 0; i < numStreams; i++) {
        const attr = V4_STREAM_ORDER[i] ?? `stream${i}`;
        const { compressed, uncompressed } = toc[i];
        const src = bytes.subarray(streamStart, streamStart + compressed);
        streamStart += compressed;

        if (uncompressed === 0) {
            decoded[attr] = new Uint8Array(0);
        } else {
            decoded[attr] = zstdDecompress(src, new Uint8Array(uncompressed));
        }

        // 校验解压大小（仅对六个已知属性）
        if (i < 6) {
            const expected = numPoints * bytesPerPoint(attr, shDegree);
            if (decoded[attr].length !== expected) {
                throw new Error(
                    `[Spz4] ${attr} 流大小异常: 期望 ${expected}，实际 ${decoded[attr].length}`
                );
            }
        }
        if (onStage) onStage(`decompress:${attr}`, (i + 1) / (numStreams + 1));
    }

    // ── 3. SH4 → SH3 截断（旧版加载器最高只认 SH3） ──
    // v4 SH 布局为「系数在外、通道在内」，前 45 字节正好是 1~3 阶全部系数
    let outShDegree = shDegree;
    if (shDegree === 4) {
        const src = decoded.sh;
        const dst = new Uint8Array(numPoints * 15 * 3);
        for (let p = 0; p < numPoints; p++) {
            dst.set(src.subarray(p * 72, p * 72 + 45), p * 45);
        }
        decoded.sh = dst;
        outShDegree = 3;
        console.warn("[Spz4] SH4 已截断为 SH3（旧版加载器上限），视角相关光照细节略有损失");
    }

    // ── 4. 拼旧版 v3：16 字节头 + 按旧顺序拼接各属性 ──
    const parts = LEGACY_PAYLOAD_ORDER.map((attr) => decoded[attr] ?? new Uint8Array(0));
    const payloadSize = parts.reduce((n, p) => n + p.length, 0);
    const out = new Uint8Array(16 + payloadSize);
    const odv = new DataView(out.buffer);
    odv.setUint32(0, SPZ_MAGIC, true);      // magic
    odv.setUint32(4, 3, true);              // version = 3
    odv.setUint32(8, numPoints, true);      // numPoints
    odv.setUint8(12, outShDegree);          // shDegree
    odv.setUint8(13, header.fractionalBits);// fractionalBits
    odv.setUint8(14, header.flags & 0x1);   // flags（只保留 antialiased 位）
    odv.setUint8(15, 0);                    // reserved
    let offset = 16;
    for (const part of parts) {
        out.set(part, offset);
        offset += part.length;
    }
    if (onStage) onStage("assemble", 0.95);

    // ── 5. gzip 压缩 ──
    const gz = await gzipCompress(out);
    console.log(
        `[Spz4] 转码完成: v4 ${(bytes.length / 1024 / 1024).toFixed(2)}MB → ` +
        `v3 ${(gz.length / 1024 / 1024).toFixed(2)}MB`
    );
    if (onStage) onStage("gzip", 1);
    return gz;
}

/** 用浏览器原生 CompressionStream 做 gzip */
async function gzipCompress(bytes) {
    if (typeof CompressionStream === "undefined") {
        throw new Error("[Spz4] 当前浏览器不支持 CompressionStream，无法 gzip 压缩");
    }
    const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
    return new Uint8Array(await new Response(stream).arrayBuffer());
}

export default { probeSpzFormat, parseSpz4Header, transcodeSpz4ToV3 };
