package net.torvald.terrarum.serialise

import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.TerrarumAppConfiguration
import net.torvald.terrarum.blockproperties.Block
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.gameworld.BlockLayerI16
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.gameworld.GameWorld.Companion.TERRAIN
import net.torvald.terrarum.gameworld.GameWorld.Companion.WALL
import net.torvald.terrarum.linearSearchBy
import net.torvald.terrarum.utils.HashArray
import java.io.StringReader

/**
 * Created by minjaesong on 2023-10-17.
 */
class PointOfInterest(
    identifier: String,
    width: Int,
    height: Int,
    tileNumberToNameMap0: HashArray<ItemID>,
    tileNameToNumberMap0: HashMap<ItemID, Int>
) : Json.Serializable {

    constructor() : this("undefined", 0,0, HashArray(), HashMap())

    constructor(numberToNameMap: HashArray<ItemID>, nameToNumberMap: HashMap<ItemID, Int>) : this("undefined", 0, 0, numberToNameMap, nameToNumberMap)

    @Transient var w = width; private set
    @Transient var h = height; private set
    @Transient var layers = ArrayList<POILayer>(); private set
    @Transient var id = identifier; private set
    @Transient var tileNumberToNameMap: HashArray<ItemID> = tileNumberToNameMap0; private set
    @Transient var tileNameToNumberMap: HashMap<ItemID, Int> = tileNameToNumberMap0; private set
    @Transient lateinit var lutFromJson: HashArray<ItemID>
    @Transient var wlenFromJson = 0

    override fun write(json: Json) {
        val tileSymbolToItemId = HashArray<ItemID>() // exported

        val uniqueTiles = ArrayList(layers.flatMap { it.getUniqueTiles() }.toSet().toList().sorted()) // contains TileNumber


        // build tileSymbolToItemId
        uniqueTiles.forEachIndexed { index, tilenum ->
            tileSymbolToItemId[index.toLong()] = if (tilenum == 65535) // largest value on BlockLayerI16
                Block.NULL
            else
                tileNumberToNameMap[tilenum.toLong()] ?: throw NullPointerException("No tileNumber->tileName mapping for $tilenum")
        }

        val itemIDtoTileSym = tileSymbolToItemId.map { it.value to it.key }.toMap()

        val wordSize = if (tileSymbolToItemId.size >= 255) 16 else 8



//        printdbg(this, "unique tiles: ${tileSymbolToItemId.size}")
//        printdbg(this, "tileSymbolToItemId=$tileSymbolToItemId")
//        printdbg(this, "itemIDtoTileSym=$itemIDtoTileSym")



        layers.forEach { it.getReadyForSerialisation(tileNumberToNameMap, itemIDtoTileSym, wordSize / 8) }

        json.setTypeName(null)
        json.writeValue("genver", TerrarumAppConfiguration.VERSION_RAW)
        json.writeValue("id", id)
        json.writeValue("wlen", wordSize)
        json.writeValue("w", w)
        json.writeValue("h", h)
        json.writeValue("lut", tileSymbolToItemId)
        json.writeValue("layers", layers)
    }

    override fun read(json: Json, jsonData: JsonValue) {
        this.id = jsonData["id"].asString()
        this.w = jsonData["w"].asInt()
        this.h = jsonData["h"].asInt()
        this.lutFromJson = json.readValue(HashArray<ItemID>().javaClass, jsonData["lut"])
        this.wlenFromJson = jsonData["wlen"].asInt()


        println("read test")
        println("id: $id, w: $w, h: $h, wlen: $wlenFromJson")
        println("lut: $lutFromJson")

        println("==== layers (bytes) ====")

        // decompress layers
        jsonData["layers"].forEachIndexed { index, jsonData ->
            print("#${index+1}: ")
            POILayer().also {
                it.w = this.w; it.h = this.h; it.id = this.id
                it.read(json, jsonData)
                layers.add(it)
            }
        }
    }

    fun getReadyToBeUsed(itemIDtoTileNum: Map<ItemID, Int>) {
        layers.forEach {
            it.getReadyToBeUsed(lutFromJson, itemIDtoTileNum, w, h, wlenFromJson / 8)
        }
    }

    /**
     * Place the specified layers onto the world. The name of the layers are case-insensitive.
     */
    fun placeOnWorld(layerNames: List<String>, world: GameWorld, bottomCentreX: Int, bottomCenterY: Int) {
        val layers = layerNames.map { name ->
            (layers.linearSearchBy { it.name.equals(name, true) } ?: throw IllegalArgumentException("Layer with name '$name' not found"))
        }
        layers.forEach {
            it.placeOnWorld(world, bottomCentreX, bottomCenterY)
        }
    }
}

/**
 * @param name name of the layer, case-insensitive.
 */
class POILayer(
    name: String
) : Json.Serializable {
    constructor() : this("undefined")

    @Transient var name = name
    @Transient internal lateinit var blockLayer: ArrayList<BlockLayerI16>
    @Transient internal lateinit var dat: Array<ByteArray>

    @Deprecated("Used for debug print", ReplaceWith("name")) @Transient internal var id = ""
    @Deprecated("Used for debug print") @Transient internal var w = 0
    @Deprecated("Used for debug print") @Transient internal var h = 0

    /**
     * @return list of unique tiles, in the form of TileNums
     */
    fun getUniqueTiles(): List<Int> {
        return blockLayer.flatMap { layer ->
            (0 until layer.height * layer.width).map { layer.unsafeGetTile(it % layer.width, it / layer.width) }
        }.toSet().toList().sorted()
    }

    /**
     * Converts `blockLayer` into `dat` internally, the hidden property used for the serialisation
     *
     * Tilenum: tile number in the block layer, identical to the any other block layers
     * TileSymbol: condensed version of the Tilenum, of which the higheset number is equal to the number of unique tiles used in the layer
     *
     * `tilenumToItemID[-1]` should return `Block.NULL`
     */
    fun getReadyForSerialisation(tilenumToItemID: HashArray<ItemID>, itemIDtoTileSym: Map<ItemID, Long>, byteLength: Int) {
        dat = blockLayer.map { layer ->
            ByteArray(layer.width * layer.height * byteLength) { i ->
                if (byteLength == 1) {
                    val tilenum = layer.unsafeGetTile(i % layer.width, i / layer.width).toLong()
                    val itemID = if (tilenum == 65535L) Block.NULL else tilenumToItemID[tilenum] ?: throw NullPointerException("No tileNumber->tileName mapping for $tilenum")
                    val tileSym = itemIDtoTileSym[itemID]!!
                    tileSym.toByte()
                }
                else if (byteLength == 2) {
                    val tilenum = layer.unsafeGetTile((i/2) % layer.width, (i/2) / layer.width).toLong()
                    val itemID = if (tilenum == 65535L) Block.NULL else tilenumToItemID[tilenum]!!
                    val tileSym = itemIDtoTileSym[itemID]!!
                    if (i % 2 == 0) tileSym.and(255).toByte() else tileSym.ushr(8).and(255).toByte()
                }
                else throw IllegalArgumentException()
            }
        }.toTypedArray()
    }

    /**
     * Converts `dat` into `blockLayer` so the Layer can be actually utilised.
     */
    fun getReadyToBeUsed(tileSymbolToItemId: HashArray<ItemID>, itemIDtoTileNum: Map<ItemID, Int>, width: Int, height: Int, byteLength: Int) {
        if (::blockLayer.isInitialized) {
            blockLayer.forEach { it.dispose() }
        }
        blockLayer = ArrayList<BlockLayerI16>()

        dat.forEachIndexed { layerIndex, layer ->
            val currentBlockLayer = BlockLayerI16(width, height).also {
                blockLayer.add(it)
            }
            for (w in 0 until layer.size / byteLength) {
                val word = if (byteLength == 1) layer[w].toUint() else if (byteLength == 2) layer.toULittleShort(2*w) else throw IllegalArgumentException("Illegal byteLength $byteLength")
                val x = w % width
                val y = w / width
                val itemID = tileSymbolToItemId[word.toLong()]!!
                val tile = if (itemID == Block.NULL)
                    -1
                else
                    itemIDtoTileNum[itemID]!!
                currentBlockLayer.unsafeSetTile(x, y, tile)
            }
        }
    }

    fun placeOnWorld(world: GameWorld, bottomCentreX: Int, bottomCenterY: Int) {
        val topLeftX = bottomCentreX - (blockLayer[0].width - 1) / 2
        val topLeftY = bottomCenterY - (blockLayer[0].height - 1)

        blockLayer.forEachIndexed { layerIndex, layer ->
            for (x in 0 until layer.width) { for (y in 0 until layer.height) {
                val tile = layer.unsafeGetTile(x, y)
                if (tile != -1 && tile != 65535) {
                    val (wx, wy) = world.coerceXY(x + topLeftX, y + topLeftY)
                    world.setTileOnLayerUnsafe(layerIndex, wx, wy, tile)
                }
            } }
        }
    }

    override fun write(json: Json) {
        if (!::dat.isInitialized) throw IllegalStateException("Internal data is not prepared! please run getReadyForSerialisation() before writing!")
        json.setTypeName(null)
        json.writeValue("name", name)
        json.writeValue("dat", dat)
    }

    override fun read(json: Json, jsonData: JsonValue) {
        name = jsonData["name"].asString()

        println(name)


        dat = jsonData["dat"].mapIndexed { index, value ->
            val zipdStr = value.asString()
            val ba = Common.strToBytes(StringReader(zipdStr)).toByteArray()
            val lname = "L${if (index == TERRAIN) "terr" else if (index == WALL) "wall" else "unk$index"}"
            if (ba.size != w * h) throw IllegalStateException("Layer size mismatch: expected ${w*h} but got ${ba.size} on POI $id Layer $name $lname")

            print("  $lname: ")
            print("(${ba.size})")
            println("[${ba.joinToString()}]")

            ba
        }.toTypedArray()
    }

}
