package ru.dbotthepony.kstarbound.server.world

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.defs.SpawnTarget
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.EnvironmentUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import java.io.DataOutputStream
import java.util.HashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean

// couples ServerWorld and ServerConnection together,
// allowing ServerConnection client to track ServerWorld state
class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) {
	private var skyVersion = 0L
	// this is required because of dumb shit regarding flash time
	// if we network sky state on each tick then it will guarantee epilepsy attack
	private var skyUpdateWaitTicks = 0

	private val isRemoved = AtomicBoolean()
	private var isActuallyRemoved = false
	private val tickets = HashMap<ChunkPos, Ticket>()
	private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
	private val entityVersions = Int2LongOpenHashMap()

	init {
		entityVersions.defaultReturnValue(-1L)

		LOGGER.info("Accepted ${client.alias()}")

		client.worldStartAcknowledged = false
		client.tracker = this
		client.worldID = world.worldID
	}

	fun send(packet: IPacket) = client.send(packet)

	// packets which interact with world must be
	// executed on world's thread
	fun enqueue(task: ServerWorld.() -> Unit) {
		if (!isRemoved.get())
			tasks.add(task)
	}

	private inner class Ticket(val ticket: ServerChunk.ITicket, val pos: ChunkPos) : IChunkListener {
		override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
			send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
		}

		override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
			// let's hope nothing bad happens from referencing live data
			send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health))
		}
	}

	init {
		val (skyData, skyVersion) = world.sky.networkedGroup.write(isLegacy = client.isLegacy)
		this.skyVersion = skyVersion

		if (client.isLegacy) {
			client.send(WorldStartPacket(
				templateData = Starbound.legacyJson { world.template.toJson() },
				skyData = skyData.toByteArray(),
				weatherData = ByteArray(0),
				playerStart = playerStart,
				playerRespawn = world.playerSpawnPosition,
				respawnInWorld = world.respawnInWorld,
				dungeonGravity = mapOf(),
				dungeonBreathable = mapOf(),
				protectedDungeonIDs = world.protectedDungeonIDs,
				worldProperties = world.copyProperties(),
				connectionID = client.connectionID,
				localInterpolationMode = false,
			))

			Starbound.legacyJson {
				client.send(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(world.centralStructure)))
			}
		}
	}

	fun isTracking(pos: ChunkPos): Boolean {
		return pos in tickets
	}

	fun isTracking(entity: AbstractEntity): Boolean {
		return entityVersions.containsKey(entity.entityID)
	}

	fun forget(entity: AbstractEntity, reason: AbstractEntity.RemovalReason) {
		val version = entityVersions.remove(entity.entityID)

		if (version != -1L) {
			send(EntityDestroyPacket(entity.entityID, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, reason.dying))
		}
	}

	fun tick() {
		if (!client.worldStartAcknowledged)
			return

		if (!client.channel.isOpen) {
			// ???
			remove()
			remove0()
			return
		}

		if (isRemoved.get()) {
			remove0()
			return
		}

		run {
			if (skyUpdateWaitTicks++ >= 20) {
				val (data, version) = world.sky.networkedGroup.write(skyVersion, isLegacy = client.isLegacy)
				skyVersion = version
				send(EnvironmentUpdatePacket(data, ByteArrayList()))
				skyUpdateWaitTicks = 0
			}
		}

		run {
			var next = tasks.poll()

			while (next != null && !isRemoved.get()) {
				next.invoke(world)
				next = tasks.poll()
			}
		}

		client.playerEntity = world.entities[client.playerID] as? PlayerEntity

		val trackingRegions = client.trackingTileRegions()

		run {
			val newTrackedChunks = ArrayList<ChunkPos>()

			for (region in trackingRegions) {
				newTrackedChunks.addAll(world.geometry.region2Chunks(region))
			}

			val itr = tickets.entries.iterator()

			for ((pos, ticket) in itr) {
				if (pos !in newTrackedChunks) {
					ticket.ticket.cancel()
					itr.remove()
				}
			}

			for (pos in newTrackedChunks) {
				if (pos !in tickets) {
					val ticket = world.permanentChunkTicket(pos).get() ?: continue
					val thisTicket = Ticket(ticket, pos)

					tickets[pos] = thisTicket
					ticket.listener = thisTicket

					ticket.chunk.thenAccept {
						if (client.isLegacy) {
							send(LegacyTileArrayUpdatePacket(it))
							it.tileDamagePackets().forEach { send(it) }
						} else {
							send(ChunkCellsPacket(it))
						}
					}
				}
			}
		}

		run {
			val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()

			for (region in trackingRegions) {
				trackingEntities.addAll(world.entityIndex.query(region.toDoubleAABB(), filter = { it.connectionID != client.connectionID }))
			}

			val unseen = IntArrayList(entityVersions.keys)
			val changePackets = Int2ObjectOpenHashMap<Int2ObjectOpenHashMap<ByteArrayList>>()

			for (entity in trackingEntities) {
				val id = entity.entityID
				unseen.rem(id)

				if (entityVersions.get(id) == -1L) {
					// never networked
					val initial = FastByteArrayOutputStream()
					entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
					val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)

					entityVersions.put(id, version)

					send(EntityCreatePacket(
						entity.type,
						ByteArrayList.wrap(initial.array, initial.length),
						data,
						entity.entityID
					))
				} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
					val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
					entityVersions.put(id, version)
					changePackets.computeIfAbsent(entity.connectionID, Int2ObjectFunction { Int2ObjectOpenHashMap() }).put(entity.entityID, data)
				}
			}

			if (changePackets.isNotEmpty()) {
				for ((connectionID, map) in changePackets) {
					send(EntityUpdateSetPacket(connectionID, map))
				}
			}

			val itr = unseen.iterator()

			while (itr.hasNext()) {
				val id = itr.nextInt()
				val entity = world.entities[id]
				val version = entityVersions.remove(id)

				if (entity == null) {
					send(EntityDestroyPacket(id, ByteArrayList(), false))
				} else {
					send(EntityDestroyPacket(id, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, false))
				}
			}
		}
	}

	private fun remove0() {
		if (isActuallyRemoved) return

		isActuallyRemoved = true
		world.clients.remove(this)
		tickets.values.forEach { it.ticket.cancel() }

		val itr = world.entities.int2ObjectEntrySet().iterator()

		for ((id, entity) in itr) {
			if (id in client.entityIDRange) {
				// remove entities owned by that player
				entity.remove(AbstractEntity.RemovalReason.REMOTE_REMOVAL)
			}
		}
	}

	fun remove(reason: String = "ServerWorldTracker got removed", nullifyVariables: Boolean = true) {
		if (isRemoved.compareAndSet(false, true)) {
			// erase all tasks just to be sure
			tasks.clear()

			val playerEntity = client.playerEntity

			if (playerEntity != null && world.worldID is WorldID.Celestial) {
				client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position))
			}

			if (nullifyVariables) {
				client.tracker = null
				client.playerEntity = null
				client.worldID = WorldID.Limbo
			}

			client.send(WorldStopPacket(reason))

			// this handles case where player is removed from world and
			// instantly added back because new world rejected us
			world.eventLoop.execute { remove0() }
		}
	}

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