package ru.dbotthepony.kstarbound.server

import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.File
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock

sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
	init {
		if (!root.exists()) {
			check(root.mkdirs()) { "Unable to create ${root.absolutePath}" }
		} else if (!root.isDirectory) {
			throw IllegalArgumentException("${root.absolutePath} is not a directory")
		}
	}

	private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
	val universe = ServerUniverse()
	val chat = ChatHandler(this)
	val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())

	val settings = ServerSettings()
	val channels = ServerChannels(this)
	val lock = ReentrantLock()
	var isClosed = false
		private set

	var serverUUID: UUID = UUID.randomUUID()
		protected set

	val universeClock = JVMClock()

	private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()

	private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
		return ServerSystemWorld.create(this, location)
	}

	fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
		return supplyAsync {
			systemWorlds.computeIfAbsent(location) {
				globalScope.async { loadSystemWorld0(location) }.asCompletableFuture()
			}
		}.thenCompose { it }
	}

	private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld {
		LOGGER.info("Creating celestial world $location")

		val template = WorldTemplate.create(location.pos, universe)
		val world = ServerWorld.create(this, template, WorldStorage.Nothing, location)

		try {
			world.sky.referenceClock = universeClock
			world.eventLoop.start()
			world.prepare().await()
		} catch (err: Throwable) {
			LOGGER.fatal("Exception while creating celestial world at $location!", err)
			world.eventLoop.shutdown()
			throw err
		}

		return world
	}

	private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld {
		val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}")
		LOGGER.info("Creating instance world $location")
		val random = random(config.seed ?: System.nanoTime())

		val visitable = when (config.type.lowercase()) {
			"terrestrial" -> TerrestrialWorldParameters.generate(config.planetType!!, config.planetSize!!, random)
			"asteroids" -> AsteroidsWorldParameters.generate(random)
			"floatingdungeon" -> FloatingDungeonWorldParameters.generate(config.dungeonWorld!!)
			else -> throw RuntimeException()
		}

		if (location.threatLevel != null) {
			visitable.threatLevel = location.threatLevel
		}

		if (config.beamUpRule != null) {
			visitable.beamUpRule = config.beamUpRule
		}

		visitable.disableDeathDrops = config.disableDeathDrops

		val template = WorldTemplate(visitable, config.skyParameters, random)
		val world = ServerWorld.create(this, template, WorldStorage.NULL, location)

		try {
			world.setProperty("ephemeral", JsonPrimitive(!config.persistent))

			if (config.useUniverseClock)
				world.sky.referenceClock = universeClock

			world.eventLoop.start()
			world.prepare().await()
		} catch (err: Throwable) {
			LOGGER.fatal("Exception while creating instance world at $location!", err)
			world.eventLoop.shutdown()
			throw err
		}

		return world
	}

	private suspend fun loadWorld0(location: WorldID): ServerWorld {
		return when (location) {
			is WorldID.ShipWorld -> throw IllegalArgumentException("Can't create ship worlds out of thin air")
			is WorldID.Instance -> loadInstanceWorld(location)
			is WorldID.Celestial -> loadCelestialWorld(location)
			is WorldID.Limbo -> throw IllegalArgumentException("Limbo was supplied as world ID")
		}
	}

	fun loadWorld(location: WorldID): CompletableFuture<ServerWorld> {
		return supplyAsync {
			var world = worlds[location]

			if (world != null && world.isCompletedExceptionally) {
				worlds.remove(location)
				world = null
			}

			if (world != null) {
				world
			} else {
				val future = globalScope.async { loadWorld0(location) }.asCompletableFuture()
				worlds[location] = future
				future
			}
		}.thenCompose { it }
	}

	fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> {
		return supplyAsync {
			val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null"))
			val existing = worlds[id]

			if (existing != null)
				throw IllegalStateException("Already has $id!")

			val world = ServerWorld.load(this, storage, id)
			worlds[id] = world
			world
		}.thenCompose { it }
	}

	fun notifyWorldUnloaded(worldID: WorldID) {
		execute {
			worlds.remove(worldID)
		}
	}

	fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
		return loadSystemWorld(location.location)
	}

	init {
		scheduleAtFixedRate(Runnable {
			channels.broadcast(UniverseTimeUpdatePacket(universeClock.time), false)
		}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)

		scheduleAtFixedRate(Runnable {
			tickNormal(Starbound.TIMESTEP)
		}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)

		scheduleAtFixedRate(Runnable {
			tickSystemWorlds()
		}, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS)

		isDaemon = false
		start()
	}

	private val occupiedNicknames = ObjectArraySet<String>()

	fun reserveNickname(name: String, alternative: String): String {
		synchronized(occupiedNicknames) {
			var name = name

			if (name.lowercase() == "server" || name.isBlank()) {
				name = alternative
			}

			while (name in occupiedNicknames) {
				name += "_"
			}

			occupiedNicknames.add(name)
			return name
		}
	}

	fun freeNickname(name: String): Boolean {
		return synchronized(occupiedNicknames) {
			occupiedNicknames.remove(name)
		}
	}

	fun clientByUUID(uuid: UUID): ServerConnection? {
		return channels.connections.firstOrNull { it.uuid == uuid }
	}

	protected abstract fun close0()
	protected abstract fun tick0(delta: Double)

	private fun tickSystemWorlds() {
		systemWorlds.values.removeIf {
			if (it.isCompletedExceptionally) {
				return@removeIf true
			}

			if (!it.isDone) {
				return@removeIf false
			}

			scope.launch {
				try {
					it.get().tick(Starbound.SYSTEM_WORLD_TIMESTEP)
				} catch (err: Throwable) {
					LOGGER.fatal("Exception in system world $it event loop", err)
				}
			}

			if (it.get().shouldClose()) {
				LOGGER.info("Stopping idling ${it.get()}")
				return@removeIf true
			}

			return@removeIf false
		}
	}

	private fun tickNormal(delta: Double) {
		try {
			// universeClock.nanos += Starbound.TIMESTEP_NANOS

			channels.connections.forEach {
				try {
					it.tick()
				} catch (err: Throwable) {
					LOGGER.error("Exception while ticking client connection", err)
					it.disconnect("Exception while ticking client connection: $err")
				}
			}

			tick0(delta)
		} catch (err: Throwable) {
			LOGGER.fatal("Exception in main server event loop", err)
			shutdown()
		}
	}

	override fun performShutdown() {
		super.performShutdown()

		scope.cancel("Server shutting down")
		channels.close()

		worlds.values.forEach {
			if (it.isDone && !it.isCompletedExceptionally) {
				it.get().eventLoop.shutdown()
			}
		}

		worlds.values.forEach {
			if (it.isDone && !it.isCompletedExceptionally) {
				it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS)

				if (!it.get().eventLoop.isTerminated) {
					LOGGER.warn("World ${it.get()} did not shutdown in 10 seconds, forcing termination. This might leave world in inconsistent state!")
					it.get().eventLoop.shutdownNow()
				}
			}

			it.cancel(true)
		}

		universe.close()
		close0()
	}

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