"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MapParser = void 0;
const _7zip_bin_1 = __importDefault(require("7zip-bin"));
const fs_1 = require("fs");
const glob_1 = require("glob");
const jimp_1 = __importDefault(require("jimp"));
const node_7z_1 = require("node-7z");
const StreamZip = __importStar(require("node-stream-zip"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const buffer_stream_1 = require("./buffer-stream");
const dxt = require("dxt-js");
const mapParserDefaultConfig = {
    verbose: false,
    mipmapSize: 4,
    skipSmt: false
};
class MapParser {
    constructor(config) {
        this.config = Object.assign({}, mapParserDefaultConfig, config);
    }
    async parseMap(mapFilePath) {
        const filePath = path.parse(mapFilePath);
        const fileName = filePath.name;
        const fileExt = filePath.ext;
        const tempDir = path.join(os.tmpdir(), fileName);
        const sigintBinding = process.on("SIGINT", async () => this.sigint(tempDir));
        try {
            if (fileExt !== ".sd7" && fileExt !== ".sdz") {
                throw new Error(`${fileExt} extension is not supported, .sd7 and .sdz only.`);
            }
            const archive = fileExt === ".sd7" ? await this.extractSd7(mapFilePath, tempDir) : await this.extractSdz(mapFilePath, tempDir);
            let info;
            if (archive.mapInfo) {
                info = await this.parseMapInfo(archive.mapInfo);
            }
            else {
                info = await this.parseSMD(archive.smd);
            }
            const smf = await this.parseSMF(archive.smf);
            let smt;
            if (!this.config.skipSmt) {
                smt = await this.parseSMT(archive.smt, smf.tileIndexMap, smf.mapWidthUnits, smf.mapHeightUnits, this.config.mipmapSize);
            }
            let scriptName = "";
            if (info.name && info.version && info.name.includes(info.version)) {
                scriptName = info.name;
            }
            else if (info.name) {
                scriptName = `${info.name} ${info.version}`;
            }
            else if (archive.smfName) {
                scriptName = archive.smfName;
            }
            this.cleanup(tempDir);
            return {
                fileName,
                scriptName,
                info,
                meta: smf,
                heightMap: smf.heightMap,
                metalMap: smf.metalMap,
                miniMap: smf.miniMap,
                typeMap: smf.typeMap,
                textureMap: smt
            };
        }
        catch (err) {
            this.cleanup(tempDir);
            sigintBinding.removeAllListeners();
            throw err;
        }
    }
    async extractSd7(sd7Path, outPath) {
        return new Promise(async (resolve) => {
            if (this.config.verbose) {
                console.log(`Extracting .sd7 to ${outPath}`);
            }
            if (!fs_1.existsSync(sd7Path)) {
                throw new Error(`File not found: ${sd7Path}`);
            }
            await fs_1.promises.mkdir(outPath, { recursive: true });
            const extractStream = node_7z_1.extractFull(sd7Path, outPath, {
                $bin: _7zip_bin_1.default.path7za,
                recursive: true,
                $cherryPick: ["*.smf", "*.smd", "*.smt", "mapinfo.lua"]
            });
            extractStream.on("end", async () => {
                const smfPath = glob_1.glob.sync(`${outPath}/**/*.smf`)[0];
                const smtPath = glob_1.glob.sync(`${outPath}/**/*.smt`)[0];
                const smdPath = glob_1.glob.sync(`${outPath}/**/*.smd`)[0];
                const mapInfoPath = glob_1.glob.sync(`${outPath}/mapinfo.lua`)[0];
                const smf = await fs_1.promises.readFile(smfPath);
                const smfName = smfPath ? path.parse(smfPath).name : undefined;
                const smt = await fs_1.promises.readFile(smtPath);
                const smd = smdPath ? await fs_1.promises.readFile(smdPath) : undefined;
                const mapInfo = mapInfoPath ? await fs_1.promises.readFile(mapInfoPath) : undefined;
                resolve({ smf, smt, smd, smfName, mapInfo });
            });
        });
    }
    extractSdz(sdzPath, outPath) {
        return new Promise(async (resolve) => {
            if (this.config.verbose) {
                console.log(`Extracting .sdz to ${outPath}`);
            }
            if (!fs_1.existsSync(sdzPath)) {
                throw new Error(`File not found: ${sdzPath}`);
            }
            await fs_1.promises.mkdir(outPath, { recursive: true });
            const zip = new StreamZip.async({ file: sdzPath });
            await zip.extract("maps/", outPath);
            await zip.close();
            const smfPath = glob_1.glob.sync(`${outPath}/**/*.smf`)[0];
            const smtPath = glob_1.glob.sync(`${outPath}/**/*.smt`)[0];
            const smdPath = glob_1.glob.sync(`${outPath}/**/*.smd`)[0];
            const mapInfoPath = glob_1.glob.sync(`${outPath}/mapinfo.lua`)[0];
            const smf = await fs_1.promises.readFile(smfPath);
            const smfName = smfPath ? path.parse(smfPath).name : undefined;
            const smt = await fs_1.promises.readFile(smtPath);
            const smd = smdPath ? await fs_1.promises.readFile(smdPath) : undefined;
            const mapInfo = mapInfoPath ? await fs_1.promises.readFile(mapInfoPath) : undefined;
            resolve({ smf, smt, smd, smfName, mapInfo });
        });
    }
    async parseSMF(smfBuffer) {
        if (this.config.verbose) {
            console.log("Parsing .smf");
        }
        const bufferStream = new buffer_stream_1.BufferStream(smfBuffer);
        const magic = bufferStream.readString(16);
        const version = bufferStream.readInt();
        const id = bufferStream.readInt(4, true);
        const mapWidth = bufferStream.readInt();
        const mapHeight = bufferStream.readInt();
        const mapWidthUnits = mapWidth / 128;
        const mapHeightUnits = mapHeight / 128;
        const squareSize = bufferStream.readInt();
        const texelsPerSquare = bufferStream.readInt();
        const tileSize = bufferStream.readInt();
        const minDepth = bufferStream.readFloat();
        const maxDepth = bufferStream.readFloat();
        const heightMapIndex = bufferStream.readInt();
        const typeMapIndex = bufferStream.readInt();
        const tileIndexMapIndex = bufferStream.readInt();
        const miniMapIndex = bufferStream.readInt();
        const metalMapIndex = bufferStream.readInt();
        const featureMapIndex = bufferStream.readInt();
        const noOfExtraHeaders = bufferStream.readInt();
        const extraHeaders = bufferStream.read(heightMapIndex - bufferStream.getPosition());
        // TODO
        // for (let i=0; i<noOfExtraHeaders; i++){
        //     const extraHeaderSize = bufferStream.readInt();
        //     const extraHeaderType = bufferStream.readInt();
        //     if (extraHeaderType === 1) { // grass
        //         const extraOffset = bufferStream.readInt();
        //         const grassMapLength = (widthPixels / 4) * (heightPixels / 4);
        //         const grassMap = bufferStream.read(grassMapLength);
        //     }
        // }
        bufferStream.destroy();
        const heightMapSize = (mapWidth + 1) * (mapHeight + 1);
        const heightMapBuffer = smfBuffer.slice(heightMapIndex, heightMapIndex + heightMapSize * 2);
        const heightMapValues = new buffer_stream_1.BufferStream(heightMapBuffer).readInts(heightMapSize, 2, true);
        const heightMapColors = heightMapValues.map(val => {
            const level = (val / 65536) * 255;
            return [level, level, level, 255];
        });
        const heightMap = new jimp_1.default({
            data: Buffer.from(heightMapColors.flat()),
            width: mapWidth + 1,
            height: mapHeight + 1
        });
        const typeMapSize = (mapWidth / 2) * (mapHeight / 2);
        const typeMapBuffer = smfBuffer.slice(typeMapIndex, typeMapIndex + typeMapSize);
        const typeMap = new jimp_1.default({
            data: singleChannelToQuadChannel(typeMapBuffer),
            width: mapWidth / 2,
            height: mapHeight / 2
        });
        const miniMapSize = 699048;
        const miniMapBuffer = smfBuffer.slice(miniMapIndex, miniMapIndex + miniMapSize);
        const miniMapRgbas = dxt.decompress(miniMapBuffer, 1024, 1024, dxt.flags.DXT1);
        const miniMapRgbaBuffer = Buffer.from(miniMapRgbas);
        const miniMap = new jimp_1.default({
            data: miniMapRgbaBuffer,
            width: 1024,
            height: 1024
        });
        const metalMapSize = (mapWidth / 2) * (mapHeight / 2);
        const metalMapBuffer = smfBuffer.slice(metalMapIndex, metalMapIndex + metalMapSize);
        const metalMap = new jimp_1.default({
            data: singleChannelToQuadChannel(metalMapBuffer),
            width: mapWidth / 2,
            height: mapHeight / 2
        });
        const tileIndexMapBufferStream = new buffer_stream_1.BufferStream(smfBuffer.slice(tileIndexMapIndex));
        const numOfTileFiles = tileIndexMapBufferStream.readInt();
        const numOfTilesInAllFiles = tileIndexMapBufferStream.readInt();
        const numOfTilesInThisFile = tileIndexMapBufferStream.readInt();
        const smtFileName = tileIndexMapBufferStream.readUntilNull().toString();
        const tileIndexMapSize = (mapWidth / 4) * (mapHeight / 4);
        const tileIndexMap = tileIndexMapBufferStream.readInts(tileIndexMapSize);
        tileIndexMapBufferStream.destroy();
        // TODO
        // const featuresBuffer = buffer.slice(featureMapIndex + 8);
        // const features: string[] = featuresBuffer.toString().split("\u0000").filter(Boolean);
        return {
            magic, version, id, mapWidth, mapWidthUnits, mapHeight, mapHeightUnits, squareSize, texelsPerSquare, tileSize, minDepth, maxDepth,
            heightMapIndex, typeMapIndex, tileIndexMapIndex, miniMapIndex, metalMapIndex, featureMapIndex, noOfExtraHeaders, extraHeaders: [],
            numOfTileFiles, numOfTilesInAllFiles, numOfTilesInThisFile, smtFileName,
            heightMap, typeMap, miniMap, metalMap, tileIndexMap,
            features: [] // TODO
        };
    }
    async parseSMT(smtBuffer, tileIndexes, mapWidthUnits, mapHeightUnits, mipmapSize) {
        if (this.config.verbose) {
            console.log(`Parsing .smt at mipmap size ${mipmapSize}`);
        }
        const bufferStream = new buffer_stream_1.BufferStream(smtBuffer);
        const magic = bufferStream.readString(16);
        const version = bufferStream.readInt();
        const numOfTiles = bufferStream.readInt();
        const tileSize = bufferStream.readInt();
        const compressionType = bufferStream.readInt();
        const startIndex = mipmapSize === 32 ? 0 : mipmapSize === 16 ? 512 : mipmapSize === 8 ? 640 : 672;
        const dxt1Size = Math.pow(mipmapSize, 2) / 2;
        const rowLength = mipmapSize * 4;
        const refTiles = [];
        for (let i = 0; i < numOfTiles; i++) {
            const dxt1 = bufferStream.read(680).slice(startIndex, startIndex + dxt1Size);
            const refTileRGBA = dxt.decompress(dxt1, mipmapSize, mipmapSize, dxt.flags.DXT1);
            const refTileRGBABuffer = Buffer.from(refTileRGBA);
            const refTile = [];
            for (let k = 0; k < mipmapSize; k++) {
                const pixelIndex = k * rowLength;
                const refTileRow = refTileRGBABuffer.slice(pixelIndex, pixelIndex + rowLength);
                refTile.push(refTileRow);
            }
            refTiles.push(refTile);
        }
        const tiles = [];
        for (let i = 0; i < tileIndexes.length; i++) {
            const refTileIndex = tileIndexes[i];
            const tile = this.cloneTile(refTiles[refTileIndex]);
            tiles.push(tile);
        }
        const tileStrips = [];
        for (let y = 0; y < mapHeightUnits * 32; y++) {
            const tileStrip = [];
            for (let x = 0; x < mapWidthUnits * 32; x++) {
                const tile = tiles.shift();
                tileStrip.push(tile);
            }
            const textureStrip = this.joinTilesHorizontally(tileStrip, mipmapSize);
            tileStrips.push(textureStrip);
        }
        return new jimp_1.default({
            data: Buffer.concat(tileStrips),
            width: mipmapSize * mapWidthUnits * 32,
            height: mipmapSize * mapHeightUnits * 32
        }).background(0x000000);
    }
    async parseMapInfo(buffer) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
        if (this.config.verbose) {
            console.log("Parsing mapinfo.lua");
        }
        const str = buffer.toString();
        // yes, all this regex is messy and expensive. no, i don't care
        const name = (_a = str.match(/(?!t).name\s*\=\s*\"(.*?)\"/i)) === null || _a === void 0 ? void 0 : _a[1];
        const shortname = (_b = str.match(/shortname\s*\=\s*\"(.*?)\"/i)) === null || _b === void 0 ? void 0 : _b[1];
        const description = (_c = str.match(/description\s*\=\s*\"(.*?)\"/i)) === null || _c === void 0 ? void 0 : _c[1];
        const author = (_d = str.match(/author\s*\=\s*\"(.*?)\"/i)) === null || _d === void 0 ? void 0 : _d[1];
        const version = (_e = str.match(/version\s*\=\s*\"(.*?)\"/i)) === null || _e === void 0 ? void 0 : _e[1];
        const mapfile = (_f = str.match(/mapfile\s*\=\s*\"(.*?)\"/i)) === null || _f === void 0 ? void 0 : _f[1];
        const modtype = Number((_g = str.match(/modtype\s*\=\s*(.*?)\,/i)) === null || _g === void 0 ? void 0 : _g[1]);
        const mapHardness = Number((_h = str.match(/maphardness\s*\=\s*(.*?)\,/i)) === null || _h === void 0 ? void 0 : _h[1]);
        const notDeformable = ((_j = str.match(/notDeformable\s*\=\s*(.*?)\,/i)) === null || _j === void 0 ? void 0 : _j[1]) === "true";
        const gravity = Number((_k = str.match(/gravity\s*\=\s*(.*?)\,/i)) === null || _k === void 0 ? void 0 : _k[1]);
        const tidalStrength = Number((_l = str.match(/tidalStrength\s*\=\s*(.*?)\,/i)) === null || _l === void 0 ? void 0 : _l[1]);
        const maxMetal = Number((_m = str.match(/maxMetal\s*\=\s*(.*?)\,/i)) === null || _m === void 0 ? void 0 : _m[1]);
        const extractorRadius = Number((_o = str.match(/extractorRadius\s*\=\s*(.*?)\,/i)) === null || _o === void 0 ? void 0 : _o[1]);
        const voidWater = ((_p = str.match(/voidWater\s*\=\s*(.*?)\,/i)) === null || _p === void 0 ? void 0 : _p[1]) === "true";
        const voidGround = ((_q = str.match(/voidGround\s*\=\s*(.*?)\,/i)) === null || _q === void 0 ? void 0 : _q[1]) === "true";
        const autoShowMetal = ((_r = str.match(/autoShowMetal\s*\=\s*(.*?)\,/i)) === null || _r === void 0 ? void 0 : _r[1]) === "true";
        const minWind = Number((_s = str.match(/minWind\s*\=\s*(.*?)\,/i)) === null || _s === void 0 ? void 0 : _s[1]);
        const maxWind = Number((_t = str.match(/maxWind\s*\=\s*(.*?)\,/i)) === null || _t === void 0 ? void 0 : _t[1]);
        const startPosMatches = str.matchAll(/startPos\s*\=\s*\{\s*x\s*\=\s*(?<x>\d*)\s*\,\s*z\s*\=\s*(?<z>\d*)/gm);
        const startPositions = [];
        let teamId = 0;
        for (const match of startPosMatches) {
            const { x, z } = match.groups;
            startPositions[teamId] = { x: Number(x), z: Number(z) };
            teamId++;
        }
        return {
            name, shortname, description, author, version, mapfile, modtype, mapHardness, notDeformable, gravity, tidalStrength,
            maxMetal, extractorRadius, voidWater, voidGround, autoShowMetal, minWind, maxWind, startPositions
        };
    }
    async parseSMD(buffer) {
        if (this.config.verbose) {
            console.log("Parsing .smd");
        }
        const smd = buffer.toString();
        const strPairs = smd.match(/(\w*)\=(.*?)\;/gm);
        const strObj = {};
        const startPositions = [];
        for (const strPair of strPairs) {
            const [key, val] = strPair.slice(0, strPair.length - 1).split("=");
            if (key === "StartPosX") {
                startPositions.push({ x: Number(val), z: 0 });
            }
            else if (key === "StartPosZ") {
                startPositions[startPositions.length - 1].z = Number(val);
            }
            else {
                strObj[key] = val;
            }
        }
        return {
            description: strObj.Description,
            tidalStrength: Number(strObj.TidalStrength),
            gravity: Number(strObj.Gravity),
            maxMetal: Number(strObj.MaxMetal),
            extractorRadius: Number(strObj.ExtractorRadius),
            mapHardness: Number(strObj.MapHardness),
            minWind: Number(strObj.MinWind),
            maxWind: Number(strObj.MaxWind),
            startPositions
        };
    }
    cloneTile(tile) {
        const clone = [];
        for (const row of tile) {
            clone.push(Buffer.from(row));
        }
        return clone;
    }
    joinTilesHorizontally(tiles, mipmapSize) {
        const tileRows = [];
        for (let y = 0; y < mipmapSize; y++) {
            for (let x = 0; x < tiles.length; x++) {
                const row = tiles[x].shift();
                tileRows.push(row);
            }
        }
        return Buffer.concat(tileRows);
    }
    async cleanup(tmpDir) {
        if (this.config.verbose) {
            console.log(`Cleaning up temp dir: ${tmpDir}`);
        }
        await fs_1.promises.rmdir(tmpDir, { recursive: true });
    }
    async sigint(tmpDir) {
        await this.cleanup(tmpDir);
        process.exit();
    }
}
exports.MapParser = MapParser;
function singleChannelToQuadChannel(buffer) {
    const outBuffer = [];
    buffer.forEach(val => {
        outBuffer.push(val, val, val, 255);
    });
    return Buffer.from(outBuffer);
}
//# sourceMappingURL=map-parser.js.map