import { BufferGeometry, Vector3 } from "three";
import { assert } from "../../../../../../core/assert.js";
import { v3_angle_cos_between } from "../../../../../../core/geom/vec3/v3_angle_cos_between.js";
import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
import { CapType } from "../CapType.js";
import { append_compute_cap_geometry_size, make_cap } from "./make_cap.js";
import { make_ring_faces } from "./make_ring_faces.js";
import { make_ring_vertices } from "./make_ring_vertices.js";
import { StreamGeometryBuilder } from "./StreamGeometryBuilder.js";


const v4_array = new Float32Array(4);

/**
 * @see https://github.com/mrdoob/three.js/blob/master/src/geometries/TubeGeometry.js
 * @see https://github.com/hofk/THREEg.js/blob/488f1128a25321a76888aa1fa19db64750318444/THREEg.js#L3483
 * @param {Float32Array|number[]} in_positions
 * @param {Vector3[]} in_normals
 * @param {Vector3[]} in_binormals
 * @param {Vector3[]} in_tangents
 * @param {number[]} shape
 * @param {number[]|Float32Array} shape_normal
 * @param {number} shape_length
 * @param {number[]|Float32Array} shape_transform
 * @param {boolean} [closed]
 * @param {CapType} [cap_type]
 * @returns {BufferGeometry}
 */
export function makeTubeGeometry(
    in_positions, in_normals, in_binormals, in_tangents,
    shape, shape_normal, shape_length, shape_transform, closed = false, cap_type = CapType.Round
) {
    assert.enum(cap_type, CapType, 'cap_type');
    assert.isBoolean(closed, 'closed');

    assert.isNumber(shape_length, 'shape_length');
    assert.isArrayLike(shape, 'shape');

    const out = new StreamGeometryBuilder();

    // helper variables

    const point_count = in_positions.length / 3;
    const tubular_segments = point_count - 1;


    const geometry_size = {
        vertex_count: (tubular_segments + 1) * (shape_length + 1),
        polygon_count: tubular_segments * shape_length * 2
    };

    if (!closed) {
        append_compute_cap_geometry_size(2, geometry_size, shape_length, cap_type);
    }

    out.allocate(
        geometry_size.vertex_count,
        geometry_size.polygon_count
    );

    // create buffer data

    if (!closed) {
        // start cap
        make_cap(
            out, 0,
            in_positions, in_normals, in_binormals, in_tangents,
            shape, shape_normal, shape_length, shape_transform, 1, cap_type
        );
    }

    const index_offset = out.cursor_vertices;

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

        generateSegment(i);

    }

    // if the geometry is not closed, generate the last row of vertices and normals
    // at the regular position on the given path
    //
    // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)

    generateSegment((closed === false) ? tubular_segments : 0);

    // finally create faces
    make_ring_faces(out, index_offset, tubular_segments, shape_length);

    if (!closed) {
        // end cap
        make_cap(
            out, point_count - 1,
            in_positions, in_normals, in_binormals, in_tangents,
            shape, shape_normal, shape_length, shape_transform, -1, cap_type
        );
    }

    /**
     *
     * @param {number} i
     */
    function generateSegment(i) {

        // we use getPointAt to sample evenly distributed points from the given path

        const i3 = i * 3;

        const Px = in_positions[i3];
        const Py = in_positions[i3 + 1];
        const Pz = in_positions[i3 + 2];

        // retrieve corresponding normal and binormal

        const N = in_normals[i];
        const B = in_binormals[i];

        // generate normals and vertices for the current segment
        compute_bend_normal(v4_array, i, tubular_segments, in_positions);

        make_ring_vertices(
            out,
            Px, Py, Pz,
            N, B, in_tangents[i],
            i / tubular_segments, v4_array,
            shape, shape_normal, shape_length, shape_transform
        );

    }


    return out.build();
}


/**
 *
 * @param {number[]|Float32Array} out
 * @param {number} index
 * @param {number} index_count
 * @param {number[]|Float32Array} positions
 */
function compute_bend_normal(
    out,
    index,
    index_count,
    positions
) {
    if (index <= 0 || index >= index_count - 1) {
        // end points, no bending

        out[0] = 0;
        out[1] = 1;
        out[2] = 0;
        out[3] = 0;

        return;
    }

    const index_next = index + 1;
    const index_prev = index - 1;

    const address_current = index * 3;
    const address_next = index_next * 3;
    const address_prev = index_prev * 3;

    const i0_x = positions[address_prev];
    const i0_y = positions[address_prev + 1];
    const i0_z = positions[address_prev + 2];

    const i1_x = positions[address_current];
    const i1_y = positions[address_current + 1];
    const i1_z = positions[address_current + 2];

    const i2_x = positions[address_next];
    const i2_y = positions[address_next + 1];
    const i2_z = positions[address_next + 2];

    const d0_x = i0_x - i1_x;
    const d0_y = i0_y - i1_y;
    const d0_z = i0_z - i1_z;

    const d1_x = i1_x - i2_x;
    const d1_y = i1_y - i2_y;
    const d1_z = i1_z - i2_z;

    // compute rotation axis
    const cross_x = d0_y * d1_z - d0_z * d1_y;
    const cross_y = d0_z * d1_x - d0_x * d1_z;
    const cross_z = d0_x * d1_y - d0_y * d1_x;

    const angle = v3_angle_cos_between(d0_x, d0_y, d0_z, d1_x, d1_y, d1_z);

    const length_inv = 1 / v3_length(cross_x, cross_y, cross_z);

    out[0] = cross_x * length_inv;
    out[1] = cross_y * length_inv;
    out[2] = cross_z * length_inv;

    // bend amount
    out[3] = (1 - Math.abs(angle)) * 0.5;
}
