
@file:Suppress("unused", "LiftReturnOrAssignment", "MemberVisibilityCanBePrivate")

package ru.dbotthepony.kstarbound.math

import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.math.intersectRectangles
import ru.dbotthepony.kommons.math.rectangleContainsRectangle
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min

/**
 * Axis Aligned Bounding Box, represented by two points, [mins] as lowermost corner of BB,
 * and [maxs] as uppermost corner of BB
 */
data class AABB(val mins: Vector2d, val maxs: Vector2d) {
	constructor(line: Line2d) : this(
		Vector2d(min(line.p0.x, line.p1.x), min(line.p0.y, line.p1.y)),
		Vector2d(max(line.p0.x, line.p1.x), max(line.p0.y, line.p1.y)),
	)

	init {
		// require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
		// require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" }
	}

	val isEmpty: Boolean
		get() = mins.x > maxs.x || mins.y > maxs.y

	val isZero: Boolean
		get() = mins == maxs

	val isEmptyOrZero: Boolean
		get() = isEmpty || isZero

	operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
	operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs)
	operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs)
	operator fun div(other: AABB) = AABB(mins / other.mins, maxs / other.maxs)

	operator fun plus(other: Vector2d) = AABB(mins + other, maxs + other)
	operator fun minus(other: Vector2d) = AABB(mins - other, maxs - other)
	operator fun times(other: Vector2d) = AABB(mins * other, maxs * other)
	operator fun div(other: Vector2d) = AABB(mins / other, maxs / other)

	operator fun times(other: Double) = AABB(mins * other, maxs * other)
	operator fun div(other: Double) = AABB(mins / other, maxs / other)

	val xSpan get() = maxs.x - mins.x
	val ySpan get() = maxs.y - mins.y
	val centre get() = (mins + maxs) * 0.5

	val A get() = mins
	val B get() = Vector2d(mins.x, maxs.y)
	val C get() = maxs
	val D get() = Vector2d(maxs.x, mins.y)

	val bottomLeft get() = A
	val topLeft get() = B
	val topRight get() = C
	val bottomRight get() = D

	val width get() = maxs.x - mins.x
	val height get() = maxs.y - mins.y

	val volume get() = max(width * height, 0.0)

	val extents get() = Vector2d(width * 0.5, height * 0.5)

	val diameter get() = mins.distance(maxs)
	val radius get() = diameter / 2.0

	val perimeter get() = (xSpan + ySpan) * 2.0

	val edges: List<Line2d> by lazy {
		immutableList {
			accept(Line2d(Vector2d(mins.x, mins.y), Vector2d(maxs.x, mins.y)))
			accept(Line2d(Vector2d(maxs.x, mins.y), Vector2d(maxs.x, maxs.y)))
			accept(Line2d(Vector2d(maxs.x, maxs.y), Vector2d(mins.x, maxs.y)))
			accept(Line2d(Vector2d(mins.x, maxs.y), Vector2d(mins.x, mins.y)))
		}
	}

	fun isInside(point: IStruct2d): Boolean {
		return point.component1() in mins.x .. maxs.x && point.component2() in mins.y .. maxs.y
	}

	fun isInside(x: Double, y: Double): Boolean {
		return x in mins.x .. maxs.x && y in mins.y .. maxs.y
	}

	operator fun contains(point: IStruct2d) = isInside(point)

	/**
	 * Checks whenever is this AABB intersect with [other]
	 *
	 * This method consider they intersect even if only one of axis are equal
	 */
	fun intersect(other: AABB): Boolean {
		return intersectRectangles(mins, maxs, other.mins, other.maxs)
	}

	fun intersect(line: Line2d): Boolean {
		return line.p0 in this || line.p1 in this || edges.any { it.intersect(line).intersects }
	}

	/**
	 * Returns whenever [other] is contained (encased) inside this AABB
	 */
	operator fun contains(other: AABB): Boolean {
		return rectangleContainsRectangle(mins, maxs, other.mins, other.maxs)
	}

	/**
	 * Checks whenever is this AABB intersect with [other]
	 *
	 * This method DOES NOT consider they intersect if only one of axis are equal
	 */
	fun intersectWeak(other: AABB): Boolean {
		if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y)
			return false

		return intersectRectangles(mins, maxs, other.mins, other.maxs)
	}

	fun intersectionDepth(other: AABB): Vector2d {
		val xDepth: Double
		val yDepth: Double

		val thisCentre = centre
		val otherCentre = other.centre

		if (thisCentre.x > otherCentre.x) {
			// считаем, что мы вошли справа
			xDepth = mins.x - other.maxs.x
		} else {
			// считаем, что мы вошли слева
			xDepth = maxs.x - other.mins.x
		}

		if (thisCentre.y > otherCentre.y) {
			// считаем, что мы вошли сверху
			yDepth = mins.y - other.maxs.y
		} else {
			// считаем, что мы вошли снизу
			yDepth = maxs.x - other.mins.x
		}

		return Vector2d(xDepth, yDepth)
	}

	fun distance(other: AABB): Double {
		val intersectX: Boolean
		val intersectY: Boolean

		if (ySpan <= other.ySpan)
			intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
		else
			intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y

		if (xSpan <= other.xSpan)
			intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
		else
			intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x

		if (intersectY && intersectX) {
			return 0.0
		}

		if (intersectX) {
			return (mins.y - other.maxs.y).absoluteValue.coerceAtMost((maxs.y - other.mins.y).absoluteValue)
		} else {
			return (mins.x - other.maxs.x).absoluteValue.coerceAtMost((maxs.x - other.mins.x).absoluteValue)
		}
	}

	fun pushOutFrom(other: AABB): Vector2d {
		if (!intersect(other))
			return Vector2d.ZERO

		val depth = intersectionDepth(other)

		if (depth.x.absoluteValue < depth.y.absoluteValue) {
			return Vector2d(x = depth.x)
		} else {
			return Vector2d(y = depth.y)
		}
	}

	fun overlap(other: AABB): AABB {
		return AABB(
			Vector2d(max(mins.x, other.mins.x), max(mins.y, other.mins.y)),
			Vector2d(min(maxs.x, other.maxs.x), min(maxs.y, other.maxs.y)),
		)
	}

	/**
	 * Returns AABB which contains both AABBs
	 */
	fun combine(other: AABB): AABB {
		if (contains(other)) return this

		val minX = mins.x.coerceAtMost(other.mins.x)
		val minY = mins.y.coerceAtMost(other.mins.y)
		val maxX = maxs.x.coerceAtLeast(other.maxs.x)
		val maxY = maxs.y.coerceAtLeast(other.maxs.y)

		return AABB(Vector2d(minX, minY), Vector2d(maxX, maxY))
	}

	/**
	 * Returns AABB which contains this AABB and specified point
	 */
	fun expand(x: Double, y: Double): AABB {
		if (isInside(x, y))
			return this

		return AABB(
			mins.coerceAtMost(x, y),
			maxs.coerceAtLeast(x, y)
		)
	}

	fun expand(value: IStruct2d) = expand(value.component1(), value.component2())
	fun padded(value: IStruct2d) = expand(value.component1(), value.component2())
	fun padded(x: Double, y: Double) = expand(x, y)

	/**
	 * Returns AABB which edges are expanded by [x] and [y] along their normals
	 */
	fun enlarge(x: Double, y: Double): AABB {
		if (x == 0.0 && y == 0.0) return this

		return AABB(
			Vector2d(mins.x - x, mins.y - y),
			Vector2d(maxs.x + x, maxs.y + y),
		)
	}

	fun encasingIntAABB(): AABBi {
		return AABBi(
			Vector2i(roundByAbsoluteValue(mins.x), roundByAbsoluteValue(mins.y)),
			Vector2i(roundByAbsoluteValue(maxs.x), roundByAbsoluteValue(maxs.y)),
		)
	}

	companion object {
		/**
		 * Rectangle with given [width] and [height]
		 */
		fun rectangle(pos: IStruct2d, width: Double, height: Double = width): AABB {
			val (x, y) = pos

			return AABB(
				Vector2d(x - width / 2.0, y - height / 2.0),
				Vector2d(x + width / 2.0, y + height / 2.0),
			)
		}

		/**
		 * Rectangle with given [width] * 2 and [height] * 2
		 */
		fun withSide(pos: IStruct2d, width: Double, height: Double = width): AABB {
			val (x, y) = pos

			return AABB(
				Vector2d(x - width, y - height),
				Vector2d(x + width, y + height),
			)
		}

		fun leftCorner(pos: Vector2d, width: Double, height: Double): AABB {
			return AABB(pos, pos + Vector2d(width, height))
		}

		@JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
		@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
	}
}
