import { Matrix4, Vector3 } from "three";
import { max2 } from "../../../../../../core/math/max2.js";
import { min2 } from "../../../../../../core/math/min2.js";
import { clamp } from "../../../../../../core/math/clamp.js";
import { vec3 } from "gl-matrix";
import { array_copy } from "../../../../../../core/collection/array/array_copy.js";

/**
 * Compute a normal vector for Frenet frame based on a tangent (direction in which vertex points)
 * @param {Vector3} result
 * @param {Vector3} tangent
 */
function computeNormalHintFromTangent(result, tangent) {
    let min = Number.MAX_VALUE;

    const tx = Math.abs(tangent.x);
    const ty = Math.abs(tangent.y);
    const tz = Math.abs(tangent.z);

    if (ty <= min) {

        min = ty;
        result.set(0, 1, 0);

    }

    if (tz < min) {

        min = tz;
        result.set(0, 0, 1);

    }

    if (tx < min) {

        min = tx;
        result.set(1, 0, 0);

    }

}

/**
 * @see https://github.com/mrdoob/three.js/blob/c12c9a166a1369cdd58622fff2aff7e3a84305d7/src/extras/core/Curve.js#L260
 * @param {Float32Array|number[]} points
 * @param {boolean} [closed]
 * @param {Vector3} [normal_hint]
 * @returns {{normals:Vector3[], binormals:Vector3[], tangents:Vector3[]}}
 */
export function computeFrenetFrames(points, closed = false, normal_hint) {
    // see http://www.cs.indiana.edu/pub/techreports/TR425.pdf

    const normal = new Vector3();

    const tangents = [];
    const normals = [];
    const binormals = [];

    const vec = new Vector3();
    const mat = new Matrix4();

    const point_count = points.length / 3;
    const segments = point_count - 1;

    const v3_0 = [];
    const v3_1 = [];

    const p0_a = [];
    const pR_a = [];
    const p1_a = [];

    // compute the tangent vectors for each segment on the curve

    for (let i = 0; i <= segments; i++) {

        // get points on either side
        const i0 = max2(i - 1, 0);
        const i1 = min2(i + 1, segments);

        array_copy(points, i0 * 3, p0_a, 0, 3);
        array_copy(points, i * 3, pR_a, 0, 3);
        array_copy(points, i1 * 3, p1_a, 0, 3);

        // get points along the lines close to the reference

        vec3.sub(v3_0, p0_a, pR_a);
        vec3.normalize(v3_0, v3_0);

        vec3.sub(v3_1, p1_a, pR_a);
        vec3.normalize(v3_1, v3_1);

        vec3.sub(v3_1, v3_1, v3_0);
        vec3.normalize(v3_1, v3_1);

        // write out
        const tangent_x = v3_1[0];
        const tangent_y = v3_1[1];
        const tangent_z = v3_1[2];

        if (tangent_x === 0 && tangent_y === 0 && tangent_z === 0) {
            // no tangent, copy previous one
            if (i > 0) {
                tangents[i] = tangents[i - 1];
            } else {
                // very first tangent is undefined, set something arbitrary
                // TODO take normal_hint into account
                tangents[i] = new Vector3(0, 0, 1);
            }
            continue;

        }

        tangents[i] = new Vector3(
            tangent_x,
            tangent_y,
            tangent_z
        );

    }

    // select an initial normal vector perpendicular to the first tangent vector,
    // and in the direction of the minimum tangent xyz component

    normals[0] = new Vector3();
    binormals[0] = new Vector3();

    if (normal_hint !== undefined) {
        normal.copy(normal_hint);
    } else {
        computeNormalHintFromTangent(normal, tangents[0]);
    }

    vec.crossVectors(tangents[0], normal).normalize();

    normals[0].crossVectors(tangents[0], vec);
    binormals[0].crossVectors(tangents[0], normals[0]);


    // compute the slowly-varying normal and binormal vectors for each segment on the curve

    for (let i = 1; i <= segments; i++) {

        normals[i] = normals[i - 1].clone();

        binormals[i] = binormals[i - 1].clone();

        vec.crossVectors(tangents[i - 1], tangents[i]);

        if (vec.length() > Number.EPSILON) {

            vec.normalize();

            const theta = Math.acos(clamp(tangents[i - 1].dot(tangents[i]), -1, 1)); // clamp for floating pt errors

            normals[i].applyMatrix4(mat.makeRotationAxis(vec, theta));

        }

        binormals[i].crossVectors(tangents[i], normals[i]);

    }

    // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

    if (closed === true) {

        let theta = Math.acos(clamp(normals[0].dot(normals[segments]), -1, 1));
        theta /= segments;

        if (tangents[0].dot(vec.crossVectors(normals[0], normals[segments])) > 0) {

            theta = -theta;

        }

        for (let i = 1; i <= segments; i++) {

            // twist a little...
            normals[i].applyMatrix4(mat.makeRotationAxis(tangents[i], theta * i));
            binormals[i].crossVectors(tangents[i], normals[i]);

        }

    }

    return {
        tangents: tangents,
        normals: normals,
        binormals: binormals
    };
}
