package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArrayList
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.io.readColor
import ru.dbotthepony.kstarbound.io.writeColor
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.mergeJson
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.DataInputStream
import java.io.DataOutputStream

enum class SkyType {
	BARREN,
	ATMOSPHERIC,
	ATMOSPHERELESS,
	ORBITAL,
	WARP,
	SPACE;

	companion object {
		val CODEC = StreamCodec.Enum(SkyType::class.java)
	}
}

enum class FlyingType(override val jsonName: String) : IStringSerializable {
	NONE("none"),
	DISEMBARKING("disembarking"),
	WARP("warp"),
	ARRIVING("arriving");

	companion object {
		val CODEC = StreamCodec.Enum(FlyingType::class.java)
	}
}

enum class WarpPhase {
	SLOWING_DOWN,
	MAINTAIN,
	SPEEDING_UP;
}

enum class SkyOrbiterType(override val jsonName: String) : IStringSerializable {
	SUN("sun"),
	MOON("moon"),
	HORIZON_CLOUD("horizoncloud"),
	SPACE_DEBRIS("scapedebris");
}

@JsonFactory
data class SkyOrbiter(val type: SkyOrbiterType, val scale: Double, val angle: Double, val image: String, val position: Vector2d)

@JsonFactory
data class SkyColoring(
	val mainColor: RGBAColor = RGBAColor.BLACK,

	val morningColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
	val dayColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
	val eveningColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
	val nightColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,

	val morningLightColor: RGBAColor = RGBAColor.BLACK,
	val dayLightColor: RGBAColor = RGBAColor.BLACK,
	val eveningLightColor: RGBAColor = RGBAColor.BLACK,
	val nightLightColor: RGBAColor = RGBAColor.BLACK,
) {
	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeColor(mainColor)

		stream.writeColor(morningColors.first)
		stream.writeColor(morningColors.second)
		stream.writeColor(dayColors.first)
		stream.writeColor(dayColors.second)
		stream.writeColor(eveningColors.first)
		stream.writeColor(eveningColors.second)
		stream.writeColor(nightColors.first)
		stream.writeColor(nightColors.second)

		stream.writeColor(morningLightColor)
		stream.writeColor(dayLightColor)
		stream.writeColor(eveningLightColor)
		stream.writeColor(nightLightColor)
	}

	companion object {
		val BLACK = SkyColoring()

		fun read(stream: DataInputStream, isLegacy: Boolean): SkyColoring {
			val mainColor = stream.readColor()

			val morningColors = stream.readColor() to stream.readColor()
			val dayColors = stream.readColor() to stream.readColor()
			val eveningColors = stream.readColor() to stream.readColor()
			val nightColors = stream.readColor() to stream.readColor()

			val morningLightColor = stream.readColor()
			val dayLightColor = stream.readColor()
			val eveningLightColor = stream.readColor()
			val nightLightColor = stream.readColor()

			return SkyColoring(
				mainColor = mainColor,
				morningColors = morningColors,
				dayColors = dayColors,
				eveningColors = eveningColors,
				nightColors = nightColors,
				morningLightColor = morningLightColor,
				dayLightColor = dayLightColor,
				eveningLightColor = eveningLightColor,
				nightLightColor = nightLightColor,
			)
		}
	}
}

data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation: Double, val layers: List<Pair<String, String>>)

@JsonFactory
data class SkyParameters(
	var skyType: SkyType = SkyType.BARREN,
	var seed: Long = 0L,
	var dayLength: Double? = null,
	var horizonClouds: Boolean = false,
	var skyColoring: SkyColoring? = null,
	var ambientLightLevel: RGBAColor? = RGBAColor.BLACK,
	var spaceLevel: Double? = null,
	var surfaceLevel: Double? = null,
	var planet: Planet? = null,
	var satellites: ImmutableList<Planet> = ImmutableList.of(),
	var horizonImages: ImmutableList<HorizonImage> = ImmutableList.of(),
) {
	@JsonFactory
	data class HorizonImage(val left: String, val right: String)

	@JsonFactory
	data class Layer(val image: String, val scale: Double)

	@JsonFactory
	data class Planet(val pos: Vector2d, val layers: ImmutableList<Layer>)

	companion object {
		private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }

		fun drawWorld(parameters: CelestialParameters, shadow: CelestialParameters? = null): List<Layer> {
			val layers = ArrayList<Layer>()
			val random = random(parameters.seed)
			// i have no clue why they added this but ok
			val shadowRandom = random(shadow?.seed ?: parameters.seed)

			when (parameters.parameters["worldType"]?.asString?.lowercase() ?: return emptyList()) {
				"terrestrial" -> {
					val visitable = parameters.visitableParameters as? TerrestrialWorldParameters ?: return emptyList()
					val gfxConfig = mergeJson(Globals.celestialConfig.terrestrialGraphics["default"] ?: return emptyList(), Globals.celestialConfig.terrestrialGraphics[visitable.typeName] ?: JsonObject())

					val liquidImages = gfxConfig.get("liquidImages", "")
					val baseImages = gfxConfig.get("baseImages", "")
					val shadowImages = gfxConfig.get("shadowImages", "")
					val baseCount = gfxConfig.get("baseCount", 0)
					val dynamicsImages = gfxConfig.get("dynamicsImages", "")

					val imageScale = parameters.parameters.get("imageScale", 1.0)

					// If the planet has water, then draw the corresponding water image as the
					// base layer, otherwise use the bottom most mask image.

					if (visitable.surfaceLiquid.isNotEmptyLiquid && liquidImages.isNotBlank()) {
						layers.add(Layer(liquidImages.replace("<liquid>", visitable.surfaceLiquid.entry!!.key), imageScale))
					} else {
						if (baseCount > 0) {
							layers.add(Layer("${baseImages.replace("<biome>", visitable.primaryBiome).replace("<num>", baseCount.toString())}?hueshift=${visitable.hueShift}", imageScale))
						}
					}

					// Then draw all the biome layers
					for (i in 0 until baseCount) {
						val baseImage = baseImages.replace("<num>", (baseCount - i).toString())
						var hueShift = ""
						var dynamicMask = ""

						if (dynamicsImages.isNotBlank())
							dynamicMask = "?addmask=" + dynamicsImages.replace("<num>", random.nextRange(gfxConfig.get("dynamicsRange", vectors)).toString())

						if (visitable.hueShift != 0.0)
							hueShift = "?hueshift=${visitable.hueShift.toInt()}"

						layers.add(Layer(baseImage + hueShift + dynamicMask, imageScale))
					}

					if (shadowImages.isNotBlank()) {
						layers.add(Layer(shadowImages.replace("<num>", shadowRandom.nextRange(gfxConfig.get("shadowNumber", vectors)).toString()), imageScale))
					}
				}

				"asteroids" -> {
					val maskImages = parameters.parameters.get("maskImages").asString
					val masks = parameters.parameters.get("masks").asInt
					val dynamicsImages = parameters.parameters.get("dynamicsImages").asString
					val imageScale = parameters.parameters.get("imageScale", 1.0)
					val dynamicsRange = parameters.parameters.get("dynamicsRange", vectors)

					for (i in 0 until masks) {
						val biomeMaskBase = maskImages.replace("<num>", (masks - i).toString())
						val dynamicMask = dynamicsImages.replace("<num>", random.nextRange(dynamicsRange).toString())
						layers.add(Layer("$biomeMaskBase?addmask=$dynamicMask", imageScale))
					}
				}

				"floatingdungeon" -> {
					val image = parameters.parameters.get("image").asString
					val imageScale = parameters.parameters.get("imageScale", 1.0)

					layers.add(Layer(image, imageScale))

					if ("dynamicsImages" in parameters.parameters && parameters.parameters.get("dynamicsImages").asString.isNotBlank()) {
						layers.add(Layer(
							parameters.parameters.get("dynamicsImages").asString
								.replace("<num>", random.nextRange(parameters.parameters.get("dynamicsRange", vectors)).toString()), imageScale))
					}
				}

				"gasgiant" -> {
					val gfxConfig = Globals.celestialConfig.gasGiantGraphics

					val baseImage = gfxConfig.get("baseImage", "")
					val shadowImages = gfxConfig.get("shadowImages", "")
					val dynamicsImages = gfxConfig.get("dynamicsImages", "")
					val overlayImages = gfxConfig.get("overlayImages", "")
					val overlayCount = gfxConfig.get("overlayCount", 0)
					val imageScale = parameters.parameters.get("imageScale", 1.0)

					val offsetRange = gfxConfig.get("hueShiftOffsetRange", vectors)
					val dynamicsRange = gfxConfig.get("dynamicsRange", vectors)
					var hueshift = random.nextRange(gfxConfig.get("primaryHueShiftRange", vectors))

					if (baseImage.isNotBlank())
						layers.add(Layer("$baseImage?hueshift=$hueshift", imageScale))

					if (overlayImages.isNotBlank()) {
						for (i in 0 until overlayCount) {
							hueshift += random.nextRange(offsetRange)
							val maskImage = dynamicsImages.replace("<num>", random.nextRange(dynamicsRange).toString())
							val overlayImage = overlayImages.replace("<num>", i.toString())
							layers.add(Layer("$overlayImage?hueshift=$hueshift?addmask=$maskImage", imageScale))
						}
					}

					if (shadowImages.isNotBlank()) {
						layers.add(Layer(shadowImages.replace("<num>", shadowRandom.nextRange(gfxConfig.get("shadowNumber", vectors)).toString()), imageScale))
					}
				}
			}

			return layers
		}

		fun horizonImages(parameters: CelestialParameters): List<HorizonImage> {
			fun getLR(value: String): HorizonImage {
				return HorizonImage(value.replace("<selector>", "l"), value.replace("<selector>", "r"))
			}

			val images = ArrayList<HorizonImage>()

			when (parameters.parameters["worldType"]?.asString?.lowercase() ?: return emptyList()) {
				"terrestrial" -> {
					val visitable = parameters.visitableParameters as? TerrestrialWorldParameters ?: return emptyList()
					val gfxConfig = mergeJson(Globals.celestialConfig.terrestrialHorizonGraphics["default"] ?: return emptyList(), Globals.celestialConfig.terrestrialHorizonGraphics[visitable.typeName] ?: JsonObject())

					val baseImages = gfxConfig.get("baseImages").asString
					val atmoTextures = gfxConfig.get("atmosphereTextures").asString
					val shadowTextures = gfxConfig.get("shadowTextures").asString
					val maskTextures = gfxConfig.get("maskTextures").asString
					val liquidTextures = gfxConfig.get("liquidTextures").asString

					val numMasks = gfxConfig.get("maskRange", vectors)
					val maskPerPlanetRange = gfxConfig.get("maskPerPlanetRange", vectors)

					val biomeHueShift = "?hueshift=${visitable.hueShift.toInt()}"

					if (visitable.surfaceLiquid.isNotEmptyLiquid) {
						val random = random(parameters.seed)

						for (i in 0 until 23)
							random.nextInt()

						images.add(getLR(liquidTextures.replace("<liquid>", visitable.surfaceLiquid.entry!!.key)))

						val masksL = ArrayList<String>()
						val masksR = ArrayList<String>()

						for (i in 0 until random.nextRange(maskPerPlanetRange)) {
							val m = random.nextRange(numMasks)
							val (l, r) = getLR(maskTextures.replace("<mask>", m.toString()))
							masksL.add(l)
							masksR.add(r)
						}

						val left = if (masksL.isNotEmpty()) "?addmask=${masksL.joinToString("+")};0;0" else ""
						val right = if (masksR.isNotEmpty()) "?addmask=${masksR.joinToString("+")};0;0" else ""

						val (l, r) = getLR(baseImages + biomeHueShift)
						images.add(HorizonImage(l + left, r + right))
					} else {
						images.add(getLR(baseImages + biomeHueShift))
					}

					// original code uses "atmosphere" parameter, but... it seems to be a leftover?
					if (!visitable.airless) {
						images.add(getLR(atmoTextures))
					}

					images.add(getLR(shadowTextures))
				}

				"asteroids" -> {
					images.add(getLR(Globals.celestialConfig.asteroidsHorizons))
				}

				"floatingdungeon" -> {
					val visitable = parameters.visitableParameters as? FloatingDungeonWorldParameters ?: return emptyList()
					val base = Globals.celestialConfig.floatingDungeonHorizons[visitable.typeName] ?: return emptyList()
					images.add(getLR(base))
				}
			}

			return images
		}

		suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters {
			if (coordinate.isSystem)
				throw IllegalArgumentException("$coordinate is system location")

			val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!")

			val random = random(params.seed)
			val selfPos = params.coordinate

			val nearbyMoons = ArrayList<Planet>()
			val sky = SkyParameters()

			for (i in 0 until 66)
				random.nextInt()

			sky.seed = random.nextLong()

			if (selfPos.isSatellite) {
				val planet = universe.parameters(selfPos.parent())

				if (planet != null) {
					val pos = Vector2d(random.nextDouble(), random.nextDouble())

					// My parent's parent is no one.
					sky.planet = Planet(pos, ImmutableList.copyOf(drawWorld(planet)))
				}
			}

			for (satellitePos in universe.children(selfPos.planet())) {
				if (satellitePos != selfPos) {
					val satellite = universe.parameters(satellitePos)

					if (satellite != null) {
						val pos = Vector2d(random.nextDouble(), random.nextDouble())
						nearbyMoons.add(Planet(pos, ImmutableList.copyOf(drawWorld(satellite, universe.parameters(satellitePos.parent())))))
					}
				}
			}

			sky.satellites = ImmutableList.copyOf(nearbyMoons)
			sky.horizonImages = ImmutableList.copyOf(horizonImages(params))

			val visitable = params.visitableParameters

			if (visitable is TerrestrialWorldParameters) {
				sky.dayLength = visitable.dayLength

				if (visitable.airless) {
					sky.skyType = SkyType.ATMOSPHERELESS
					sky.horizonClouds = false
				} else {
					sky.skyType = SkyType.ATMOSPHERIC
					sky.horizonClouds = true
				}

				sky.skyColoring = visitable.skyColoring
				sky.ambientLightLevel = null
				sky.spaceLevel = visitable.spaceLayer.layerMinHeight.toDouble()
				sky.surfaceLevel = visitable.surfaceLayer.layerMinHeight.toDouble()
			} else {
				sky.skyType = SkyType.BARREN
				sky.horizonClouds = false

				if (visitable is AsteroidsWorldParameters) {
					sky.ambientLightLevel = visitable.ambientLightLevel
				} else if (visitable is FloatingDungeonWorldParameters) {
					sky.ambientLightLevel = visitable.ambientLightLevel
				} else {
					sky.ambientLightLevel = RGBAColor.BLACK
				}
			}

			return sky
		}
	}
}

@JsonFactory
data class SkyGlobalConfig(
	val stars: Stars,
	val disembarkOrigin: PathPiece,
	val disembarkPath: ImmutableList<PathPiece>,
	val spaceDisembarkOrigin: PathPiece,
	val spaceDisembarkPath: ImmutableList<PathPiece>,
	val arrivalOrigin: PathPiece,
	val arrivalPath: ImmutableList<PathPiece>,
	val spaceArrivalOrigin: PathPiece,
	val spaceArrivalPath: ImmutableList<PathPiece>,
	val correctionPower: Double = 0.0,
	val speedupTime: Double = 1.0,
	val hyperspaceSpeedupTime: Double = 2.4,
	val slowdownTime: Double = 1.0,
	val hyperspaceSlowdownTime: Double = 2.0,
	val flyMaxVelocity: Double = 0.0,
	val starVelocityFactor: Double = 0.0,
	val flyingTimer: Double = 0.0,
	val flashTimer: Double = 1.0,
) {
	@JsonFactory
	data class Stars(
		val frames: Int,
		val list: ImmutableList<AssetPath>,
		val hyperlist: ImmutableList<AssetPath>,
	)

	@JsonFactory
	data class PathPiece(
		val offset: Vector2d,
		val rotation: Double = 0.0,
		val time: Double = 0.0
	) {
		val rotationRad = Math.toDegrees(rotation)
	}
}
