import { v3_dot } from "../../../../../../core/geom/vec3/v3_dot.js";
import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
import { clamp } from "../../../../../../core/math/clamp.js";
import { max2 } from "../../../../../../core/math/max2.js";
import {
    computeNonuniformCaltmullRomSplineDerivative
} from "../../../../../../core/math/spline/computeNonuniformCaltmullRomSplineDerivative.js";
import { PathNormalType } from "../PathNormalType.js";
import { computeFrenetFrames } from "./computeFrenetFrames.js";
import { makeTubeGeometry } from "./makeTubeGeometry.js";


const scratch_array_0 = [];
const scratch_array_1 = [];
const scratch_array_2 = [];
const scratch_array_3 = [];

/**
 *
 * @param {number[]} positions
 * @param {number[]} derivatives
 * @param {number} result_offset
 * @param {Path} path
 * @param {number} offset
 */
function sample_path(positions, derivatives, result_offset, path, offset) {


    if (!path.find_index_and_normalized_distance(scratch_array_0, offset)) {
        return 0;
    }


    /**
     *
     * @type {number}
     */
    const i1 = scratch_array_0[0];

    /**
     *
     * @type {number}
     */
    const t = scratch_array_0[1];


    const input_length = path.getPointCount();

    const max_index = input_length - 1;

    const i0 = clamp(i1 - 1, 0, max_index);
    const i2 = clamp(i1 + 1, 0, max_index);
    const i3 = clamp(i1 + 2, 0, max_index);

    path.readPositionToArray(i0, scratch_array_0, 0);
    path.readPositionToArray(i1, scratch_array_1, 0);
    path.readPositionToArray(i2, scratch_array_2, 0);
    path.readPositionToArray(i3, scratch_array_3, 0);

    computeNonuniformCaltmullRomSplineDerivative(
        positions, result_offset,
        derivatives, result_offset,
        scratch_array_0, scratch_array_1, scratch_array_2, scratch_array_3,
        3, t, 0.5
    );

    // normalize derivative
    const dx = derivatives[result_offset];
    const dy = derivatives[result_offset + 1];
    const dz = derivatives[result_offset + 2];

    const mag = v3_length(dx, dy, dz);

    if (mag !== 0) {
        const inv_mag = 1 / mag;

        derivatives[result_offset] = dx * inv_mag;
        derivatives[result_offset + 1] = dy * inv_mag;
        derivatives[result_offset + 2] = dz * inv_mag;
    }

    return i1;
}

/**
 *
 * @param {Path} path
 * @param {TubePathStyle} style
 * @param {number[]} shape
 * @param {number[]|Float32Array} shape_normal
 * @param {number[]} shape_transform
 * @param {number} segment_start
 * @param {number} segment_end
 * @return {THREE.BufferGeometry}
 */
export function build_geometry_catmullrom(
    path, style,
    shape, shape_normal, shape_transform,
    segment_start, segment_end
) {

    const point_count = path.getPointCount();

    // resample curve
    const total_points = Math.ceil(point_count * style.resolution);

    const reference_step_size = 1 / max2(0.00001, style.resolution);
    const reference_min_step_size = reference_step_size * 0.25;

    let step_size = reference_step_size;

    const path_length = path.length;

    const sample_positions_f32 = [];
    const sample_derivatives_f32 = [];

    let added_points = 0;

    // initial segment
    let last_knot_index = sample_path(
        sample_positions_f32,
        sample_derivatives_f32,
        added_points * 3,
        path,
        segment_start * path_length
    );

    added_points++;

    let current_offset = segment_start * path_length;

    let step_resized_direction = 0;

    const absolute_end_offset = segment_end * path_length;

    for (; current_offset < absolute_end_offset; current_offset += step_size) {


        const point_address = added_points * 3;

        const knot_index = sample_path(
            sample_positions_f32,
            sample_derivatives_f32,
            point_address,
            path,
            current_offset
        );

        // check difference with previous derivative
        const previous_point_index = added_points - 1;
        const previous_point_address = previous_point_index * 3;

        // derivatives are basically normals, and dot product is has Cosine value of angle between two vectors
        const dot = v3_dot(
            sample_derivatives_f32[previous_point_address], sample_derivatives_f32[previous_point_address + 1], sample_derivatives_f32[previous_point_address + 2],
            sample_derivatives_f32[point_address], sample_derivatives_f32[point_address + 1], sample_derivatives_f32[point_address + 2]
        );

        // angular difference results in larger visual error for longer runs
        const error_size = (1 - dot) * step_size;

        if (
            (step_size > reference_min_step_size)
            && (
                (
                    // check if we jumped over a knot, so that we can sample down to get closer to it
                    knot_index > last_knot_index
                    && step_size > reference_min_step_size
                )
                || (
                    error_size * 10 > reference_min_step_size
                    && step_resized_direction <= 0
                )
                || (
                    // check if we're getting too close to the end of the segment, this lets us create a nice end
                    current_offset + step_size > absolute_end_offset
                )
            )
        ) {

            // step looks to be too large
            current_offset -= step_size;
            step_size *= 0.5;
            step_resized_direction = -1;
            continue; // retry

        } else if (
            error_size < 0.02
            && step_resized_direction >= 0
        ) {

            // step looks to be too small
            current_offset -= step_size;
            step_size *= 2;
            step_resized_direction = 1;
            continue; // retry

        }

        // reset adaptive step direction
        step_resized_direction = 0;
        // remember the last knot, so we know when we get to the end of the segment
        last_knot_index = knot_index;

        added_points++
    }

    if ((absolute_end_offset - current_offset + step_size) > 0.0001) {
        // end

        sample_path(sample_positions_f32, sample_derivatives_f32, added_points * 3, path, absolute_end_offset);

        added_points++;
    }

    // console.log(`Total Points ${added_points}`) // DEBUG info

    const normal_hint = style.path_normal_type === PathNormalType.FixedStart ? style.path_normal : undefined;

    const frames = computeFrenetFrames(sample_positions_f32, false, normal_hint);

    return makeTubeGeometry(
        sample_positions_f32, frames.normals, frames.binormals, frames.tangents,
        shape, shape_normal, shape.length / 2, shape_transform, false, style.cap_type
    );
}
