package ru.dbotthepony.kstarbound.world.terrain

import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.vector.Vector2d
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.roundToInt

class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<DisplacementTerrainSelector.Data>(data, parameters) {
	@JsonFactory
	data class Data(
		val xType: PerlinNoiseParameters.Type,
		val xScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
		val xOctaves: Int,
		val xFreq: Double,
		val xAmp: Double,
		val xBias: Double = 0.0,
		val xAlpha: Double = 2.0,
		val xBeta: Double = 2.0,

		val yType: PerlinNoiseParameters.Type,
		val yScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
		val yOctaves: Int,
		val yFreq: Double,
		val yAmp: Double,
		val yBias: Double = 0.0,
		val yAlpha: Double = 2.0,
		val yBeta: Double = 2.0,

		val xXInfluence: Double = 1.0,
		val xYInfluence: Double = 1.0,
		val yXInfluence: Double = 1.0,
		val yYInfluence: Double = 1.0,

		val yClamp: Vector2d? = null,
		val yClampSmoothing: Double = 0.0,

		val xClamp: Vector2d? = null,
		val xClampSmoothing: Double = 0.0,

		val seedBias: Long = 0L,
		val source: JsonElement,
	)

	val xFn: AbstractPerlinNoise
	val yFn: AbstractPerlinNoise
	val source: AbstractTerrainSelector<*>

	init {
		// This allows to have multiple nested displacement selectors with different seeds
		// original engine isn't capable of this because nested selectors will have the same seed
		val random = parameters.random()

		xFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
			type = data.xType,
			scale = data.xScale,
			octaves = data.xOctaves,
			frequency = data.xFreq,
			amplitude = data.xAmp,
			bias = data.xBias,
			alpha = data.xAlpha,
			beta = data.xBeta,
		))

		yFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
			type = data.yType,
			scale = data.yScale,
			octaves = data.yOctaves,
			frequency = data.yFreq,
			amplitude = data.yAmp,
			bias = data.yBias,
			alpha = data.yAlpha,
			beta = data.yBeta,
		))

		xFn.init(random.nextLong())
		yFn.init(random.nextLong())
		source = TerrainSelectorType.create(data.source, parameters.withSeed(parameters.seed + data.seedBias))
	}

	override fun get(x: Int, y: Int): Double {
		return source[x + clampX(xFn[x * data.xXInfluence, y * data.xYInfluence]).roundToInt(), y + clampY(yFn[x * data.yXInfluence, y * data.yYInfluence]).roundToInt()]
	}

	private fun clampX(v: Double): Double {
		if (data.xClamp == null)
			return v

		if (data.xClampSmoothing == 0.0)
			return v.coerceIn(data.xClamp.x, data.xClamp.y)

		return 0.2 * ((v - data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)
			+ (v - 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)
			+ (v).coerceIn(data.xClamp.x, data.xClamp.y)
			+ (v + 0.5 * data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y)
			+ (v + data.xClampSmoothing).coerceIn(data.xClamp.x, data.xClamp.y))
	}

	private fun clampY(v: Double): Double {
		if (data.yClamp == null)
			return v

		if (data.xClampSmoothing == 0.0)
			return v.coerceIn(data.yClamp.x, data.yClamp.y)

		return 0.2 * ((v - data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)
			+ (v - 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)
			+ (v).coerceIn(data.yClamp.x, data.yClamp.y)
			+ (v + 0.5 * data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y)
			+ (v + data.yClampSmoothing).coerceIn(data.yClamp.x, data.yClamp.y))
	}

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