package ru.dbotthepony.kstarbound.server.world

import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.chainOptionalFutures
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.VersionRegistry
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.json.VersionedJson
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.entities.AbstractEntity
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.ByteArrayInputStream
import java.io.Closeable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CompletableFuture
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream

abstract class WorldStorage : Closeable {
	data class Metadata(val geometry: WorldGeometry, val data: VersionedJson)

	abstract fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Pair<Object2DArray<out AbstractCell>, ChunkState>>>
	abstract fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>>
	abstract fun loadMetadata(): CompletableFuture<KOptional<Metadata>>

	protected fun readEntities(pos: ChunkPos, data: ByteArray): List<AbstractEntity> {
		val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data))))
		val i = reader.readVarInt()
		val objects = ArrayList<AbstractEntity>()

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

			val type = EntityType.entries.firstOrNull { it.storeName == obj.id }

			if (type != null) {
				try {
					objects.add(type.fromStorage(obj.content as JsonObject))
				} catch (err: Throwable) {
					LOGGER.error("Unable to deserialize entity in chunk $pos", err)
				}
			} else {
				LOGGER.error("Unknown entity type in chunk $pos: ${obj.id}")
			}
		}

		reader.close()
		return objects
	}

	protected fun writeEntities(entities: Collection<AbstractEntity>): ByteArray {
		val buff = FastByteArrayOutputStream()
		val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(buff)))

		stream.writeVarInt(entities.size)

		for (entity in entities) {
			Starbound.storeJson {
				val data = JsonObject()
				entity.serialize(data)
				VersionRegistry.make(entity.type.storeName, data).write(stream)
			}
		}

		stream.close()
		return buff.array.copyOf(buff.length)
	}

	open fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>): Boolean {
		return false
	}

	open fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>, state: ChunkState): Boolean {
		return false
	}

	open fun saveMetadata(data: Metadata): Boolean {
		return false
	}

	override fun close() {

	}

	private class Fixed(private val cell: ImmutableCell) : WorldStorage() {
		override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Pair<Object2DArray<out AbstractCell>, ChunkState>>> {
			return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, cell) to ChunkState.FULL))
		}

		override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
			return CompletableFuture.completedFuture(KOptional.of(emptyList()))
		}

		override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
			return CompletableFuture.completedFuture(KOptional())
		}
	}

	object Nothing : WorldStorage() {
		override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Pair<Object2DArray<out AbstractCell>, ChunkState>>> {
			return CompletableFuture.completedFuture(KOptional())
		}

		override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
			return CompletableFuture.completedFuture(KOptional())
		}

		override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
			return CompletableFuture.completedFuture(KOptional())
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		val NULL: WorldStorage = Fixed(AbstractCell.NULL)
		val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY)
	}

	class Dispatch(vararg storage: WorldStorage) : WorldStorage() {
		private val children = ArrayList<WorldStorage>()

		init {
			storage.forEach { children.add(it) }
		}

		override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Pair<Object2DArray<out AbstractCell>, ChunkState>>> {
			return chainOptionalFutures(children) { it.loadCells(pos) }
		}

		override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
			return chainOptionalFutures(children) { it.loadEntities(pos) }
		}

		override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
			return chainOptionalFutures(children) { it.loadMetadata() }
		}

		override fun saveEntities(pos: ChunkPos, data: Collection<AbstractEntity>): Boolean {
			return children.any { it.saveEntities(pos, data) }
		}

		override fun saveCells(pos: ChunkPos, data: Object2DArray<out AbstractCell>, state: ChunkState): Boolean {
			return children.any { it.saveCells(pos, data, state) }
		}

		override fun saveMetadata(data: Metadata): Boolean {
			return children.any { it.saveMetadata(data) }
		}

		override fun close() {
			children.forEach { it.close() }
		}
	}
}
