package ru.dbotthepony.kstarbound.world.terrain

import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
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 kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.sin

class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<IslandSurfaceTerrainSelector.Data>(data, parameters) {
	@JsonFactory
	data class Data(
		val islandElevation: Double,
		val islandTaperPoint: Double,
		val islandHeight: PerlinNoiseParameters,
		val islandDepth: PerlinNoiseParameters,
		val islandDecision: PerlinNoiseParameters,
	)

	private val islandHeightSeed: Long
	private val islandDepthSeed: Long
	private val islandDecisionSeed: Long

	init {
		val random = parameters.random()

		for (i in 0 .. 7)
			random.nextLong() // extra randomness

		islandHeightSeed = random.nextLong()
		islandDepthSeed = random.nextLong()
		islandDecisionSeed = random.nextLong()
	}

	val islandHeight by lazy {
		val perlin = AbstractPerlinNoise.of(this.data.islandHeight)

		if (!perlin.hasSeedSpecified) {
			perlin.init(islandHeightSeed)
		}

		perlin
	}

	val islandDepth by lazy {
		val perlin = AbstractPerlinNoise.of(this.data.islandDepth)

		if (!perlin.hasSeedSpecified) {
			perlin.init(islandDepthSeed)
		}

		perlin
	}

	val islandDecision by lazy {
		val perlin = AbstractPerlinNoise.of(this.data.islandDecision)

		if (!perlin.hasSeedSpecified) {
			perlin.init(islandDecisionSeed)
		}

		perlin
	}

	private fun compute(x: Int): Column {
		val (noiseX, noiseY) = parameters.noiseAngle(x)
		val thisIslandDecision = islandDecision[noiseX, noiseY]

		if (thisIslandDecision > 0.0) {
			val taperFactor = if (thisIslandDecision < data.islandTaperPoint) sin(0.5 * PI * thisIslandDecision) / data.islandTaperPoint else 1.0

			return Column(
				data.islandElevation + parameters.baseHeight + taperFactor * islandHeight[noiseX, noiseY],
				data.islandElevation + parameters.baseHeight - taperFactor * islandDepth[noiseX, noiseY],
			)
		} else {
			return baseHeight
		}
	}

	private val baseHeight = Column(parameters.baseHeight, parameters.baseHeight)

	private class Column(topLevel: Double, bottomLevel: Double) {
		val halfMinus = (topLevel - bottomLevel) / 2.0
		val halfPlus = (topLevel + bottomLevel) / 2.0
	}

	private val cache = Caffeine.newBuilder()
		.maximumSize(512L)
		//.executor(Starbound.EXECUTOR)
		//.scheduler(Starbound)
		.build<Int, Column>(::compute)

	override fun get(x: Int, y: Int): Double {
		val column = cache.get(x)
		return column.halfMinus - (column.halfPlus - y).absoluteValue
	}

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