package ru.dbotthepony.kstarbound.world

import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.divideUp
import kotlin.math.absoluteValue
import kotlin.math.pow

fun positiveModulo(a: Int, b: Int): Int {
	val result = a % b
	return if (result < 0) result + b else result
}

fun positiveModulo(a: Double, b: Int): Double {
	val result = a % b
	return if (result < 0.0) result + b else result
}

fun positiveModulo(a: Double, b: Double): Double {
	val result = a % b
	return if (result < 0.0) result + b else result
}

fun positiveModulo(a: Float, b: Int): Float {
	val result = a % b
	return if (result < 0f) result + b else result
}

abstract class CoordinateMapper {
	abstract val chunks: Int

	abstract fun cell(value: Int): Int
	abstract fun cell(value: Double): Double
	abstract fun cell(value: Float): Float
	abstract fun chunk(value: Int): Int

	abstract fun chunkFromCell(value: Int): Int
	fun chunkFromCell(value: Float): Int = chunkFromCell(value.toInt())
	fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt())

	data class SplitResult(val rangeA: Vector2i, val rangeB: Vector2i?, val offset: Int)
	data class SplitResultDouble(val rangeA: Vector2d, val rangeB: Vector2d?, val offset: Double)

	/**
	 * One of:
	 * * returns provided range as-is if it is contained in this range
	 * * splits in two if it wraps around one of edges
	 * * clamps if _this_ range is contained in provided range
	 */
	fun split(range: IntRange): SplitResult {
		return split(range.first, range.last)
	}

	abstract fun split(first: Int, last: Int): SplitResult

	// splitting doubles is tricky, because we have to decide what is world's edge
	// In case of ints, world edge is determined to be width - 1, but what about doubles?
	// width - 0.1 is clearly not out of bounds, but how can we represent this?
	abstract fun split(first: Double, last: Double): SplitResultDouble

	// inside world bounds
	abstract fun inBoundsCell(value: Int): Boolean
	abstract fun inBoundsChunk(value: Int): Boolean

	// whenever provided index is legal (if world is wrapped around then provided index is legal outside world bounds)
	open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value)
	open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value)

	abstract fun diff(a: Int, b: Int): Int
	abstract fun diff(a: Double, b: Double): Double

	abstract fun nearestTo(source: Int, target: Int): Int
	abstract fun nearestTo(source: Double, target: Double): Double

	class Wrapper(private val cells: Int) : CoordinateMapper() {
		override val chunks = divideUp(cells, CHUNK_SIZE)
		private val cellsD = cells.toDouble()

		// try to approach reasonable "edge" value, which is just little less than `cells`
		private var cellsEdge = cellsD

		init {
			var power = -64.0

			do {
				cellsEdge -= 2.0.pow(++power)
			} while (cellsEdge >= cellsD)
		}

		private val edgeDelta = cellsD - cellsEdge

		override fun inBoundsCell(value: Int) = value in 0 until cells
		override fun inBoundsChunk(value: Int) = value in 0 until chunks

		override fun isValidCellIndex(value: Int) = true
		override fun isValidChunkIndex(value: Int) = true

		@Suppress("NAME_SHADOWING")
		override fun diff(a: Int, b: Int): Int {
			val a = cell(a)
			val b = cell(b)

			var diff = a - b

			if (diff > cells / 2)
				diff -= cells
			else if (diff < -cells / 2)
				diff += cells

			return diff
		}

		@Suppress("NAME_SHADOWING")
		override fun diff(a: Double, b: Double): Double {
			val a = cell(a)
			val b = cell(b)

			var diff = a - b

			if (diff > cells / 2)
				diff -= cells
			else if (diff < -cells / 2)
				diff += cells

			return diff
		}

		override fun nearestTo(source: Int, target: Int): Int {
			if ((target - source).absoluteValue < cells / 2) {
				return target
			} else {
				return diff(target, source) + source
			}
		}

		override fun nearestTo(source: Double, target: Double): Double {
			if ((target - source).absoluteValue < cells / 2) {
				return target
			} else {
				return diff(target, source) + source
			}
		}

		override fun chunkFromCell(value: Int): Int {
			return chunk(value shr CHUNK_SIZE_BITS)
		}

		override fun cell(value: Int): Int = positiveModulo(value, cells)
		override fun cell(value: Double): Double = positiveModulo(value, cells)
		override fun cell(value: Float): Float = positiveModulo(value, cells)
		override fun chunk(value: Int): Int = positiveModulo(value, chunks)

		override fun split(first: Int, last: Int): SplitResult {
			if (first >= last) {
				// point or empty range
				val wrap = cell(first)
				return SplitResult(Vector2i(wrap, wrap), null, 0)
			} else if (first <= 0 && last >= cells) {
				// covers entire world along this axis
				return SplitResult(Vector2i(0, cells - 1), null, 0)
			} else if (first >= 0 && last < cells) {
				// within range along this axis
				return SplitResult(Vector2i(first, last), null, 0)
			} else {
				val newFirst = cell(first)
				val newLast = cell(last)

				if (first < 0) {
					// wrapped around left edge

					return SplitResult(
						Vector2i(0, newLast),
						Vector2i(newFirst, cells - 1),
						newFirst - cells
					)
				} else {
					// wrapped around right edge

					return SplitResult(
						Vector2i(newFirst, cells - 1),
						Vector2i(0, newLast),
						-newLast - 1
					)
				}
			}
		}

		override fun split(first: Double, last: Double): SplitResultDouble {
			if (first >= last) {
				// point or empty range
				val wrap = cell(first)
				return SplitResultDouble(Vector2d(wrap, wrap), null, 0.0)
			} else if (first <= 0.0 && last >= cellsEdge) {
				// covers entire world along this axis
				return SplitResultDouble(Vector2d(0.0, cellsEdge), null, 0.0)
			} else if (first >= 0.0 && last < cellsEdge) {
				// within range along this axis
				return SplitResultDouble(Vector2d(first, last), null, 0.0)
			} else {
				val newFirst = cell(first)
				val newLast = cell(last)

				if (first < 0.0) {
					// wrapped around left edge

					return SplitResultDouble(
						Vector2d(0.0, newLast.coerceAtLeast(edgeDelta)), // handles case where splitting happens *exactly* at world's border
						Vector2d(newFirst, cellsEdge),
						newFirst - cellsD
					)
				} else {
					// wrapped around right edge

					return SplitResultDouble(
						Vector2d(newFirst, cellsEdge),
						Vector2d(0.0, newLast.coerceAtLeast(edgeDelta)), // handles case where splitting happens *exactly* at world's border
						-newLast - 1
					)
				}
			}
		}
	}

	class Clamper(private val cells: Int) : CoordinateMapper() {
		override val chunks = divideUp(cells, CHUNK_SIZE)
		private val cellsD = cells.toDouble()
		private val cellsF = cells.toFloat()

		// try to approach reasonable "edge" value, which is just little less than `cells`
		private var cellsEdge = cellsD

		init {
			var power = -64.0

			do {
				cellsEdge -= 2.0.pow(++power)
			} while (cellsEdge >= cellsD)
		}

		private var cellsEdgeFloat = cellsF

		override fun diff(a: Int, b: Int): Int {
			return (a - b).coerceIn(0, cells)
		}

		override fun diff(a: Double, b: Double): Double {
			return (a - b).coerceIn(0.0, cellsD)
		}

		init {
			var power = -64f

			do {
				cellsEdgeFloat -= 2f.pow(++power)
			} while (cellsEdgeFloat >= cellsF)
		}

		override fun inBoundsCell(value: Int): Boolean {
			return value in 0 until cells
		}

		override fun chunkFromCell(value: Int): Int {
			return chunk(value shr CHUNK_SIZE_BITS)
		}

		override fun inBoundsChunk(value: Int): Boolean {
			return value in 0 until chunks
		}

		override fun split(first: Int, last: Int): SplitResult {
			// just clamp
			return SplitResult(Vector2i(cell(first), cell(last)), null, 0)
		}

		override fun split(first: Double, last: Double): SplitResultDouble {
			// just clamp
			return SplitResultDouble(Vector2d(cell(first), cell(last)), null, 0.0)
		}

		override fun cell(value: Int): Int {
			return value.coerceIn(0, cells - 1)
		}

		override fun cell(value: Double): Double {
			return value.coerceIn(0.0, cellsEdge)
		}

		override fun cell(value: Float): Float {
			return value.coerceIn(0f, cellsEdgeFloat)
		}

		override fun chunk(value: Int): Int {
			return value.coerceIn(0, chunks - 1)
		}

		override fun nearestTo(source: Int, target: Int): Int {
			return diff(source, target)
		}

		override fun nearestTo(source: Double, target: Double): Double {
			return diff(source, target)
		}
	}
}
