package ru.dbotthepony.kstarbound.server.world

import com.google.gson.Gson
import com.google.gson.JsonArray
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.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.pairListAdapter
import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap
import java.util.stream.Collectors

class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
	data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap<Planet>)

	@JsonFactory
	data class Planet(val parameters: CelestialParameters, val satellites: Int2ObjectMap<CelestialParameters>)

	val systems = HashMap<Vector3i, System>()
	val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>()

	fun parameters(coordinate: UniversePos): CelestialParameters? {
		val system = systems[coordinate.location] ?: return null

		if (coordinate.isSystem)
			return system.parameters
		else if (coordinate.isPlanet)
			return system.planets[coordinate.planetOrbit]?.parameters
		else if (coordinate.isSatellite)
			return system.planets[coordinate.planetOrbit]?.satellites?.get(coordinate.satelliteOrbit)
		else
			throw RuntimeException("unreachable code")
	}

	fun toNetwork(): CelestialResponsePacket.ChunkData {
		return CelestialResponsePacket.ChunkData(
			chunkPos,
			ArrayList(constellations),
			systems.entries.associate { it.key to it.value.parameters },
			systems.entries.associate { it.key to it.value.planets.int2ObjectEntrySet().associate { it.intKey to CelestialResponsePacket.PlanetData(it.value.parameters, HashMap(it.value.satellites)) } },
		)
	}

	class Adapter(gson: Gson) : TypeAdapter<UniverseChunk>() {
		private val vectors = gson.getAdapter(Vector2i::class.java)
		private val vectors3 = gson.getAdapter(Vector3i::class.java)
		private val systems = gson.pairListAdapter<Vector3i, CelestialParameters>()
		private val params = gson.getAdapter(CelestialParameters::class.java)
		private val lines = gson.pairAdapter<Vector2i, Vector2i>()

		override fun write(out: JsonWriter, value: UniverseChunk?) {
			if (value == null)
				out.nullValue()
			else {
				out.beginObject()

				out.name("chunkIndex")
				vectors.write(out, value.chunkPos)

				out.name("systemParameters")
				out.beginArray()

				for (system in value.systems) {
					out.beginArray()
					vectors3.write(out, system.key)
					params.write(out, system.value.parameters)
					out.endArray()
				}

				out.endArray()

				out.name("systemObjects")
				out.beginArray()

				for (system in value.systems) {
					out.beginArray()
					vectors3.write(out, system.key)

					out.beginArray()
					for ((orbit, planet) in system.value.planets) {
						out.beginArray()
						out.value(orbit)

						out.beginObject()
						out.name("parameters")
						params.write(out, planet.parameters)

						out.name("satellites")
						out.beginArray()
						for ((satelliteOrbit, satellite) in planet.satellites) {
							out.beginArray()
							out.value(satelliteOrbit)
							params.write(out, satellite)
							out.endArray()
						}
						out.endArray()

						out.endObject()

						out.endArray()
					}
					out.endArray()

					out.endArray()
				}

				out.endArray()

				out.name("constellations")
				out.beginArray()
				out.beginArray()

				for (pair in value.constellations) {
					lines.write(out, pair)
				}

				out.endArray()
				out.endArray()

				out.endObject()
			}
		}

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

			val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
			val chunkPos = read.get("chunkIndex", vectors)
			val chunk = UniverseChunk(chunkPos)

			// WARNING: Original game engine stores constellations as List<List<Line>>
			// We, on the other hand, don't care and store it as List<Line>, since constellations
			// are just pairs of system coordinates.
			// List<List<Line>> seems to be a leftover from when constellations were supposed to be
			// something bigger than just pairs of coordinates, or special processing were required for them.
			// (since original game only ever generate a single constellation, or none at all).
			// To avoid creating two separate chunk formats, we will save/load constellations as nested list too.
			for (sublist in read.getArray("constellations")) {
				for (constellation in sublist.asJsonArray) {
					chunk.constellations.add(lines.fromJsonTree(constellation))
				}
			}

			read.get("systemParameters", systems).forEach {
				chunk.systems[it.first] = System(it.second, Int2ObjectOpenHashMap())
			}

			// [[pos, [[orbit, {data}]]]]
			val systemObjects = read.getArray("systemObjects")

			for (elem in systemObjects) {
				elem as JsonArray
				val pos = vectors3.fromJsonTree(elem[0])
				val system = chunk.systems[pos] ?: continue
				val planets = elem[1] as JsonArray

				for (planetPair in planets) {
					planetPair as JsonArray
					val orbit = planetPair[0].asInt
					val params = planetPair[1] as JsonObject

					val planet = Planet(this.params.fromJsonTree(params["parameters"]), Int2ObjectOpenHashMap())
					val satellitesPairs = params["satellites"] as JsonArray

					for (satellitePair in satellitesPairs) {
						satellitePair as JsonArray
						val satelliteOrbit = satellitePair[0].asInt
						val satelliteData = this.params.fromJsonTree(satellitePair[1])
						planet.satellites[satelliteOrbit] = satelliteData
					}

					system.planets[orbit] = planet
				}
			}

			return chunk
		}
	}

	companion object : TypeAdapterFactory {
		override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type.rawType == UniverseChunk::class.java) {
				return Adapter(gson) as TypeAdapter<T>
			}

			return null
		}
	}
}
