package net.torvald.terrarum.modulebasegame.serialise

import com.badlogic.gdx.graphics.Pixmap
import net.torvald.terrarum.*
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.console.Echo
import net.torvald.terrarum.gameworld.BlockLayerI16
import net.torvald.terrarum.gameworld.BlockLayerI16F16
import net.torvald.terrarum.gameworld.BlockLayerI16I8
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.gameworld.GameWorld.Companion.CHUNK_LOADED
import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.FancyWorldReadLoadScreen
import net.torvald.terrarum.modulebasegame.IngameRenderer
import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.savegame.*
import net.torvald.terrarum.savegame.VDFileID.SAVEGAMEINFO
import net.torvald.terrarum.serialise.Common
import net.torvald.terrarum.worlddrawer.WorldCamera
import java.io.File
import java.io.Reader
import java.util.logging.Level
import kotlin.experimental.or

/**
 * It's your responsibility to create a new VirtualDisk if your save is new, and create a backup for modifying existing save.
 *
 * Created by minjaesong on 2021-09-03.
 */
object WriteSavegame {

    enum class SaveMode {
        META, PLAYER, WORLD, SHARED, QUICK_WORLD
    }

    @Volatile var savingStatus = -1 // -1: not started, 0: saving in progress, 255: saving finished
    @Volatile var saveProgress = 0f
    @Volatile var saveProgressMax = 1f

    private fun getSaveThread(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) = when (mode) {
        SaveMode.WORLD -> WorldSavingThread(time_t, disk, outFile, ingame, isAuto, callback, errorHandler)
        SaveMode.PLAYER -> PlayerSavingThread(time_t, disk, outFile, ingame, isAuto, callback, errorHandler)
        SaveMode.QUICK_WORLD -> QuickSingleplayerWorldSavingThread(time_t, disk, outFile, ingame, isAuto, callback, errorHandler)
        else -> throw IllegalArgumentException("$mode")
    }

    private fun installScreencap() {
        IngameRenderer.screencapExportCallback = { fb ->
            printdbg(this, "Generating thumbnail...")

            val w = 960
            val h = 640

            val cx = /*1-*/(WorldCamera.x % 2)
            val cy = /*1-*/(WorldCamera.y % 2)

            val x = (fb.width - w) / 2 - cx // force the even-numbered position
            val y = (fb.height - h) / 2 - cy // force the even-numbered position

            val p = Pixmap.createFromFrameBuffer(x, y, w, h)
            IngameRenderer.fboRGBexport = p
            //PixmapIO2._writeTGA(gzout, p, true, true)
            //p.dispose()

            printdbg(this, "Done thumbnail generation")
        }
    }

    operator fun invoke(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) {
        savingStatus = 0
        printdbg(this, "Save queued")

        installScreencap()
        try { printdbg(this, "ScreencapExport installed: ${IngameRenderer.screencapExportCallback}") }
        catch (e: UninitializedPropertyAccessException) { printdbg(this, "ScreencapExport installed: no") }
        IngameRenderer.requestScreencap()

        val savingThread = Thread(getSaveThread(time_t, mode, disk, outFile, ingame, isAuto, errorHandler, callback), "TerrarumBasegameGameSaveThread")
        savingThread.start()

        // it is caller's job to keep the game paused or keep a "save in progress" ui up
        // use callback to fire the after-the-saving-progress job
    }


    fun immediate(time_t: Long, mode: SaveMode, disk: VirtualDisk, outFile: File, ingame: TerrarumIngame, isAuto: Boolean, errorHandler: (Throwable) -> Unit, callback: () -> Unit) {
        savingStatus = 0
        printdbg(this, "Immediate save fired")

        installScreencap()
        try { printdbg(this, "ScreencapExport installed: ${IngameRenderer.screencapExportCallback}") }
        catch (e: UninitializedPropertyAccessException) { printdbg(this, "ScreencapExport installed: no") }
        IngameRenderer.requestScreencap()

        val savingThread = Thread(getSaveThread(time_t, mode, disk, outFile, ingame, isAuto, errorHandler, callback), "TerrarumBasegameGameSaveThread")
        savingThread.start()

        // it is caller's job to keep the game paused or keep a "save in progress" ui up
        // use callback to fire the after-the-saving-progress job
    }

}




/**
 * Load and setup the game for the first load.
 *
 * To load additional actors/worlds, use ReadActor/ReadWorld.
 *
 * Created by minjaesong on 2021-09-03.
 */
object LoadSavegame {

    fun getWorldName(worldDisk: SimpleFileSystem) = worldDisk.getDiskName(Common.CHARSET)
    fun getWorldSavefileName(world: GameWorld) = "${world.worldIndex}"
    fun getPlayerSavefileName(player: IngamePlayer) = "${player.uuid}"

    fun getFileBytes(disk: SimpleFileSystem, id: Long): ByteArray64 = disk.getFile(id)!!.bytes
    fun getFileReader(disk: SimpleFileSystem, id: Long): Reader = ByteArray64Reader(getFileBytes(disk, id), Common.CHARSET)

    operator fun invoke(diskPair: DiskPair) = invoke(diskPair.player, diskPair.world)

    private val getGenver = Regex("""(?<="genver" ?: ?)[0-9]+""")

    /**
     * @param playerDisk DiskSkimmer representing the Player.
     * @param worldDisk0 DiskSkimmer representing the World to be loaded.
     *     If unset, last played world for the Player will be loaded.
     */
    operator fun invoke(playerDisk: DiskSkimmer, worldDisk0: DiskSkimmer? = null) {
        val newIngame = TerrarumIngame(App.batch)
        playerDisk.rebuild()

        val playerDiskSavegameInfo = ByteArray64Reader(playerDisk.getFile(SAVEGAMEINFO)!!.bytes, Common.CHARSET)

        val player = ReadActor.invoke(playerDisk, playerDiskSavegameInfo) as IngamePlayer

        printdbg(this, "Player localhash: ${player.localHashStr}, hasSprite: ${player.sprite != null}")

        val currentWorldId = player.worldCurrentlyPlaying
        val worldDisk = worldDisk0 ?: App.savegameWorlds[currentWorldId]!!.loadable()
        worldDisk.rebuild()
        val worldDiskSavegameInfo = ByteArray64Reader(worldDisk.getFile(SAVEGAMEINFO)!!.bytes, Common.CHARSET)
        val world = ReadWorld(worldDiskSavegameInfo, worldDisk.diskFile)


        world.layerTerrain = BlockLayerI16(world.width, world.height)
        world.layerWall = BlockLayerI16(world.width, world.height)
        world.layerOres = BlockLayerI16I8(world.width, world.height)
        world.layerFluids = BlockLayerI16F16(world.width, world.height)
        world.chunkFlags = Array(world.height / LandUtil.CHUNK_H) { ByteArray(world.width / LandUtil.CHUNK_W) }

        newIngame.world = world // must be set before the loadscreen, otherwise the loadscreen will try to read from the NullWorld which is already destroyed
        newIngame.worldDisk =  VDUtil.readDiskArchive(worldDisk.diskFile, Level.INFO)
        newIngame.playerDisk = VDUtil.readDiskArchive(playerDisk.diskFile, Level.INFO)
        newIngame.worldName = getWorldName(worldDisk)
        newIngame.worldSavefileName = getWorldSavefileName(world)
        newIngame.playerSavefileName = getPlayerSavefileName(player)

//        worldDisk.dispose()
//        playerDisk.dispose()


        val loadJob = { it: LoadScreenBase ->
            val loadscreen = it as FancyWorldReadLoadScreen
            loadscreen.addMessage(Lang["MENU_IO_LOADING"])


            val worldGenver = CharArray(128).let {
                worldDiskSavegameInfo.read(it, 0, 128)
                getGenver.find(String(it))?.value?.toLong()!!
            }
            val playerGenver = CharArray(128).let {
                playerDiskSavegameInfo.read(it, 0, 128)
                getGenver.find(String(it))?.value?.toLong()!!
            }


            val actors = world.actors.distinct()
            val worldParam = TerrarumIngame.Codices(newIngame.worldDisk, world, actors, player, worldGenver, playerGenver) {}


            newIngame.gameLoadInfoPayload = worldParam
            newIngame.gameLoadMode = TerrarumIngame.GameLoadMode.LOAD_FROM

            printdbg(this, "World dim: ${world.width}x${world.height}, ${world.width / LandUtil.CHUNK_W}x${world.height / LandUtil.CHUNK_H}")

            // load all the world blocklayer chunks
            val cw = LandUtil.CHUNK_W
            val ch = LandUtil.CHUNK_H
            val chunkCount = world.width * world.height / (cw * ch)
            val worldLayer = intArrayOf(0,1,2).map { world.getLayer(it) }
            for (chunk in 0L until chunkCount) {
                for (layer in worldLayer.indices) {
                    loadscreen.addMessage(Lang["MENU_IO_LOADING"])

                    newIngame.worldDisk.getFile(0x1_0000_0000L or layer.toLong().shl(24) or chunk)?.let { chunkFile ->
                        val (cx, cy) = LandUtil.chunkNumToChunkXY(world, chunk.toInt())

                        ReadWorld.decodeChunkToLayer(chunkFile.getContent(), worldLayer[layer]!!, cx, cy)
                        world.chunkFlags[cy][cx] = world.chunkFlags[cy][cx] or CHUNK_LOADED
                    }
                }
                loadscreen.progress.getAndAdd(1)
            }

            loadscreen.addMessage(Lang["MENU_IO_LOAD_UPDATING_BLOCK_MAPPINGS"])
            world.renumberTilesAfterLoad()


            Echo("${ccW}World loaded: $ccY${newIngame.worldDisk.getDiskName(Common.CHARSET)}")
            printdbg(this, "World loaded: ${newIngame.worldDisk.getDiskName(Common.CHARSET)}")
        }

        val loadScreen = FancyWorldReadLoadScreen(newIngame, world.width, world.height, loadJob)
        Terrarum.setCurrentIngameInstance(newIngame)
        App.setLoadScreen(loadScreen)
    }

}