"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 luaparse = __importStar(require("luaparse"));
const node_7z_1 = require("node-7z");
const node_stream_zip_1 = __importDefault(require("node-stream-zip"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const buffer_stream_1 = require("./buffer-stream");
const image_size_1 = require("./image-size");
const map_model_1 = require("./map-model");
const parse_dxt_1 = require("./parse-dxt");
const mapParserDefaultConfig = {
    verbose: false,
    mipmapSize: 4,
    skipSmt: false,
    path7za: _7zip_bin_1.default.path7za,
    water: true,
    parseSpecular: false
};
class MapParser {
    constructor(config) {
        this.config = Object.assign({}, mapParserDefaultConfig, config);
    }
    async parseMap(mapFilePath) {
        var _a, _b, _c, _d, _e, _f;
        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 mapInfo;
            let smd;
            if (archive.mapInfo) {
                mapInfo = await this.parseMapInfo(archive.mapInfo);
            }
            else {
                smd = 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);
            }
            const minHeight = (_c = (_b = (_a = mapInfo === null || mapInfo === void 0 ? void 0 : mapInfo.smf) === null || _a === void 0 ? void 0 : _a.minheight) !== null && _b !== void 0 ? _b : smd === null || smd === void 0 ? void 0 : smd.minHeight) !== null && _c !== void 0 ? _c : smf === null || smf === void 0 ? void 0 : smf.minDepth;
            const maxHeight = (_f = (_e = (_d = mapInfo === null || mapInfo === void 0 ? void 0 : mapInfo.smf) === null || _d === void 0 ? void 0 : _d.maxheight) !== null && _e !== void 0 ? _e : smd === null || smd === void 0 ? void 0 : smd.maxHeight) !== null && _f !== void 0 ? _f : smf === null || smf === void 0 ? void 0 : smf.maxDepth;
            if (this.config.water && smt) {
                this.applyWater({
                    textureMap: smt,
                    heightMapValues: smf.heightMapValues,
                    minHeight,
                    maxHeight
                });
            }
            await this.cleanup(tempDir);
            let scriptName = "";
            if (mapInfo && mapInfo.name && mapInfo.version && mapInfo.name.includes(mapInfo.version)) {
                scriptName = mapInfo.name;
            }
            else if (mapInfo && mapInfo.name) {
                scriptName = `${mapInfo.name} ${mapInfo.version}`;
            }
            else if (archive.smfName) {
                scriptName = archive.smfName;
            }
            sigintBinding.removeAllListeners();
            return {
                fileName: filePath.name,
                fileNameWithExt: filePath.base,
                scriptName,
                minHeight,
                maxHeight,
                mapInfo,
                smd,
                smf,
                heightMap: smf.heightMap,
                metalMap: smf.metalMap,
                miniMap: smf.miniMap,
                typeMap: smf.typeMap,
                textureMap: smt,
                specularMap: archive.specular
            };
        }
        catch (err) {
            await this.cleanup(tempDir);
            sigintBinding.removeAllListeners();
            console.error(err);
            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: this.config.path7za,
                recursive: true,
                $cherryPick: ["*.smf", "*.smd", "*.smt", "mapinfo.lua", "*.png", "*.dds"]
            });
            extractStream.on("end", async () => {
                const archiveFiles = await this.extractArchiveFiles(outPath);
                resolve(archiveFiles);
            });
        });
    }
    async extractSdz(sdzPath, outPath) {
        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 node_stream_zip_1.default.async({ file: sdzPath });
        await zip.extract("maps/", outPath);
        await zip.close();
        return this.extractArchiveFiles(outPath);
    }
    async extractArchiveFiles(outPath) {
        const files = glob_1.glob.sync(`${outPath}/**/*`);
        const smfPath = files.find(filePath => filePath.match(/.*\.smf/));
        const smtPath = files.find(filePath => filePath.match(/.*\.smt/));
        const smdPath = files.find(filePath => filePath.match(/.*\.smd/));
        const mapInfoPath = files.find(filePath => path.resolve(filePath) === path.join(outPath, "/", "mapinfo.lua"));
        const specularPath = files.find(filePath => filePath.match(/.*spec.*/i));
        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;
        let specular = undefined;
        if (specularPath && this.config.parseSpecular) {
            const specularType = path.extname(specularPath);
            if (specularType === ".dds") {
                const specularBuffer = await fs_1.promises.readFile(specularPath);
                const specularDimensions = image_size_1.sizeOfDDS(specularBuffer);
                specular = new jimp_1.default({
                    data: parse_dxt_1.parseDxt(specularBuffer, specularDimensions.width, specularDimensions.height),
                    width: specularDimensions.width,
                    height: specularDimensions.height
                });
            }
            else {
                specular = await jimp_1.default.read(specularPath);
            }
        }
        return { smf, smt, smd, smfName, mapInfo, specular };
    }
    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 largeHeightMapValues = new buffer_stream_1.BufferStream(heightMapBuffer).readInts(heightMapSize, 2, true);
        const heightMapValues = [];
        const heightMapColors = largeHeightMapValues.map((val, i) => {
            const percent = val / 65536; // 2 bytes
            heightMapValues.push(percent);
            const level = percent * 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 miniMapRgbaBuffer = parse_dxt_1.parseDxt(miniMapBuffer, 1024, 1024);
        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, heightMapValues,
            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 refTileRGBABuffer = parse_dxt_1.parseDxt(dxt1, mipmapSize, mipmapSize);
            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) {
        if (this.config.verbose) {
            console.log("Parsing mapinfo.lua");
        }
        const mapInfoStr = buffer.toString();
        const parsedMapInfo = luaparse.parse(mapInfoStr, { encodingMode: "x-user-defined", comments: false });
        const rootObj = parsedMapInfo.body[0];
        const rootTable = rootObj.init.find(block => block.type === "TableConstructorExpression");
        const obj = this.parseMapInfoFields(rootTable.fields);
        return obj;
    }
    parseMapInfoFields(fields) {
        const arr = [];
        const obj = {};
        for (const field of fields) {
            if (field.type === "TableKeyString") {
                if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") {
                    obj[field.key.name] = field.value.value;
                }
                else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") {
                    obj[field.key.name] = -field.value.argument.value;
                }
                else if (field.value.type === "TableConstructorExpression") {
                    obj[field.key.name] = this.parseMapInfoFields(field.value.fields);
                }
            }
            else if (field.type === "TableValue") {
                if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") {
                    const val = field.value.value;
                    arr.push(val);
                }
            }
            else if (field.type === "TableKey") {
                if (field.value.type === "StringLiteral" || field.value.type === "NumericLiteral" || field.value.type === "BooleanLiteral") {
                    if (field.key.type === "NumericLiteral") {
                        arr[field.key.type] = field.value.value;
                    }
                }
                else if (field.value.type === "UnaryExpression" && field.value.argument.type === "NumericLiteral") {
                    arr[field.key.type] = -field.value.argument.value;
                }
                else if (field.value.type === "TableConstructorExpression") {
                    arr.push(this.parseMapInfoFields(field.value.fields));
                }
            }
        }
        if (arr.length) {
            return arr;
        }
        return obj;
    }
    async parseSMD(buffer) {
        if (this.config.verbose) {
            console.log("Parsing .smd");
        }
        const smd = buffer.toString();
        const matches = smd.matchAll(/\s(?<key>\w+)\s*\=\s?(?<val>.*?)\;/g);
        const obj = {};
        const startPositions = [];
        for (const match of matches) {
            const key = match.groups.key;
            let val = Number(match.groups.val);
            if (val === NaN) {
                val = match.groups.val;
            }
            if (key === "StartPosX") {
                startPositions.push({ x: Number(val), z: 0 });
            }
            else if (key === "StartPosZ") {
                startPositions[startPositions.length - 1].z = Number(val);
            }
            else {
                obj[key] = val;
            }
        }
        return {
            description: obj.Description,
            tidalStrength: obj.TidalStrength,
            gravity: obj.Gravity,
            maxMetal: obj.MaxMetal,
            extractorRadius: obj.ExtractorRadius,
            mapHardness: obj.MapHardness,
            minWind: obj.MinWind,
            maxWind: obj.MaxWind,
            minHeight: obj.minheight,
            maxHeight: obj.maxheight,
            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);
    }
    applyWater(options) {
        var _a, _b;
        if (options.minHeight >= 0) {
            // water level is always at 0, so if minDepth is above 0 then map has no water
            return;
        }
        const width = options.textureMap.getWidth();
        const height = options.textureMap.getHeight();
        const heightMapRatio = this.config.mipmapSize / 4;
        const heightMapWidth = Math.floor(width / heightMapRatio) + 1;
        const heightMapHeight = Math.floor(height / heightMapRatio) + 1;
        const depthRange = options.maxHeight - options.minHeight;
        const waterLevelPercent = Math.abs(options.minHeight / depthRange);
        const color = (_a = options.rgbColor) !== null && _a !== void 0 ? _a : map_model_1.defaultWaterOptions.rgbColor;
        const colorModifier = (_b = options.rgbColor) !== null && _b !== void 0 ? _b : map_model_1.defaultWaterOptions.rgbModifier;
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const pixelHex = options.textureMap.getPixelColor(x, y);
                const pixelRGBA = jimp_1.default.intToRGBA(pixelHex);
                const heightMapY = Math.floor((y + 1) / heightMapRatio);
                const heightMapX = Math.floor(((x + 1) % width) / heightMapRatio);
                const heightValue = options.heightMapValues[heightMapWidth * heightMapY + heightMapX];
                if (heightValue < waterLevelPercent) {
                    const waterDepth = heightValue / waterLevelPercent;
                    pixelRGBA.r = Math.min(Math.max(((color.r + (pixelRGBA.r * waterDepth)) / 2) * colorModifier.r, 0), 255);
                    pixelRGBA.g = Math.min(Math.max(((color.g + (pixelRGBA.g * waterDepth)) / 2) * colorModifier.g, 0), 255);
                    pixelRGBA.b = Math.min(Math.max(((color.b + (pixelRGBA.b * waterDepth)) / 2) * colorModifier.b, 0), 255);
                    const newHex = jimp_1.default.rgbaToInt(pixelRGBA.r, pixelRGBA.g, pixelRGBA.b, pixelRGBA.a);
                    options.textureMap.setPixelColor(newHex, x, y);
                }
            }
        }
    }
    async cleanup(tmpDir) {
        if (this.config.verbose) {
            console.log(`Cleaning up temp dir: ${tmpDir}`);
        }
        await fs_1.promises.rm(tmpDir, { recursive: true, force: 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