package ru.dbotthepony.kstarbound.math

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.absoluteValue

private operator fun Vector2d.compareTo(other: Vector2d): Int {
	var cmp = x.compareTo(other.x)
	if (cmp == 0) cmp = y.compareTo(y)
	return cmp
}

@JsonAdapter(Line2d.Adapter::class)
data class Line2d(val p0: Vector2d, val p1: Vector2d) {
	constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d(isLegacy), stream.readVector2d(isLegacy))

	val normal: Vector2d

	init {
		val diff = (p1 - p0).unitVector
		normal = Vector2d(-diff.y, diff.x)
	}

	operator fun plus(other: IStruct2d): Line2d {
		return Line2d(p0 + other, p1 + other)
	}

	operator fun minus(other: IStruct2d): Line2d {
		return Line2d(p0 - other, p1 - other)
	}

	operator fun times(other: IStruct2d): Line2d {
		return Line2d(p0 * other, p1 * other)
	}

	operator fun times(other: Double): Line2d {
		return Line2d(p0 * other, p1 * other)
	}

	data class Intersection(val intersects: Boolean, val point: KOptional<Vector2d>, val t: KOptional<Double>, val coincides: Boolean, val glances: Boolean) {
		companion object {
			val EMPTY = Intersection(false, KOptional(), KOptional(), false, false)
		}
	}

	fun difference(): Vector2d {
		return p1 - p0
	}

	fun reverse(): Line2d {
		return Line2d(p1, p0)
	}

	fun write(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeStruct2d(p0, isLegacy)
		stream.writeStruct2d(p1, isLegacy)
	}

	// original source of this intersection algorithm:
	// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
	// article "Intersection of two lines in three-space" by Ronald Goldman, published in Graphics Gems, page 304
	fun intersect(other: Line2d, infinite: Boolean = false): Intersection {
		val (c, d) = other

		val ab = difference()
		val cd = other.difference()

		val abCross = p0.cross(p1)
		val cdCross = c.cross(d)

		val denominator = ab.cross(cd)
		val xNumber = abCross * cd.x - cdCross * ab.x
		val yNumber = abCross * cd.y - cdCross * ab.y

		if (denominator.absoluteValue <= NEAR_ZERO) {
			if (xNumber.absoluteValue <= NEAR_ZERO && yNumber.absoluteValue <= NEAR_ZERO) {
				val intersects = infinite || (p0 >= c && p0 <= d) || (c >= p0 && c <= p1)
				var point: Vector2d? = null
				var t = 0.0

				if (intersects) {
					if (infinite) {
						point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)
					} else {
						point = if (p0 < c) c else p0
					}
				}

				if (p0 < c) {
					if (c.x != p0.x) {
						t = (c.x - p0.x) / ab.x
					} else {
						t = (c.y - p0.y) / ab.y
					}
				} else if (p0 > d) {
					if (d.x != p0.x) {
						t = (d.x - p0.x) / ab.x
					} else {
						t = (d.y - p0.y) / ab.y
					}
				}

				return Intersection(intersects, KOptional.ofNullable(point), KOptional(t), true, intersects)
			} else {
				return Intersection.EMPTY
			}
		} else {
			val ta = (c - p0).cross(cd) / denominator
			val tb = (c - p0).cross(ab) / denominator

			val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0)

			return Intersection(
				intersects = intersects,
				t = KOptional(ta),
				point = KOptional((p1 - p0) * ta + p0),
				coincides = false,
				glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE)
			)
		}
	}

	fun project(axis: IStruct2d): Double {
		val diff = difference()
		val (x, y) = axis
		return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared
	}

	fun distanceTo(other: IStruct2d, infinite: Boolean = false): Double {
		var proj = project(other)

		if (!infinite)
			proj = proj.coerceIn(0.0, 1.0)

		return (Vector2d(other) - p0 + difference() * proj).length
	}

	fun distanceTo(other: Vector2d, infinite: Boolean = false): Double {
		var proj = project(other)

		if (!infinite)
			proj = proj.coerceIn(0.0, 1.0)

		return (other - p0 + difference() * proj).length
	}

	class Adapter(gson: Gson) : TypeAdapter<Line2d>() {
		private val pair = gson.getAdapter<Pair<Vector2d, Vector2d>>()

		override fun write(out: JsonWriter, value: Line2d) {
			pair.write(out, value.p0 to value.p1)
		}

		override fun read(`in`: JsonReader): Line2d {
			val (a, b) = pair.read(`in`)
			return Line2d(a, b)
		}
	}

	companion object {
		const val NEAR_ZERO = Double.MIN_VALUE * 2.0
		const val NEAR_ONE = 1.0 - NEAR_ZERO
	}
}
