// SP2Map.js — Steel Panthers 2 Scenario → SPWW2 v3 Map Conversion
// Handles both SP2 file formats:
//
//   COMPRESSED   (header "STEEL2_SAVE_V100") — later SP2 patches, mandatory.
//                Already in sectioned SPWW2 v2.2 binary format.
//                Passed directly to SPCAMOMap.js convertV2toV3().
//
//   UNCOMPRESSED (no header, starts with scenario name text) — early SP2 1.0
//                Flat binary, sections concatenated at fixed offsets.
//                Map sections extracted and assembled into a synthetic
//                STEEL2_SAVE_V100 blob, then passed to convertV2toV3().
//
// Both paths require SPCAMOMap.js to be loaded first (provides convertV2toV3).

"use strict";

// ─── Constants ───────────────────────────────────────────────────────────────

const SP2_COMPRESSED_HEADER   = "STEEL2_SAVE_V100";
const SP2_COMPRESSED_HEADER_LEN = 16;

// Known SP2 uncompressed file sizes (two variants exist — 705376 and 705480).
// The 104-byte difference is in tail sections after S39; map offsets are identical.
const SP2_UNCOMP_FILE_SIZE  = 705376;   // early variant
const SP2_UNCOMP_FILE_SIZE2 = 705480;   // later variant

// Flat binary offsets for map sections in SP2 Uncompressed files.
// Confirmed by cross-referencing decompressed sections from the matching
// SP2 Compressed file and verified with arithmetic chain:
//   S28_start == S9_end + S10..S27_sizes  (== 573939)  ✓
//   S29_start == S28_start + 104000       (== 677939)  ✓
//   S38_start == S29_end   + S30..S37     (== 705144)  ✓
const SP2_FLAT_MAP_SECTIONS = [
    // [ sectionId, byteOffset, byteSize ]
    [  2,   88000,  16000 ],   // map tile layer 1
    [  3,  104000,  16000 ],   // map tile layer 2
    [  4,  120000,  16000 ],   // map tile layer 3
    [  5,  136000,  16000 ],   // map tile layer 4
    [  6,  152000,  16000 ],   // map tile layer 5
    [  7,  168000,  16000 ],   // map tile layer 6
    [  8,  184000,  96000 ],   // altitude / terrain detail (12 bytes/cell × 8000 cells)
    [ 28,  573939, 104000 ],   // secondary tile layer
    [ 29,  677939,   8000 ],   // terrain flags
    [ 38,  705144,      4 ],   // MapSizeX = 100
    [ 39,  705148,      4 ],   // MapSizeY = 80
];

// Valid range covers both known variants plus a small tolerance for edge cases
const SP2_UNCOMP_MIN = SP2_UNCOMP_FILE_SIZE  - 8192;   // ~697 KB
const SP2_UNCOMP_MAX = SP2_UNCOMP_FILE_SIZE2 + 8192;   // ~713 KB

// ─── Helpers ─────────────────────────────────────────────────────────────────

/**
 * Read the 16-byte ASCII header from an ArrayBuffer.
 */
function _readHeader(arrayBuffer) {
    return String.fromCharCode(
        ...new Uint8Array(arrayBuffer, 0, Math.min(16, arrayBuffer.byteLength))
    );
}

/**
 * Return true if the byte at position 0 looks like printable ASCII
 * (i.e. a scenario name string, as in uncompressed files).
 */
function _looksLikeText(arrayBuffer) {
    const b = new Uint8Array(arrayBuffer, 0, 1)[0];
    return b >= 0x20 && b <= 0x7e;
}

// ─── SP2 Compressed ──────────────────────────────────────────────────────────

/**
 * SP2 Compressed files are already STEEL2_SAVE_V100 — identical input format
 * to what SPCAMOMap.js expects.  Just validate and delegate.
 */
async function _convertSP2Compressed(arrayBuffer) {
    const hdr = _readHeader(arrayBuffer);
    if (hdr !== SP2_COMPRESSED_HEADER) {
        throw new Error(
            `Not a SP2 Compressed file. ` +
            `Expected "${SP2_COMPRESSED_HEADER}", got "${hdr.replace(/\0/g, "\\0")}".`
        );
    }
    // Direct pass-through — same pipeline as the CONVERT→WW2 2.x Map menu action
    return await convertV2toV3(arrayBuffer);
}

// ─── SP2 Uncompressed ────────────────────────────────────────────────────────

/**
 * RLE-compress a byte array using the SPWW2 RLE scheme:
 *   byte > 0x80  → (byte - 0x80) repetitions of the next byte
 *   byte ≤ 0x80  → copy next (byte) literal bytes
 * Returns a Uint8Array.
 */
function _rleCompress(data) {
    const out = [];
    let i = 0;
    while (i < data.length) {
        // Try run-length (repeated byte)
        let runLen = 1;
        while (runLen < 127 && i + runLen < data.length && data[i + runLen] === data[i]) {
            runLen++;
        }
        if (runLen >= 3) {
            out.push(0x80 + runLen, data[i]);
            i += runLen;
            continue;
        }
        // Literal run — collect bytes until a run of ≥3 appears or limit reached
        let litStart = i;
        let litLen = 0;
        while (litLen < 0x80 && i < data.length) {
            // Peek ahead: would a run start here?
            let ahead = 1;
            while (ahead < 3 && i + ahead < data.length && data[i + ahead] === data[i]) {
                ahead++;
            }
            if (ahead >= 3 && litLen > 0) break;  // flush literals before run
            litLen++;
            i++;
        }
        out.push(litLen);
        for (let j = litStart; j < litStart + litLen; j++) out.push(data[j]);
    }
    return new Uint8Array(out);
}

/**
 * Write one SPWW2 section into an output byte array:
 *   4 bytes  section ID     (little-endian int32)
 *   4 bytes  byte count     (COMPRESSED payload size — this is what the reader
 *                            uses to know how many bytes to consume from the file)
 *   1 byte   compressed flag (0 = raw, 1 = RLE)
 *   N bytes  payload
 *
 * We always try to compress; if the result is larger we write raw instead.
 */
function _writeSection(sectId, data) {
    const compressed = _rleCompress(data);
    const useComp    = compressed.length < data.length;
    const payload    = useComp ? compressed : data;

    const buf = new Uint8Array(9 + payload.length);
    const dv  = new DataView(buf.buffer);
    dv.setInt32(0, sectId,          true);  // section ID
    dv.setInt32(4, payload.length,  true);  // compressed payload byte count (file-read size)
    buf[8] = useComp ? 1 : 0;              // compression flag
    buf.set(payload, 9);
    return buf;
}

/**
 * Build a minimal STEEL2_SAVE_V100 file containing only the map sections
 * extracted from a flat SP2 Uncompressed binary.
 *
 * The resulting blob can be consumed directly by SPCAMOMap.js convertV2toV3().
 * Non-map sections are absent; SPCAMOMap uses its own blank CLRWW2v3.DAT template
 * for everything else anyway.
 */
function _buildSteelFileFromFlat(flatBytes) {
    const parts = [];

    // 16-byte header + 4 null bytes (V1_1_START = 0x14)
    const headerBuf = new Uint8Array(20);
    const headerStr = SP2_COMPRESSED_HEADER;  // "STEEL2_SAVE_V100"
    for (let i = 0; i < headerStr.length; i++) headerBuf[i] = headerStr.charCodeAt(i);
    parts.push(headerBuf);

    for (const [sid, offset, size] of SP2_FLAT_MAP_SECTIONS) {
        if (offset + size > flatBytes.length) {
            throw new Error(
                `SP2 Uncompressed: section ${sid} extends past end of file ` +
                `(offset ${offset} + size ${size} = ${offset+size}, file is ${flatBytes.length} bytes).`
            );
        }
        const sectionData = flatBytes.slice(offset, offset + size);
        parts.push(_writeSection(sid, sectionData));
    }

    // Concatenate all parts into a single Uint8Array
    const totalLen = parts.reduce((s, p) => s + p.length, 0);
    const out = new Uint8Array(totalLen);
    let pos = 0;
    for (const p of parts) { out.set(p, pos); pos += p.length; }
    return out.buffer;
}

/**
 * Convert an SP2 Uncompressed scenario to SPWW2 v3.
 */
async function _convertSP2Uncompressed(arrayBuffer) {
    const size = arrayBuffer.byteLength;
    if (size < SP2_UNCOMP_MIN || size > SP2_UNCOMP_MAX) {
        throw new Error(
            `File size ${size.toLocaleString()} bytes is outside the expected range for ` +
            `SP2 Uncompressed scenarios ` +
            `(${SP2_UNCOMP_MIN.toLocaleString()}–${SP2_UNCOMP_MAX.toLocaleString()} bytes). ` +
            `Is this really an SP2 Uncompressed .dat?`
        );
    }

    const flatBytes = new Uint8Array(arrayBuffer);

    // Quick sanity check: first byte should be printable ASCII (scenario name)
    if (flatBytes[0] < 0x20 || flatBytes[0] > 0x7e) {
        throw new Error(
            `SP2 Uncompressed file does not start with a printable ASCII scenario name ` +
            `(got byte 0x${flatBytes[0].toString(16).padStart(2,'0')}).`
        );
    }

    // Build synthetic STEEL2_SAVE_V100 from the flat map sections
    const steelAB = _buildSteelFileFromFlat(flatBytes);

    // Convert via SPCAMOMap.js
    return await convertV2toV3(steelAB);
}

// ─── Public API ──────────────────────────────────────────────────────────────

/**
 * Detect the SP2 format and run the appropriate pipeline.
 *
 * Accepts either:
 *   • SP2 Compressed  (STEEL2_SAVE_V100 header)  → direct pass to convertV2toV3
 *   • SP2 Uncompressed (flat binary, ASCII start) → extract map → synthetic STEEL2 → convertV2toV3
 *
 * Returns a Promise<Blob> of the SPWW2 v3 output file.
 */
async function convertSP2ToV3(arrayBuffer) {
    if (arrayBuffer.byteLength < 16) {
        throw new Error("File is too small to be a valid SP2 scenario.");
    }

    const hdr = _readHeader(arrayBuffer);

    if (hdr === SP2_COMPRESSED_HEADER) {
        return await _convertSP2Compressed(arrayBuffer);
    }

    if (_looksLikeText(arrayBuffer)) {
        return await _convertSP2Uncompressed(arrayBuffer);
    }

    throw new Error(
        `Unrecognised SP2 file format. ` +
        `Expected either a STEEL2_SAVE_V100 compressed file or a plain-text-header uncompressed file. ` +
        `Header bytes: ${new Uint8Array(arrayBuffer, 0, 8).reduce((s,b)=>s+b.toString(16).padStart(2,'0'),' ')}`
    );
}

// Backwards-compatible alias for code that calls the old name
async function convertSP2CompressedToV3(arrayBuffer) {
    return await convertSP2ToV3(arrayBuffer);
}

// Node/test export
if (typeof module !== "undefined" && module.exports) {
    module.exports = {
        convertSP2ToV3,
        convertSP2CompressedToV3,
        _buildSteelFileFromFlat,
        _rleCompress,
        SP2_FLAT_MAP_SECTIONS,
    };
}
