package ru.dbotthepony.kstarbound.server.world

import com.google.common.collect.ImmutableList
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectCreatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemObjectDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipCreatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.SystemWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataOutputStream
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.function.Supplier
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin

class ServerSystemWorld : SystemWorld {
	@JsonFactory
	data class JsonData(
		val location: Vector3i,
		val lastSpawn: Double,
		val objectSpawnTime: Double,
		val objects: ImmutableList<JsonObject> = ImmutableList.of(),
	)

	override val entities = HashMap<UUID, ServerEntity>()
	override val ships = HashMap<UUID, ServerShip>()

	private class Task<T>(val supplier: Supplier<T>) : Runnable {
		val future = CompletableFuture<T>()

		override fun run() {
			try {
				future.complete(supplier.get())
			} catch (err: Throwable) {
				future.completeExceptionally(err)
			}
		}
	}

	private val tasks = ConcurrentLinkedQueue<Task<*>>()

	override fun toString(): String {
		return "ServerSystemWorld at $systemLocation"
	}

	@Suppress("NAME_SHADOWING")
	private fun addClient0(client: ServerConnection, shipSpeed: Double, location: SystemWorldLocation): ServerShip {
		if (!client.isConnected || client.uuid == null)
			throw IllegalStateException("Trying to add disconnected client, or client without UUID: $client")

		if (client.uuid!! in ships)
			throw IllegalStateException("Already has client $client in $this!")

		var location = location

		if (location is SystemWorldLocation.Entity && location.uuid !in entities)
			location = SystemWorldLocation.Transit

		if (location == SystemWorldLocation.Transit)
			location = SystemWorldLocation.Position(randomArrivalPosition())

		val objects = entities.values.map { it.writeNetwork(client.isLegacy) }
		val ships = ships.values.map { it.writeNetwork(client.isLegacy) }

		val ship = ServerShip(client, location)
		ship.speed = shipSpeed

		client.send(SystemWorldStartPacket(this.location, objects, ships, client.uuid!!, location))

		val legacyPacket by lazy { SystemShipCreatePacket(ship.writeNetwork(true)) }
		val nativePacket by lazy { SystemShipCreatePacket(ship.writeNetwork(false)) }

		for (otherShip in this.ships.values) {
			if (otherShip != ship) {
				otherShip.client.send(if (otherShip.client.isLegacy) legacyPacket else nativePacket)
			}
		}

		return ship
	}

	private fun removeClient0(client: ServerConnection) {
		val ship = ships.remove(client.uuid) ?: throw IllegalStateException("No client $client in $this!")

		val packet = SystemShipDestroyPacket(ship.uuid)

		ships.values.forEach {
			it.forget(ship.uuid)
			it.client.send(packet)
		}
	}

	fun addClient(client: ServerConnection, shipSpeed: Double = Globals.systemWorld.clientShip.speed, location: SystemWorldLocation = SystemWorldLocation.Transit): CompletableFuture<ServerShip> {
		val task = Task { addClient0(client, shipSpeed, location) }
		tasks.add(task)
		return task.future
	}

	fun removeClient(client: ServerConnection): CompletableFuture<Unit> {
		val task = Task { removeClient0(client) }
		tasks.add(task)
		return task.future
	}

	private fun flyShip0(client: ServerConnection, location: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) {
		val ship = ships[client.uuid] ?: throw IllegalStateException("No client $client in $this!")
		ship.destination(location, future)
	}

	fun flyShip(client: ServerConnection, location: SystemWorldLocation): CompletableFuture<SystemWorldLocation> {
		val future = CompletableFuture<SystemWorldLocation>()
		val task = Task { flyShip0(client, location, future) }
		tasks.add(task)
		return future
	}

	val server: StarboundServer
	var objectSpawnTime: Double
		private set
	var lastSpawn = 0.0
		private set

	private constructor(server: StarboundServer, location: Vector3i) : super(location, server.universeClock, server.universe) {
		this.server = server
		this.lastSpawn = clock.time - Globals.systemWorld.objectSpawnCycle
		objectSpawnTime = random.nextRange(Globals.systemWorld.objectSpawnInterval)
	}

	private constructor(server: StarboundServer, data: JsonData) : super(data.location, server.universeClock, server.universe) {
		this.server = server
		objectSpawnTime = data.objectSpawnTime

		for (obj in data.objects) {
			ServerEntity(obj)
		}

		this.lastSpawn = data.lastSpawn
	}

	private suspend fun spawnInitialObjects() {
		val random = random(staticRandom64("SystemWorldGeneration", location.toString()))

		Globals.systemWorld.initialObjectPools.sample(random).ifPresent {
			for (i in 0 until it.first) {
				val name = it.second.sample(random).orNull() ?: return@ifPresent
				val uuid = UUID(random.nextLong(), random.nextLong())
				val prototype = Globals.systemObjects[name] ?: throw NullPointerException("Tried to create $name system world object, but there is no such object in /system_objects.config!")
				val create = ServerEntity(prototype.create(uuid, name), uuid, randomObjectSpawnPosition(), clock.time)
				create.enterOrbit(UniversePos(location), Vector2d.ZERO, clock.time) // orbit center of system
			}
		}
	}

	private suspend fun spawnObjects() {
		var diff = Globals.systemWorld.objectSpawnCycle.coerceAtMost(clock.time - lastSpawn)
		lastSpawn = clock.time - diff

		while (diff > objectSpawnTime) {
			lastSpawn += objectSpawnTime
			objectSpawnTime = random.nextRange(Globals.systemWorld.objectSpawnInterval)
			diff = clock.time - lastSpawn

			Globals.systemWorld.objectSpawnPool.sample(random).ifPresent {
				val uuid = UUID(random.nextLong(), random.nextLong())
				val config = Globals.systemObjects[it]?.create(uuid, it) ?: throw NullPointerException("Tried to create $it system world object, but there is no such object in /system_objects.config!")
				val pos = randomObjectSpawnPosition()

				if (clock.time > lastSpawn + objectSpawnTime && config.moving) {
					// if this is not the last object we're spawning, and it's moving, immediately put it in orbit around a planet
					val targets = universe.children(systemLocation).filter { child ->
						entities.values.none { it.orbit.map { it.target == child }.orElse(false) }
					}

					if (targets.isNotEmpty()) {
						val target = targets.random(random)
						val targetPosition = planetPosition(target)
						val relativeOrbit = (pos - targetPosition).unitVector * (clusterSize(target) / 2.0 + config.orbitDistance)
						ServerEntity(config, uuid, targetPosition + relativeOrbit, lastSpawn).enterOrbit(target, targetPosition, lastSpawn)
					} else {
						ServerEntity(config, uuid, pos, lastSpawn)
					}
				} else {
					ServerEntity(config, uuid, pos, lastSpawn)
				}
			}
		}
	}

	private suspend fun randomObjectSpawnPosition(): Vector2d {
		val spawnRanges = ArrayList<Vector2d>()
		val orbits = universe.children(systemLocation)

		suspend fun addSpawn(inner: UniversePos, outer: UniversePos) {
			val min = planetOrbitDistance(inner) + clusterSize(inner) / 2.0 + Globals.systemWorld.objectSpawnPadding
			val max = planetOrbitDistance(outer) - clusterSize(outer) / 2.0 - Globals.systemWorld.objectSpawnPadding
			spawnRanges.add(Vector2d(min, max))
		}

		addSpawn(systemLocation, orbits.first())

		for (i in 1 until orbits.size)
			addSpawn(orbits[i - 1], orbits[i])

		val outer = orbits.last()
		val rim = planetOrbitDistance(outer) + clusterSize(outer) / 2.0 + Globals.systemWorld.objectSpawnPadding
		spawnRanges.add(Vector2d(rim, rim + Globals.systemWorld.objectSpawnPadding))

		val range = spawnRanges.random(random)
		val angle = random.nextDouble() * PI * 2.0
		val mag = range.x + random.nextDouble() * (range.y - range.x)
		return Vector2d(cos(angle) * mag, sin(angle) * mag)
	}

	fun toJson(): JsonObject {
		return Starbound.gson.toJsonTree(JsonData(
			location = location,
			lastSpawn = lastSpawn,
			objectSpawnTime = objectSpawnTime,
			objects = entities.values.stream().map { it.toJson() }.collect(ImmutableList.toImmutableList()),
		)) as JsonObject
	}

	private var ticksWithoutPlayers = 0

	fun shouldClose(): Boolean {
		return ticksWithoutPlayers > 1800
	}

	// in original engine, ticking happens at 20 updates per second
	// Since there is no Lua driven code, we can tick as fast as we want
	suspend fun tick(delta: Double) {
		var next = tasks.poll()

		while (next != null) {
			next.run()
			next = tasks.poll()
		}

		entities.values.forEach { it.tick(delta) }
		ships.values.forEach { it.tick(delta) }

		entities.values.removeIf {
			if (it.hasExpired && ships.values.none { ship -> ship.location == it.location }) {
				val packet = SystemObjectDestroyPacket(it.uuid)

				ships.values.forEach { ship ->
					ship.forget(it.uuid)
					ship.client.send(packet)
				}

				return@removeIf true
			}

			return@removeIf false
		}

		// spawnObjects()

		ships.values.forEach { it.sendUpdates() }

		if (ships.isEmpty())
			ticksWithoutPlayers++
		else
			ticksWithoutPlayers = 0
	}

	inner class ServerShip(val client: ServerConnection, location: SystemWorldLocation) : Ship(client.uuid!!, location) {
		init {
			ships[uuid] = this
		}

		private val netVersions = Object2LongOpenHashMap<UUID>()

		suspend fun tick(delta: Double) {
			val orbit = destination as? SystemWorldLocation.Orbit

			// if destination is an orbit we haven't started orbiting yet, update the time
			if (orbit != null)
				destination = SystemWorldLocation.Orbit(orbit.position.copy(enterTime = clock.time))

			suspend fun nearPlanetOrbit(planet: UniversePos): Orbit {
				val toShip = planetPosition(planet) - position
				val angle = toShip.toAngle()
				return Orbit(planet, 1, clock.time, Vector2d(cos(angle), sin(angle)) * (planetSize(planet) / 2.0 + Globals.systemWorld.clientShip.orbitDistance))
			}

			if (location is SystemWorldLocation.Celestial) {
				if (this.orbit?.target != (location as SystemWorldLocation.Celestial).position)
					this.orbit = nearPlanetOrbit((location as SystemWorldLocation.Celestial).position)
			} else if (location == SystemWorldLocation.Transit) {
				departTimer = (departTimer - delta).coerceAtLeast(0.0)

				if (departTimer > 0.0)
					return

				if (destination is SystemWorldLocation.Celestial) {
					if (this.orbit?.target != (destination as SystemWorldLocation.Celestial).position)
						this.orbit = nearPlanetOrbit((destination as SystemWorldLocation.Celestial).position)
				} else {
					this.orbit = null
				}

				val destination: Vector2d

				if (this.orbit != null) {
					this.orbit = this.orbit!!.copy(enterTime = clock.time)
					destination = orbitPosition(this.orbit!!)
				} else {
					destination = this.destination.resolve(this@ServerSystemWorld) ?: position
				}

				val toTarget = destination - position
				// don't overshoot
				position += toTarget.unitVector * (speed * delta).coerceAtMost(toTarget.length.coerceAtMost(1.0))

				if (destination.distanceSquared(position) <= 0.1) {
					location = this.destination
					this.destination = SystemWorldLocation.Transit
					destinationFuture.complete(location)
				} else {
					return
				}
			}

			if (this.orbit != null) {
				position = orbitPosition(this.orbit!!)
			} else {
				position = location.resolve(this@ServerSystemWorld) ?: position
			}
		}

		fun sendUpdates() {
			val objects = HashMap<UUID, ByteArrayList>()
			val ships = HashMap<UUID, ByteArrayList>()

			for ((id, ship) in this@ServerSystemWorld.ships) {
				val version = netVersions.getLong(id)

				if (version == 0L || ship.networkGroup.upstream.hasChangedSince(version)) {
					val (data, newVersion) = ship.networkGroup.write(version, client.isLegacy)
					netVersions.put(id, newVersion)
					ships[id] = data
				}
			}

			for ((id, entity) in this@ServerSystemWorld.entities) {
				val version = netVersions.getLong(id)

				if (version == 0L || entity.networkGroup.upstream.hasChangedSince(version)) {
					val (data, newVersion) = entity.networkGroup.write(version, client.isLegacy)
					netVersions.put(id, newVersion)
					objects[id] = data
				}
			}

			client.send(SystemWorldUpdatePacket(objects, ships))
		}

		fun forget(id: UUID) {
			netVersions.removeLong(id)
		}

		private var destinationFuture = CompletableFuture<SystemWorldLocation>()

		fun destination(destination: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) {
			destinationFuture.cancel(false)
			destinationFuture = future

			if (location is SystemWorldLocation.Celestial || location is SystemWorldLocation.Entity)
				departTimer = Globals.systemWorld.clientShip.departTime
			else if (destination == SystemWorldLocation.Transit)
				departTimer = Globals.systemWorld.clientShip.spaceDepartTime

			this.destination = destination
			this.location = SystemWorldLocation.Transit
		}

		fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeUUID(uuid)
			location.write(stream, isLegacy)
		}

		fun writeNetwork(isLegacy: Boolean): ByteArrayList {
			val data = FastByteArrayOutputStream()
			writeNetwork(DataOutputStream(data), isLegacy)
			return ByteArrayList.wrap(data.array, data.length)
		}
	}

	inner class ServerEntity(data: SystemWorldObjectConfig.Data, uuid: UUID, position: Vector2d, spawnTime: Double = 0.0, parameters: JsonObject = JsonObject()) : Entity(data, uuid, position, spawnTime, parameters) {
		constructor(data: EntityJsonData) : this(
			Globals.systemObjects[data.name]?.create(data.actualUUID, data.name) ?: throw NullPointerException("Tried to create ${data.name} system world object, but there is no such object in /system_objects.config!"),
			data.actualUUID,
			data.position,
			data.spawnTime,
			data.parameters
		) {
			if (data.orbit != null) {
				orbit = KOptional(data.orbit)
			}
		}

		constructor(data: JsonObject) : this(Starbound.gson.fromJson(data, EntityJsonData::class.java))

		init {
			entities[uuid] = this

			val legacy by lazy { SystemObjectCreatePacket(writeNetwork(true)) }
			val native by lazy { SystemObjectCreatePacket(writeNetwork(false)) }

			ships.values.forEach {
				it.client.send(if (it.client.isLegacy) legacy else native)
			}
		}

		val location = SystemWorldLocation.Entity(uuid)

		var hasExpired = false
			private set

		suspend fun tick(delta: Double) {
			if (!data.permanent && spawnTime > 0.0 && clock.time > spawnTime + data.lifeTime)
				hasExpired = true

			val orbit = orbit.orNull()

			if (orbit != null) {
				position = orbitPosition(orbit)
			} else if (data.permanent || !data.moving) {
				// permanent locations always have a solar orbit
				enterOrbit(systemLocation, Vector2d.ZERO, clock.time)
			} else if (approach != null) {
				if (ships.values.any { it.location == location })
					return

				if (approach!!.isPlanet) {
					val approach = planetPosition(approach!!)
					val toApproach = approach - position

					position += toApproach.unitVector * data.speed * delta

					if ((approach - position).length < planetSize(this.approach!!) + data.orbitDistance)
						enterOrbit(this.approach!!, approach, clock.time)
				} else {
					enterOrbit(approach!!, Vector2d.ZERO, clock.time)
				}
			} else {
				val planets = universe.children(systemLocation).filter { child ->
					entities.values.none { it.orbit.map { it.target == child }.orElse(false) }
				}

				if (planets.isNotEmpty())
					approach = planets.random(random)
			}
		}

		fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeUUID(uuid)
			stream.writeBinaryString(data.name)
			stream.writeStruct2d(position, isLegacy)
			stream.writeJsonObject(parameters)
		}

		fun writeNetwork(isLegacy: Boolean): ByteArrayList {
			val data = FastByteArrayOutputStream()
			writeNetwork(DataOutputStream(data), isLegacy)
			return ByteArrayList.wrap(data.array, data.length)
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()

		suspend fun create(server: StarboundServer, location: Vector3i): ServerSystemWorld {
			LOGGER.info("Creating new System World at $location")
			val world = ServerSystemWorld(server, location)
			world.spawnInitialObjects()
			world.spawnObjects()
			return world
		}

		suspend fun load(server: StarboundServer, data: JsonElement): ServerSystemWorld {
			val load = Starbound.gson.fromJson(data, JsonData::class.java)
			LOGGER.info("Loading System World at ${load.location}")
			val world = ServerSystemWorld(server, load)
			world.spawnObjects()
			return world
		}
	}
}
