package ru.dbotthepony.kstarbound.world.terrain

import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import ru.dbotthepony.kommons.arrays.Double2DArray
import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.time.Duration
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.sign
import kotlin.math.sin
import kotlin.math.sqrt

class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<WormCaveTerrainSelector.Data>(data, parameters) {
	@JsonFactory
	data class Data(
		val sectorSize: Int = 64,
		val lruCacheSize: Int = 32,
		val numberOfWormsPerSectorRange: Vector2d,
		val wormSizeRange: Vector2d,
		val wormLengthRange: Vector2d,
		val wormTaperDistance: Double,
		val wormAngleRange: Vector2d,
		val wormTurnChance: Double,
		val wormTurnRate: Double,
		val wormSpeed: Double = 1.0,
		val sectorRadius: Int,
	)

	private data class Worm(
		var pos: Vector2d,
		var angle: Double,
		var goalAngle: Double,
		val size: Double,
		var length: Double = 0.0,
		val goalLength: Double,
	)

	private inner class Sector(val sector: Vector2i) {
		private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize)
		private var maxValue = data.wormSizeRange.y / 2.0

		init {
			val worms = ArrayList<Worm>()

			// determine worms for the neighbouring sectors
			val sectorRadius = data.sectorRadius * data.sectorSize

			// wormy
			for (x in (sector.x - sectorRadius .. sector.x + sectorRadius).step(data.sectorSize)) {
				for (y in (sector.y - sectorRadius .. sector.y + sectorRadius).step(data.sectorSize)) {
					// worm
					val random = random(staticRandom64(x, y, parameters.seed))
					val numberOfWorms = random.nextRange(data.numberOfWormsPerSectorRange) * parameters.commonality
					var intNumberOfWorms = numberOfWorms.toInt()

					if (random.nextDouble() < numberOfWorms - intNumberOfWorms)
						intNumberOfWorms++

					for (n in 0 until intNumberOfWorms) {
						// wurm
						worms.add(Worm(
							pos = Vector2d(x + random.nextDouble(0.0, data.sectorSize.toDouble()), y + random.nextDouble(0.0, data.sectorSize.toDouble())),
							angle = random.nextRange(data.wormAngleRange),
							goalAngle = random.nextRange(data.wormAngleRange),
							size = random.nextRange(data.wormSizeRange) * parameters.commonality,
							goalLength = random.nextRange(data.wormLengthRange) * parameters.commonality
						))
					}
				}
			}

			while (worms.isNotEmpty()) {
				val itr = worms.iterator()

				for (worm in itr) {
					// taper size
					var wormRadius = worm.size / 2.0

					val taperFactor = if (worm.length < data.wormTaperDistance)
						sin(0.5 * PI * worm.length / data.wormTaperDistance)
					else if (worm.goalLength - worm.length < data.wormTaperDistance)
						sin(0.5 * PI * (worm.goalLength - worm.length) / data.wormTaperDistance)
					else
						1.0

					//wormRadius *= taperFactor

					// carve out worm area
					if (wormRadius >= 1.0) {
						val size = ceil(wormRadius)
						var dx = -size
						var dy = -size

						while (dx <= size) {
							while (dy <= size) {
								val m = sqrt(dx * dx + dy * dy)

								if (m < wormRadius) {
									// TODO: maybe roundToInt()?
									val x = (dx + worm.pos.x).toInt()
									val y = (dy + worm.pos.y).toInt()

									if (isInside(x, y)) {
										this[x, y] = this[x, y].coerceAtLeast(wormRadius - m)
									}
								}

								dy += 1.0
							}

							dy = -size
							dx += 1.0
						}
					}

					// move the worm, slowing down a bit as we
					// reach the ends to reduce stutter
					val thisSpeed = (data.wormSpeed * taperFactor).coerceAtLeast(0.75)
					worm.length += thisSpeed
					worm.pos += Vector2d(cos(worm.angle) * thisSpeed, sin(worm.angle) * thisSpeed)

					// maybe set new goal angle
					// wormy-o!
					if (staticRandomDouble(worm.pos.x, worm.pos.y, parameters.seed, 1) < data.wormTurnChance * thisSpeed) {
						worm.goalAngle = positiveModulo(linearInterpolation(staticRandomDouble(worm.pos.x, worm.pos.y, parameters.seed, 2), data.wormAngleRange.x, data.wormAngleRange.y), PI * 2.0)
					}

					// vroom?
					if (worm.angle != worm.goalAngle) {
						// turn the worm toward goal angle
						var angleDiff = worm.goalAngle - worm.angle

						// stop if we're close enough
						if (angleDiff.absoluteValue < data.wormTurnRate * thisSpeed)
							worm.angle = worm.goalAngle
						else {
							// turn the shortest angular distance
							if (angleDiff.absoluteValue > PI * 2.0)
								angleDiff = -angleDiff

							worm.angle = positiveModulo(worm.angle + data.wormTurnRate * thisSpeed * angleDiff.sign, 2.0 * PI)
						}
					}

					if (worm.length >= worm.goalLength) {
						itr.remove()
					}
				}
			}
		}

		private fun isInside(x: Int, y: Int): Boolean {
			val diffX = x - sector.x
			val diffY = y - sector.y

			return diffX in 0 until data.sectorSize &&
				diffY in 0 until data.sectorSize
		}

		operator fun get(x: Int, y: Int): Double {
			val get = values[x - sector.x, y - sector.y]
			return if (get > 0.0) get else -maxValue
		}

		private operator fun set(x: Int, y: Int, value: Double) {
			values[x - sector.x, y - sector.y] = value
		}
	}

	private val sectors = Caffeine.newBuilder()
		.maximumSize(512L)
		.softValues()
		.expireAfterAccess(Duration.ofMinutes(1))
		.scheduler(Starbound)
		.executor(Starbound.EXECUTOR)
		.build<Vector2i, Sector>(::Sector)

	override fun get(x: Int, y: Int): Double {
		val sector = Vector2i(x - positiveModulo(x, data.sectorSize), y - positiveModulo(y, data.sectorSize))
		return sectors[sector][x, y]
	}

	override val type: TerrainSelectorType
		get() = TerrainSelectorType.WORM_CAVE
}
