@file:Suppress("PackageDirectoryMismatch", "unused")

package godot.core

import godot.annotation.CoreTypeHelper
import godot.annotation.CoreTypeLocalCopy
import godot.common.util.CMP_EPSILON
import godot.common.util.RealT
import godot.common.util.isEqualApprox
import godot.common.util.toRealT
import kotlin.math.abs

class Plane(
    p_normal: Vector3,
    var d: RealT = 0.0
) : CoreType {

    @PublishedApi
    internal var _normal = Vector3(p_normal)


    //PROPERTIES
    /** Return a copy of the normal Vector3
     * Warning: Writing normal.x = 2 will only modify a copy, not the actual object.
     * To modify it, use normal().
     * */
    @CoreTypeLocalCopy
    var normal
        get() = Vector3(_normal)
        set(value) {
            _normal = Vector3(value)
        }

    @CoreTypeHelper
    inline fun <T> normal(block: Vector3.() -> T): T {
        return _normal.block()
    }


    //CONSTANTS
    companion object {
        val PLANE_YZ: Plane
            get() = Plane(1, 0, 0, 0)
        val PLANE_XZ: Plane
            get() = Plane(0, 1, 0, 0)
        val PLANE_XY: Plane
            get() = Plane(0, 0, 1, 0)
    }


    //CONSTRUCTOR
    constructor() :
        this(Vector3(0, 0, 0), 0)

    constructor(other: Plane) :
        this(other._normal, other.d)

    constructor(p1: Number, p2: Number, p3: Number, p4: Number) :
        this(Vector3(p1.toRealT(), p2.toRealT(), p3.toRealT()), p4.toRealT())

    constructor(point1: Vector3, point2: Vector3, point3: Vector3) : this() {
        _normal = (point1 - point3).cross(point1 - point2)
        _normal.normalize()
        d = _normal.dot(point1)
    }

    constructor(normal: Vector3, d: Number) :
        this(normal, d.toRealT())

    constructor(normal: Vector3, point: Vector3) :
        this(normal, normal.dot(point))

    //API

    /**
     * Returns the shortest distance from the plane to the position point.
     */
    fun distanceTo(point: Vector3): RealT {
        return _normal.dot(point) - d
    }

    /**
     * Returns the center of the plane.
     */
    fun getCenter(): Vector3 {
        return _normal * d;
    }

    /**
     * Returns true if [point] is inside the plane. Comparison uses a custom minimum [tolerance] threshold.
     */
    fun hasPoint(point: Vector3, tolerance: Float = 1e-05f) = abs(normal.dot(point) - d) <= tolerance

    /**
     * Returns true if point is inside the plane (by a very minimum epsilon threshold).
     */
    fun hasPoint(point: Vector3, epsilon: RealT): Boolean {
        val dist = abs(_normal.dot(point) - d)
        return dist <= epsilon
    }

    /**
     * Returns the intersection point of the three planes b, c and this plane.
     * If no intersection is found, null is returned.
     */
    fun intersect3(plane1: Plane, plane2: Plane): Vector3? {
        val plane0 = this
        val normal0 = plane0._normal
        val normal1 = plane1._normal
        val normal2 = plane2._normal

        val denom = normal0.cross(normal1).dot(normal2)

        if (abs(denom) <= CMP_EPSILON) {
            return null
        }

        val result: Vector3?
        result = (normal1.cross(normal2) * plane0.d) +
            (normal2.cross(normal0) * plane1.d) +
            (normal0.cross(normal1) * plane2.d) / denom
        return result
    }

    /**
     * Returns the intersection point of a ray consisting of the position from and the direction normal dir with this plane.
     * If no intersection is found, null is returned.
     */
    fun intersectsRay(from: Vector3, dir: Vector3): Vector3? {
        val den = _normal.dot(dir)

        if (abs(den) <= CMP_EPSILON) {
            return null
        }

        val dist = (_normal.dot(from) - d) / den

        if (dist > CMP_EPSILON) {
            return null
        }

        return from + dir * (-dist)
    }

    /**
     * Returns the intersection point of a segment from position begin to position end with this plane.
     * If no intersection is found, null is returned.
     */
    fun intersectsSegment(p_begin: Vector3, p_end: Vector3): Vector3? {
        val segment = p_begin - p_end
        val den = _normal.dot(segment)

        if (abs(den) <= CMP_EPSILON) {
            return null
        }

        val dist = (_normal.dot(p_begin) - d) / den

        if (dist < -CMP_EPSILON || dist > (1.0 + CMP_EPSILON)) {
            return null
        }

        return p_begin + segment * (-dist)
    }

    /**
     * Returns true if this plane and plane are approximately equal, by running isEqualApprox on each component.
     */
    fun isEqualApprox(other: Plane): Boolean {
        return this._normal.isEqualApprox(other._normal) && this.d.isEqualApprox(other.d)
    }

    /**
     * Returns `true` if this plane is finite, by calling @GlobalScope.is_finite on each component.
     */
    fun isFinite() = normal.isFinite() && d.isFinite()

    /**
     * Returns true if point is located above the plane.
     */
    fun isPointOver(point: Vector3): Boolean {
        return _normal.dot(point) > d
    }

    /**
     * Returns a copy of the plane, normalized.
     */
    fun normalized(): Plane {
        val p = this
        p.normalize()
        return p
    }

    internal fun normalize() {
        val l = _normal.length()
        if (l.isEqualApprox(0.0)) {
            this._normal = Vector3()
            this.d = 0.0
            return
        }
        _normal /= l
        d /= l
    }

    /**
     * Returns the orthogonal projection of point p into a point in the plane.
     */
    fun project(point: Vector3): Vector3 {
        return point - _normal * distanceTo(point)
    }

    override fun toString(): String {
        return "Plane(normal=$_normal, d=$d)"
    }

    override fun equals(other: Any?): Boolean = when (other) {
        is Plane -> _normal == other._normal && d == other.d
        else -> false
    }

    override fun hashCode(): Int {
        var result = _normal.hashCode()
        result = 31 * result + d.hashCode()
        return result
    }
}
