package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readNullable
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates

// uint8_t
enum class BeamUpRule(override val jsonName: String) : IStringSerializable {
	NOWHERE("Nowhere"),
	SURFACE("Surface"),
	ANYWHERE("Anywhere"),
	ANYWHERE_WITH_WARNING("AnywhereWithWarning");
}

// uint8_t
enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializable {
	NONE("None"),
	TOP("Top"),
	BOTTOM("Bottom"),
	TOP_AND_BOTTOM("TopAndBottom");
}

// uint8_t
enum class VisitableWorldParametersType(override val jsonName: String, val token: TypeToken<out VisitableWorldParameters>) : IStringSerializable {
	TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)),
	ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)),
	FLOATING_DUNGEON("FloatingDungeonWorldParameters", TypeToken.get(FloatingDungeonWorldParameters::class.java));

	companion object : TypeAdapterFactory {
		val ADAPTER = DispatchingAdapter("type", { type }, { token }, entries)

		override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) {
				return object : TypeAdapter<VisitableWorldParameters>() {
					override fun write(out: JsonWriter, value: VisitableWorldParameters?) {
						if (value == null)
							out.nullValue()
						else
							out.value(value.toJson())
					}

					override fun read(`in`: JsonReader): VisitableWorldParameters? {
						if (`in`.consumeNull())
							return null

						val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters
						instance.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
						return instance
					}
				} as TypeAdapter<T>
			}

			return null
		}
	}
}

// using notnull delegate because this will look very ugly
// if done as immutable class
abstract class VisitableWorldParameters {
	var threatLevel: Double = 0.0
	var typeName: String by Delegates.notNull()
		protected set
	var worldSize: Vector2i by Delegates.notNull()
		protected set
	var gravity: Vector2d = Vector2d.ZERO
		protected set
	var airless: Boolean = false
		protected set
	var environmentStatusEffects: Set<String> by Delegates.notNull()
		protected set
	var overrideTech: Set<String>? = null
		protected set
	var globalDirectives: Set<String>? = null
		protected set
	var beamUpRule: BeamUpRule by Delegates.notNull()
	var disableDeathDrops: Boolean = false
	var terraformed: Boolean = false
	var worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.NONE
		protected set
	var weatherPool: WeightedList<String>? = null
		protected set

	fun createLayout(seed: Long): WorldLayout {
		return createLayout(random(seed))
	}

	abstract fun createLayout(random: RandomGenerator): WorldLayout

	abstract val type: VisitableWorldParametersType

	// lazy but effective, and less error prone
	@JsonFactory
	data class StoreData(
		val threatLevel: Double,
		val typeName: String,
		val worldSize: Vector2i,
		val gravity: Either<Double, Vector2d>,
		val airless: Boolean,
		val environmentStatusEffects: Set<String>,
		val overrideTech: Set<String>?,
		val globalDirectives: Set<String>?,
		val beamUpRule: BeamUpRule,
		val disableDeathDrops: Boolean,
		val terraformed: Boolean,
		val worldEdgeForceRegions: WorldEdgeForceRegion,
		val weatherPool: WeightedList<String>?,
	)

	open fun fromJson(data: JsonObject) {
		val read = Starbound.gson.fromJson(data, StoreData::class.java)

		this.threatLevel = read.threatLevel
		this.typeName = read.typeName
		this.worldSize = read.worldSize
		this.gravity = read.gravity.map({ Vector2d(y = it) }, { it })
		this.airless = read.airless
		this.environmentStatusEffects = read.environmentStatusEffects
		this.overrideTech = read.overrideTech
		this.globalDirectives = read.globalDirectives
		this.beamUpRule = read.beamUpRule
		this.disableDeathDrops = read.disableDeathDrops
		this.terraformed = read.terraformed
		this.worldEdgeForceRegions = read.worldEdgeForceRegions
		this.weatherPool = read.weatherPool
	}

	open fun toJson(data: JsonObject, isLegacy: Boolean = Starbound.IS_LEGACY_JSON) {
		val store = StoreData(
			threatLevel,
			typeName,
			worldSize,
			if (isLegacy) Either.left(gravity.y) else Either.right(gravity),
			airless,
			environmentStatusEffects,
			overrideTech,
			globalDirectives,
			beamUpRule,
			disableDeathDrops,
			terraformed,
			worldEdgeForceRegions,
			weatherPool,
		)

		for ((k, v) in (Starbound.gson.toJsonTree(store) as JsonObject).entrySet()) {
			data[k] = v
		}

		data["type"] = type.jsonName
	}

	fun toJson(isLegacy: Boolean = Starbound.IS_LEGACY_JSON): JsonObject {
		val data = JsonObject()
		toJson(data, isLegacy)
		return data
	}

	// called only for legacy protocol
	protected open fun read0(stream: DataInputStream) {
		typeName = stream.readInternedString()
		threatLevel = stream.readFloat().toDouble()
		worldSize = stream.readVector2i()
		gravity = Vector2d(y = stream.readFloat().toDouble())
		airless = stream.readBoolean()

		val collection = stream.readCollection { readDouble() to readInternedString() }

		if (collection.isNotEmpty())
			weatherPool = WeightedList(ImmutableList.copyOf(collection))

		environmentStatusEffects = ImmutableSet.copyOf(stream.readCollection { readInternedString() })
		overrideTech = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) }
		globalDirectives = stream.readNullable { ImmutableSet.copyOf(readCollection { readInternedString() }) }

		beamUpRule = BeamUpRule.entries[stream.readUnsignedByte()]
		disableDeathDrops = stream.readBoolean()
		terraformed = stream.readBoolean()
		worldEdgeForceRegions = WorldEdgeForceRegion.entries[stream.readUnsignedByte()]
	}

	// called only for legacy protocol
	protected open fun write0(stream: DataOutputStream) {
		stream.writeBinaryString(typeName)
		stream.writeFloat(threatLevel.toFloat())
		stream.writeStruct2i(worldSize)
		stream.writeFloat(gravity.y.toFloat())
		stream.writeBoolean(airless)

		if (weatherPool == null)
			stream.writeByte(0)
		else
			stream.writeCollection(weatherPool!!.parent) { writeDouble(it.first); writeBinaryString(it.second) }

		stream.writeCollection(environmentStatusEffects) { writeBinaryString(it) }
		stream.writeNullable(overrideTech) { writeCollection(it) { writeBinaryString(it) } }
		stream.writeNullable(globalDirectives) { writeCollection(it) { writeBinaryString(it) } }
		stream.writeByte(beamUpRule.ordinal)
		stream.writeBoolean(disableDeathDrops)
		stream.writeBoolean(terraformed)
		stream.writeByte(worldEdgeForceRegions.ordinal)
	}

	// because writing as json is too easy.
	// Tard.
	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		if (isLegacy) {
			val wrapper = FastByteArrayOutputStream()
			wrapper.write(type.ordinal)
			write0(DataOutputStream(wrapper))
			stream.writeByteArray(wrapper.array, 0, wrapper.length)
		} else {
			stream.writeJsonObject(toJson())
		}
	}

	companion object {
		fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): VisitableWorldParameters? {
			if (isLegacy) {
				val readData = stream.readByteArray()

				if (readData.isEmpty()) {
					return null
				}

				val data = DataInputStream(FastByteArrayInputStream(readData))

				val create = when (VisitableWorldParametersType.entries[data.readUnsignedByte()]) {
					VisitableWorldParametersType.TERRESTRIAL -> TerrestrialWorldParameters()
					VisitableWorldParametersType.ASTEROIDS -> AsteroidsWorldParameters()
					VisitableWorldParametersType.FLOATING_DUNGEON -> FloatingDungeonWorldParameters()
				}

				create.read0(data)
				return create
			} else {
				if (!stream.readBoolean())
					return null

				return Starbound.gson.fromJson(stream.readJsonObject(), VisitableWorldParameters::class.java)
			}
		}
	}
}
