package ru.dbotthepony.kstarbound.math.vector

import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.math.matrix.Matrix2d
import ru.dbotthepony.kstarbound.math.matrix.Matrix3d
import ru.dbotthepony.kstarbound.math.matrix.Matrix4d
import ru.dbotthepony.kstarbound.math.matrix.Matrix4dStack
import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

data class Vector2d(
	val x: Double = 0.0,
	val y: Double = 0.0,
) : IStruct2d, Vector() {
	constructor(value: IStruct2d) : this(value.component1(), value.component2())
	constructor(value: Vector2d) : this(value.x, value.y)

	override val lengthSquared: Double
		get() = x * x + y * y

	override val isFinite: Boolean
		get() = x.isFinite() && y.isFinite()
	override val isNaN: Boolean
		get() = x.isNaN() || y.isNaN()

	val unitVector: Vector2d
		get() {
		var length = lengthSquared
		if (length == 0.0 || length == 1.0) return this
		length = sqrt(length)
		return Vector2d(x / length, y / length)
	}

	val absoluteValue: Vector2d
		get() {
		val x = x.absoluteValue
		val y = y.absoluteValue
		if (x != this.x || y != this.y) return Vector2d(x, y)
		return this
	}

	val r get() = x
	val g get() = y

	val s get() = x
	val t get() = y

	fun plus(x: Double = 0.0, y: Double = 0.0) = Vector2d(this.x + x, this.y + y)
	fun minus(x: Double = 0.0, y: Double = 0.0) = Vector2d(this.x - x, this.y - y)
	fun times(x: Double = 1.0, y: Double = 1.0) = Vector2d(this.x * x, this.y * y)
	fun div(x: Double = 1.0, y: Double = 1.0) = Vector2d(this.x / x, this.y / y)

	operator fun plus(value: IStruct2d): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: IStruct2d): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: IStruct2d): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: IStruct2d): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Vector2d): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: Vector2d): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: Vector2d): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: Vector2d): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Double) = plus(value, value)
	operator fun minus(value: Double) = minus(value, value)
	operator fun times(value: Double) = times(value, value)
	operator fun div(value: Double) = div(value, value)

	fun plus(x: Int = 0, y: Int = 0) = Vector2d(this.x + x, this.y + y)
	fun minus(x: Int = 0, y: Int = 0) = Vector2d(this.x - x, this.y - y)
	fun times(x: Int = 1, y: Int = 1) = Vector2d(this.x * x, this.y * y)
	fun div(x: Int = 1, y: Int = 1) = Vector2d(this.x / x, this.y / y)

	operator fun plus(value: IStruct2i): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: IStruct2i): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: IStruct2i): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: IStruct2i): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Vector2i): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: Vector2i): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: Vector2i): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: Vector2i): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Int) = plus(value, value)
	operator fun minus(value: Int) = minus(value, value)
	operator fun times(value: Int) = times(value, value)
	operator fun div(value: Int) = div(value, value)

	fun plus(x: Float = 0f, y: Float = 0f) = Vector2d(this.x + x, this.y + y)
	fun minus(x: Float = 0f, y: Float = 0f) = Vector2d(this.x - x, this.y - y)
	fun times(x: Float = 1f, y: Float = 1f) = Vector2d(this.x * x, this.y * y)
	fun div(x: Float = 1f, y: Float = 1f) = Vector2d(this.x / x, this.y / y)

	operator fun plus(value: IStruct2f): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: IStruct2f): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: IStruct2f): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: IStruct2f): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Vector2f): Vector2d { val (x, y) = value; return plus(x, y) }
	operator fun minus(value: Vector2f): Vector2d { val (x, y) = value; return minus(x, y) }
	operator fun times(value: Vector2f): Vector2d { val (x, y) = value; return times(x, y) }
	operator fun div(value: Vector2f): Vector2d { val (x, y) = value; return div(x, y) }

	operator fun plus(value: Float) = plus(value, value)
	operator fun minus(value: Float) = minus(value, value)
	operator fun times(value: Float) = times(value, value)
	operator fun div(value: Float) = div(value, value)

	operator fun unaryMinus(): Vector2d = Vector2d(-x, -y)

	override fun equals(other: Any?): Boolean {
		return other is Vector2d && x == other.x && y == other.y
	}

	override fun hashCode(): Int {
		var x = x.hashCode().rotateLeft(16) xor y.hashCode()
		// avalanche bits using murmur3 hash
		x = x xor (x ushr 16)
		x *= -0x7a143595
		x = x xor (x ushr 13)
		x *= -0x3d4d51cb
		x = x xor (x ushr 16)
		return x
	}

	override fun toString(): String {
		return "[$x, $y]"
	}

	/**
	 * 2D vector cross product (Z coordinate)
	 */
	fun cross(other: IStruct2d): Double {
		return x * other.component2() - other.component1() * y
	}

	/**
	 * 2D vector cross product (Z coordinate)
	 */
	fun cross(other: IStruct2f): Double {
		return x * other.component2() - other.component1() * y
	}

	/**
	 * 2D vector cross product (Z coordinate)
	 */
	fun cross(other: IStruct2i): Double {
		return x * other.component2() - other.component1() * y
	}

	fun dot(x: Double, y: Double) = this.x * x + this.y * y
	fun dot(value: IStruct2d): Double { val (x, y) = value; return dot(x, y) }
	fun dot(value: Vector2d): Double { val (x, y) = value; return dot(x, y) }

	fun distanceSquared(x: Double, y: Double): Double {
		val dx = (this.x - x).toDouble()
		val dy = (this.y - y).toDouble()
		return dx * dx + dy * dy
	}

	fun distanceSquared(value: IStruct2d): Double { val (x, y) = value; return distanceSquared(x, y) }
	fun distanceSquared(value: Vector2d): Double { val (x, y) = value; return distanceSquared(x, y) }
	fun distance(x: Double, y: Double) = sqrt(distanceSquared(x, y))
	fun distance(value: IStruct2d): Double { val (x, y) = value; return sqrt(distanceSquared(x, y)) }
	fun distance(value: Vector2d): Double { val (x, y) = value; return sqrt(distanceSquared(x, y)) }

	fun coerceAtLeast(x: Double, y: Double) = Vector2d(this.x.coerceAtLeast(x), this.y.coerceAtLeast(y))
	fun coerceAtMost(x: Double, y: Double) = Vector2d(this.x.coerceAtMost(x), this.y.coerceAtMost(y))

	fun coerceIn(minimum: Vector2d, maximum: Vector2d) = Vector2d(
		this.x.coerceIn(minimum.x, maximum.x),
		this.y.coerceIn(minimum.y, maximum.y))

	fun coerceIn(minimum: IStruct2d, maximum: IStruct2d) = Vector2d(
		this.x.coerceIn(minimum.component1(), maximum.component1()),
		this.y.coerceIn(minimum.component2(), maximum.component2()))

	fun coerceAtLeast(value: IStruct2d): Vector2d { val (x, y) = value; return coerceAtLeast(x, y) }
	fun coerceAtMost(value: IStruct2d): Vector2d { val (x, y) = value; return coerceAtMost(x, y) }
	fun coerceAtLeast(value: Vector2d): Vector2d { val (x, y) = value; return coerceAtLeast(x, y) }
	fun coerceAtMost(value: Vector2d): Vector2d { val (x, y) = value; return coerceAtMost(x, y) }

	fun rotate(angle: Double): Vector2d {
		val s = sin(angle)
		val c = cos(angle)

		return Vector2d(x * c - s * y, s * x + c * y)
	}

	fun toAngle(zeroAngle: Vector2d): Double {
		val dot = unitVector.dot(zeroAngle.unitVector)

		return if (y > 0.0) {
			acos(dot)
		} else {
			-acos(dot)
		}
	}

	fun toAngle(zeroAngle: IStruct2d): Double {
		return toAngle(Vector2d(zeroAngle))
	}

	fun toAngle(): Double {
		return atan2(y, x)
	}

	operator fun times(value: Matrix4d): Vector2d {
		return Vector2d(
			x * value.c00 + y * value.c10 + value.c30,
			x * value.c01 + y * value.c11 + value.c31,
		)
	}

	operator fun times(value: Matrix3d): Vector2d {
		return Vector2d(
			x * value.c00 + y * value.c10 + value.c20,
			x * value.c01 + y * value.c11 + value.c21,
		)
	}

	operator fun times(value: Matrix2d): Vector2d {
		return Vector2d(
			x * value.c00 + y * value.c10,
			x * value.c01 + y * value.c11,
		)
	}

	operator fun times(value: Matrix4dStack) = times(value.last())

	fun toDoubleVector() = this
	fun toFloatVector() = Vector2f(x.toFloat(), y.toFloat())

	companion object {
		@JvmField val ZERO = Vector2d()
		@JvmField val POSITIVE_X = Vector2d(x = 1.0)
		@JvmField val NEGATIVE_X = Vector2d(x = -1.0)
		@JvmField val POSITIVE_Y = Vector2d(y = 1.0)
		@JvmField val NEGATIVE_Y = Vector2d(y = -1.0)

		@JvmField val POSITIVE_XY = Vector2d(1.0, 1.0)
		@JvmField val NEGATIVE_XY = Vector2d(-1.0, -1.0)

		fun angle(angle: Double, amplitude: Double = 1.0): Vector2d {
			if (amplitude == 0.0)
				return ZERO

			val sin = sin(angle)
			val cos = cos(angle)

			return Vector2d(cos * amplitude, sin * amplitude)
		}
	}
}
