package ru.dbotthepony.kstarbound.server.world

import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.util.CarriedExecutor
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
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.ByteArrayInputStream
import java.io.Closeable
import java.io.DataInputStream
import java.util.concurrent.CompletableFuture
import java.util.function.Function
import java.util.function.Supplier
import java.util.zip.InflaterInputStream

class LegacyWorldStorage(val loader: Loader) : WorldStorage() {
	fun interface Loader : Closeable {
		operator fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>>

		override fun close() {

		}
	}

	override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
		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 loader(key).thenApplyAsync(Function {
			it.map {
				val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
				val generationLevel = reader.readVarInt()
				val tileSerializationVersion = reader.readVarInt()

				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>
			}
		}, 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 loader(key).thenApplyAsync(Function {
			it.map {
				val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
				val i = reader.readVarInt()
				val objects = ArrayList<AbstractEntity>()

				for (i2 in 0 until i) {
					val obj = VersionedJson(reader)

					if (obj.identifier == "ObjectEntity") {
						try {
							objects.add(WorldObject.fromJson(obj.content.asJsonObject))
						} catch (err: Throwable) {
							LOGGER.error("Unable to deserialize entity in chunk $pos", err)
						}
					} else {
						LOGGER.error("Unknown entity type in chunk $pos: ${obj.identifier}")
					}
				}

				reader.close()
				objects
			}
		}, Starbound.EXECUTOR)
	}

	override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
		return loader(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 close() {
		loader.close()
	}

	companion object {
		private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
		private val LOGGER = LogManager.getLogger()
		private val metadataKey = ByteKey(0, 0, 0, 0, 0)

		fun file(file: BTreeDB5): LegacyWorldStorage {
			val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)

			val loader = object : Loader {
				override fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>> {
					return CompletableFuture.supplyAsync(Supplier { file.read(at) }, carrier)
				}

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

			return LegacyWorldStorage(loader)
		}

		fun memory(backing: Map<ByteKey, KOptional<ByteArray>>): LegacyWorldStorage {
			return LegacyWorldStorage { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) }
		}
	}
}
