package ru.dbotthepony.kstarbound.world

import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.io.readUUID
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.SpawnTarget
import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpMode
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
import kotlin.math.PI
import kotlin.math.absoluteValue

sealed class SystemWorldLocation {
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
	abstract suspend fun resolve(system: SystemWorld): Vector2d?
	abstract suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>>
	abstract suspend fun skyParameters(system: SystemWorld): SkyParameters

	protected suspend fun appendParameters(parameters: SkyParameters, system: SystemWorld, orbit: UniversePos): SkyParameters {
		val planets = ArrayList<UniversePos>()

		// in original code, they only append nearby planet list only if we are orbiting a planet,
		// and not a satellite. So let's fix that!
		if (orbit.isSatellite) {
			planets.add(orbit.parent())
			planets.addAll(system.universe.children(orbit.parent()))
		} else if (orbit.isPlanet) {
			planets.add(orbit)
			planets.addAll(system.universe.children(orbit))
		}

		val random = random(staticRandom64(orbit.toString()))
		val satellites = ArrayList(parameters.satellites)

		for (body in planets) {
			val pos = Vector2d(random.nextDouble(), random.nextDouble())
			val bodyParams = system.universe.parameters(body) ?: continue

			satellites.add(SkyParameters.Planet(pos, ImmutableList.copyOf(SkyParameters.drawWorld(bodyParams))))
		}

		parameters.satellites = ImmutableList.copyOf(satellites)

		return parameters
	}

	object Transit : SystemWorldLocation() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(0)
		}

		override suspend fun resolve(system: SystemWorld): Vector2d? {
			return null
		}

		override suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> {
			return KOptional()
		}

		override suspend fun skyParameters(system: SystemWorld): SkyParameters {
			return Globals.systemWorld.emptySkyParameters
		}
	}

	// orbiting around specific planet
	data class Celestial(val position: UniversePos) : SystemWorldLocation() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			position.write(stream, isLegacy)
		}

		override suspend fun resolve(system: SystemWorld): Vector2d {
			return system.planetPosition(position)
		}

		override suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> {
			return KOptional(WarpAction.World(WorldID.Celestial(position)) to WarpMode.BEAM_OR_DEPLOY)
		}

		override suspend fun skyParameters(system: SystemWorld): SkyParameters {
			return SkyParameters.create(position, system.universe)
		}
	}

	// orbiting around celestial body
	data class Orbit(val position: SystemWorld.Orbit) : SystemWorldLocation() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)
			position.write(stream, isLegacy)
		}

		override suspend fun resolve(system: SystemWorld): Vector2d {
			return system.orbitPosition(position)
		}

		override suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> {
			return KOptional()
		}

		override suspend fun skyParameters(system: SystemWorld): SkyParameters {
			// original code here is different,
			// since it is assumed we can't "outer" orbit satellites
			// (but that is still technically possible to outer-orbit a satellite)
			return appendParameters(Globals.systemWorld.emptySkyParameters.copy(), system, position.target)
		}
	}

	data class Entity(val uuid: UUID) : SystemWorldLocation() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(3)
			stream.writeUUID(uuid)
		}

		override suspend fun resolve(system: SystemWorld): Vector2d? {
			return system.entities[uuid]?.position
		}

		override suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> {
			val action = system.entities[uuid]?.data?.warpAction ?: return KOptional()
			return KOptional(action to WarpMode.DEPLOY_ONLY)
		}

		override suspend fun skyParameters(system: SystemWorld): SkyParameters {
			val get = system.entities[uuid] ?: return Globals.systemWorld.emptySkyParameters

			val orbit = get.orbit.orNull()?.target
			var sky = get.data.skyParameters

			// original code here is slightly different again,
			// if entity is orbiting a satellite, it won't add planets and
			// its satellites to sky parameters
			if (orbit != null && !orbit.isSystem) {
				sky = sky.copy()
				appendParameters(sky, system, orbit)
			}

			return sky
		}
	}

	data class Position(val position: Vector2d) : SystemWorldLocation() {
		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(4)
			stream.writeStruct2d(position, isLegacy)
		}

		override suspend fun resolve(system: SystemWorld): Vector2d {
			return position
		}

		override suspend fun orbitalAction(system: SystemWorld): KOptional<Pair<WarpAction, WarpMode>> {
			// player can beam to asteroid fields simply by being in proximity to them
			for (child in system.universe.children(system.systemLocation)) {
				if ((system.planetPosition(child).length - position.length).absoluteValue > Globals.systemWorld.asteroidBeamDistance) {
					continue
				}

				val params = system.universe.parameters(child) ?: continue
				val visitable = params.visitableParameters ?: continue

				if (visitable is AsteroidsWorldParameters) {
					val targetX = position.toAngle() / (2.0 * PI) * visitable.worldSize.x
					return KOptional(WarpAction.World(WorldID.Celestial(child), SpawnTarget.X(targetX)) to WarpMode.DEPLOY_ONLY)
				}
			}

			return KOptional()
		}

		override suspend fun skyParameters(system: SystemWorld): SkyParameters {
			for (child in system.universe.children(system.systemLocation)) {
				if ((system.planetPosition(child).length - position.length).absoluteValue > Globals.systemWorld.asteroidBeamDistance) {
					continue
				}

				val params = system.universe.parameters(child) ?: continue
				val visitable = params.visitableParameters ?: continue

				if (visitable is AsteroidsWorldParameters) {
					return SkyParameters.create(child, system.universe)
				}
			}

			return Globals.systemWorld.emptySkyParameters
		}
	}

	companion object {
		val CODEC = nativeCodec(::read, SystemWorldLocation::write)
		val LEGACY_CODEC = legacyCodec(::read, SystemWorldLocation::write)

		fun read(stream: DataInputStream, isLegacy: Boolean): SystemWorldLocation {
			return when (val type = stream.readUnsignedByte()) {
				0 -> Transit
				1 -> Celestial(UniversePos(stream, isLegacy))
				2 -> Orbit(SystemWorld.Orbit(stream, isLegacy))
				3 -> Entity(stream.readUUID())
				4 -> Position(stream.readVector2d(isLegacy))
				else -> throw IllegalStateException("Unknown SystemWorldLocation type $type!")
			}
		}
	}
}
