// SPZip.js — Steel Panthers ZIP Pack Handler
// Opens a ZIP file, finds all DAT scenario files, and reads their associated
// CMT (comment) files to provide human-readable scenario titles.
//
// CMT format: 200-byte null-padded fixed-length record.
//             The scenario title is a null-terminated ASCII string at offset 0.
//             Example: "The Capture of Balta I 8/41\0\0\0..."
//
// Matching rule: SCEN042.DAT pairs with SCEN042.CMT (case-insensitive stem match).
// DATs without a matching CMT are still included, with title null.
//
// Requires: fflate (https://cdn.jsdelivr.net/npm/fflate/umd/index.js)
//           Loaded by the host HTML before this script.

"use strict";

// ─── CMT Parsing ─────────────────────────────────────────────────────────────

/**
 * Extract the null-terminated title string from a 200-byte CMT file.
 * Returns a trimmed string, or null if the data is empty/invalid.
 */
function _parseCmt(uint8) {
    // Find null terminator
    let end = 0;
    while (end < uint8.length && uint8[end] !== 0) end++;
    if (end === 0) return null;
    return new TextDecoder("windows-1252").decode(uint8.subarray(0, end)).trim() || null;
}

// ─── ZIP Scanning ─────────────────────────────────────────────────────────────

/**
 * Scan a ZIP ArrayBuffer for SP scenario files.
 *
 * Returns a Promise that resolves to an array of scenario descriptor objects,
 * sorted by DAT filename (case-insensitive), one per DAT found:
 *
 *   {
 *     filename:  "Scen042.DAT",          // original filename inside ZIP
 *     stem:      "Scen042",              // filename without extension
 *     title:     "The Capture of Balta I 8/41",  // from CMT, or null
 *     label:     "Scen042 — The Capture of Balta I 8/41",  // display string
 *     datBytes:  Uint8Array,             // raw DAT file bytes
 *     size:      510224,                 // DAT file size in bytes
 *   }
 *
 * Throws if the file is not a valid ZIP or contains no DAT files.
 */
async function scanZip(arrayBuffer) {
    // fflate.unzipSync returns { filename: Uint8Array, ... }
    let files;
    try {
        files = fflate.unzipSync(new Uint8Array(arrayBuffer));
    } catch (e) {
        throw new Error(`Failed to open ZIP: ${e.message}`);
    }

    // Index all entries by uppercase name for case-insensitive lookup
    const byUpperName = {};
    for (const name of Object.keys(files)) {
        byUpperName[name.toUpperCase()] = name;
    }

    // Collect all DAT entries
    const datNames = Object.keys(files).filter(n => n.toUpperCase().endsWith('.DAT'));
    if (datNames.length === 0) {
        throw new Error("No .DAT files found inside this ZIP.");
    }

    // Sort DAT names case-insensitively
    datNames.sort((a, b) => a.toUpperCase().localeCompare(b.toUpperCase()));

    const scenarios = [];

    for (const datName of datNames) {
        // Derive the stem (everything before the last dot)
        const stem = datName.replace(/\.[^.]+$/, "");

        // Collect companion files (.CMT and .TXT) if present
        const cmtKey = (stem + ".CMT").toUpperCase();
        const txtKey = (stem + ".TXT").toUpperCase();
        const cmtBytes = byUpperName[cmtKey] ? files[byUpperName[cmtKey]] : null;
        const txtBytes = byUpperName[txtKey] ? files[byUpperName[txtKey]] : null;

        // Preserve original companion filenames (casing from ZIP)
        const cmtFilename = byUpperName[cmtKey] ? byUpperName[cmtKey].split("/").pop() : null;
        const txtFilename = byUpperName[txtKey] ? byUpperName[txtKey].split("/").pop() : null;

        // Parse title from CMT bytes
        const title = cmtBytes ? _parseCmt(cmtBytes) : null;
        const label = title ? `${stem} \u2014 ${title}` : stem;

        scenarios.push({
            filename: datName,
            stem,
            title,
            label,
            datBytes:    files[datName],
            size:        files[datName].length,
            cmtBytes,
            cmtFilename,
            txtBytes,
            txtFilename,
        });
    }

    return scenarios;
}

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

/**
 * Open a ZIP file from a File object (e.g. from a file input or drop event).
 * Returns a Promise<scenario[]> — see scanZip() for the object shape.
 */
function openZipFile(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload  = ev => scanZip(ev.target.result).then(resolve).catch(reject);
        reader.onerror = ()  => reject(new Error("Failed to read ZIP file."));
        reader.readAsArrayBuffer(file);
    });
}

// Node/test export
if (typeof module !== "undefined" && module.exports) {
    module.exports = { openZipFile, scanZip, _parseCmt };
}
