package ru.dbotthepony.kstarbound.world

import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import kotlin.collections.ArrayList
import kotlin.math.pow
import kotlin.math.sqrt

data class RayCastResult(
	val traversedTiles: List<HitCell>,
	val hitTile: HitCell?,
	val fraction: Double,
	val startPos: Vector2d,
	val hitPos: Vector2d,
	val direction: Vector2d
) {
	constructor(startPos: Vector2d, direction: Vector2d) : this(listOf(), null, 0.0, startPos, startPos, direction)

	data class HitCell(val pos: Vector2i, val normal: RayDirection, val borderCross: Vector2d, val cell: AbstractCell)
}

enum class RayFilterResult(val hit: Boolean, val write: Boolean) {
	// stop tracing, write hit tile into traversed tiles list
	HIT(true, true),
	// stop tracing, don't write hit tile into traversed tiles list
	HIT_SKIP(true, false),
	// continue tracing, don't write hit tile into traversed tiles list
	SKIP(false, false),
	// continue tracing, write hit tile into traversed tiles list
	CONTINUE(false, true);

	companion object {
		fun of(boolean: Boolean): RayFilterResult {
			return if (boolean) HIT else SKIP
		}
	}
}

fun interface TileRayFilter {
	/**
	 * [x] and [y] are wrapped around positions
	 */
	fun test(cell: AbstractCell, fraction: Double, x: Int, y: Int, normal: RayDirection, borderX: Double, borderY: Double): RayFilterResult
}

val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE }
val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.value.collisionKind.isEmpty) }

fun ICellAccess.castRay(startPos: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter) = castRay(startPos, startPos + direction * length, filter)

// https://www.youtube.com/watch?v=NbSee-XM7WA
fun ICellAccess.castRay(
	start: Vector2d,
	end: Vector2d,
	filter: TileRayFilter
): RayCastResult {
	if (start == end)
		return RayCastResult(start, Vector2d.ZERO)

	val hitTiles = ArrayList<RayCastResult.HitCell>()
	var cellPosX = roundTowardsNegativeInfinity(start.x)
	var cellPosY = roundTowardsNegativeInfinity(start.y)
	var cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(start, Vector2d.ZERO)

	val direction = (end - start).unitVector

	var result = filter.test(cell, 0.0, cellPosX, cellPosY, RayDirection.NONE, start.x, start.y)
	if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell))
	if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell), 0.0, start, start, direction)

	val distance = start.distance(end)
	var travelled = 0.0
	val unitStepSizeX = sqrt(1 + (direction.y / direction.x).pow(2.0))
	val unitStepSizeY = sqrt(1 + (direction.x / direction.y).pow(2.0))

	val stepX: Int
	val stepY: Int

	val xNormal: RayDirection
	val yNormal: RayDirection

	var rayLengthX: Double
	var rayLengthY: Double

	if (direction.x < 0.0) {
		stepX = -1
		rayLengthX = (start.x - cellPosX) * unitStepSizeX
		xNormal = RayDirection.RIGHT
	} else {
		stepX = 1
		rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX
		xNormal = RayDirection.LEFT
	}

	if (direction.y < 0.0) {
		stepY = -1
		rayLengthY = (start.y - cellPosY) * unitStepSizeY
		yNormal = RayDirection.UP
	} else {
		stepY = 1
		rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY
		yNormal = RayDirection.DOWN
	}

	while (travelled < distance) {
		val normal: RayDirection

		if (rayLengthX < rayLengthY) {
			cellPosX += stepX
			travelled = rayLengthX
			rayLengthX += unitStepSizeX
			normal = xNormal
		} else {
			cellPosY += stepY
			travelled = rayLengthY
			rayLengthY += unitStepSizeY
			normal = yNormal
		}

		cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(hitTiles, null, travelled / distance, start, start + direction * travelled, direction)
		result = filter.test(cell, 0.0, cellPosX, cellPosY, normal, start.x + direction.x * travelled, start.y + direction.y * travelled)

		val c = if (result.write || result.hit) {
			RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), normal, start + direction * travelled, cell)
		} else {
			null
		}

		if (result.write) hitTiles.add(c!!)
		if (result.hit) return RayCastResult(hitTiles, c, travelled / distance, start, start + direction * travelled, direction)
	}

	return RayCastResult(hitTiles, null, 1.0, start, start + direction * distance, direction)
}
