package ru.dbotthepony.kstarbound.world

import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVector3i
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct3i
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
import java.io.DataOutputStream

/**
 * Specifies coordinates to either a planetary system, a planetary body, or a
 * satellite around such a planetary body.  The terms here are meant to be very
 * generic, a "planetary body" could be an asteroid field, or a ship, or
 * anything in orbit around the center of mass of a specific planetary system.
 * The terms are really simply meant as a hierarchy of orbits.
 *
 * No validity checking is done here, any coordinate to any body whether it
 * exists in a specific universe or not can be expressed.
 */
@JsonAdapter(UniversePos.Adapter::class)
data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: Int = 0, val satelliteOrbit: Int = 0) : Comparable<UniversePos> {
	constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector3i(), if (isLegacy) stream.readInt() else stream.readVarInt(), if (isLegacy) stream.readInt() else stream.readVarInt())

	init {
		require(planetOrbit >= 0) { "Negative planetOrbit: $planetOrbit" }
		require(satelliteOrbit >= 0) { "Negative satelliteOrbit: $satelliteOrbit" }
	}

	override fun toString(): String {
		if (planetOrbit == 0 && satelliteOrbit == 0)
			return "${location.x}:${location.y}:${location.z}"
		else if (satelliteOrbit == 0)
			return "${location.x}:${location.y}:${location.z}:$planetOrbit"
		else
			return "${location.x}:${location.y}:${location.z}:$planetOrbit:$satelliteOrbit"
	}

	override fun compareTo(other: UniversePos): Int {
		var cmp = location.x.compareTo(other.location.x)
		if (cmp != 0) cmp = location.y.compareTo(other.location.y)
		if (cmp != 0) cmp = location.z.compareTo(other.location.z)
		if (cmp != 0) cmp = planetOrbit.compareTo(other.planetOrbit)
		if (cmp != 0) cmp = satelliteOrbit.compareTo(other.satelliteOrbit)
		return cmp
	}

	val isSystem: Boolean
		get() = planetOrbit == 0

	val isPlanet: Boolean
		get() = planetOrbit != 0 && satelliteOrbit == 0

	val isSatellite: Boolean
		get() = planetOrbit != 0 && satelliteOrbit != 0

	val orbitNumber: Int
		get() = if (isSatellite) satelliteOrbit else if (isPlanet) planetOrbit else 0

	fun system(): UniversePos {
		if (planetOrbit == 0 && satelliteOrbit == 0)
			return this

		return UniversePos(location)
	}

	fun planet(): UniversePos {
		if (satelliteOrbit == 0)
			return this

		return UniversePos(location, planetOrbit)
	}

	fun satellite(): UniversePos {
		return this
	}

	fun parent(): UniversePos {
		if (isSatellite)
			return planet()
		else if (isPlanet)
			return system()
		else
			return this
	}

	fun child(orbit: Int): UniversePos {
		if (isSatellite)
			throw IllegalStateException("Satellite can't have children!")
		else if (isPlanet)
			return UniversePos(location, planetOrbit, orbit)
		else
			return UniversePos(location, orbit)
	}

	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeStruct3i(location)

		if (isLegacy) {
			stream.writeInt(planetOrbit)
			stream.writeInt(satelliteOrbit)
		} else {
			stream.writeVarInt(planetOrbit)
			stream.writeVarInt(satelliteOrbit)
		}
	}

	class Adapter(gson: Gson) : TypeAdapter<UniversePos>() {
		private val vectors = gson.getAdapter(Vector3i::class.java)

		override fun write(out: JsonWriter, value: UniversePos) {
			out.beginObject()

			out.name("location")
			vectors.write(out, value.location)

			out.name("planet")
			out.value(value.planetOrbit)

			out.name("satellite")
			out.value(value.satelliteOrbit)

			out.endObject()
		}

		override fun read(`in`: JsonReader): UniversePos {
			if (`in`.peek() == JsonToken.BEGIN_OBJECT) {
				val values = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!
				val location = values.get("location", vectors)
				val planet = values.get("planet", 0)
				val orbit = values.get("orbit", 0)
				return UniversePos(location, planet, orbit)
			}

			if (`in`.peek() == JsonToken.STRING) {
				val read = `in`.nextString().trim()

				if (read == "" || read.lowercase() == "")
					return ZERO
				else {
					try {
						return parse(read)
					} catch (err: Throwable) {
						throw JsonSyntaxException("Error parsing UniversePos from string", err)
					}
				}
			}

			throw JsonSyntaxException("Invalid data type for UniversePos: ${`in`.peek()}")
		}
	}

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

		private val splitter = Regex("[ _:]")
		val ZERO = UniversePos()

		fun parse(value: String): UniversePos {
			if (value.isBlank())
				return ZERO

			val split = value.split(splitter)
			val x = split[0].toInt()
			val y = split[1].toInt()
			val z = split[2].toInt()

			val planet = if (split.size > 3) split[3].toInt() else 0
			val orbit = if (split.size > 4) split[4].toInt() else 0

			if (planet <= 0) // TODO: ??? Determine, if this is a bug in original code
				throw IndexOutOfBoundsException("Non-positive planetary orbit: $planet (in $value)")

			if (orbit < 0)
				throw IndexOutOfBoundsException("Negative satellite orbit: $orbit (in $value)")

			return UniversePos(Vector3i(x, y, z), planet, orbit)
		}
	}
}
