package ru.dbotthepony.kstarbound.world

import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import java.util.concurrent.CompletableFuture

abstract class Universe {
	abstract val region: AABBi
	abstract val baseInformation: CelestialBaseInformation

	abstract suspend fun parameters(pos: UniversePos): CelestialParameters?

	suspend fun name(pos: UniversePos): String? {
		return parameters(pos)?.name
	}

	/**
	 * Return all valid system coordinates in the given x/y range.  All systems
	 * are guaranteed to have unique x/y coordinates, and are meant to be viewed
	 * from the top in 2d.  The z-coordinate is there simpy as a validation
	 * parameter.
	 *
	 * [callback] determines when to stop scanning (returning non empty KOptional will stop scanning)
	 */
	abstract suspend fun <T> scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional<T>): KOptional<T>
	abstract suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean = false): List<Pair<Vector2i, Vector2i>>

	/**
	 * Similar to [scanSystems], but scans for ALL systems in given range, on multiple threads
	 */
	abstract suspend fun findSystems(region: AABBi, includedTypes: Set<String>? = null): List<UniversePos>

	abstract suspend fun hasChildren(pos: UniversePos): Boolean
	abstract suspend fun children(pos: UniversePos): List<UniversePos>

	/**
	 * Returns false if part or all of the specified region is not loaded.  This
	 * is only relevant for calls to scanSystems and scanConstellationLines, and
	 * does not imply that each individual system in the given region is fully
	 * loaded with all planets moons etc, only that scanSystem and
	 * scanConstellationLines are not waiting on missing data.
	 */
	abstract fun scanRegionFullyLoaded(region: AABBi): Boolean

	fun chunkPositions(region: AABBi): List<Vector2i> {
		if (region.mins == region.maxs)
			return emptyList()

		val result = ArrayList<Vector2i>()

		val mins = world2chunk(region.mins)
		val maxs = world2chunk(region.maxs - Vector2i.POSITIVE_XY)

		for (x in mins.x .. maxs.x) {
			for (y in mins.y .. maxs.y) {
				result.add(Vector2i(x, y))
			}
		}

		return result
	}

	fun world2chunk(pos: IStruct2i): Vector2i {
		val (x, y) = pos

		return Vector2i(
			(x - positiveModulo(x, baseInformation.chunkSize)) / baseInformation.chunkSize,
			(y - positiveModulo(y, baseInformation.chunkSize)) / baseInformation.chunkSize)
	}

	fun chunkRegion(chunkPos: IStruct2i): AABBi {
		val mins = Vector2i(chunkPos) * baseInformation.chunkSize
		return AABBi(mins, mins + Vector2i.POSITIVE_XY * baseInformation.chunkSize)
	}
}
