package net.torvald.terrarum.worlddrawer

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.utils.GdxRuntimeException
import net.torvald.terrarum.*
import net.torvald.terrarum.blockproperties.BlockCodex
import net.torvald.terrarum.blockproperties.Fluid
import net.torvald.terrarum.gameworld.GameWorld
import kotlin.math.roundToInt

/**
 * This class implements work_files/dynamic_shape_2_0.psd
 *
 * Tile at (0,0) AND (5,0) must be transparent. Former is because block 0 is considered as an air, and the latter
 * is because it's breakage of 0, and 0 means no breakage. Breakage part is hard-coded in the tiling shader.
 *
 * Any real tiles must begin from (0,16), the first 256x16 section is reserved for special purpose (terrain: breakage, fluid: empty)
 *
 * Created by minjaesong on 2019-02-28.
 */
object CreateTileAtlas {

    const val MAX_TEX_SIZE = TerrarumAppConfiguration.MAX_TEX_SIZE
    const val TILE_SIZE = TerrarumAppConfiguration.TILE_SIZE
    const val TILES_IN_X = MAX_TEX_SIZE / TILE_SIZE

    private val TOTAL_TILES = TILES_IN_X * TILES_IN_X

    lateinit var atlas: Pixmap
    lateinit var atlasAutumn: Pixmap
    lateinit var atlasWinter: Pixmap
    lateinit var atlasSpring: Pixmap
    lateinit var atlasFluid: Pixmap
    lateinit var atlasGlow: Pixmap // glowing won't be affected by the season... for now
    lateinit var itemTerrainTexture: Texture
    lateinit var itemWallTexture: Texture
    lateinit var terrainTileColourMap: GdxColorMap
    internal lateinit var tags: HashMap<Int, RenderTag>
        private set
    private val defaultRenderTag = RenderTag(3, RenderTag.CONNECT_SELF, RenderTag.MASK_NA) // 'update' block
    var initialised = false
        private set

    /** 0000.tga, 1.tga.gz, 3242423.tga, 000033.tga.gz */
    // for right now, TGA file only, no gzip
    private val validFluidTilesFilename = Regex("""fluid_[0-9]+\.tga""")
    private val tileNameRegex = Regex("""[0-9]+\.tga""")
    private val tileGlowNameRegex = Regex("""[0-9]+_glow\.tga""")

    // 16 tiles are reserved for internal use: solid black, solid white, breakage stages.
    // 0th tile is complete transparent tile and is also a BlockID of zero: air.
    private var atlasCursor = 0
    private var atlasCursorGlow = 0
    private val atlasInit = "./assets/graphics/blocks/init.tga"

    /**
     * Must be called AFTER mods' loading so that all the block props are loaded
     */
    operator fun invoke(updateExisting: Boolean = false) { if (updateExisting || !initialised) {

        tags = HashMap<Int, RenderTag>()
        tags[0] = RenderTag(0, RenderTag.CONNECT_SELF, RenderTag.MASK_NA)

        atlas = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        atlasAutumn = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        atlasWinter = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        atlasSpring = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        atlasFluid = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        atlasGlow = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)

        atlas.blending = Pixmap.Blending.None
        atlasAutumn.blending = Pixmap.Blending.None
        atlasWinter.blending = Pixmap.Blending.None
        atlasSpring.blending = Pixmap.Blending.None
        atlasFluid.blending = Pixmap.Blending.None
        atlasGlow.blending = Pixmap.Blending.None

        val initMap = Pixmap(Gdx.files.internal(atlasInit))
        drawToAtlantes(initMap, nullTile, 16)
        initMap.dispose()


        // get all the files applicable
        // first, get all the '/blocks' directory, and add all the files, regardless of their extension, to the list
        val tgaList = ArrayList<FileHandle>()
        ModMgr.getGdxFilesFromEveryMod("blocks").forEach {
            if (!it.isDirectory) {
                throw Error("Path '${it.path()}' is not a directory")
            }

            it.list().forEach { tgaFile ->
                if (!tgaFile.isDirectory && (tgaFile.name().matches(tileNameRegex))) {
                    tgaList.add(tgaFile)
                }
            }
        }

        // Sift through the file list for blocks, but TGA format first
        tgaList.forEach {
            try {
                val glowFile = Gdx.files.internal(it.path().dropLast(4) + "_glow.tga") // assuming strict ".tga" file
                fileToAtlantes(it, if (glowFile.exists()) glowFile else null)
            }
            catch (e: GdxRuntimeException) {
                System.err.println("Couldn't load file $it, skipping...")
            }
        }


        // test print
        //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/atlas.tga"), atlas, false)
        //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/atlasGlow.tga"), atlasGlow, false)



        // Sift throuth the file list, second TGA.GZ
        /*tgaList.filter { it.name().toUpperCase().endsWith(".TGA.GZ") }.forEach {
            try {
                fileToAtlantes(it)
            }
            catch (e: GdxRuntimeException) {
                System.err.println("Couldn't load file $it, skipping...")
            }
        }*/


        // Sift through the file list for fluids, but TGA format first
        val fluidMasterPixmap = Pixmap(TILE_SIZE * 47, TILE_SIZE * 8, Pixmap.Format.RGBA8888)
        tgaList.filter { it.name().matches(validFluidTilesFilename) && it.extension().toUpperCase() == "TGA" }.forEachIndexed { fluidLevel, it ->
            val pixmap = Pixmap(it)
            // dirty manual copy
            repeat(5) {
                fluidMasterPixmap.drawPixmap(pixmap,
                        it * TILE_SIZE * 7, fluidLevel * TILE_SIZE,
                        0, TILE_SIZE * it,
                        TILE_SIZE * 7, TILE_SIZE
                )
            }
            repeat(2) {
                fluidMasterPixmap.drawPixmap(pixmap,
                        (35 + it * 6) * TILE_SIZE, fluidLevel * TILE_SIZE,
                        0, TILE_SIZE * (5 + it),
                        TILE_SIZE * 6, TILE_SIZE
                )
            }

            pixmap.dispose()
        }
        // test print
        //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/fluidpixmapmaster.tga"), fluidMasterPixmap, false)

        // occupy the fluid pixmap with software rendering
        for (i in BlockCodex.MAX_TERRAIN_TILES..BlockCodex.highestNumber) {
            val fluid = Color(BlockCodex[i].colour)

            // pixmap <- (color SCREEN fluidMasterPixmap)
            // then occupy the atlasFluid
            val pixmap = Pixmap(fluidMasterPixmap.width, fluidMasterPixmap.height, Pixmap.Format.RGBA8888)
            pixmap.blending = Pixmap.Blending.None

            for (y in 0 until pixmap.height) {
                for (x in 0 until pixmap.width) {
                    val inColour = Color(fluidMasterPixmap.getPixel(x, y))
                    // SCREEN for RGB, MUL for A.
                    inColour.r = 1f - (1f - fluid.r) * (1f - inColour.r)
                    inColour.g = 1f - (1f - fluid.g) * (1f - inColour.g)
                    inColour.b = 1f - (1f - fluid.b) * (1f - inColour.b)
                    inColour.a = fluid.a * inColour.a

                    pixmap.drawPixel(x, y, inColour.toRGBA())
                }
            }

            // test print
            //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/$i.tga"), pixmap, false)

            // to the atlas
            val atlasTargetPos = 16 + 47 * 8 * (i - BlockCodex.MAX_TERRAIN_TILES)
            for (k in 0 until 47 * 8) {
                val srcX = (k % 47) * TILE_SIZE
                val srcY = (k / 47) * TILE_SIZE
                val destX = ((atlasTargetPos + k) % TILES_IN_X) * TILE_SIZE
                val destY = ((atlasTargetPos + k) / TILES_IN_X) * TILE_SIZE
                atlasFluid.drawPixmap(pixmap, srcX, srcY, TILE_SIZE, TILE_SIZE, destX, destY, TILE_SIZE, TILE_SIZE)
            }

            pixmap.dispose()
        }


        fluidMasterPixmap.dispose()

        // create item_wall images

        fun maskTypetoTileIDForItemImage(maskType: Int) = when(maskType) {
            CreateTileAtlas.RenderTag.MASK_47 -> 17
            CreateTileAtlas.RenderTag.MASK_PLATFORM -> 7
            else -> 0
        }

        val itemTerrainPixmap = Pixmap(16 * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        val itemWallPixmap = Pixmap(16 * TILE_SIZE, TILES_IN_X * TILE_SIZE, Pixmap.Format.RGBA8888)
        val terrainColormapPixmap = Pixmap(16, TILES_IN_X, Pixmap.Format.RGBA8888)

        CreateTileAtlas.tags.toMap().forEach { t, u ->
            val tilePosFromAtlas = u.tileNumber + maskTypetoTileIDForItemImage(u.maskType)
            val srcX = (tilePosFromAtlas % TILES_IN_X) * TILE_SIZE
            val srcY = (tilePosFromAtlas / TILES_IN_X) * TILE_SIZE
            val destX = (t % 16) * TILE_SIZE
            val destY = (t / 16) * TILE_SIZE
            itemTerrainPixmap.drawPixmap(CreateTileAtlas.atlas, srcX, srcY, TILE_SIZE, TILE_SIZE, destX, destY, TILE_SIZE, TILE_SIZE)
            itemWallPixmap.drawPixmap(CreateTileAtlas.atlas, srcX, srcY, TILE_SIZE, TILE_SIZE, destX, destY, TILE_SIZE, TILE_SIZE)
        }
        // darken things for the wall
        for (y in 0 until itemWallPixmap.height) {
            for (x in 0 until itemWallPixmap.width) {
                val c = Color(itemWallPixmap.getPixel(x, y)).mulAndAssign(BlocksDrawer.wallOverlayColour).toRGBA()
                itemWallPixmap.drawPixel(x, y, c)
            }
        }
        // create terrain colourmap
        val pxCount = TILE_SIZE * TILE_SIZE
        for (id in 0 until BlockCodex.MAX_TERRAIN_TILES) {
            val tx = (id % 16) * TILE_SIZE
            val ty = (id / 16) * TILE_SIZE
            var r = 0; var g = 0; var b = 0; var a = 0
            // average out the whole block
            for (y in ty until ty + TILE_SIZE) {
                for (x in tx until tx + TILE_SIZE) {
                    val data = itemTerrainPixmap.getPixel(x, y)
                    r += (data ushr 24) and 255
                    g += (data ushr 16) and 255
                    b += (data ushr  8) and 255
                    a += data and 255
                }
            }

            terrainColormapPixmap.drawPixel(tx / TILE_SIZE, ty / TILE_SIZE,
                    (r / pxCount).shl(24) or
                            (g / pxCount).shl(16) or
                            (b / pxCount).shl(8) or
                            (a / pxCount)
            )
        }

        //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/terrain_colormap.tga"), terrainColormapPixmap, false)
        //PixmapIO2.writeTGA(Gdx.files.absolute("${AppLoader.defaultDir}/terrainitem.tga"), itemTerrainPixmap, false)

        terrainTileColourMap = GdxColorMap(terrainColormapPixmap)
        itemTerrainTexture = Texture(itemTerrainPixmap)
        itemWallTexture = Texture(itemWallPixmap)
        itemTerrainPixmap.dispose()
        itemWallPixmap.dispose()

        initialised = true
    } }

    fun getRenderTag(blockID: Int): RenderTag {
        return tags.getOrDefault(blockID, defaultRenderTag)
    }

    fun fluidFillToTileLevel(fill: Float) = fill.times(8).roundToInt().coerceIn(0, 8)

    fun fluidToTileNumber(fluid: GameWorld.FluidInfo): Int {
        val fluidLevel = fluidFillToTileLevel(fluid.amount)
        return if (fluid.type == Fluid.NULL || fluidLevel == 0) 0 else
            16 + (376 * (fluid.type.abs() - 1)) + (47 * (fluidLevel - 1))
    }

    private val nullTile = Pixmap(TILE_SIZE * 16, TILE_SIZE * 16, Pixmap.Format.RGBA8888)

    private fun fileToAtlantes(matte: FileHandle, glow: FileHandle?) {
        val tilesPixmap = Pixmap(matte)
        val tilesGlowPixmap = if (glow != null) Pixmap(glow) else nullTile
        val blockID = matte.nameWithoutExtension().toInt()

        // determine the type of the block (populate tags list)
        // predefined by the image dimension: 16x16 for (1,0)
        if (tilesPixmap.width == TILE_SIZE && tilesPixmap.height == TILE_SIZE) {
            addTag(blockID, RenderTag.CONNECT_SELF, RenderTag.MASK_NA)
            drawToAtlantes(tilesPixmap, tilesGlowPixmap, RenderTag.maskTypeToTileCount(RenderTag.MASK_NA))
        }
        // predefined by the image dimension: 64x16 for (2,3)
        else if (tilesPixmap.width == TILE_SIZE * 4 && tilesPixmap.height == TILE_SIZE) {
            addTag(blockID, RenderTag.CONNECT_WALL_STICKER, RenderTag.MASK_TORCH)
            drawToAtlantes(tilesPixmap, tilesGlowPixmap, RenderTag.maskTypeToTileCount(RenderTag.MASK_TORCH))
        }
        // predefined by the image dimension: 128x16 for (3,4)
        else if (tilesPixmap.width == TILE_SIZE * 8 && tilesPixmap.height == TILE_SIZE) {
            addTag(blockID, RenderTag.CONNECT_WALL_STICKER_CONNECT_SELF, RenderTag.MASK_PLATFORM)
            drawToAtlantes(tilesPixmap, tilesGlowPixmap, RenderTag.maskTypeToTileCount(RenderTag.MASK_PLATFORM))
        }
        // 112x112 or 224x224
        else {
            if (tilesPixmap.width != tilesPixmap.height && tilesPixmap.width % (7 * TILE_SIZE) >= 2) {
                throw IllegalArgumentException("Unrecognized image dimension: ${tilesPixmap.width}x${tilesPixmap.height}")
            }
            // figure out the tags
            var connectionType = 0
            var maskType = 0
            for (bit in 0 until TILE_SIZE) {
                val x = (7 * TILE_SIZE - 1) - bit
                val y1 = 5 * TILE_SIZE; val y2 = y1 + 1
                val pixel1 = (tilesPixmap.getPixel(x, y1).and(255) >= 128).toInt()
                val pixel2 = (tilesPixmap.getPixel(x, y2).and(255) >= 128).toInt()

                connectionType += pixel1 shl bit
                maskType += pixel2 shl bit
            }

            addTag(blockID, connectionType, maskType)
            val tileCount = RenderTag.maskTypeToTileCount(maskType)
            drawToAtlantes(tilesPixmap, tilesGlowPixmap, tileCount)
        }

        tilesPixmap.dispose()
    }

    /**
     * This function must precede the drawToAtlantes() function, as the marking requires the variable
     * 'atlasCursor' and the draw function modifies it!
     */
    private fun addTag(id: Int, connectionType: Int, maskType: Int) {
        if (tags.containsKey(id)) {
            throw Error("Block $id already exists")
        }

        tags[id] = RenderTag(atlasCursor, connectionType, maskType)
    }

    private fun drawToAtlantes(pixmap: Pixmap, glow: Pixmap, tilesCount: Int) {
        if (atlasCursor >= TOTAL_TILES) {
            throw Error("Too much tiles for $MAX_TEX_SIZE texture size: $atlasCursor")
        }

        val seasonal = pixmap.width == pixmap.height && pixmap.width == 14 * TILE_SIZE
        val txOfPixmap = pixmap.width / TILE_SIZE
        val txOfPixmapGlow = glow.width / TILE_SIZE
        for (i in 0 until tilesCount) {
            //printdbg(this, "Rendering to atlas, tile# $atlasCursor, tilesCount = $tilesCount, seasonal = $seasonal")

            // different texture for different seasons (224x224)
            if (seasonal) {
                val i = if (i < 41) i else i + 1 // to compensate the discontinuity between 40th and 41st tile
                _drawToAtlantes(pixmap, atlasCursor, i % 7, i / 7, 1)
                _drawToAtlantes(pixmap, atlasCursor, i % 7 + 7, i / 7, 2)
                _drawToAtlantes(pixmap, atlasCursor, i % 7 + 7, i / 7 + 7, 3)
                _drawToAtlantes(pixmap, atlasCursor, i % 7, i / 7 + 7, 4)
                _drawToAtlantes(glow, atlasCursor, i % 7, i / 7, 6)
                atlasCursor += 1
            }
            else {
                val i = if (i < 41) i else i + 1 // to compensate the discontinuity between 40th and 41st tile
                _drawToAtlantes(pixmap, atlasCursor, i % txOfPixmap, i / txOfPixmap, 0)
                _drawToAtlantes(glow, atlasCursor, i % txOfPixmapGlow, i / txOfPixmapGlow, 6)
                atlasCursor += 1
            }
        }
    }

    /**
     * mode: 0 for all the atlantes, 1-4 for summer/autumn/winter/spring atlas
     */
    private fun _drawToAtlantes(pixmap: Pixmap, destTileNum: Int, srcTileX: Int, srcTileY: Int, mode: Int) {
        if (mode == 0) {
            _drawToAtlantes(pixmap, destTileNum, srcTileX, srcTileY, 1)
            _drawToAtlantes(pixmap, destTileNum, srcTileX, srcTileY, 2)
            _drawToAtlantes(pixmap, destTileNum, srcTileX, srcTileY, 3)
            _drawToAtlantes(pixmap, destTileNum, srcTileX, srcTileY, 4)
        }
        else {
            val atlasX = (destTileNum % TILES_IN_X) * TILE_SIZE
            val atlasY = (destTileNum / TILES_IN_X) * TILE_SIZE
            val sourceX = srcTileX * TILE_SIZE
            val sourceY = srcTileY * TILE_SIZE

            //if (mode == 1) printdbg(this, "atlaspos: ($atlasX, $atlasY), srcpos: ($sourceX, $sourceY), srcpixmap = $pixmap")

            when (mode) {
                1 -> atlas.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
                2 -> atlasAutumn.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
                3 -> atlasWinter.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
                4 -> atlasSpring.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
                5 -> atlasFluid.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
                6 -> atlasGlow.drawPixmap(pixmap, sourceX, sourceY, TILE_SIZE, TILE_SIZE, atlasX, atlasY, TILE_SIZE, TILE_SIZE)
            }
        }
    }

    data class RenderTag(val tileNumber: Int, val connectionType: Int, val maskType: Int) {
        companion object {
            const val CONNECT_MUTUAL = 0
            const val CONNECT_SELF = 1
            const val CONNECT_WALL_STICKER = 2
            const val CONNECT_WALL_STICKER_CONNECT_SELF = 3

            const val MASK_NA = 0
            const val MASK_16 = 1
            const val MASK_47 = 2
            const val MASK_TORCH = 3
            const val MASK_PLATFORM = 4

            fun maskTypeToTileCount(maskType: Int) = when (maskType) {
                MASK_NA -> 1
                MASK_16 -> 16
                MASK_47 -> 47
                MASK_TORCH -> 4
                MASK_PLATFORM -> 8
                else -> throw IllegalArgumentException("Unknown maskType: $maskType")
            }
        }
    }

    fun dispose() {
        atlas.dispose()
        atlasAutumn.dispose()
        atlasWinter.dispose()
        atlasSpring.dispose()
        atlasFluid.dispose()
        atlasGlow.dispose()

        nullTile.dispose()
    }
}