// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2023 lacklustr@protonmail.com https://github.com/eadf
// This file is part of the hronn crate.

use crate::geo::ConvertTo;
use crate::meshanalyzer::SearchResult;
use crate::prelude::MaximumTracker;
use crate::probe::{
    PlaneMetadata, QueryParameters, TriangleMetadata, ball_end_to_edge::ball_end_to_edge_collision,
    xy_distance_to_line_squared,
};
use crate::{HronnError, is_inside_2d_triangle};
use vector_traits::prelude::{GenericVector3, HasXYZ};

pub(crate) fn shared_ball_nose_precompute_logic<T: GenericVector3, MESH: HasXYZ + ConvertTo<T>>(
    vertices: &[MESH],
    indices: &[u32],
    probe_radius: T::Scalar,
) -> Result<Vec<TriangleMetadata<T, MESH>>, HronnError> {
    indices
        .as_ref()
        .chunks(3)
        .map(|triangle| {
            TriangleMetadata::<T, MESH>::new_for_ball_nose(
                probe_radius,
                vertices[triangle[0] as usize],
                vertices[triangle[1] as usize],
                vertices[triangle[2] as usize],
            )
        })
        .collect::<Result<Vec<_>, HronnError>>()
}

// this function must use the CollisionFn signature
pub(crate) fn ball_nose_compute_collision<T: GenericVector3, MESH: HasXYZ + ConvertTo<T>>(
    qp: &QueryParameters<'_, T, MESH>,
    site_index: u32,
    center: T::Vector2,
    mt: &mut MaximumTracker<SearchResult<T>>,
) {
    let triangle_index = site_index * 3;
    let TriangleMetadata {
        scalar: delta_z,
        plane,
        ..
    } = &qp.meta_data[site_index as usize];

    if let Some(PlaneMetadata {
        pft,
        translated_triangle: [p0, p1, p2],
        ..
    }) = plane
    {
        if is_inside_2d_triangle(center, *p0, *p1, *p2) {
            // if the sphere center is inside the pre-computed triangle, we know that the plane detection
            // will result in the highest point, so there is no need to test the edges.
            mt.insert(SearchResult::new(
                site_index,
                pft.compute_z(center) + *delta_z,
            ));

            // no need to proceed with the edge detection
            return;
        }
    }
    //let probe_radius = T::Scalar::to_f64(qp.probe_radius);
    let p0 = qp.vertices[qp.indices[triangle_index as usize] as usize].to();
    let p1 = qp.vertices[qp.indices[(triangle_index + 1) as usize] as usize].to();
    let p2 = qp.vertices[qp.indices[(triangle_index + 2) as usize] as usize].to();

    let closest_edge = xy_distance_to_line_squared(center, p0, p1).min_two(
        xy_distance_to_line_squared(center, p1, p2),
        xy_distance_to_line_squared(center, p2, p0),
    );

    ball_end_to_edge_collision::<T>(
        closest_edge.0,
        BallEndProperties::new(qp.probe_radius),
        site_index,
        mt,
    );
    ball_end_to_edge_collision::<T>(
        closest_edge.1,
        BallEndProperties::new(qp.probe_radius),
        site_index,
        mt,
    );
}

pub(crate) struct BallEndProperties<T: GenericVector3> {
    /// The radius
    pub r: T::Scalar,
    /// The square of the radius, precomputed for efficiency.
    pub r_sq: T::Scalar,
}

impl<T: GenericVector3> BallEndProperties<T> {
    pub fn new(radius: T::Scalar) -> Self {
        Self {
            r: radius,
            r_sq: radius * radius,
        }
    }
}
