package ru.dbotthepony.kstarbound.math.matrix

import ru.dbotthepony.kommons.arrays.Double2DArray
import ru.dbotthepony.kommons.arrays.mulMatrixComponents
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct3d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector3d
import java.nio.ByteBuffer
import java.nio.DoubleBuffer
import kotlin.math.cos
import kotlin.math.sin

sealed class Matrix3d : Double2DArray() {
	abstract var c00: Double
	abstract var c01: Double
	abstract var c02: Double
	abstract var c10: Double
	abstract var c11: Double
	abstract var c12: Double
	abstract var c20: Double
	abstract var c21: Double
	abstract var c22: Double

	fun c00() = c00
	fun c00(value: Double): Matrix3d { c00 = value; return this }
	fun c01() = c01
	fun c01(value: Double): Matrix3d { c01 = value; return this }
	fun c02() = c02
	fun c02(value: Double): Matrix3d { c02 = value; return this }
	fun c10() = c10
	fun c10(value: Double): Matrix3d { c10 = value; return this }
	fun c11() = c11
	fun c11(value: Double): Matrix3d { c11 = value; return this }
	fun c12() = c12
	fun c12(value: Double): Matrix3d { c12 = value; return this }
	fun c20() = c20
	fun c20(value: Double): Matrix3d { c20 = value; return this }
	fun c21() = c21
	fun c21(value: Double): Matrix3d { c21 = value; return this }
	fun c22() = c22
	fun c22(value: Double): Matrix3d { c22 = value; return this }

	var r00: Double get() = c00; set(value) { c00 = value } // row 0 column 0
	var r01: Double get() = c10; set(value) { c10 = value } // row 1 column 0
	var r02: Double get() = c20; set(value) { c20 = value } // row 2 column 0
	var r10: Double get() = c01; set(value) { c01 = value } // row 0 column 1
	var r11: Double get() = c11; set(value) { c11 = value } // row 1 column 1
	var r12: Double get() = c21; set(value) { c21 = value } // row 2 column 1
	var r20: Double get() = c02; set(value) { c02 = value } // row 0 column 2
	var r21: Double get() = c12; set(value) { c12 = value } // row 1 column 2
	var r22: Double get() = c22; set(value) { c22 = value } // row 2 column 2

	fun r00() = r00
	fun r00(value: Double): Matrix3d { r00 = value; return this }
	fun r01() = r01
	fun r01(value: Double): Matrix3d { r01 = value; return this }
	fun r02() = r02
	fun r02(value: Double): Matrix3d { r02 = value; return this }
	fun r10() = r10
	fun r10(value: Double): Matrix3d { r10 = value; return this }
	fun r11() = r11
	fun r11(value: Double): Matrix3d { r11 = value; return this }
	fun r12() = r12
	fun r12(value: Double): Matrix3d { r12 = value; return this }
	fun r20() = r20
	fun r20(value: Double): Matrix3d { r20 = value; return this }
	fun r21() = r21
	fun r21(value: Double): Matrix3d { r21 = value; return this }
	fun r22() = r22
	fun r22(value: Double): Matrix3d { r22 = value; return this }

	final override val rows: Int
		get() = 3
	final override val columns: Int
		get() = 3

	final override val determinant: Double
		get() = super.determinant!!
	final override val cofactorMatrix
		get() = from(super.cofactorMatrix!!)
	final override val adjugateMatrix
		get() = from(super.adjugateMatrix!!)
	final override val inversed
		get() = super.inversed?.let { from(it) }

	final override val transposed get() =
		columnMajor(
			c00 = r00,
			c01 = r01,
			c02 = r02,
			c10 = r10,
			c11 = r11,
			c12 = r12,
			c20 = r20,
			c21 = r21,
			c22 = r22,
		)

	final override fun get(column: Int, row: Int): Double {
		return when (column or (row shl 2)) {
			C00 -> c00
			C01 -> c01
			C02 -> c02
			C10 -> c10
			C11 -> c11
			C12 -> c12
			C20 -> c20
			C21 -> c21
			C22 -> c22
			else -> throw IndexOutOfBoundsException("Column $column; Row $row; while size of this matrix are 4x4")
		}
	}

	override fun set(column: Int, row: Int, value: Double) {
		when (column or (row shl 2)) {
			C00 -> c00 = value
			C01 -> c01 = value
			C02 -> c02 = value
			C10 -> c10 = value
			C11 -> c11 = value
			C12 -> c12 = value
			C20 -> c20 = value
			C21 -> c21 = value
			C22 -> c22 = value
			else -> throw IndexOutOfBoundsException("Column $column; Row $row; while size of this matrix are 4x4")
		}
	}

	fun mul(other: Matrix3d): Matrix3d {
		val c00 = mulMatrixComponents(this, other, 0, 0)
		val c01 = mulMatrixComponents(this, other, 0, 1)
		val c02 = mulMatrixComponents(this, other, 0, 2)
		val c10 = mulMatrixComponents(this, other, 1, 0)
		val c11 = mulMatrixComponents(this, other, 1, 1)
		val c12 = mulMatrixComponents(this, other, 1, 2)
		val c20 = mulMatrixComponents(this, other, 2, 0)
		val c21 = mulMatrixComponents(this, other, 2, 1)
		val c22 = mulMatrixComponents(this, other, 2, 2)
		this.c00 = c00
		this.c01 = c01
		this.c02 = c02
		this.c10 = c10
		this.c11 = c11
		this.c12 = c12
		this.c20 = c20
		this.c21 = c21
		this.c22 = c22
		return this
	}

	fun mulIntoOther(other: Matrix3d): Matrix3d {
		val c00 = mulMatrixComponents(this, other, 0, 0)
		val c01 = mulMatrixComponents(this, other, 0, 1)
		val c02 = mulMatrixComponents(this, other, 0, 2)
		val c10 = mulMatrixComponents(this, other, 1, 0)
		val c11 = mulMatrixComponents(this, other, 1, 1)
		val c12 = mulMatrixComponents(this, other, 1, 2)
		val c20 = mulMatrixComponents(this, other, 2, 0)
		val c21 = mulMatrixComponents(this, other, 2, 1)
		val c22 = mulMatrixComponents(this, other, 2, 2)
		other.c00 = c00
		other.c01 = c01
		other.c02 = c02
		other.c10 = c10
		other.c11 = c11
		other.c12 = c12
		other.c20 = c20
		other.c21 = c21
		other.c22 = c22
		return other
	}

	fun mul(
		c00: Double = 1.0,
		c01: Double = 0.0,
		c02: Double = 0.0,
		c10: Double = 0.0,
		c11: Double = 1.0,
		c12: Double = 0.0,
		c20: Double = 0.0,
		c21: Double = 0.0,
		c22: Double = 1.0,
	): Matrix3d {
		val fc00 = this.c00 * c00 + this.c10 * c01 + this.c20 * c02
		val fc01 = this.c01 * c00 + this.c11 * c01 + this.c21 * c02
		val fc02 = this.c02 * c00 + this.c12 * c01 + this.c22 * c02
		val fc10 = this.c00 * c10 + this.c10 * c11 + this.c20 * c12
		val fc11 = this.c01 * c10 + this.c11 * c11 + this.c21 * c12
		val fc12 = this.c02 * c10 + this.c12 * c11 + this.c22 * c12
		val fc20 = this.c00 * c20 + this.c10 * c21 + this.c20 * c22
		val fc21 = this.c01 * c20 + this.c11 * c21 + this.c21 * c22
		val fc22 = this.c02 * c20 + this.c12 * c21 + this.c22 * c22
		this.c00 = fc00
		this.c01 = fc01
		this.c02 = fc02
		this.c10 = fc10
		this.c11 = fc11
		this.c12 = fc12
		this.c20 = fc20
		this.c21 = fc21
		this.c22 = fc22
		return this
	}

	fun mulIntoThis(
		c00: Double = 1.0,
		c01: Double = 0.0,
		c02: Double = 0.0,
		c10: Double = 0.0,
		c11: Double = 1.0,
		c12: Double = 0.0,
		c20: Double = 0.0,
		c21: Double = 0.0,
		c22: Double = 1.0,
	): Matrix3d {
		val fc00 = c00 * this.c00 + c10 * this.c01 + c20 * this.c02
		val fc01 = c01 * this.c00 + c11 * this.c01 + c21 * this.c02
		val fc02 = c02 * this.c00 + c12 * this.c01 + c22 * this.c02
		val fc10 = c00 * this.c10 + c10 * this.c11 + c20 * this.c12
		val fc11 = c01 * this.c10 + c11 * this.c11 + c21 * this.c12
		val fc12 = c02 * this.c10 + c12 * this.c11 + c22 * this.c12
		val fc20 = c00 * this.c20 + c10 * this.c21 + c20 * this.c22
		val fc21 = c01 * this.c20 + c11 * this.c21 + c21 * this.c22
		val fc22 = c02 * this.c20 + c12 * this.c21 + c22 * this.c22
		this.c00 = fc00
		this.c01 = fc01
		this.c02 = fc02
		this.c10 = fc10
		this.c11 = fc11
		this.c12 = fc12
		this.c20 = fc20
		this.c21 = fc21
		this.c22 = fc22
		return this
	}

	final override fun add(other: Double): Matrix3d {
		return super.add(other) as Matrix3d
	}

	final override fun sub(other: Double): Matrix3d {
		return super.sub(other) as Matrix3d
	}

	final override fun mul(other: Double): Matrix3d {
		return super.mul(other) as Matrix3d
	}

	final override fun div(other: Double): Matrix3d {
		return super.div(other) as Matrix3d
	}

	final override fun add(other: Double2DArray): Matrix3d {
		return super.add(other) as Matrix3d
	}

	final override fun sub(other: Double2DArray): Matrix3d {
		return super.sub(other) as Matrix3d
	}

	fun to2d() = Matrix2d.from(this)
	fun to4d(filler: Double) = Matrix4d.from(this, filler)
	fun to4d() = Matrix4d.from(this)

	fun copy(): Matrix3d {
		return Impl(c00, c01, c02, c10, c11, c12, c20, c21, c22)
	}

	@JvmOverloads
	fun translate(x: Double = 0.0, y: Double = 0.0): Matrix3d {
		return mul(c20 = x, c21 = y)
	}

	fun translate(value: IStruct2d) = translate(value.component1(), value.component2())
	fun translate(value: Vector2d) = translate(value.component1(), value.component2())

	@JvmOverloads
	fun preTranslate(x: Double = 0.0, y: Double = 0.0): Matrix3d {
		return mulIntoThis(c20 = x, c21 = y)
	}

	fun preTranslate(value: IStruct2d) = preTranslate(value.component1(), value.component2())
	fun preTranslate(value: Vector2d) = preTranslate(value.component1(), value.component2())

	@JvmOverloads
	fun scale(x: Double = 1.0, y: Double = 1.0, z: Double = 1.0): Matrix3d {
		return mul(c00 = x, c11 = y, c22 = z)
	}

	fun scale(value: IStruct2d) = scale(value.component1(), value.component2())
	fun scale(value: IStruct3d) = scale(value.component1(), value.component2(), value.component3())

	fun scale(value: Vector2d) = scale(value.component1(), value.component2())
	fun scale(value: Vector3d) = scale(value.component1(), value.component2(), value.component3())

	@JvmOverloads
	fun preScale(x: Double = 1.0, y: Double = 1.0, z: Double = 1.0): Matrix3d {
		return mulIntoThis(c00 = x, c11 = y, c22 = z)
	}

	fun preScale(value: IStruct2d) = preScale(value.component1(), value.component2())
	fun preScale(value: IStruct3d) = preScale(value.component1(), value.component2(), value.component3())

	fun preScale(value: Vector2d) = preScale(value.component1(), value.component2())
	fun preScale(value: Vector3d) = preScale(value.component1(), value.component2(), value.component3())

	fun rotateAroundZ(angle: Double): Matrix3d {
		val cos = cos(angle)
		val sin = sin(angle)

		return mul(
			c00 = cos,
			c10 = -sin,
			c01 = sin,
			c11 = cos,
		)
	}

	fun preRotateAroundZ(angle: Double): Matrix3d {
		val cos = cos(angle)
		val sin = sin(angle)

		return mulIntoThis(
			c00 = cos,
			c10 = -sin,
			c01 = sin,
			c11 = cos,
		)
	}

	private data class Impl(
		override var c00: Double,
		override var c01: Double,
		override var c02: Double,
		override var c10: Double,
		override var c11: Double,
		override var c12: Double,
		override var c20: Double,
		override var c21: Double,
		override var c22: Double,
	) : Matrix3d() {
		override fun toString(): String {
			return "Matrix3d" + super.toString()
		}

		override fun equals(other: Any?): Boolean {
			if (other !is Matrix3d) return false

			return this === other || c00 == other.c00 &&
				c01 == other.c01 &&
				c02 == other.c02 &&
				c10 == other.c10 &&
				c11 == other.c11 &&
				c12 == other.c12 &&
				c20 == other.c20 &&
				c21 == other.c21 &&
				c22 == other.c22
		}
	}

	private class View(private val parent: Matrix3d) : Matrix3d() {
		override var c00: Double get() = parent.c00; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c01: Double get() = parent.c01; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c02: Double get() = parent.c02; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c10: Double get() = parent.c10; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c11: Double get() = parent.c11; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c12: Double get() = parent.c12; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c20: Double get() = parent.c20; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c21: Double get() = parent.c21; set(_) { throw UnsupportedOperationException("Read-Only view") }
		override var c22: Double get() = parent.c22; set(_) { throw UnsupportedOperationException("Read-Only view") }

		override fun set(column: Int, row: Int, value: Double) {
			throw UnsupportedOperationException("Read-Only view")
		}

		override fun equals(other: Any?): Boolean {
			return other === this || parent == other
		}

		override fun hashCode(): Int {
			return parent.hashCode()
		}

		override fun toString(): String {
			return "View = $parent"
		}
	}

	companion object {
		@JvmStatic
		fun all(value: Double) = construct3(value, Companion::columnMajor)

		@JvmStatic
		fun zero() = all(0.0)

		@JvmStatic
		fun identity() = columnMajor()

		@JvmStatic
		fun unmodifiable(value: Matrix3d): Matrix3d {
			if (value is View)
				return value
			else
				return View(value)
		}

		@JvmStatic
		fun columnMajor(
			c00: Double = 1.0, c01: Double = 0.0, c02: Double = 0.0,
			c10: Double = 0.0, c11: Double = 1.0, c12: Double = 0.0,
			c20: Double = 0.0, c21: Double = 0.0, c22: Double = 1.0,
		): Matrix3d {
			return Impl(c00, c01, c02, c10, c11, c12, c20, c21, c22)
		}

		@JvmStatic
		fun rowMajor(
			r00: Double = 1.0, r01: Double = 0.0, r02: Double = 0.0,
			r10: Double = 0.0, r11: Double = 1.0, r12: Double = 0.0,
			r20: Double = 0.0, r21: Double = 0.0, r22: Double = 1.0,
		): Matrix3d {
			return Impl(
				r00, r10, r20,
				r01, r11, r21,
				r02, r12, r22,
			)
		}

		@JvmStatic
		fun from(matrix: Double2DArray, filler: Double) = construct3(matrix, Double2DArray::get, Companion::columnMajor, filler)
		@JvmStatic
		fun from(matrix: Double2DArray) = construct3(matrix, Double2DArray::get, Companion::columnMajor, 0.0)
		@JvmStatic
		fun from(matrix: Matrix3d) = construct3(matrix, Double2DArray::get, Companion::columnMajor)
		@JvmStatic
		fun from(matrix: Matrix4d) = construct3(matrix, Double2DArray::get, Companion::columnMajor)

		@JvmStatic
		fun fromTransposed(matrix: Double2DArray, filler: Double) = construct3(matrix, Double2DArray::get, Companion::rowMajor, filler)
		@JvmStatic
		fun fromTransposed(matrix: Double2DArray) = construct3(matrix, Double2DArray::get, Companion::rowMajor, 0.0)
		@JvmStatic
		fun fromTransposed(matrix: Matrix3d) = construct3(matrix, Double2DArray::get, Companion::rowMajor)
		@JvmStatic
		fun fromTransposed(matrix: Matrix4d) = construct3(matrix, Double2DArray::get, Companion::rowMajor)

		@JvmStatic
		fun fromColumnMajor(buffer: DoubleBuffer) = construct3(buffer, DoubleBuffer::get, Companion::columnMajor)
		@JvmStatic
		fun fromColumnMajor(buffer: ByteBuffer) = construct3(buffer, ByteBuffer::getDouble, Companion::columnMajor)
		@JvmStatic
		fun fromRowMajor(buffer: DoubleBuffer) = construct3(buffer, DoubleBuffer::get, Companion::rowMajor)
		@JvmStatic
		fun fromRowMajor(buffer: ByteBuffer) = construct3(buffer, ByteBuffer::getDouble, Companion::rowMajor)

		/**
		 * Constructs new ortho projection matrix, with Y coordinate flipped (useful for OpenGL GUI drawing).
		 *
		 * Inversion of Y means that X 0 Y 0 will be top left position on screen (in OpenGL), not bottom left.
		 */
		@JvmStatic
		fun ortho(left: Double, right: Double, bottom: Double, top: Double): Matrix3d {
			return rowMajor(
				r00 = 2.0 / (right - left),
				r11 = -2.0 / (top - bottom),
				r02 = -(right + left) / (right - left),
				r12 = -(top + bottom) / (top - bottom) + 2f,
			)
		}

		/**
		 * Constructs new ortho projection matrix
		 */
		@JvmStatic
		fun orthoDirect(left: Double, right: Double, bottom: Double, top: Double): Matrix3d {
			return rowMajor(
				r00 = 2.0 / (right - left),
				r11 = 2.0 / (top - bottom),
				r02 = -(right + left) / (right - left),
				r12 = -(top + bottom) / (top - bottom),
			)
		}

		// kotlin compiler bug? it refuses to use TABLESWITCH if these are inlined
		const val C00 = (0 or (0 shl 2))
		const val C01 = (0 or (1 shl 2))
		const val C02 = (0 or (2 shl 2))
		const val C10 = (1 or (0 shl 2))
		const val C11 = (1 or (1 shl 2))
		const val C12 = (1 or (2 shl 2))
		const val C20 = (2 or (0 shl 2))
		const val C21 = (2 or (1 shl 2))
		const val C22 = (2 or (2 shl 2))
	}
}
