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.StreamCodec
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
import kotlin.math.roundToInt

// original game has MVariant here
// MVariant prepends InvalidValue to Variant<> template
// Variant<> itself works with LookupTypeIndex<MatchType, 0, FirstType, RestTypes...>
// 0 is responsible for holding current template comparison check
// typedef MVariant<WarpToWorld, WarpToPlayer, WarpAlias> WarpAction;
// -> Variant<InvalidType, WarpToWorld, WarpToPlayer, WarpAlias> WarpAction
// hence WarpToWorld has index 1, WarpToPlayer 2, WarpAlias 3
@JsonAdapter(SpawnTarget.Adapter::class)
sealed class SpawnTarget {
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	abstract fun resolve(world: ServerWorld): Vector2d?

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

		override fun resolve(world: ServerWorld): Vector2d {
			return world.playerSpawnPosition
		}

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

	data class Entity(val id: String) : SpawnTarget() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			stream.writeBinaryString(id)
		}

		override fun resolve(world: ServerWorld): Vector2d? {
			return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position
		}

		override fun toString(): String {
			return id
		}
	}

	data class Position(val position: Vector2d) : SpawnTarget() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)

			if (isLegacy) {
				stream.writeStruct2f(position.toFloatVector())
			} else {
				stream.writeStruct2d(position)
			}
		}

		override fun toString(): String {
			return "${position.x.roundToInt()}.${position.y.roundToInt()}"
		}

		override fun resolve(world: ServerWorld): Vector2d {
			return position
		}
	}

	data class X(val position: Double) : SpawnTarget() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(3)

			if (isLegacy) {
				stream.writeFloat(position.toFloat())
			} else {
				stream.writeDouble(position)
			}
		}

		override fun toString(): String {
			return position.roundToInt().toString()
		}

		override fun resolve(world: ServerWorld): Vector2d {
			TODO("Not yet implemented")
		}
	}

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

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

	companion object {
		private val position = Regex("\\d+.\\d+")
		private val positionX = Regex("\\d+")

		fun read(stream: DataInputStream, isLegacy: Boolean): SpawnTarget {
			return when (val type = stream.readUnsignedByte()) {
				0 -> Whatever
				1 -> Entity(stream.readInternedString())
				2 -> Position(if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d())
				3 -> X(if (isLegacy) stream.readFloat().toDouble() else stream.readDouble())
				else -> throw IllegalArgumentException("Unknown SpawnTarget type $type!")
			}
		}

		fun parse(value: String): SpawnTarget {
			val matchPos = position.matchEntire(value)

			if (matchPos != null) {
				return Position(Vector2d(matchPos.groups[0]!!.value.toDouble(), matchPos.groups[0]!!.value.toDouble()))
			}

			val matchX = positionX.matchEntire(value)

			if (matchX != null) {
				return X(matchX.groups[0]!!.value.toDouble())
			}

			return Entity(value)
		}
	}
}

@JsonAdapter(WarpAction.Adapter::class)
sealed class WarpAction {
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	abstract fun resolve(connection: ServerConnection): WorldID

	data class World(val worldID: WorldID, val target: SpawnTarget = SpawnTarget.Whatever) : WarpAction() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			worldID.write(stream, isLegacy)
			target.write(stream, isLegacy)
		}

		override fun resolve(connection: ServerConnection): WorldID {
			return worldID
		}

		override fun toString(): String {
			if (target != SpawnTarget.Whatever)
				return "$worldID=$target"

			return "$worldID"
		}
	}

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

		override fun resolve(connection: ServerConnection): WorldID {
			if (connection.uuid == uuid)
				return connection.world?.worldID ?: WorldID.Limbo

			return connection.server.clientByUUID(uuid)?.world?.worldID ?: WorldID.Limbo
		}

		override fun toString(): String {
			return "Player:$uuid"
		}
	}

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

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

	companion object {
		fun read(stream: DataInputStream, isLegacy: Boolean): WarpAction {
			return when (val type = stream.readUnsignedByte()) {
				1 -> World(WorldID.read(stream, isLegacy), SpawnTarget.read(stream, isLegacy))
				2 -> Player(stream.readUUID())
				3 -> {
					when (val type2 = stream.readInt()) {
						0 -> WarpAlias.Return
						1 -> WarpAlias.OrbitedWorld
						2 -> WarpAlias.OwnShip
						else -> throw IllegalArgumentException("Unknown WarpAlias type $type2!")
					}
				}

				else -> throw IllegalArgumentException("Unknown WarpAction type $type!")
			}
		}

		fun parse(value: String): WarpAction {
			if (value.lowercase() == "return") {
				return WarpAlias.Return
			} else if (value.lowercase() == "orbitedworld") {
				return WarpAlias.OrbitedWorld
			} else if (value.lowercase().startsWith("player:")) {
				return Player(UUID.fromString(value.substring(7)))
			} else {
				val parts = value.split('=')
				val world = WorldID.parse(parts[0])
				var spawnTarget: SpawnTarget = SpawnTarget.Whatever

				if (parts.size == 2) {
					spawnTarget = SpawnTarget.parse(parts[1])
				}

				return World(world, spawnTarget)
			}
		}

		val CODEC = nativeCodec(::read, WarpAction::write)
		val LEGACY_CODEC = legacyCodec(::read, WarpAction::write)
	}
}

sealed class WarpAlias(val index: Int) : WarpAction() {
	final override fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.write(3)
		// because it is defined as enum class WarpAlias, without specifying uint8_t as type
		stream.writeInt(index)
	}

	abstract fun remap(connection: ServerConnection): WarpAction

	final override fun resolve(connection: ServerConnection): WorldID {
		throw RuntimeException("Trying to use WarpAlias as regular warp action")
	}

	object Return : WarpAlias(0) {
		override fun remap(connection: ServerConnection): WarpAction {
			return connection.returnWarp ?: World(connection.shipWorld.worldID)
		}

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

	object OrbitedWorld : WarpAlias(1) {
		override fun remap(connection: ServerConnection): WarpAction {
			return connection.orbitalWarpAction.orNull()?.first ?: World(connection.shipWorld.worldID)
		}

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

	object OwnShip : WarpAlias(2) {
		override fun remap(connection: ServerConnection): WarpAction {
			return World(connection.shipWorld.worldID)
		}

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

enum class WarpMode(override val jsonName: String) : IStringSerializable {
	NONE("None"),
	BEAM_ONLY("BeamOnly"),
	DEPLOY_ONLY("DeployOnly"),
	BEAM_OR_DEPLOY("BeamOrDeploy");

	companion object {
		val CODEC = StreamCodec.Enum(WarpMode::class.java)
	}
}
