package ru.dbotthepony.kstarbound.defs

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID

@JsonAdapter(WorldID.Adapter::class)
sealed class WorldID {
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	val isLimbo: Boolean get() = this is Limbo

	object Limbo : WorldID() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(0)
		}

		override fun toString(): String {
			return "Nowhere"
		}
	}

	data class Celestial(val pos: UniversePos) : WorldID() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			pos.write(stream, isLegacy)
		}

		override fun toString(): String {
			return "CelestialWorld:$pos"
		}
	}

	data class ShipWorld(val uuid: UUID) : WorldID() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)
			stream.writeUUID(uuid)
		}

		override fun toString(): String {
			return "ClientShipWorld:${uuid.toStarboundString()}"
		}
	}

	data class Instance(val name: String, val uuid: UUID? = null, val threatLevel: Double? = null) : WorldID() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(3)
			stream.writeBinaryString(name)

			stream.writeBoolean(uuid != null)
			if (uuid != null) stream.writeUUID(uuid)

			stream.writeBoolean(threatLevel != null)
			if (threatLevel != null) {
				if (isLegacy) {
					stream.writeFloat(threatLevel.toFloat())
				} else {
					stream.writeDouble(threatLevel)
				}
			}
		}

		override fun toString(): String {
			return "InstanceWorld:$name:${uuid?.toStarboundString() ?: "-"}:${threatLevel ?: "-"}"
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<WorldID>() {
		override fun write(out: JsonWriter, value: WorldID) {
			out.value(value.toString())
		}

		override fun read(`in`: JsonReader): WorldID {
			return parse(`in`.nextString())
		}
	}

	companion object {
		val CODEC = nativeCodec(::read, WorldID::write)
		val LEGACY_CODEC = legacyCodec(::read, WorldID::write)

		fun parse(value: String): WorldID {
			if (value.isBlank())
				return Limbo

			val parts = value.split(':')

			return when (val type = parts[0].lowercase()) {
				"nowhere" -> Limbo
				"instanceworld" -> {
					val rest = parts[1].split(':')

					if (rest.isEmpty() || rest.size > 3) {
						throw IllegalArgumentException("Malformed InstanceWorld string: $value")
					}

					val name = rest[0]
					var uuid: UUID? = null
					var threatLevel: Double? = null

					if (rest.size > 1) {
						uuid = if (rest[1] == "-") null else uuidFromStarboundString(rest[1])

						if (rest.size > 2) {
							threatLevel = if (rest[2] == "-") null else rest[2].toDouble()

							if (threatLevel != null && threatLevel < 0.0)
								throw IllegalArgumentException("InstanceWorld threat level is negative: $value")
						}
					}

					Instance(name, uuid, threatLevel)
				}

				"celestialworld" -> Celestial(UniversePos.parse(parts[1]))
				"clientshipworld" -> ShipWorld(uuidFromStarboundString(parts[1]))
				else -> throw IllegalArgumentException("Invalid WorldID type: $type (input: $value)")
			}
		}

		fun read(stream: DataInputStream, isLegacy: Boolean): WorldID {
			return when (val type = stream.readUnsignedByte()) {
				0 -> Limbo
				1 -> Celestial(UniversePos(stream, isLegacy))
				2 -> ShipWorld(stream.readUUID())
				3 -> {
					val name = stream.readInternedString()
					val uuid = if (stream.readBoolean()) stream.readUUID() else null
					val level: Double?

					if (stream.readBoolean()) {
						if (isLegacy) {
							level = stream.readFloat().toDouble()
						} else {
							level = stream.readDouble()
						}
					} else {
						level = null
					}

					Instance(name, uuid, level)
				}
				else -> throw IllegalArgumentException("Unknown WorldID type $type!")
			}
		}
	}
}
