package ru.dbotthepony.kstarbound.server.world

import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialConfig
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable
import java.io.File
import java.time.Duration
import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList

class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeable {
	constructor() : this(null) {
		sources.add(NativeUniverseSource(null, this).generator)
	}

	constructor(folder: File) : this(null) {
		val nativeFile = File(folder, "universe.kchunks")
		val legacyFile = File(folder, "universe.chunks")

		val native = if (!nativeFile.exists()) {
			NativeUniverseSource(BTreeDB6.create(nativeFile, sync = false), this)
		} else {
			NativeUniverseSource(BTreeDB6(nativeFile, sync = false), this)
		}

		val legacy = if (legacyFile.exists()) {
			LegacyUniverseSource(BTreeDB5(legacyFile))
		} else {
			null
		}

		sources.add(native.reader)
		if (legacy != null) sources.add(legacy)
		sources.add(native.generator)

		closeables.add(native)
		if (legacy != null) closeables.add(legacy)
	}

	override val baseInformation: CelestialBaseInformation
		get() = Globals.celestialBaseInformation

	val generationInformation: CelestialConfig
		get() = Globals.celestialConfig

	private val sources = ArrayList<UniverseSource>()
	private val closeables = ArrayList<Closeable>()

	override suspend fun parameters(pos: UniversePos): CelestialParameters? {
		return getChunk(pos)?.parameters(pos)
	}

	override suspend fun hasChildren(pos: UniversePos): Boolean {
		val system = getChunk(pos)?.systems?.get(pos.location) ?: return false

		if (pos.isSystem)
			return system.planets.isNotEmpty()
		else if (pos.isPlanet)
			return system.planets[pos.orbitNumber]?.satellites?.isNotEmpty() ?: false

		return false
	}

	override suspend fun children(pos: UniversePos): List<UniversePos> {
		val chunk = getChunk(pos) ?: return emptyList()
		val system = chunk.systems[pos.location] ?: return listOf()

		if (pos.isSystem)
			return system.planets.keys.intStream().mapToObj { UniversePos(pos.location, it) }.toList()
		else if (pos.isPlanet)
			return system.planets[pos.planetOrbit]?.satellites?.keys?.intStream()?.mapToObj { UniversePos(pos.location, pos.planetOrbit, it) }?.toList() ?: listOf()

		return listOf()
	}

	override suspend fun findSystems(region: AABBi, includedTypes: Set<String>?): List<UniversePos> {
		val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null
		val futures = ArrayList<CompletableFuture<List<UniversePos>>>()

		for (pos in chunkPositions(region)) {
			val f = getChunkFuture(pos).thenApply {
				it.map<List<UniversePos>> {
					val result = ArrayList<UniversePos>()

					if (copy == null) {
						for (system in it.systems.keys) {
							result.add(UniversePos(system))
						}
					} else {
						for ((system, params) in it.systems) {
							if (params.parameters.parameters.get("typeName", "") in copy) {
								result.add(UniversePos(system))
							}
						}
					}

					result
				}.orElse(listOf())
			}

			futures.add(f)
		}

		CompletableFuture.allOf(*futures.toTypedArray()).await()
		return futures.stream().flatMap { it.get().stream() }.toList()
	}

	override suspend fun <T> scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional<T>): KOptional<T> {
		for (pos in chunkPositions(region)) {
			val chunk = getChunk(pos) ?: continue

			for (system in chunk.systems.keys) {
				val result = callback(UniversePos(system))

				if (result.isPresent) {
					return result
				}
			}
		}

		return KOptional()
	}

	suspend fun findRandomWorld(tries: Int, range: Int, seed: Long? = null, visitAll: Boolean = false, predicate: suspend (UniversePos) -> Boolean = { true }): UniversePos? {
		require(tries > 0) { "Non-positive amount of tries: $tries" }
		require(range > 0) { "Non-positive range: $range" }

		val random = random(seed ?: System.nanoTime())
		val rect = AABBi(Vector2i(-range, -range), Vector2i(range, range))

		for (i in 0 until tries) {
			val x = random.nextRange(baseInformation.xyCoordRange)
			val y = random.nextRange(baseInformation.xyCoordRange)

			val result = scanSystems<UniversePos>(rect + Vector2i(x, y)) {
				val children = ArrayList<UniversePos>()

				for (planet in children(it)) {
					children.add(planet)

					for (satellite in children(planet)) {
						children.add(satellite)
					}
				}

				if (children.isEmpty())
					return@scanSystems KOptional()

				if (visitAll) {
					for (world in children)
						if (predicate(world))
							return@scanSystems KOptional(world)
				} else {
					// This sucks, 50% of the time will try and return satellite, not really
					// balanced probability wise
					val world = children.random(random)

					if (predicate(world))
						return@scanSystems KOptional(world)
				}

				KOptional()
			}

			if (result.isPresent) {
				return result.value
			}
		}

		return null
	}

	override suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean): List<Pair<Vector2i, Vector2i>> {
		val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>()

		for (pos in chunkPositions(region)) {
			val f = getChunkFuture(pos).thenApply {
				it.map<List<Pair<Vector2i, Vector2i>>> { ObjectArrayList(it.constellations) }.orElse(listOf())
			}

			futures.add(f)
		}

		CompletableFuture.allOf(*futures.toTypedArray()).await()
		return futures.stream().flatMap { it.get().stream() }.toList()
	}

	override fun scanRegionFullyLoaded(region: AABBi): Boolean {
		return true
	}

	override fun close() {
		closeables.forEach { it.close() }
	}

	// Edge case: we load so many chunks AND try to load same chunk twice that it gets loaded/generated twice
	// shouldn't cause actual issues though
	private val chunkCache: Cache<Vector2i, CompletableFuture<KOptional<UniverseChunk>>> = Caffeine
		.newBuilder()
		.expireAfterAccess(Duration.ofMinutes(10L))
		.maximumSize(1024L)
		.softValues()
		.scheduler(Starbound)
		.executor(Starbound.EXECUTOR)
		.build()

	fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {
		return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } }
	}

	suspend fun getChunk(pos: UniversePos): UniverseChunk? {
		return getChunk(world2chunk(Vector2i(pos.location)))
	}

	suspend fun getChunk(pos: Vector2i): UniverseChunk? {
		val get = getChunkFuture(pos).await()

		if (get.isPresent) {
			return get.value
		} else {
			return null
		}
	}

	override val region: AABBi = AABBi(Vector2i(baseInformation.xyCoordRange.x, baseInformation.xyCoordRange.x), Vector2i(baseInformation.xyCoordRange.y, baseInformation.xyCoordRange.y))

}
