import { vec3 } from "gl-matrix";
import { EPSILON } from "../../../../../math/EPSILON.js";

const scratch_m3 = new Float32Array(9);

/**
 * Quadric Error Metric
 * Upper-triangular portion of a symmetric 4x4 matrix encoding a quadric form.
 *
 * Layout (10 elements):
 *   [0]=a2 [1]=ab [2]=ac [3]=ad
 *          [4]=b2 [5]=bc [6]=bd
 *                 [7]=c2 [8]=cd
 *                        [9]=d2
 */
export class Quadric3 extends Float64Array {
    constructor() {
        super(10);
    }

    get a2() {
        return this[0];
    }

    set a2(v) {
        this[0] = v;
    }

    get ab() {
        return this[1];
    }

    set ab(v) {
        this[1] = v;
    }

    get ac() {
        return this[2];
    }

    set ac(v) {
        this[2] = v;
    }

    get ad() {
        return this[3];
    }

    set ad(v) {
        this[3] = v;
    }

    get b2() {
        return this[4];
    }

    set b2(v) {
        this[4] = v;
    }

    get bc() {
        return this[5];
    }

    set bc(v) {
        this[5] = v;
    }

    get bd() {
        return this[6];
    }

    set bd(v) {
        this[6] = v;
    }

    get c2() {
        return this[7];
    }

    set c2(v) {
        this[7] = v;
    }

    get cd() {
        return this[8];
    }

    set cd(v) {
        this[8] = v;
    }

    get d2() {
        return this[9];
    }

    set d2(v) {
        this[9] = v;
    }

    /**
     *
     * @param {ArrayLike<number>|number[]|Float32Array} result
     */
    toVector3(result) {
        result[0] = this[3];
        result[1] = this[6];
        result[2] = this[8];
    }

    /**
     *
     * @param {ArrayLike<number>|number[]|Float32Array} m3
     */
    toTensorM3(m3) {
        const ab = this[1];
        const ac = this[2];
        const bc = this[5];

        m3[0] = this[0];
        m3[1] = ab;
        m3[2] = ac;

        m3[3] = ab;
        m3[4] = this[4];
        m3[5] = bc;

        m3[6] = ac;
        m3[7] = bc;
        m3[8] = this[7];
    }

    /**
     * Equivalent to taking a tensor followed by matrix inversion
     * @param {ArrayLike<number>|number[]|Float32Array} m3
     * @param {number} epsilon
     * @returns {boolean} whether operation was successful or not
     */
    toTensorM3Inverse(m3, epsilon) {
        const a2 = this[0];
        const ab = this[1];
        const ac = this[2];
        const b2 = this[4];
        const bc = this[5];
        const c2 = this[7];

        const det = a2 * (b2 * c2 - bc * bc) -
            ab * (ab * c2 - ac * bc) +
            ac * (ab * bc - ac * b2);

        if (Math.abs(det) > epsilon) {
            // avoid division by zero

            const invdet = 1.0 / det;

            m3[0] = (b2 * c2 - bc * bc) * invdet;
            m3[3] = (bc * ac - ab * c2) * invdet;
            m3[6] = (ab * bc - b2 * ac) * invdet;

            m3[1] = (ac * bc - ab * c2) * invdet;
            m3[4] = (a2 * c2 - ac * ac) * invdet;
            m3[7] = (ab * ac - a2 * bc) * invdet;

            m3[2] = (ab * bc - ac * b2) * invdet;
            m3[5] = (ac * ab - a2 * bc) * invdet;
            m3[8] = (a2 * b2 - ab * ab) * invdet;

            return true;
        }

        // determinant is zero, matrix is singular, cannot invert

        return false;
    }

    /**
     * Set from plane expressed as a 4 component vector
     * @param {number} x plane normal
     * @param {number} y plane normal
     * @param {number} z plane normal
     * @param {number} w plane offset
     */
    setFromVector4(x, y, z, w) {
        this[0] = x * x;
        this[1] = x * y;
        this[2] = x * z;
        this[3] = x * w;

        this[4] = y * y;
        this[5] = y * z;
        this[6] = y * w;

        this[7] = z * z;
        this[8] = z * w;

        this[9] = w * w;
    }

    /**
     * Squared distance from the given point to plane encoded by the quadric
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @returns {number}
     */
    evaluate(x, y, z) {
        // Evaluate vAv + 2bv + c

        return (x * x * this[0]) + (2 * x * y * this[1]) + (2 * x * z * this[2]) + (2 * x * this[3])    // A
            + (y * y * this[4]) + (2 * y * z * this[5]) + (2 * y * this[6])                             // B
            + (z * z * this[7]) + (2 * z * this[8])                                                     // C
            + this[9];                                                                                  // D
    }

    /**
     * Find the optimal vertex position based on the quadric
     *
     * @param {number[]} out vector3
     * @returns {boolean}
     */
    optimize(out) {

        // Based on Blender's implementation: https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenlib/intern/quadric.c

        if (this.toTensorM3Inverse(scratch_m3, EPSILON)) {

            this.toVector3(out);

            vec3.transformMat3(out, out, scratch_m3);
            vec3.negate(out, out);

            return true;
        }

        return false;

    }

    /**
     *
     * @param {Quadric3} Q
     */
    add(Q) {
        this[0] += Q[0];
        this[1] += Q[1];
        this[2] += Q[2];
        this[3] += Q[3];
        this[4] += Q[4];
        this[5] += Q[5];
        this[6] += Q[6];
        this[7] += Q[7];
        this[8] += Q[8];
        this[9] += Q[9];
    }

    /**
     *
     * @param {number} v
     */
    multiplyScalar(v) {
        this[0] *= v;
        this[1] *= v;
        this[2] *= v;
        this[3] *= v;
        this[4] *= v;
        this[5] *= v;
        this[6] *= v;
        this[7] *= v;
        this[8] *= v;
        this[9] *= v;
    }

    /**
     *
     * @returns {Quadric3}
     */
    clone() {
        const r = new Quadric3();

        r.copy(this);

        return r;
    }

    /**
     *
     * @param {Quadric3} other
     */
    copy(other) {
        this.set(other);
    }

    /**
     * Set all matrix values to 0
     */
    clear() {
        this.fill(0);
    }
}

/**
 * Useful for type checking
 * @readonly
 * @type {boolean}
 */
Quadric3.prototype.isQuadric3 = true;
