import { HexBuffer } from '../HexBuffer';
import { W3Buffer } from '../W3Buffer';
import { type WarResult, type JsonResult } from '../CommonInterfaces';
import { Translator } from './Translator';
import { Terrain } from '../data/Terrain';

function splitLargeArrayIntoWidthArrays(array: unknown[], width: number) {
    const rows: unknown[][] = [];
    for (let i = 0; i < array.length / width; i++) {
        rows.push(array.slice(i * width, (i + 1) * width));
    }
    return rows;
}

export class TerrainTranslator implements Translator<Terrain> {

    private static instance: TerrainTranslator;

    private constructor() {}

    public static getInstance() {
        if (!this.instance) {
            this.instance = new this();
        }
        return this.instance;
    }

    public static jsonToWar(terrain: Terrain): WarResult {
        return this.getInstance().jsonToWar(terrain);
    }

    public static warToJson(buffer: Buffer): JsonResult<Terrain> {
        return this.getInstance().warToJson(buffer);
    }

    public jsonToWar(terrainJson: Terrain): WarResult {
        const outBufferToWar = new HexBuffer();

        /*
         * Header
         */
        outBufferToWar.addChars('W3E!'); // file id
        outBufferToWar.addInt(12); // file version
        outBufferToWar.addChar(terrainJson.tileset); // base tileset
        outBufferToWar.addInt(+terrainJson.customTileset); // 1 = using custom tileset, 0 = not

        /*
         * Tiles
         */
        outBufferToWar.addInt(terrainJson.tilePalette?.length || 0);
        terrainJson.tilePalette?.forEach((tile) => {
            outBufferToWar.addChars(tile);
        });

        /*
         * Cliffs
         */
        outBufferToWar.addInt(terrainJson.cliffTilePalette?.length || 0);
        terrainJson.cliffTilePalette?.forEach((cliffTile) => {
            outBufferToWar.addChars(cliffTile);
        });

        /*
         * Map size data
         */
        outBufferToWar.addInt(terrainJson.map.width + 1);
        outBufferToWar.addInt(terrainJson.map.height + 1);

        /*
         * Map offset
         */
        outBufferToWar.addFloat(terrainJson.map.offset.x);
        outBufferToWar.addFloat(terrainJson.map.offset.y);

        /*
         * Tile points
         */
        // Partition the terrainJson masks into "chunks" (i.e. rows) of (width+1) length,
        // reverse that list of rows (due to vertical flipping), and then write the rows out
        const rows = {
            groundHeight: splitLargeArrayIntoWidthArrays(terrainJson.groundHeight, terrainJson.map.width + 1) as number[][],
            waterHeight: splitLargeArrayIntoWidthArrays(terrainJson.waterHeight, terrainJson.map.width + 1) as number[][],
            boundaryFlag: splitLargeArrayIntoWidthArrays(terrainJson.boundaryFlag, terrainJson.map.width + 1) as boolean[][],
            flags: splitLargeArrayIntoWidthArrays(terrainJson.flags, terrainJson.map.width + 1) as number[][],
            groundTexture: splitLargeArrayIntoWidthArrays(terrainJson.groundTexture, terrainJson.map.width + 1) as number[][],
            groundVariation: splitLargeArrayIntoWidthArrays(terrainJson.groundVariation, terrainJson.map.width + 1) as number[][],
            cliffVariation: splitLargeArrayIntoWidthArrays(terrainJson.cliffVariation, terrainJson.map.width + 1) as number[][],
            cliffTexture: splitLargeArrayIntoWidthArrays(terrainJson.cliffTexture, terrainJson.map.width + 1) as number[][],
            layerHeight: splitLargeArrayIntoWidthArrays(terrainJson.layerHeight, terrainJson.map.width + 1) as number[][],
            tileset: '',
            customTileset: false,
            tilePalette: [],
            cliffTilePalette: [],
        };

        rows.groundHeight.reverse();
        rows.waterHeight.reverse();
        rows.boundaryFlag.reverse();
        rows.flags.reverse();
        rows.groundTexture.reverse();
        rows.groundVariation.reverse();
        rows.cliffVariation.reverse();
        rows.cliffTexture.reverse();
        rows.layerHeight.reverse();

        for (let i = 0; i < rows.groundHeight.length; i++) {
            for (let j = 0; j < rows.groundHeight[i].length; j++) {
                // these bit operations are based off documentation from https://github.com/stijnherfst/HiveWE/wiki/war3map.w3e-Terrain
                const groundHeight = rows.groundHeight[i][j];
                const waterHeight = rows.waterHeight[i][j];
                const boundaryFlag = rows.boundaryFlag[i][j];
                const flags = rows.flags[i][j];
                const groundTexture = rows.groundTexture[i][j];
                const groundVariation = rows.groundVariation[i][j];
                const cliffVariation = rows.cliffVariation[i][j];
                const cliffTexture = rows.cliffTexture[i][j];
                const layerHeight = rows.layerHeight[i][j];

                const hasBoundaryFlag = boundaryFlag ? 0x4000 : 0;

                outBufferToWar.addShort(groundHeight);
                outBufferToWar.addShort(waterHeight | hasBoundaryFlag);
                outBufferToWar.addShort((flags << 2) | groundTexture);
                outBufferToWar.addByte(groundVariation | cliffVariation);
                outBufferToWar.addByte(cliffTexture | layerHeight);
            }
        }

        return {
            errors: [],
            buffer: outBufferToWar.getBuffer()
        };
    }

    public warToJson(buffer: Buffer): JsonResult<Terrain> {
        // create buffer
        const result: Terrain = {
            tileset: '',
            customTileset: false,
            tilePalette: [],
            cliffTilePalette: [],
            map: {
                width: 1,
                height: 1,
                offset: {
                    x: 0,
                    y: 0
                }
            },
            groundHeight: [],
            waterHeight: [],
            boundaryFlag: [],
            flags: [],
            groundTexture: [],
            groundVariation: [],
            cliffVariation: [],
            cliffTexture: [],
            layerHeight: []
        };
        const outBufferToJSON = new W3Buffer(buffer);

        /**
         * Header
         */
        const w3eHeader = outBufferToJSON.readChars(4); // W3E!
        const version = outBufferToJSON.readInt(); // 0C 00 00 00
        const tileset = outBufferToJSON.readChars(1); // tileset
        const customTileset = (outBufferToJSON.readInt() === 1);

        result.tileset = tileset;
        result.customTileset = customTileset;

        /**
         * Tiles
         */
        const numTilePalettes = outBufferToJSON.readInt();
        const tilePalettes: string[] = [];
        for (let i = 0; i < numTilePalettes; i++) {
            tilePalettes.push(outBufferToJSON.readChars(4));
        }

        result.tilePalette = tilePalettes;

        /**
         * Cliffs
         */
        const numCliffTilePalettes = outBufferToJSON.readInt();
        const cliffPalettes: string[] = [];
        for (let i = 0; i < numCliffTilePalettes; i++) {
            const cliffPalette = outBufferToJSON.readChars(4);
            cliffPalettes.push(cliffPalette);
        }

        result.cliffTilePalette = cliffPalettes;

        /**
         * map dimensions
         */
        const width = outBufferToJSON.readInt() - 1;
        const height = outBufferToJSON.readInt() - 1;
        result.map = { width, height, offset: { x: 0, y: 0 } };

        const offsetX = outBufferToJSON.readFloat();
        const offsetY = outBufferToJSON.readFloat();
        result.map.offset = { x: offsetX, y: offsetY };

        /**
         * map tiles
         */
        const arrGroundHeight: number[] = [];
        const arrWaterHeight: number[] = [];
        const arrBoundaryFlag: boolean[] = [];
        const arrFlags: number[] = [];
        const arrGroundTexture: number[] = [];
        const arrGroundVariation: number[] = [];
        const arrCliffVariation: number[] = [];
        const arrCliffTexture: number[] = [];
        const arrLayerHeight: number[] = [];

        while (!outBufferToJSON.isExhausted()) {
            const groundHeight = outBufferToJSON.readShort();
            const waterHeightAndBoundary = outBufferToJSON.readShort();
            const waterHeight = waterHeightAndBoundary & 32767;
            const boundaryFlag = (waterHeightAndBoundary & 0x4000) === 0x4000;

            let flags;
            let groundTexture;
            if (version >= 12){
                const flagsAndGroundTexture = outBufferToJSON.readShort();
                flags = (flagsAndGroundTexture & 0xFFC0) >> 2;
                groundTexture = flagsAndGroundTexture & 0x3F;
            } else {
                const flagsAndGroundTexture = outBufferToJSON.readByte();
                flags = flagsAndGroundTexture & 0xF0;
                groundTexture = flagsAndGroundTexture & 0x0F;
            }
            
            const groundAndCliffVariation = outBufferToJSON.readByte();
            const cliffTextureAndLayerHeight = outBufferToJSON.readByte();

            const groundVariation = groundAndCliffVariation & 0xF8;
            const cliffVariation = groundAndCliffVariation & 0x07;
            const cliffTexture = cliffTextureAndLayerHeight & 0XF0;
            const layerHeight = cliffTextureAndLayerHeight & 0x0F;

            arrGroundHeight.push(groundHeight);
            arrWaterHeight.push(waterHeight);
            arrBoundaryFlag.push(boundaryFlag);
            arrFlags.push(flags); //TODO: properly parse flags
            arrGroundTexture.push(groundTexture);
            arrGroundVariation.push(groundVariation);
            arrCliffVariation.push(cliffVariation);
            arrCliffTexture.push(cliffTexture);
            arrLayerHeight.push(layerHeight);
        }

        function convertArrayOfArraysIntoFlatArray(arr: unknown[][]): unknown {
            return arr.reduce((a: unknown[], b: unknown[]) => {
                return [...a, ...b];
            });
        }

        // The map was read in "backwards" because wc3 maps have origin (0,0)
        // at the bottom left instead of top left as we desire. Flip the rows
        // vertically to fix this.
        result.groundHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundHeight, result.map.width + 1).reverse()) as number[][];
        result.waterHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrWaterHeight, result.map.width + 1).reverse()) as number[][];
        result.boundaryFlag = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrBoundaryFlag, result.map.width + 1).reverse()) as boolean[][];
        result.flags = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrFlags, result.map.width + 1).reverse()) as number[];
        result.groundTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundTexture, result.map.width + 1).reverse()) as number[][];
        result.groundVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrGroundVariation, result.map.width + 1).reverse()) as number[][];
        result.cliffVariation = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrCliffVariation, result.map.width + 1).reverse()) as number[][];
        result.cliffTexture = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrCliffTexture, result.map.width + 1).reverse()) as number[][];
        result.layerHeight = convertArrayOfArraysIntoFlatArray(splitLargeArrayIntoWidthArrays(arrLayerHeight, result.map.width + 1).reverse()) as number[][];

        return {
            errors: [],
            json: result
        };
    }
}
