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.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomFloat
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.time.Duration
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.math.sin

class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<KarstCaveTerrainSelector.Data>(data, parameters) {
	@JsonFactory
	data class Data(
		val sectorSize: Int = 64,
		// we don't actually care
		val layerPerlinsCacheSize: Int = 32,
		// we don't actually care
		val sectorCacheSize: Int = 32,
		val layerResolution: Int,
		val layerDensity: Double,
		val bufferHeight: Int,
		val caveTaperPoint: Int,

		val caveDecision: PerlinNoiseParameters,
		val layerHeightVariation: PerlinNoiseParameters,
		val caveHeightVariation: PerlinNoiseParameters,
		val caveFloorVariation: PerlinNoiseParameters,
	)

	private inner class Layer(y: Int) {
		val caveSeed: Long
		val layerHeightVariationSeed: Long
		val caveHeightVariationSeed: Long
		val caveFloorVariationSeed: Long

		init {
			val random = random(parameters.seed + y)
			caveSeed = random.nextLong()
			layerHeightVariationSeed = random.nextLong()
			caveHeightVariationSeed = random.nextLong()
			caveFloorVariationSeed = random.nextLong()
		}

		val cave = AbstractPerlinNoise.of(data.caveDecision).also { it.init(caveSeed) }
		val layerHeightVariation = AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) }
		val caveHeightVariation = AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) }
		val caveFloorVariation = AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) }
	}

	private val layers = Caffeine.newBuilder()
		.maximumSize(2048L)
		.softValues()
		.expireAfterAccess(Duration.ofMinutes(1))
		.scheduler(Starbound)
		.executor(Starbound.EXECUTOR)
		.build<Int, Layer>(::Layer)

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

		init {
			for (y in sector.y - data.bufferHeight until sector.y + data.sectorSize + data.bufferHeight) {
				val layerChance = data.layerDensity * data.layerResolution
				// determine whether this layer has caves

				if (y % data.layerResolution != 0 || staticRandomFloat(parameters.seed, y, "karst") > layerChance)
					continue

				val layer = layers[y]

				// carve out cave layer
				for (x in sector.x until sector.x + data.sectorSize) {
					// use wrapping noise
					val (noiseX, noiseY) = parameters.noiseAngle(x)

					// determine where caves be at
					val isThereACaveHere = layer.cave[noiseX, noiseY]
					if (isThereACaveHere <= 0.0) continue

					val taperFactor = if (isThereACaveHere < data.caveTaperPoint) sin((0.5 * PI * isThereACaveHere) / data.caveTaperPoint) else 1.0

					val baseY = y + layer.layerHeightVariation[noiseX, noiseY]
					val ceilingY = baseY + layer.caveHeightVariation[noiseX, noiseY] * taperFactor
					val floorY = baseY + layer.caveFloorVariation[noiseX, noiseY] * taperFactor

					val halfHeight = (ceilingY - floorY + 1.0).absoluteValue / 2.0
					val midpointY = (floorY + ceilingY) / 2.0

					maxValue = maxValue.coerceAtLeast(halfHeight)

					for (pointY in floorY.roundToInt() until ceilingY.roundToInt()) {
						if (isInside(x, pointY)) {
							this[x, pointY] = this[x, pointY].coerceAtLeast(halfHeight - (midpointY - pointY).absoluteValue)
						}
					}
				}
			}
		}

		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.KARST_CAVE
}
