package ru.dbotthepony.kstarbound.server

import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import io.netty.channel.ChannelHandlerContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WarpMode
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.CelestialResponsePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.ActionPacer
import ru.dbotthepony.kstarbound.world.SystemWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import java.util.stream.Collectors
import kotlin.collections.HashMap
import kotlin.math.min
import kotlin.properties.Delegates

// serverside part of connection
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
	var tracker: ServerWorldTracker? = null
	var worldStartAcknowledged = false
	var returnWarp: WarpAction? = null
	private var systemWorld: ServerSystemWorld? = null

	val world: ServerWorld?
		get() = tracker?.world

	// packets which interact with world must be
	// executed on world's thread
	fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): Boolean {
		if (isDisconnecting.get())
			return false

		val isInWorld = tracker?.enqueue(task) != null

		if (!isInWorld) {
			LOGGER.warn("$this tried to interact with world, but they are not in one.")
		}

		return isInWorld
	}

	lateinit var shipWorld: ServerWorld
		private set

	var playerSpecies: String by Delegates.notNull()
	var uuid: UUID? = null
	var systemWorldShip: SystemWorld.Ship? = null
		private set

	val shipUpgradesQueue = Channel<ShipUpgrades>(256)

	private suspend fun shipUpgradesLoop() {
		while (true) {
			var upgrades = shipUpgradesQueue.receive()

			val ships = Globals.universeServer.speciesShips[playerSpecies] ?: continue
			val oldLevel = shipWorld.getProperty("ship.level", JsonPrimitive(0)).asInt
			val newLevel = min(ships.size - 1, upgrades.shipLevel)

			if (oldLevel < newLevel) {
				for (i in oldLevel + 1 .. newLevel) {
					val structure = ships[i].value.await() ?: continue
					shipWorld.replaceCentralStructure(structure).join()
					upgrades = upgrades.apply(Starbound.gson.fromJson(structure.config["shipUpgrades"] ?: JsonNull.INSTANCE) ?: continue)
				}
			}

			shipWorld.setProperty("ship.level", JsonPrimitive(upgrades.shipLevel))
			shipWorld.setProperty("ship.maxFuel", JsonPrimitive(upgrades.maxFuel))
			shipWorld.setProperty("ship.crewSize", JsonPrimitive(upgrades.crewSize))
			shipWorld.setProperty("ship.fuelEfficiency", JsonPrimitive(upgrades.fuelEfficiency))
			systemWorldShip?.speed = upgrades.shipSpeed
			shipUpgrades = upgrades
		}
	}

	init {
		connectionID = server.channels.nextConnectionID()

		rpc.add("team.fetchTeamStatus") {
			JsonObject()
		}

		rpc.add("ship.applyShipUpgrades") {
			// we must consider upgrades sequentially since upgrading the ship will modify ship upgrades data
			shipUpgradesQueue.trySend(shipUpgrades.apply(Starbound.gson.fromJson(it) ?: return@add InternedJsonElementAdapter.of(true)))
			InternedJsonElementAdapter.of(true)
		}
	}

	override fun toString(): String {
		val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>"
		val world = tracker?.world?.toString() ?: "<not in world>"
		return "ServerConnection[$nickname $uuid ID=$connectionID channel=$channel / $world]"
	}

	fun alias(): String {
		return "$nickname <$connectionID/$uuid>"
	}

	private val legacyWorldStorage = LegacyWorldStorage.Memory {
		// TODO: uncomment this once ALL entity types are implemented
		// sendContextUpdates(it)
	}

	override fun setupLegacy() {
		super.setupLegacy()
	}

	override fun setupNative() {
		super.setupNative()
	}

	fun loadShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
		legacyWorldStorage.load(chunks.entries.stream().map { it.key to it.value.orNull() }.filter { it.second != null }.collect(Collectors.toMap({ it.first }, { it.second!! })))
	}

	private var remoteVersion = 0L
	private var saveClientContextTask: Future<*>? = null

	private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?)
	private val warpQueue = Channel<WarpRequest>(capacity = 10)

	private suspend fun warpEventLoop() {
		while (true) {
			var (request, deploy, ifFailed) = warpQueue.receive()

			if (request is WarpAlias)
				request = request.remap(this)

			if (ifFailed is WarpAlias)
				ifFailed = ifFailed.remap(this)

			LOGGER.info("Trying to warp ${alias()} to $request")

			val resolve = request.resolve(this)

			if (resolve.isLimbo) {
				send(PlayerWarpResultPacket(false, request, true))
			} else if (tracker?.world?.worldID == resolve) {
				LOGGER.info("${alias()} tried to warp into world they are already in.")
				send(PlayerWarpResultPacket(true, request, false))
			} else {
				val world = try {
					server.loadWorld(resolve).await()
				} catch (err: Throwable) {
					send(PlayerWarpResultPacket(false, request, false))
					LOGGER.error("Unable to warp ${alias()} to $request", err)

					if (ifFailed != null) {
						enqueueWarp(ifFailed)
					}

					continue
				}

				try {
					world.acceptClient(this, request).await()

					if (resolve is WorldID.ShipWorld) {
						val connection = server.channels.connectionByUUID(resolve.uuid)

						if (connection != null) {
							orbitalWarpAction = connection.currentOrbitalWarpAction
						}
					} else {
						orbitalWarpAction = KOptional()
					}
				} catch (err: Throwable) {
					send(PlayerWarpResultPacket(false, request, false))

					if (world == shipWorld) {
						disconnect("ShipWorld refused to accept its owner: $err")
					} else {
						enqueueWarp(returnWarp ?: WarpAlias.OwnShip)
					}
				}
			}
		}
	}

	private val flyShipQueue = Channel<Pair<Vector3i, SystemWorldLocation>>(capacity = 40)

	fun flyShip(system: Vector3i, location: SystemWorldLocation) {
		flyShipQueue.trySend(system to location)
	}

	private var currentOrbitalWarpAction = KOptional<Pair<WarpAction, WarpMode>>()

	private suspend fun findStartingSystem(): UniversePos? {
		shipWorld.sky.skyType = SkyType.ORBITAL
		shipWorld.sky.startFlying(true, true)
		var visited = 0

		LOGGER.info("Finding starter world for ${alias()}...")
		val params = Globals.universeServer.findStarterWorldParameters

		// visit all since randomly trying to find specific world is not healthy performance wise
		val found = server.universe.findRandomWorld(params.tries, params.range, visitAll = true, predicate = {
			if (++visited % 600 == 0) {
				LOGGER.info("Still finding starter world for ${alias()}...")
			}

			val parameters = server.universe.parameters(it) ?: return@findRandomWorld false
			if (parameters.visitableParameters == null) return@findRandomWorld false
			if (!params.starterWorld.test(parameters.visitableParameters!!)) return@findRandomWorld false

			val children = ArrayList<VisitableWorldParameters>()

			for (child in server.universe.children(it.system())) {
				val p = server.universe.parameters(child)

				if (p?.visitableParameters != null) {
					children.add(p.visitableParameters!!)
				}

				for (child2 in server.universe.children(child)) {
					val p2 = server.universe.parameters(child2)

					if (p2?.visitableParameters != null) {
						children.add(p2.visitableParameters!!)
					}
				}
			}

			params.requiredSystemWorlds.all { predicate -> children.any { predicate.test(it) } }
		})

		if (found == null) {
			LOGGER.fatal("Unable to find starter world for $this!")
			disconnect("Unable to find starter world")
			return null
		}

		LOGGER.info("Found appropriate starter world at $found for ${alias()}")
		return found
	}

	private var systemWorldLocation: SystemWorldLocation = SystemWorldLocation.Transit

	// coordinates ship flight
	private suspend fun shipFlightEventLoop(initialLocation: Vector3i, inWorldLocation: SystemWorldLocation) {
		val worldPromise = server.loadSystemWorld(initialLocation)
		val loadWorld = worldPromise.await()
		var actualInWorldLocation = inWorldLocation

		var world: ServerSystemWorld

		if (loadWorld == null) {
			LOGGER.warn("Tried to put player to system world at $initialLocation, but we are unable to load it")
			// we ended up nowhere, try to find new starter location
			val find = findStartingSystem() ?: return
			val tryAnother = server.loadSystemWorld(find.location).await()

			if (tryAnother == null) {
				// how?
				disconnect("Unable to put player in system world")
				return
			} else {
				actualInWorldLocation = SystemWorldLocation.Celestial(find)
				world = tryAnother
			}
		} else {
			world = loadWorld
		}

		this.systemWorld = world
		var ship = world.addClient(this, location = actualInWorldLocation).await()
		systemWorldShip = ship
		shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
		shipCoordinate = UniversePos(world.location)
		systemWorldLocation = actualInWorldLocation

		run {
			val action = ship.location.orbitalAction(world)
			currentOrbitalWarpAction = action

			for (client in shipWorld.clients) {
				client.client.orbitalWarpAction = action
			}
		}

		var currentFlightJob: Job? = null

		suspend fun flyShipInSystem(location: SystemWorldLocation) {
			currentFlightJob?.cancel()
			val flight = world.flyShip(this, location)

			shipWorld.eventLoop.execute {
				shipWorld.sky.startFlying(false)
			}

			currentFlightJob = scope.launch {
				systemWorldLocation = location
				val coords = flight.await()
				val action = coords.orbitalAction(world)
				currentOrbitalWarpAction = action

				for (client in shipWorld.clients) {
					client.client.orbitalWarpAction = action
				}

				val sky = coords.skyParameters(world)

				shipWorld.eventLoop.execute {
					shipWorld.sky.stopFlyingAt(sky)
				}
			}

			currentOrbitalWarpAction = KOptional()

			for (client in shipWorld.clients) {
				client.client.orbitalWarpAction = KOptional()
			}
		}

		while (true) {
			val (system, location) = flyShipQueue.receive()

			if (system == world.location) {
				// fly ship in current system
				flyShipInSystem(location)
			} else {
				// we need to travel to other system
				val exists = server.universe.parameters(UniversePos(system)) != null

				if (!exists)
					continue

				currentFlightJob?.cancel()
				world.removeClient(this)

				shipWorld.eventLoop.execute {
					shipWorld.sky.startFlying(true)
				}

				LOGGER.info("${alias()} is flying to new system: ${UniversePos(system)}")

				currentOrbitalWarpAction = KOptional()

				for (client in shipWorld.clients) {
					client.client.orbitalWarpAction = KOptional()
				}

				world = server.loadSystemWorld(system).await() ?: world
				shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination
				this.systemWorld = world
				ship = world.addClient(this).await()
				systemWorldShip = ship

				val newParams = ship.location.skyParameters(world)

				shipWorld.eventLoop.execute {
					shipWorld.sky.stopFlyingAt(newParams)
				}

				// this seems to be way too big isn't it?
				delay((Globals.universeServer.queuedFlightWaitTime * 1000.0).toLong())

				if (location != SystemWorldLocation.Transit) {
					// player clicked on specific planet in remote system view
					flyShipInSystem(location)
				}
			}
		}
	}

	val celestialRequestQueue = Channel<Either<Vector2i, Vector3i>>(capacity = 100)

	fun pushCelestialRequests(requests: Iterable<Either<Vector2i, Vector3i>>) {
		requests.forEach {
			celestialRequestQueue.trySend(it)
		}
	}

	private val celestialRequestsBudget = ActionPacer(64, 512)

	private suspend fun handleCelestialRequests(requests: Collection<Either<Vector2i, Vector3i>>) {
		val responses = ArrayList<Either<CelestialResponsePacket.ChunkData, CelestialResponsePacket.SystemData>>()

		for (request in requests) {
			if (request.isLeft) {
				val chunkPos = request.left()

				val constellations = server.universe.chunkConstellations(chunkPos)
				val systems = server.universe.chunkSystems(chunkPos)
				val systemParameters = HashMap<Vector3i, CelestialParameters>()
				val planets = HashMap<Vector3i, HashMap<Int, CelestialResponsePacket.PlanetData>>()

				coroutineScope {
					for (system in systems) {
						launch {
							celestialRequestsBudget.consume()
							systemParameters[system.location] = server.universe.parameters(system) ?: return@launch

							val systemPlanets = HashMap<Int, CelestialResponsePacket.PlanetData>()
							planets[system.location] = systemPlanets

							for (planetPos in server.universe.children(system)) {
								val parameters = server.universe.parameters(planetPos) ?: continue
								val satelliteMap = HashMap<Int, CelestialParameters>()

								for (satellitePos in server.universe.children(planetPos)) {
									satelliteMap[satellitePos.satelliteOrbit] = server.universe.parameters(satellitePos) ?: continue
								}

								systemPlanets[planetPos.planetOrbit] = CelestialResponsePacket.PlanetData(parameters, satelliteMap)
							}
						}
					}
				}

				responses.add(Either.left(CelestialResponsePacket.ChunkData(
					chunkPos, constellations, systemParameters, planets
				)))
			} else {
				// even if malicious actor will do spread-out requests, which will generate new chunk each time
				// we still handle requests sequentially
				celestialRequestsBudget.consume()
				val systemPos = UniversePos(request.right())
				val map = HashMap<Int, CelestialResponsePacket.PlanetData>()

				for (planet in server.universe.children(systemPos)) {
					val planetData = server.universe.parameters(planet) ?: continue
					val children = HashMap<Int, CelestialParameters>()

					for (satellite in server.universe.children(planet)) {
						children[satellite.satelliteOrbit] = server.universe.parameters(satellite) ?: continue
					}

					map[planet.planetOrbit] = CelestialResponsePacket.PlanetData(planetData, children)
				}

				responses.add(Either.right(CelestialResponsePacket.SystemData(systemPos.location, map)))
			}
		}

		send(CelestialResponsePacket(responses))
	}

	// protects from malicious actors
	private suspend fun celestialRequestsHandler() {
		while (true) {
			val next = celestialRequestQueue.receive()
			val requests = ArrayList<Either<Vector2i, Vector3i>>()
			requests.add(next)

			while (true) {
				val tryNext = celestialRequestQueue.tryReceive().getOrNull() ?: break
				requests.add(tryNext)
			}

			handleCelestialRequests(requests)
		}
	}

	fun enqueueWarp(destination: WarpAction, deploy: Boolean = false, ifFailed: WarpAction? = null) {
		warpQueue.trySend(WarpRequest(destination, deploy, ifFailed))
	}

	private var sendContextUpdatesTask: Future<*>? = null
	private var sendUniverseTimeTask: Future<*>? = null

	private fun sendContextUpdates(shipWorldChanges: Map<ByteKey, KOptional<ByteArray>> = mapOf()) {
		if (isConnected && isReady) {
			val entries = rpc.write()

			if (entries != null || shipWorldChanges.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) {
				val (data, version) = server2clientGroup.write(remoteVersion, isLegacy)
				remoteVersion = version

				send(ClientContextUpdatePacket(
					entries ?: listOf(),
					KOptional(shipWorldChanges),
					KOptional(data)))
			}
		}
	}

	// set to true so failed connection attempts don't appear in chat
	private var announcedDisconnect = true
	private val isDisconnecting = AtomicBoolean()
	private val disconnectSignal = CompletableFuture<Boolean>()

	fun waitDisconnect(howLong: Long, timeUnit: TimeUnit): Boolean {
		try {
			return disconnectSignal.get(howLong, timeUnit)
		} catch (err: Throwable) {
			return false
		}
	}

	private suspend fun disconnect0(reason: String) {
		// make server forget shipworld right away
		// This should account for extremely unlikely case
		// where client initiated disconnect,
		// and reconnects before this function finishes running
		if (::shipWorld.isInitialized) {
			server.notifyWorldUnloaded(shipWorld.worldID)
		}

		// stop being counted towards player count, vanish from connection list
		server.channels.freeConnectionID(connectionID)
		server.channels.connections.remove(this)
		server.freeNickname(nickname)

		if (countedTowardsPlayerCount) {
			countedTowardsPlayerCount = false
			server.channels.decrementPlayerCount()
		}

		// big try-catch block to cleanup mess if we had exception during disconnection
		try {
			// announce disconnect
			if (!announcedDisconnect) {
				if (reason.isBlank()) {
					server.chat.systemMessage("Player '$nickname' disconnected")
				} else {
					server.chat.systemMessage("Player '$nickname' disconnected ($reason)")
				}

				announcedDisconnect = true
			}

			// cancel periodic tasks
			sendContextUpdatesTask?.cancel(false)
			sendUniverseTimeTask?.cancel(false)
			saveClientContextTask?.cancel(false)

			// remove from world before saving client context
			tracker?.remove("Disconnect: $reason")
			tracker = null

			// write server state
			saveClientContext()

			// remove from system world
			systemWorld?.removeClient(this)
			systemWorld = null

			playerEntity = null

			// stop coroutines
			scope.cancel(CancellationException("Client disconnect: $reason"))

			if (channel.isOpen && ::shipWorld.isInitialized) {
				// if channel is still open, send one last update packet
				shipWorld.eventLoop.shutdown()

				while (!shipWorld.eventLoop.isTerminated) {
					delay(10L)
				}

				legacyWorldStorage.commit()
				legacyWorldStorage.waitAsync()

				// send pending updates
				sendContextUpdates()
				channel.flush()
			}

			isReady = false

			if (channel.isOpen) {
				// say goodbye, if channel is still open
				channel.write(ServerDisconnectPacket(reason))
				channel.flush()
				channel.close()
			}

			disconnectSignal.complete(true)
		} catch (err: Throwable) {
			LOGGER.error("Exception while disconnecting $this", err)
			disconnectSignal.complete(false)
		} finally {
			// don't leave residue entities in worlds if we encountered an exception
			tracker?.remove("Disconnect: $reason")
			tracker = null
			systemWorld?.removeClient(this)
			systemWorld = null
		}
	}

	override fun disconnect(reason: String) {
		if (isDisconnecting.compareAndSet(false, true)) {
			// launch with global scope because server will pause its own thread
			// and wait for us to disconnect (if server is shutting down)
			Starbound.GLOBAL_SCOPE.launch { disconnect0(reason) }
		}
	}

	override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
		if (!channel.isOpen || isDisconnecting.get())
			return

		if (msg is IServerPacket) {
			try {
				msg.play(this)
			} catch (err: Throwable) {
				LOGGER.error("Failed to handle serverbound packet $msg", err)
				disconnect("Incoming packet caused an exception: $err")
			}
		} else {
			LOGGER.error("Unknown serverbound packet type $msg")
			disconnect("Unknown serverbound packet type $msg")
		}
	}

	private var countedTowardsPlayerCount = false

	@JsonFactory
	data class ClientContextData(
		val shipCoordinate: Vector3i,
		val systemLocation: SystemWorldLocation,
		val returnWarp: WarpAction? = null
	)

	fun saveClientContext() {
		if (server.isShutdown && !server.isSameThread())
			return

		val data = ClientContextData(shipCoordinate.location, systemWorldLocation, returnWarp)
		server.writeClientContext(uuid!!, Starbound.gson.toJsonTree(data) as JsonObject)
	}

	private suspend fun loadDataAndDispatchEventLoops() {
		val context = try {
			Starbound.gson.fromJson(server.loadClientContext(uuid!!).await(), ClientContextData::class.java)
		} catch (err: Throwable) {
			LOGGER.warn("Exception deserializing player context for ${alias()}, considering fresh context", err)
			null
		}

		scope.launch { warpEventLoop() }

		if (context == null) {
			shipWorld.eventLoop.execute { shipWorld.sky.startFlying(true, true) }
			enqueueWarp(WarpAlias.OwnShip)
			val startingLocation = findStartingSystem() ?: return
			scope.launch { shipFlightEventLoop(startingLocation.location, SystemWorldLocation.Celestial(startingLocation)) }
		} else {
			if (context.returnWarp != null) {
				enqueueWarp(context.returnWarp, ifFailed = WarpAlias.OwnShip)
			} else {
				enqueueWarp(WarpAlias.OwnShip)
			}

			scope.launch { shipFlightEventLoop(context.shipCoordinate, context.systemLocation) }
		}

		saveClientContextTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { saveClientContext() }, 0L, 10L, TimeUnit.SECONDS)

		scope.launch { saveClientContext() }
	}

	override fun inGame() {
		super.inGame()
		announcedDisconnect = false
		server.chat.systemMessage("Player '$nickname' connected")
		countedTowardsPlayerCount = true
		server.channels.incrementPlayerCount()

		sendContextUpdatesTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
			sendContextUpdates()
		}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)

		sendUniverseTimeTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
			if (isReady)
				send(UniverseTimeUpdatePacket(server.universeClock.time), false)
		}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)

		scope.launch { celestialRequestsHandler() }

		if (isLegacy) {
			server.loadShipWorld(this, legacyWorldStorage).thenAccept {
				if (!isConnected || !channel.isOpen) {
					LOGGER.warn("$this disconnected before loaded their ShipWorld")
					it.eventLoop.shutdown()
				} else {
					shipWorld = it
					shipWorld.sky.referenceClock  = server.universeClock
					// shipWorld.sky.startFlying(true, true)

					if (!shipWorld.eventLoop.isAlive)
						shipWorld.eventLoop.start()

					scope.launch { shipUpgradesLoop() }
					scope.launch { loadDataAndDispatchEventLoops() }
				}
			}.exceptionally {
				LOGGER.error("Error while initializing shipworld for $this", it)
				disconnect("Error while initializing shipworld for player: $it")
				null
			}
		}
	}

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