package net.torvald.terrarum.modulebasegame.serialise

import net.torvald.gdx.graphics.PixmapIO2
import net.torvald.terrarum.*
import net.torvald.terrarum.gameworld.GameWorld.Companion.CHUNK_LOADED
import net.torvald.terrarum.modulebasegame.IngameRenderer
import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulebasegame.gameactors.FixtureBase
import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer
import net.torvald.terrarum.modulebasegame.gameactors.Pocketed
import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.savegame.*
import net.torvald.terrarum.serialise.Common
import java.io.File
import java.util.zip.GZIPOutputStream
import kotlin.experimental.and

/**
 * Created by minjaesong on 2021-09-14.
 */
class WorldSavingThread(
    val time_t: Long,
    val disk: VirtualDisk,
    val outFile: File,
    val ingame: TerrarumIngame,
    val isAuto: Boolean,
    val callback: () -> Unit,
    val errorHandler: (Throwable) -> Unit
) : SavingThread(errorHandler) {
    /**
     * Will happily overwrite existing entry
     */
    private fun addFile(disk: VirtualDisk, file: DiskEntry) {
        disk.entries[file.entryID] = file
        file.parentEntryID = 0
        val dir = VDUtil.getAsDirectory(disk, 0)
        if (!dir.contains(file.entryID)) dir.add(file.entryID)
    }


    override fun save() {
        App.printdbg(this, "outFile: ${outFile.path}")

        disk.saveMode = 2 * isAuto.toInt() // no quick
        disk.saveKind = VDSaveKind.WORLD_DATA
        disk.saveOrigin = disk.saveOrigin and 15 // remove flag "imported" if applicable
        disk.snapshot = TerrarumAppConfiguration.VERSION_SNAPSHOT

        // wait for screencap
        var emergencyStopCnt = 0
        while (IngameRenderer.screencapBusy) {
//            printdbg(this, "spinning for screencap to be taken")
            Thread.sleep(4L)
            emergencyStopCnt += 1
            if (emergencyStopCnt >= SCREENCAP_WAIT_TRY_MAX) throw InterruptedException("Waiting screencap to be taken for too long")
        }

        val allTheActors = ingame.actorContainerActive.cloneToList() + ingame.actorContainerInactive.cloneToList()

        val playersList: List<IngamePlayer> = allTheActors.filterIsInstance<IngamePlayer>()
        val actorsList = allTheActors.filter { WriteWorld.actorAcceptable(it) }
        val layers = intArrayOf(0,1,2).map { ingame.world.getLayer(it) }
        val cw = ingame.world.width / LandUtil.CHUNK_W
        val ch = ingame.world.height / LandUtil.CHUNK_H

        WriteSavegame.saveProgress = 0f
        WriteSavegame.saveProgressMax = 3f + (cw * ch * layers.size) + actorsList.size


        val tgaout = ByteArray64GrowableOutputStream()
        val gzout = GZIPOutputStream(tgaout)

        App.printdbg(this, "Writing metadata...")

        val creation_t = ingame.world.creationTime


        // Write subset of Ingame.ItemCodex
        // The existing ItemCodex must be rewritten to clear out obsolete records

        // We're assuming the dynamic item generated by players does exist in the world, and it's recorded
        // into the world's dynamicToStaticTable, therefore every item recorded into the world's dynamicToStaticTable
        // can be found in this world without need to look up the players
        ingame.world.dynamicToStaticTable.clear()
        ingame.world.dynamicItemInventory.clear()
        actorsList.filterIsInstance<Pocketed>().forEach { actor ->
            actor.inventory.forEach { (itemid, _) ->

                App.printdbg(this, "World side dynamicitem: $itemid contained in $actor")

                if (itemid.startsWith("${ReferencingRanges.PREFIX_DYNAMICITEM}:")) {
                    ingame.world.dynamicToStaticTable[itemid] = ItemCodex.dynamicToStaticID(itemid)
                    ingame.world.dynamicItemInventory[itemid] = ItemCodex[itemid]!!
                }
            }
        }
        actorsList.filterIsInstance<FixtureBase>().forEach { fixture ->
            fixture.inventory?.forEach { (itemid, _) ->

                App.printdbg(this, "World side dynamicitem: $itemid contained in $fixture")

                if (itemid.startsWith("${ReferencingRanges.PREFIX_DYNAMICITEM}:")) {
                    ingame.world.dynamicToStaticTable[itemid] = ItemCodex.dynamicToStaticID(itemid)
                    ingame.world.dynamicItemInventory[itemid] = ItemCodex[itemid]!!
                }
            }
        }



        PixmapIO2._writeTGA(gzout, IngameRenderer.fboRGBexport, true, true)
        IngameRenderer.fboRGBexport.dispose()

        val thumbContent = EntryFile(tgaout.toByteArray64())
        val thumb = DiskEntry(VDFileID.WORLD_SCREENSHOT, VDFileID.ROOT, creation_t, time_t, thumbContent)
        addFile(disk, thumb)



        WriteSavegame.saveProgress += 1f

        // Write World //

        val worldMeta = EntryFile(WriteWorld.encodeToByteArray64(ingame, time_t, actorsList, playersList))
        val world = DiskEntry(VDFileID.SAVEGAMEINFO, VDFileID.ROOT, creation_t, time_t, worldMeta)
        addFile(disk, world)

        WriteSavegame.saveProgress += 1f


        for (layer in layers.indices) {
            for (cx in 0 until cw) {
                for (cy in 0 until ch) {
                    val chunkFlag = ingame.world.chunkFlags[cy][cx]
                    val chunkNumber = LandUtil.chunkXYtoChunkNum(ingame.world, cx, cy).toLong()

                    if (chunkFlag and 0x7F == CHUNK_LOADED) {
//                    Echo("Writing chunks... ${(cw*ch*layer) + chunkNumber + 1}/${cw*ch*layers.size}")

                        val chunkBytes = WriteWorld.encodeChunk(layers[layer]!!, cx, cy)
                        val entryID = 0x1_0000_0000L or layer.toLong().shl(24) or chunkNumber

                        val entryContent = EntryFile(chunkBytes)
                        val entry = DiskEntry(entryID, VDFileID.ROOT, creation_t, time_t, entryContent)
                        // "W1L0-92,15"
                        addFile(disk, entry)
                    }

                    WriteSavegame.saveProgress += 1
                }
            }
        }


        // Write Actors //
        actorsList.forEachIndexed { count, it ->
//            Echo("Writing actors... ${count+1}/${actorsList.size}")

            val actorContent = EntryFile(WriteActor.encodeToByteArray64(it))
            val actor = DiskEntry(it.referenceID.toLong(), VDFileID.ROOT, creation_t, time_t, actorContent)
            addFile(disk, actor)

            WriteSavegame.saveProgress += 1
        }


        // write loadorder //
        val loadOrderBa64Writer = ByteArray64Writer(Common.CHARSET)
        loadOrderBa64Writer.write(ModMgr.getLoadOrderTextForSavegame())
        loadOrderBa64Writer.flush(); loadOrderBa64Writer.close()
        val loadOrderText = loadOrderBa64Writer.toByteArray64()
        val loadOrderContents = EntryFile(loadOrderText)
        addFile(disk, DiskEntry(VDFileID.LOADORDER, VDFileID.ROOT, creation_t, time_t, loadOrderContents))



//        Echo("Writing file to disk...")

        disk.entries[0]!!.creationDate = creation_t
        disk.entries[0]!!.modificationDate = time_t
        // entry zero MUST NOT be used to get lastPlayDate, but we'll update it anyway
        // use entry -1 for that purpose!
        disk.capacity = 0

        VDUtil.dumpToRealMachine(disk, outFile)



        App.printdbg(this, "Game saved with size of ${outFile.length()} bytes")


//        IngameRenderer.screencapBusy = false
        WriteSavegame.savingStatus = 255


        callback()
    }
}