package ru.dbotthepony.kstarbound.server.world

import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CompletableFuture
import java.util.function.Function
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream

class LegacyWorldStorage(private val engine: StorageEngine) : WorldStorage() {
	override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Pair<Object2DArray<out AbstractCell>, ChunkState>>> {
		val chunkX = pos.x
		val chunkY = pos.y
		val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())

		return engine.load(key).thenApplyAsync(Function {
			it.map {
				val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
				val generationLevel = reader.readVarInt()
				val tileSerializationVersion = reader.readVarInt()

				val state = when (generationLevel) {
					0 -> ChunkState.EMPTY
					1 -> ChunkState.TERRAIN
					2 -> ChunkState.MICRO_DUNGEONS
					3 -> ChunkState.CAVE_LIQUID
					4 -> ChunkState.FULL
					else -> ChunkState.FULL
				}

				val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE)

				for (y in 0 until CHUNK_SIZE) {
					for (x in 0 until CHUNK_SIZE) {
						result[x, y] = MutableCell().readLegacy(reader, tileSerializationVersion).immutable()
					}
				}

				reader.close()
				result as Object2DArray<out AbstractCell> to state
			}
		}, Starbound.EXECUTOR)
	}

	override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
		val chunkX = pos.x
		val chunkY = pos.y
		val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())

		return engine.load(key).thenApplyAsync(Function {
			it.map { readEntities(pos, it) }
		}, Starbound.EXECUTOR)
	}

	override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
		return engine.load(metadataKey).thenApplyAsync(Function {
			it.flatMap {
				val stream = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it))))

				val width = stream.readInt()
				val height = stream.readInt()

				val json = VersionedJson(stream)

				KOptional(Metadata(WorldGeometry(Vector2i(width, height), true, false), json))
			}
		}, Starbound.EXECUTOR)
	}

	override fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>): Boolean {
		val chunkX = pos.x
		val chunkY = pos.y
		val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())

		engine.write(key, writeEntities(data))
		return true
	}

	override fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>, state: ChunkState): Boolean {
		val buff = FastByteArrayOutputStream()
		val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(buff)))

		stream.writeVarInt(when (state) {
			ChunkState.FRESH -> 0
			ChunkState.EMPTY -> 0
			ChunkState.TERRAIN -> 1
			ChunkState.MICRO_DUNGEONS -> 2
			ChunkState.CAVE_LIQUID -> 3
			ChunkState.FULL -> 4
		})

		stream.writeVarInt(418)

		for (y in 0 until CHUNK_SIZE) {
			for (x in 0 until CHUNK_SIZE) {
				val cell = data.getOrNull(x, y) ?: AbstractCell.NULL
				cell.writeLegacy(stream)
			}
		}

		val chunkX = pos.x
		val chunkY = pos.y
		val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())

		stream.close()
		engine.write(key, buff.array.copyOf(buff.length))
		return true
	}

	override fun saveMetadata(data: Metadata): Boolean {
		val buff = FastByteArrayOutputStream()
		val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(buff)))

		stream.writeInt(data.geometry.size.x)
		stream.writeInt(data.geometry.size.y)
		data.data.write(stream)

		stream.close()
		engine.write(metadataKey, buff.array.copyOf(buff.length))
		return true
	}

	override fun close() {
		engine.close()
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private val metadataKey = ByteKey(0, 0, 0, 0, 0)
	}
}
