//! Native port of `PrimMesher.cs` profile and extrusion geometry.

#![allow(clippy::cast_possible_truncation)] // Reference index and step calculations narrow explicitly.
#![allow(clippy::cast_precision_loss)] // C# geometry converts integer sides and steps to Single.
#![allow(clippy::cast_sign_loss)] // Bounds checks precede C# float-to-index conversions.
#![allow(clippy::approx_constant)] // Decimal compatibility constants are intentional.
#![allow(clippy::float_cmp)] // Exact parameter branches are part of the golden algorithm.
#![allow(clippy::inconsistent_struct_constructor)] // Field order is fixed by the public API map.
#![allow(clippy::inherent_to_string)] // The mapped C# methods are named ToString.
#![allow(clippy::inherent_to_string_shadow_display)] // Coord also needs Display for raw output.
#![allow(clippy::many_single_char_names)] // Vector and UV formulas follow the reference notation.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility mapping.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface.
#![allow(clippy::needless_pass_by_value)] // Owned value parameters preserve mapped signatures.
#![allow(clippy::should_implement_trait)] // Operator entry points have fixed generated names.
#![allow(clippy::similar_names)] // Profile start/stop and step names mirror the reference.
#![allow(clippy::struct_excessive_bools)] // PrimMesh exposes the reference mode flags.
#![allow(clippy::struct_field_names)] // Angle.angle is the corresponding C# field.
#![allow(clippy::too_many_arguments)] // Constructor and extrusion helpers mirror fixed APIs.
#![allow(clippy::too_many_lines)] // The extrusion sequence remains reviewable against C#.

use crate::{Error, PathType, VertexIndexer};
use std::f32::consts::{PI, TAU};
use std::fmt;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;

const MAG_THRESHOLD: f32 = 0.000_000_1;
const MAX_PROFILE_VERTICES: usize = 65_536;
const MAX_PATH_NODES: usize = 65_536;
const MAX_MESH_VERTICES: usize = 16_777_216;

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Coord {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

impl Coord {
    pub fn new(x: f32, y: f32, z: f32) -> Result<Self, Error> {
        Ok(Self { x, y, z })
    }
    pub fn length(&self) -> Result<f32, Error> {
        Ok((self.x * self.x + self.y * self.y + self.z * self.z).sqrt())
    }
    pub fn invert(&mut self) -> Result<Self, Error> {
        self.x = -self.x;
        self.y = -self.y;
        self.z = -self.z;
        Ok(*self)
    }
    pub fn normalize(&mut self) -> Result<Self, Error> {
        let mag = self.length()?;
        if mag > MAG_THRESHOLD {
            let inverse = 1.0 / mag;
            self.x *= inverse;
            self.y *= inverse;
            self.z *= inverse;
        } else {
            self.x = 0.0;
            self.y = 0.0;
            self.z = 0.0;
        }
        Ok(*self)
    }
    pub fn cross(c1: Self, c2: Self) -> Result<Self, Error> {
        Ok(Self::cross_raw(c1, c2))
    }
    fn cross_raw(c1: Self, c2: Self) -> Self {
        Self {
            x: c1.y * c2.z - c2.y * c1.z,
            y: c1.z * c2.x - c2.z * c1.x,
            z: c1.x * c2.y - c2.x * c1.y,
        }
    }
    pub fn add(v: Self, a: Self) -> Self {
        Self {
            x: v.x + a.x,
            y: v.y + a.y,
            z: v.z + a.z,
        }
    }
    pub fn mul_with_coord_coord(v: Self, m: Self) -> Self {
        Self {
            x: v.x * m.x,
            y: v.y * m.y,
            z: v.z * m.z,
        }
    }
    pub fn mul_with_coord_quat(v: Self, q: Quat) -> Self {
        Self {
            x: q.w * q.w * v.x + 2.0 * q.y * q.w * v.z - 2.0 * q.z * q.w * v.y
                + q.x * q.x * v.x
                + 2.0 * q.y * q.x * v.y
                + 2.0 * q.z * q.x * v.z
                - q.z * q.z * v.x
                - q.y * q.y * v.x,
            y: 2.0 * q.x * q.y * v.x
                + q.y * q.y * v.y
                + 2.0 * q.z * q.y * v.z
                + 2.0 * q.w * q.z * v.x
                - q.z * q.z * v.y
                + q.w * q.w * v.y
                - 2.0 * q.x * q.w * v.z
                - q.x * q.x * v.y,
            z: 2.0 * q.x * q.z * v.x + 2.0 * q.y * q.z * v.y + q.z * q.z * v.z
                - 2.0 * q.w * q.y * v.x
                - q.y * q.y * v.z
                + 2.0 * q.w * q.x * v.y
                - q.x * q.x * v.z
                + q.w * q.w * v.z,
        }
    }
}

impl fmt::Display for Coord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {} {}", self.x, self.y, self.z)
    }
}
impl Coord {
    pub fn to_string(&self) -> String {
        format!("{self}")
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Quat {
    pub w: f32,
    pub x: f32,
    pub y: f32,
    pub z: f32,
}
impl Quat {
    pub fn new_with_single_single_single_single(
        x: f32,
        y: f32,
        z: f32,
        w: f32,
    ) -> Result<Self, Error> {
        Ok(Self { x, y, z, w })
    }
    pub fn new_with_coord_single(mut axis: Coord, mut angle: f32) -> Result<Self, Error> {
        axis.normalize()?;
        angle *= 0.5;
        let mut value = Self {
            x: axis.x * angle.sin(),
            y: axis.y * angle.sin(),
            z: axis.z * angle.sin(),
            w: angle.cos(),
        };
        value.normalize()?;
        Ok(value)
    }
    pub fn length(&self) -> Result<f32, Error> {
        Ok((self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt())
    }
    pub fn normalize(&mut self) -> Result<Self, Error> {
        let mag = self.length()?;
        if mag > MAG_THRESHOLD {
            let i = 1.0 / mag;
            self.x *= i;
            self.y *= i;
            self.z *= i;
            self.w *= i;
        } else {
            self.x = 0.0;
            self.y = 0.0;
            self.z = 0.0;
            self.w = 1.0;
        }
        Ok(*self)
    }
    pub fn mul(q1: Self, q2: Self) -> Self {
        Self {
            x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
            y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
            z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
            w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
        }
    }
    pub fn to_string(&self) -> String {
        format!(
            "< X: {}, Y: {}, Z: {}, W: {}>",
            self.x, self.y, self.z, self.w
        )
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct UVCoord {
    pub u: f32,
    pub v: f32,
}
impl UVCoord {
    pub fn new(u: f32, v: f32) -> Result<Self, Error> {
        Ok(Self { u, v })
    }
    pub fn flip(&mut self) -> Result<Self, Error> {
        self.u = 1.0 - self.u;
        self.v = 1.0 - self.v;
        Ok(*self)
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Face {
    pub n1: i32,
    pub n2: i32,
    pub n3: i32,
    pub prim_face: i32,
    pub uv1: i32,
    pub uv2: i32,
    pub uv3: i32,
    pub v1: i32,
    pub v2: i32,
    pub v3: i32,
}
impl Face {
    fn vertices(v1: i32, v2: i32, v3: i32) -> Self {
        Self {
            v1,
            v2,
            v3,
            ..Self::default()
        }
    }
    pub fn new_with_int32_int32_int32(v1: i32, v2: i32, v3: i32) -> Result<Self, Error> {
        Ok(Self::vertices(v1, v2, v3))
    }
    pub fn new_with_int32_int32_int32_int32_int32_int32(
        v1: i32,
        v2: i32,
        v3: i32,
        n1: i32,
        n2: i32,
        n3: i32,
    ) -> Result<Self, Error> {
        Ok(Self {
            v1,
            v2,
            v3,
            n1,
            n2,
            n3,
            ..Self::default()
        })
    }
    pub fn surface_normal(&mut self, coords: Vec<Coord>) -> Result<Coord, Error> {
        surface_normal_for(&coords, *self)
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ViewerFace {
    pub coord_index1: i32,
    pub coord_index2: i32,
    pub coord_index3: i32,
    pub n1: Coord,
    pub n2: Coord,
    pub n3: Coord,
    pub prim_face_number: i32,
    pub uv1: UVCoord,
    pub uv2: UVCoord,
    pub uv3: UVCoord,
    pub v1: Coord,
    pub v2: Coord,
    pub v3: Coord,
}
impl ViewerFace {
    pub fn new(prim_face_number: i32) -> Result<Self, Error> {
        Ok(Self {
            prim_face_number,
            coord_index1: -1,
            coord_index2: -1,
            coord_index3: -1,
            ..Self::default()
        })
    }
    pub fn scale(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> {
        let m = Coord { x, y, z };
        self.v1 = Coord::mul_with_coord_coord(self.v1, m);
        self.v2 = Coord::mul_with_coord_coord(self.v2, m);
        self.v3 = Coord::mul_with_coord_coord(self.v3, m);
        Ok(())
    }
    pub fn add_pos(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> {
        let p = Coord { x, y, z };
        self.v1 = Coord::add(self.v1, p);
        self.v2 = Coord::add(self.v2, p);
        self.v3 = Coord::add(self.v3, p);
        Ok(())
    }
    pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> {
        self.v1 = Coord::mul_with_coord_quat(self.v1, q);
        self.v2 = Coord::mul_with_coord_quat(self.v2, q);
        self.v3 = Coord::mul_with_coord_quat(self.v3, q);
        self.n1 = Coord::mul_with_coord_quat(self.n1, q);
        self.n2 = Coord::mul_with_coord_quat(self.n2, q);
        self.n3 = Coord::mul_with_coord_quat(self.n3, q);
        Ok(())
    }
    pub fn calc_surface_normal(&mut self) -> Result<(), Error> {
        let e1 = Coord {
            x: self.v2.x - self.v1.x,
            y: self.v2.y - self.v1.y,
            z: self.v2.z - self.v1.z,
        };
        let e2 = Coord {
            x: self.v3.x - self.v1.x,
            y: self.v3.y - self.v1.y,
            z: self.v3.z - self.v1.z,
        };
        let mut n = Coord::cross_raw(e1, e2);
        n.normalize()?;
        self.n1 = n;
        self.n2 = n;
        self.n3 = n;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Default)]
struct Angle {
    angle: f32,
    x: f32,
    y: f32,
}

fn make_angles(sides: i32, start: f32, stop: f32) -> Result<(Vec<Angle>, Vec<Coord>), Error> {
    if sides < 1 || stop <= start || !start.is_finite() || !stop.is_finite() {
        return Err(Error::Argument);
    }
    let sides_usize = usize::try_from(sides).map_err(|_| Error::Argument)?;
    if sides_usize > MAX_PROFILE_VERTICES {
        return Err(Error::Argument);
    }
    if matches!(sides, 3 | 4 | 24) {
        return make_special_angles(sides, start, stop);
    }
    let step = TAU / (sides as f32);
    let first = (start / step).floor() as i32;
    let last = ((stop / step).ceil() as i32).min(sides);
    let mut angles =
        Vec::with_capacity(usize::try_from(last - first + 1).map_err(|_| Error::Argument)?);
    for index in first..=last {
        let a = (index as f32) * step;
        angles.push(Angle {
            angle: a,
            x: a.cos(),
            y: a.sin(),
        });
    }
    if start > angles.first().map_or(start, |a| a.angle) {
        let first = angles.first().copied().ok_or(Error::Argument)?;
        let second = angles.get(1).copied().ok_or(Error::Argument)?;
        angles[0] = intersect_angle(first, second, start);
    }
    if angles.len() > 1 {
        let end = angles.len() - 1;
        if stop < angles[end].angle {
            angles[end] = intersect_angle(angles[end - 1], angles[end], stop);
        }
    }
    let normals = if sides < 5 {
        angles
            .iter()
            .map(|a| {
                let previous = a.angle - step * 0.5;
                let mut n = Coord {
                    x: previous.cos(),
                    y: previous.sin(),
                    z: 0.0,
                };
                let _ = n.normalize();
                n
            })
            .collect()
    } else {
        Vec::new()
    };
    Ok((angles, normals))
}

fn make_special_angles(
    sides: i32,
    start: f32,
    stop: f32,
) -> Result<(Vec<Angle>, Vec<Coord>), Error> {
    let start = start / TAU;
    let stop = stop / TAU;
    let source: Vec<Angle> = (0..=sides)
        .map(|index| {
            let angle = (index as f32) / (sides as f32);
            Angle {
                angle,
                x: (angle * TAU).cos(),
                y: (angle * TAU).sin(),
            }
        })
        .collect();
    let start_index = (start * sides as f32) as usize;
    let mut end_index = source.len() - 1;
    if stop < 1.0 {
        end_index = (stop * sides as f32) as usize + 1;
    }
    if end_index == start_index {
        end_index += 1;
    }
    let mut angles = source
        .get(start_index..=end_index)
        .ok_or(Error::Argument)?
        .to_vec();
    let normal_source: Vec<Coord> = if sides < 5 {
        (0..=sides)
            .map(|index| {
                let angle = ((index as f32) + 0.5) * TAU / sides as f32;
                Coord {
                    x: angle.cos(),
                    y: angle.sin(),
                    z: 0.0,
                }
            })
            .collect()
    } else {
        Vec::new()
    };
    let normals = if sides < 5 {
        normal_source[start_index..=end_index].to_vec()
    } else {
        Vec::new()
    };
    if start > 0.0 {
        let first = angles[0];
        let second = angles[1];
        angles[0] = interpolate_angle(start, first, second);
    }
    if stop < 1.0 {
        let last = angles.len() - 1;
        angles[last] = interpolate_angle(stop, angles[last - 1], angles[last]);
    }
    Ok((angles, normals))
}

fn interpolate_angle(point: f32, first: Angle, second: Angle) -> Angle {
    let ratio = (point - first.angle) / (second.angle - first.angle);
    Angle {
        angle: point,
        x: first.x + ratio * (second.x - first.x),
        y: first.y + ratio * (second.y - first.y),
    }
}

fn intersect_angle(p1: Angle, p2: Angle, angle: f32) -> Angle {
    let x3 = 0.0_f64;
    let y3 = 0.0_f64;
    let x4 = f64::from(angle.cos());
    let y4 = f64::from(angle.sin());
    let x1 = f64::from(p1.x);
    let y1 = f64::from(p1.y);
    let x2 = f64::from(p2.x);
    let y2 = f64::from(p2.y);
    let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    if denominator == 0.0 {
        return Angle {
            angle,
            x: p1.x,
            y: p1.y,
        };
    }
    let numerator = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
    let u = numerator / denominator;
    Angle {
        angle,
        x: (x1 + u * (x2 - x1)) as f32,
        y: (y1 + u * (y2 - y1)) as f32,
    }
}

fn idx(value: i32, len: usize) -> Result<usize, Error> {
    let value = usize::try_from(value).map_err(|_| Error::IndexOutOfRange)?;
    if value >= len {
        Err(Error::IndexOutOfRange)
    } else {
        Ok(value)
    }
}
fn surface_normal_for(coords: &[Coord], face: Face) -> Result<Coord, Error> {
    let c1 = coords[idx(face.v1, coords.len())?];
    let c2 = coords[idx(face.v2, coords.len())?];
    let c3 = coords[idx(face.v3, coords.len())?];
    let e1 = Coord {
        x: c2.x - c1.x,
        y: c2.y - c1.y,
        z: c2.z - c1.z,
    };
    let e2 = Coord {
        x: c3.x - c1.x,
        y: c3.y - c1.y,
        z: c3.z - c1.z,
    };
    let mut n = Coord::cross_raw(e1, e2);
    n.normalize()?;
    Ok(n)
}

#[derive(Clone, Debug, Default)]
pub struct Profile {
    pub bottom_face_number: i32,
    pub calc_vertex_normals: bool,
    pub coords: Vec<Coord>,
    pub cut1_coord_indices: Vec<i32>,
    pub cut2_coord_indices: Vec<i32>,
    pub cut_normal1: Coord,
    pub cut_normal2: Coord,
    pub error_message: String,
    pub face_normal: Coord,
    pub face_numbers: Vec<i32>,
    pub face_u_vs: Vec<UVCoord>,
    pub faces: Vec<Face>,
    pub hollow_coord_indices: Vec<i32>,
    pub hollow_face_number: i32,
    pub num_hollow_verts: i32,
    pub num_outer_verts: i32,
    pub num_prim_faces: i32,
    pub outer_coord_indices: Vec<i32>,
    pub outer_face_number: i32,
    pub us: Vec<f32>,
    pub vertex_normals: Vec<Coord>,
}

impl Profile {
    pub fn new_with_constructor() -> Result<Self, Error> {
        Ok(Self {
            face_normal: Coord {
                x: 0.0,
                y: 0.0,
                z: 1.0,
            },
            hollow_face_number: -1,
            outer_face_number: -1,
            ..Self::default()
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn new_with_int32_single_single_single_int32_boolean_boolean(
        sides: i32,
        profile_start: f32,
        profile_end: f32,
        hollow: f32,
        hollow_sides: i32,
        create_faces: bool,
        calc_vertex_normals: bool,
    ) -> Result<Self, Error> {
        let mut value = Self::new_with_constructor()?;
        value.calc_vertex_normals = calc_vertex_normals;
        let (angles, outer_normals) = make_angles(sides, profile_start * TAU, profile_end * TAU)?;
        let has_hollow = hollow > 0.0;
        let has_cut = profile_start > 0.0 || profile_end < 1.0;
        let simple = sides < 5 && !has_hollow && !has_cut;
        let (x_scale, y_scale) = if sides == 4 {
            (0.707_107, 0.707_107)
        } else {
            (0.5, 0.5)
        };
        value.num_outer_verts = i32::try_from(angles.len()).map_err(|_| Error::Argument)?;
        let hollow_angles = if has_hollow {
            if hollow_sides == sides {
                angles.clone()
            } else {
                make_angles(hollow_sides, profile_start * TAU, profile_end * TAU)?.0
            }
        } else {
            Vec::new()
        };
        value.num_hollow_verts = i32::try_from(hollow_angles.len()).map_err(|_| Error::Argument)?;
        let total = angles
            .len()
            .checked_add(hollow_angles.len())
            .and_then(|n| n.checked_add(1))
            .ok_or(Error::Argument)?;
        if total > MAX_PROFILE_VERTICES {
            return Err(Error::Argument);
        }

        if !has_hollow && !simple {
            value.coords.push(Coord::default());
            if calc_vertex_normals {
                value.vertex_normals.push(value.face_normal);
            }
            value.us.push(0.0);
        }
        for (i, angle) in angles.iter().enumerate() {
            value.coords.push(Coord {
                x: angle.x * x_scale,
                y: angle.y * y_scale,
                z: 0.0,
            });
            if calc_vertex_normals {
                value
                    .outer_coord_indices
                    .push(i32::try_from(value.coords.len() - 1).map_err(|_| Error::Argument)?);
                value.vertex_normals.push(if sides < 5 {
                    outer_normals.get(i).copied().unwrap_or(Coord {
                        x: angle.x,
                        y: angle.y,
                        z: 0.0,
                    })
                } else {
                    Coord {
                        x: angle.x,
                        y: angle.y,
                        z: 0.0,
                    }
                });
                value.us.push(angle.angle);
            }
            if !has_hollow && !simple && create_faces && angle.angle > 0.0001 {
                let index = i32::try_from(i).map_err(|_| Error::Argument)?;
                value.faces.push(Face::vertices(0, index, index + 1));
            }
        }

        if has_hollow {
            let mut inner = Vec::with_capacity(hollow_angles.len());
            let mut inner_normals = Vec::with_capacity(hollow_angles.len());
            let mut inner_us = Vec::with_capacity(hollow_angles.len());
            for (i, angle) in hollow_angles.iter().enumerate() {
                inner.push(Coord {
                    x: hollow * x_scale * angle.x,
                    y: hollow * y_scale * angle.y,
                    z: 0.0,
                });
                if calc_vertex_normals {
                    let mut normal = if hollow_sides < 5 {
                        make_angles(hollow_sides, profile_start * TAU, profile_end * TAU)?
                            .1
                            .get(i)
                            .copied()
                            .unwrap_or(Coord {
                                x: angle.x,
                                y: angle.y,
                                z: 0.0,
                            })
                    } else {
                        Coord {
                            x: angle.x,
                            y: angle.y,
                            z: 0.0,
                        }
                    };
                    normal.x = -normal.x;
                    normal.y = -normal.y;
                    normal.z = -normal.z;
                    inner_normals.push(normal);
                    inner_us.push(if hollow_sides == 4 {
                        angle.angle * hollow * 0.707_107
                    } else {
                        angle.angle * hollow
                    });
                }
            }
            inner.reverse();
            inner_normals.reverse();
            inner_us.reverse();
            if create_faces {
                triangulate_ring(&angles, &hollow_angles, &mut value.faces)?;
            }
            for coord in inner {
                value.coords.push(coord);
                if calc_vertex_normals {
                    value
                        .hollow_coord_indices
                        .push(i32::try_from(value.coords.len() - 1).map_err(|_| Error::Argument)?);
                }
            }
            value.vertex_normals.extend(inner_normals);
            value.us.extend(inner_us);
        }

        if simple && create_faces {
            if sides == 3 {
                value.faces.push(Face::vertices(0, 1, 2));
            } else if sides == 4 {
                value.faces.push(Face::vertices(0, 1, 2));
                value.faces.push(Face::vertices(0, 2, 3));
            }
        }

        if calc_vertex_normals && has_cut {
            value.initialize_cut_normals(has_hollow)?;
        }
        value.make_face_u_vs()?;
        if calc_vertex_normals {
            value.assign_face_numbers(sides, has_hollow, has_cut);
        }
        Ok(value)
    }

    fn initialize_cut_normals(&mut self, has_hollow: bool) -> Result<(), Error> {
        let last_outer = usize::try_from(self.num_outer_verts - 1).map_err(|_| Error::Argument)?;
        if has_hollow {
            self.cut1_coord_indices.extend([
                0,
                i32::try_from(self.coords.len() - 1).map_err(|_| Error::Argument)?,
            ]);
            self.cut2_coord_indices.extend([
                i32::try_from(last_outer + 1).map_err(|_| Error::Argument)?,
                i32::try_from(last_outer).map_err(|_| Error::Argument)?,
            ]);
            self.cut_normal1 = Coord {
                x: self.coords[0].y - self.coords[self.coords.len() - 1].y,
                y: -(self.coords[0].x - self.coords[self.coords.len() - 1].x),
                z: 0.0,
            };
            self.cut_normal2 = Coord {
                x: self.coords[last_outer + 1].y - self.coords[last_outer].y,
                y: -(self.coords[last_outer + 1].x - self.coords[last_outer].x),
                z: 0.0,
            };
        } else {
            self.cut1_coord_indices.extend([0, 1]);
            self.cut2_coord_indices
                .extend([i32::try_from(last_outer).map_err(|_| Error::Argument)?, 0]);
            let first = *self.vertex_normals.get(1).ok_or(Error::IndexOutOfRange)?;
            let last = *self
                .vertex_normals
                .get(self.vertex_normals.len().saturating_sub(2))
                .ok_or(Error::IndexOutOfRange)?;
            self.cut_normal1 = Coord {
                x: first.y,
                y: -first.x,
                z: 0.0,
            };
            self.cut_normal2 = Coord {
                x: -last.y,
                y: last.x,
                z: 0.0,
            };
        }
        self.cut_normal1.normalize()?;
        self.cut_normal2.normalize()?;
        Ok(())
    }

    fn assign_face_numbers(&mut self, sides: i32, has_hollow: bool, has_cut: bool) {
        let mut face = 1;
        self.outer_face_number = face;
        if has_cut && !has_hollow {
            self.face_numbers.push(-1);
        }
        for i in 0..self.num_outer_verts - 1 {
            if sides < 5 && i <= sides {
                self.face_numbers.push(face);
                face += 1;
            } else {
                self.face_numbers.push(face);
            }
        }
        if has_cut {
            self.face_numbers.push(-1);
        } else {
            self.face_numbers.push(face);
            face += 1;
        }
        if sides > 4 && (has_hollow || has_cut) {
            face += 1;
        }
        if sides < 5 && (has_hollow || has_cut) && self.num_outer_verts < sides {
            face += 1;
        }
        if has_hollow {
            for _ in 0..self.num_hollow_verts {
                self.face_numbers.push(face);
            }
            self.hollow_face_number = face;
            face += 1;
        }
        self.bottom_face_number = face;
        face += 1;
        if has_hollow && has_cut {
            self.face_numbers.push(face);
            face += 1;
        }
        for number in &mut self.face_numbers {
            if *number == -1 {
                *number = face;
                face += 1;
            }
        }
        self.num_prim_faces = face;
    }

    pub fn make_face_u_vs(&mut self) -> Result<(), Error> {
        self.face_u_vs = self
            .coords
            .iter()
            .map(|c| UVCoord {
                u: 1.0 - (0.5 + c.x),
                v: 1.0 - (0.5 - c.y),
            })
            .collect();
        Ok(())
    }
    pub fn copy_with_method(&self) -> Result<Self, Error> {
        self.copy_with_boolean(true)
    }
    pub fn copy_with_boolean(&self, need_faces: bool) -> Result<Self, Error> {
        let mut copy = self.clone();
        if !need_faces {
            copy.faces.clear();
        }
        Ok(copy)
    }
    pub fn add_pos_with_coord(&mut self, v: Coord) -> Result<(), Error> {
        self.add_pos_with_single_single_single(v.x, v.y, v.z)
    }
    pub fn add_pos_with_single_single_single(
        &mut self,
        x: f32,
        y: f32,
        z: f32,
    ) -> Result<(), Error> {
        for c in &mut self.coords {
            c.x += x;
            c.y += y;
            c.z += z;
        }
        Ok(())
    }
    pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> {
        for c in &mut self.coords {
            *c = Coord::mul_with_coord_quat(*c, q);
        }
        if self.calc_vertex_normals {
            for n in &mut self.vertex_normals {
                *n = Coord::mul_with_coord_quat(*n, q);
            }
            self.face_normal = Coord::mul_with_coord_quat(self.face_normal, q);
            self.cut_normal1 = Coord::mul_with_coord_quat(self.cut_normal1, q);
            self.cut_normal2 = Coord::mul_with_coord_quat(self.cut_normal2, q);
        }
        Ok(())
    }
    pub fn scale(&mut self, x: f32, y: f32) -> Result<(), Error> {
        for c in &mut self.coords {
            c.x *= x;
            c.y *= y;
        }
        Ok(())
    }
    pub fn flip_normals(&mut self) -> Result<(), Error> {
        for f in &mut self.faces {
            std::mem::swap(&mut f.v1, &mut f.v3);
        }
        if self.calc_vertex_normals
            && let Some(n) = self.vertex_normals.last_mut()
        {
            n.z = -n.z;
        }
        self.face_normal.x = -self.face_normal.x;
        self.face_normal.y = -self.face_normal.y;
        self.face_normal.z = -self.face_normal.z;
        for uv in &mut self.face_u_vs {
            uv.v = 1.0 - uv.v;
        }
        Ok(())
    }
    pub fn add_value2_face_vertex_indices(&mut self, num: i32) -> Result<(), Error> {
        for f in &mut self.faces {
            f.v1 = f.v1.checked_add(num).ok_or(Error::Argument)?;
            f.v2 = f.v2.checked_add(num).ok_or(Error::Argument)?;
            f.v3 = f.v3.checked_add(num).ok_or(Error::Argument)?;
        }
        Ok(())
    }
    pub fn add_value2_face_normal_indices(&mut self, num: i32) -> Result<(), Error> {
        if self.calc_vertex_normals {
            for f in &mut self.faces {
                f.n1 = f.n1.checked_add(num).ok_or(Error::Argument)?;
                f.n2 = f.n2.checked_add(num).ok_or(Error::Argument)?;
                f.n3 = f.n3.checked_add(num).ok_or(Error::Argument)?;
            }
        }
        Ok(())
    }
    pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), Error> {
        dump_raw_geometry(&self.coords, &self.faces, path, name, title)
    }
}

fn triangulate_ring(outer: &[Angle], inner: &[Angle], faces: &mut Vec<Face>) -> Result<(), Error> {
    let outer_count = outer.len();
    let inner_count = inner.len();
    let total = outer_count
        .checked_add(inner_count)
        .ok_or(Error::Argument)?;
    if outer_count == inner_count {
        for i in 0..outer_count.saturating_sub(1) {
            faces.push(Face::vertices(
                i32::try_from(i).map_err(|_| Error::Argument)?,
                i32::try_from(i + 1).map_err(|_| Error::Argument)?,
                i32::try_from(total - i - 1).map_err(|_| Error::Argument)?,
            ));
            faces.push(Face::vertices(
                i32::try_from(i + 1).map_err(|_| Error::Argument)?,
                i32::try_from(total - i - 2).map_err(|_| Error::Argument)?,
                i32::try_from(total - i - 1).map_err(|_| Error::Argument)?,
            ));
        }
        return Ok(());
    }
    let mut oi = 0usize;
    let mut hi = 0usize;
    while oi + 1 < outer_count || hi + 1 < inner_count {
        let outer_next = outer.get(oi + 1).map_or(f32::INFINITY, |a| a.angle);
        let inner_next = inner.get(hi + 1).map_or(f32::INFINITY, |a| a.angle);
        let inner_current = total - hi - 1;
        if outer_next <= inner_next {
            faces.push(Face::vertices(
                i32::try_from(oi).map_err(|_| Error::Argument)?,
                i32::try_from(oi + 1).map_err(|_| Error::Argument)?,
                i32::try_from(inner_current).map_err(|_| Error::Argument)?,
            ));
            oi += 1;
        } else {
            faces.push(Face::vertices(
                i32::try_from(oi).map_err(|_| Error::Argument)?,
                i32::try_from(inner_current - 1).map_err(|_| Error::Argument)?,
                i32::try_from(inner_current).map_err(|_| Error::Argument)?,
            ));
            hi += 1;
        }
    }
    Ok(())
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PathNode {
    pub percent_of_path: f32,
    pub position: Coord,
    pub rotation: Quat,
    pub x_scale: f32,
    pub y_scale: f32,
}

#[derive(Clone, Debug)]
pub struct Path {
    pub dimple_begin: f32,
    pub dimple_end: f32,
    pub hole_size_x: f32,
    pub hole_size_y: f32,
    pub path_cut_begin: f32,
    pub path_cut_end: f32,
    pub path_nodes: Vec<PathNode>,
    pub radius: f32,
    pub revolutions: f32,
    pub skew: f32,
    pub steps_per_revolution: i32,
    pub taper_x: f32,
    pub taper_y: f32,
    pub top_shear_x: f32,
    pub top_shear_y: f32,
    pub twist_begin: f32,
    pub twist_end: f32,
}
impl Default for Path {
    fn default() -> Self {
        Self {
            dimple_begin: 0.0,
            dimple_end: 1.0,
            hole_size_x: 1.0,
            hole_size_y: 0.25,
            path_cut_begin: 0.0,
            path_cut_end: 1.0,
            path_nodes: Vec::new(),
            radius: 0.0,
            revolutions: 1.0,
            skew: 0.0,
            steps_per_revolution: 24,
            taper_x: 0.0,
            taper_y: 0.0,
            top_shear_x: 0.0,
            top_shear_y: 0.0,
            twist_begin: 0.0,
            twist_end: 0.0,
        }
    }
}
impl Path {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn create(&mut self, path_type: PathType, mut steps: i32) -> Result<(), Error> {
        if steps < 1
            || self.path_cut_end <= self.path_cut_begin
            || !self.path_cut_begin.is_finite()
            || !self.path_cut_end.is_finite()
        {
            return Err(Error::Argument);
        }
        self.taper_x = self.taper_x.clamp(-0.999, 0.999);
        self.taper_y = self.taper_y.clamp(-0.999, 0.999);
        self.path_nodes.clear();
        if matches!(path_type, PathType::Linear | PathType::Flexible) {
            let length = self.path_cut_end - self.path_cut_begin;
            let twist_total = self.twist_end - self.twist_begin;
            if twist_total.abs() > 0.01 {
                steps = steps
                    .checked_add((twist_total.abs() * 3.66) as i32)
                    .ok_or(Error::Argument)?;
            }
            let count = usize::try_from(steps)
                .map_err(|_| Error::Argument)?
                .checked_add(1)
                .ok_or(Error::Argument)?;
            if count > MAX_PATH_NODES {
                return Err(Error::Argument);
            }
            self.path_nodes.reserve(count);
            let step_size = length / (steps as f32);
            let percent_step = step_size * 0.999_999;
            let mut percent = self.path_cut_begin;
            let mut x = self.top_shear_x * self.path_cut_begin;
            let mut y = self.top_shear_y * self.path_cut_begin;
            let mut z = -0.5 + percent;
            let dx = self.top_shear_x * length / (steps as f32);
            let dy = self.top_shear_y * length / (steps as f32);
            for step in 0..=steps {
                let x_scale = if self.taper_x == 0.0 {
                    1.0
                } else if self.taper_x > 0.0 {
                    1.0 - percent * self.taper_x
                } else {
                    1.0 + (1.0 - percent) * self.taper_x
                };
                let y_scale = if self.taper_y == 0.0 {
                    1.0
                } else if self.taper_y > 0.0 {
                    1.0 - percent * self.taper_y
                } else {
                    1.0 + (1.0 - percent) * self.taper_y
                };
                let twist = self.twist_begin + twist_total * percent;
                self.path_nodes.push(PathNode {
                    percent_of_path: percent,
                    position: Coord { x, y, z },
                    rotation: Quat::new_with_coord_single(
                        Coord {
                            x: 0.0,
                            y: 0.0,
                            z: 1.0,
                        },
                        twist,
                    )?,
                    x_scale,
                    y_scale,
                });
                if step < steps {
                    percent += percent_step;
                    x += dx;
                    y += dy;
                    z += step_size;
                    if percent > self.path_cut_end {
                        break;
                    }
                }
            }
        } else {
            if self.steps_per_revolution < 1 || self.revolutions <= 0.0 {
                return Err(Error::Argument);
            }
            let twist_total = self.twist_end - self.twist_begin;
            if twist_total.abs() > 0.01 {
                if twist_total.abs() > PI * 1.5 {
                    steps.checked_mul(2).ok_or(Error::Argument)?;
                }
                if twist_total.abs() > PI * 3.0 {
                    steps.checked_mul(4).ok_or(Error::Argument)?;
                }
            }
            let y_path_scale = self.hole_size_y * 0.5;
            let path_length = self.path_cut_end - self.path_cut_begin;
            let total_skew = self.skew * 2.0 * path_length;
            let skew_start = self.path_cut_begin * 2.0 * self.skew - self.skew;
            let x_shear = self.top_shear_x * (0.25 + 0.5 * (0.5 - self.hole_size_y));
            let y_comp = 1.0 + self.top_shear_y.abs() * 0.25;
            let start = TAU * self.path_cut_begin * self.revolutions - self.top_shear_y * 0.9;
            let end = TAU * self.path_cut_end * self.revolutions - self.top_shear_y * 0.9;
            let step_size = TAU / (self.steps_per_revolution as f32);
            let estimated = ((end - start) / step_size).ceil() as usize + 2;
            if estimated > MAX_PATH_NODES {
                return Err(Error::Argument);
            }
            self.path_nodes.reserve(estimated);
            let mut step = (start / step_size) as i32;
            let mut angle = start;
            loop {
                let percent = angle / (TAU * self.revolutions);
                let angle_percent = (angle - start) / (end - start);
                let mut xs = (1.0 - self.skew.abs()) * self.hole_size_x;
                let mut ys = self.hole_size_y;
                if self.taper_x > 0.01 {
                    xs *= 1.0 - percent * self.taper_x;
                } else if self.taper_x < -0.01 {
                    xs *= 1.0 + (1.0 - percent) * self.taper_x;
                }
                if self.taper_y > 0.01 {
                    ys *= 1.0 - percent * self.taper_y;
                } else if self.taper_y < -0.01 {
                    ys *= 1.0 + (1.0 - percent) * self.taper_y;
                }
                let radius_scale = if self.radius > 0.001 {
                    1.0 - self.radius * percent
                } else if self.radius < 0.001 {
                    1.0 + self.radius * (1.0 - percent)
                } else {
                    1.0
                };
                let twist = self.twist_begin + twist_total * percent;
                let x = 0.5 * (skew_start + total_skew * angle_percent) + angle.sin() * x_shear;
                let y = y_comp * angle.cos() * (0.5 - y_path_scale) * radius_scale;
                let z = (angle + self.top_shear_y).sin() * (0.5 - y_path_scale) * radius_scale;
                let mut rotation = Quat::new_with_coord_single(
                    Coord {
                        x: 1.0,
                        y: 0.0,
                        z: 0.0,
                    },
                    angle + self.top_shear_y,
                )?;
                if twist_total != 0.0 || self.twist_begin != 0.0 {
                    rotation = Quat::mul(
                        rotation,
                        Quat::new_with_coord_single(
                            Coord {
                                x: 0.0,
                                y: 0.0,
                                z: 1.0,
                            },
                            twist,
                        )?,
                    );
                }
                self.path_nodes.push(PathNode {
                    percent_of_path: percent,
                    position: Coord { x, y, z },
                    rotation,
                    x_scale: xs,
                    y_scale: ys,
                });
                if self.path_nodes.len() > MAX_PATH_NODES {
                    return Err(Error::Argument);
                }
                if angle >= end - 0.01 {
                    break;
                }
                step = step.checked_add(1).ok_or(Error::Argument)?;
                angle = step_size * (step as f32);
                if angle > end {
                    angle = end;
                }
            }
        }
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub struct PrimMesh {
    pub calc_vertex_normals: bool,
    pub coords: Vec<Coord>,
    pub dimple_begin: f32,
    pub dimple_end: f32,
    pub error_message: String,
    pub faces: Vec<Face>,
    pub hole_size_x: f32,
    pub hole_size_y: f32,
    pub normals: Vec<Coord>,
    pub num_prim_faces: i32,
    pub path_cut_begin: f32,
    pub path_cut_end: f32,
    pub radius: f32,
    pub revolutions: f32,
    pub skew: f32,
    pub sphere_mode: bool,
    pub steps_per_revolution: i32,
    pub taper_x: f32,
    pub taper_y: f32,
    pub top_shear_x: f32,
    pub top_shear_y: f32,
    pub twist_begin: i32,
    pub twist_end: i32,
    pub viewer_faces: Vec<ViewerFace>,
    pub viewer_mode: bool,
    sides: i32,
    profile_start: f32,
    profile_end: f32,
    hollow: f32,
    hollow_sides: i32,
    normals_processed: bool,
    profile_outer_face_number: i32,
    profile_hollow_face_number: i32,
    has_profile_cut: bool,
    has_hollow: bool,
}
impl PrimMesh {
    pub fn new(
        mut sides: i32,
        mut profile_start: f32,
        mut profile_end: f32,
        mut hollow: f32,
        mut hollow_sides: i32,
    ) -> Result<Self, Error> {
        if !profile_start.is_finite() || !profile_end.is_finite() || !hollow.is_finite() {
            return Err(Error::Argument);
        }
        sides = sides.max(3);
        hollow_sides = hollow_sides.max(3);
        profile_start = profile_start.max(0.0);
        profile_end = profile_end.clamp(0.02, 1.0);
        if profile_start >= profile_end {
            profile_start = profile_end - 0.02;
        }
        hollow = hollow.clamp(0.0, 0.99);
        Ok(Self {
            calc_vertex_normals: false,
            coords: Vec::new(),
            dimple_begin: 0.0,
            dimple_end: 1.0,
            error_message: String::new(),
            faces: Vec::new(),
            hole_size_x: 1.0,
            hole_size_y: 0.25,
            normals: Vec::new(),
            num_prim_faces: 0,
            path_cut_begin: 0.0,
            path_cut_end: 1.0,
            radius: 0.0,
            revolutions: 1.0,
            skew: 0.0,
            sphere_mode: false,
            steps_per_revolution: 24,
            taper_x: 0.0,
            taper_y: 0.0,
            top_shear_x: 0.0,
            top_shear_y: 0.0,
            twist_begin: 0,
            twist_end: 0,
            viewer_faces: Vec::new(),
            viewer_mode: false,
            sides,
            profile_start,
            profile_end,
            hollow,
            hollow_sides,
            normals_processed: false,
            profile_outer_face_number: -1,
            profile_hollow_face_number: -1,
            has_profile_cut: profile_start > 0.0 || profile_end < 1.0,
            has_hollow: hollow > 0.0,
        })
    }

    pub fn extrude(&mut self, path_type: PathType) -> Result<(), Error> {
        self.validate_parameters()?;
        self.coords.clear();
        self.faces.clear();
        self.normals.clear();
        self.viewer_faces.clear();
        self.normals_processed = false;
        if self.viewer_mode {
            self.calc_vertex_normals = true;
        }
        let twist_begin = self.twist_begin as f32;
        let twist_end = self.twist_end as f32;
        let twist_total = twist_end - twist_begin;
        let mut steps: i32 = 1;
        if twist_total.abs() > 0.01 {
            steps = steps
                .checked_add((twist_total.abs() * 3.66) as i32)
                .ok_or(Error::Argument)?;
        }
        let need_end_faces = if path_type == PathType::Circular {
            self.path_cut_begin != 0.0
                || self.path_cut_end != 1.0
                || self.taper_x != 0.0
                || self.taper_y != 0.0
                || self.skew != 0.0
                || twist_total != 0.0
                || self.radius != 0.0
        } else {
            true
        };
        let mut adjusted_hollow = self.hollow;
        let mut initial_rotation = 0.0;
        if path_type == PathType::Circular {
            match self.sides {
                3 => {
                    initial_rotation = PI;
                    if self.hollow_sides == 4 {
                        adjusted_hollow = adjusted_hollow.min(0.7) * 0.707;
                    } else {
                        adjusted_hollow *= 0.5;
                    }
                }
                4 => {
                    initial_rotation = 0.25 * PI;
                    if self.hollow_sides != 4 {
                        adjusted_hollow *= 0.707;
                    }
                }
                _ if self.sides > 4 => {
                    initial_rotation = PI;
                    if self.hollow_sides == 4 {
                        adjusted_hollow = adjusted_hollow.min(0.7) / 0.7;
                    }
                }
                _ => {}
            }
        } else {
            match self.sides {
                3 => {
                    if self.hollow_sides == 4 {
                        adjusted_hollow = adjusted_hollow.min(0.7) * 0.707;
                    } else {
                        adjusted_hollow *= 0.5;
                    }
                }
                4 => {
                    initial_rotation = 1.25 * PI;
                    if self.hollow_sides != 4 {
                        adjusted_hollow *= 0.707;
                    }
                }
                24 if self.hollow_sides == 4 => adjusted_hollow *= 1.414,
                _ => {}
            }
        }
        let mut profile = Profile::new_with_int32_single_single_single_int32_boolean_boolean(
            self.sides,
            self.profile_start,
            self.profile_end,
            adjusted_hollow,
            self.hollow_sides,
            true,
            self.calc_vertex_normals,
        )?;
        self.error_message.clone_from(&profile.error_message);
        let mut cut1_face = profile.bottom_face_number + 1;
        let mut cut2_face = cut1_face + 1;
        if !need_end_faces {
            cut1_face -= 2;
            cut2_face -= 2;
        }
        self.profile_outer_face_number = profile.outer_face_number - i32::from(!need_end_faces);
        self.profile_hollow_face_number = if self.has_hollow {
            profile.hollow_face_number - i32::from(!need_end_faces)
        } else {
            -1
        };
        let cut1_vert = if self.has_profile_cut {
            if self.has_hollow {
                i32::try_from(profile.coords.len() - 1).map_err(|_| Error::Argument)?
            } else {
                0
            }
        } else {
            -1
        };
        let cut2_vert = if self.has_profile_cut {
            if self.has_hollow {
                profile.num_outer_verts - 1
            } else {
                profile.num_outer_verts
            }
        } else {
            -1
        };
        if initial_rotation != 0.0 {
            profile.add_rot(Quat::new_with_coord_single(
                Coord {
                    x: 0.0,
                    y: 0.0,
                    z: 1.0,
                },
                initial_rotation,
            )?)?;
            if self.viewer_mode {
                profile.make_face_u_vs()?;
            }
        }
        let mut path = Path {
            twist_begin,
            twist_end,
            top_shear_x: self.top_shear_x,
            top_shear_y: self.top_shear_y,
            path_cut_begin: self.path_cut_begin,
            path_cut_end: self.path_cut_end,
            dimple_begin: self.dimple_begin,
            dimple_end: self.dimple_end,
            skew: self.skew,
            hole_size_x: self.hole_size_x,
            hole_size_y: self.hole_size_y,
            taper_x: self.taper_x,
            taper_y: self.taper_y,
            radius: self.radius,
            revolutions: self.revolutions,
            steps_per_revolution: self.steps_per_revolution,
            ..Path::default()
        };
        path.create(path_type, steps)?;
        let total_vertices = path
            .path_nodes
            .len()
            .checked_mul(profile.coords.len())
            .ok_or(Error::Argument)?;
        if total_vertices > MAX_MESH_VERTICES {
            return Err(Error::Argument);
        }
        self.coords.reserve(total_vertices);
        let mut last_cut1 = Coord::default();
        let mut last_cut2 = Coord::default();
        let mut last_v = 0.0;
        for (node_index, node) in path.path_nodes.iter().copied().enumerate() {
            let coords_offset = self.coords.len();
            let normals_offset = self.normals.len();
            let transformed: Vec<Coord> = profile
                .coords
                .iter()
                .map(|v| {
                    let scaled = Coord {
                        x: v.x * node.x_scale,
                        y: v.y * node.y_scale,
                        z: v.z,
                    };
                    Coord::add(
                        Coord::mul_with_coord_quat(scaled, node.rotation),
                        node.position,
                    )
                })
                .collect();
            self.coords.extend_from_slice(&transformed);
            if self.calc_vertex_normals {
                self.normals.extend(
                    profile
                        .vertex_normals
                        .iter()
                        .map(|n| Coord::mul_with_coord_quat(*n, node.rotation)),
                );
            }
            if node.percent_of_path < self.path_cut_begin + 0.01
                || node.percent_of_path > self.path_cut_end - 0.01
            {
                for source in &profile.faces {
                    let mut face = *source;
                    face.v1 = add_index(face.v1, coords_offset)?;
                    face.v2 = add_index(face.v2, coords_offset)?;
                    face.v3 = add_index(face.v3, coords_offset)?;
                    if self.calc_vertex_normals {
                        face.n1 = add_index(face.n1, normals_offset)?;
                        face.n2 = add_index(face.n2, normals_offset)?;
                        face.n3 = add_index(face.n3, normals_offset)?;
                    }
                    self.faces.push(face);
                }
            }
            let this_v = 1.0 - node.percent_of_path;
            if node_index > 0 {
                self.add_side_faces(
                    &profile,
                    coords_offset,
                    this_v,
                    last_v,
                    cut1_vert,
                    cut2_vert,
                    cut1_face,
                    cut2_face,
                    need_end_faces,
                    last_cut1,
                    last_cut2,
                    node.rotation,
                )?;
            }
            last_cut1 = profile.cut_normal1;
            last_cut2 = profile.cut_normal2;
            last_v = this_v;
            if need_end_faces && node_index + 1 == path.path_nodes.len() && self.viewer_mode {
                for face in &profile.faces {
                    let v1 = idx(face.v1, transformed.len())?;
                    let v2 = idx(face.v2, transformed.len())?;
                    let v3 = idx(face.v3, transformed.len())?;
                    let mut viewer = ViewerFace::new(0)?;
                    viewer.v1 = transformed[v1];
                    viewer.v2 = transformed[v2];
                    viewer.v3 = transformed[v3];
                    viewer.coord_index1 = add_index(face.v1, coords_offset)?;
                    viewer.coord_index2 = add_index(face.v2, coords_offset)?;
                    viewer.coord_index3 = add_index(face.v3, coords_offset)?;
                    let normal = profile.face_normal;
                    viewer.n1 = normal;
                    viewer.n2 = normal;
                    viewer.n3 = normal;
                    viewer.uv1 = profile.face_u_vs[v1];
                    viewer.uv2 = profile.face_u_vs[v2];
                    viewer.uv3 = profile.face_u_vs[v3];
                    if path_type == PathType::Linear {
                        viewer.uv1.flip()?;
                        viewer.uv2.flip()?;
                        viewer.uv3.flip()?;
                    }
                    self.viewer_faces.push(viewer);
                }
            }
        }
        if self.viewer_mode {
            self.num_prim_faces = self
                .viewer_faces
                .iter()
                .map(|face| face.prim_face_number)
                .max()
                .unwrap_or(-1)
                + 1;
        } else {
            self.num_prim_faces = profile.num_prim_faces;
        }
        self.validate_geometry()?;
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    fn add_side_faces(
        &mut self,
        profile: &Profile,
        offset: usize,
        this_v: f32,
        last_v: f32,
        cut1: i32,
        cut2: i32,
        cut1_face: i32,
        cut2_face: i32,
        need_end_faces: bool,
        last_cut1: Coord,
        last_cut2: Coord,
        rotation: Quat,
    ) -> Result<(), Error> {
        let count = profile.coords.len();
        let mut start = offset.checked_add(1).ok_or(Error::Argument)?;
        if self.sides < 5 || self.has_profile_cut || self.has_hollow {
            start -= 1;
        }
        let end = offset.checked_add(count).ok_or(Error::Argument)?;
        for current in start..end {
            let next = if current + 1 == end {
                start
            } else {
                current + 1
            };
            let previous = current.checked_sub(count).ok_or(Error::IndexOutOfRange)?;
            let previous_next = next.checked_sub(count).ok_or(Error::IndexOutOfRange)?;
            let f1 = Face {
                v1: to_i32(current)?,
                v2: to_i32(previous)?,
                v3: to_i32(next)?,
                n1: to_i32(current)?,
                n2: to_i32(previous)?,
                n3: to_i32(next)?,
                ..Face::default()
            };
            let f2 = Face {
                v1: to_i32(next)?,
                v2: to_i32(previous)?,
                v3: to_i32(previous_next)?,
                n1: to_i32(next)?,
                n2: to_i32(previous)?,
                n3: to_i32(previous_next)?,
                ..Face::default()
            };
            self.faces.push(f1);
            self.faces.push(f2);
            if !self.viewer_mode {
                continue;
            }
            let which = i32::try_from(current - start).map_err(|_| Error::Argument)?;
            let mut prim = profile
                .face_numbers
                .get(usize::try_from(which).map_err(|_| Error::Argument)?)
                .copied()
                .or_else(|| profile.face_numbers.first().copied())
                .unwrap_or(0);
            if !need_end_faces {
                prim = (prim - 1).max(0);
            }
            let mut vf1 = ViewerFace::new(prim)?;
            let mut vf2 = ViewerFace::new(prim)?;
            let mut u_index = usize::try_from(which).map_err(|_| Error::Argument)?;
            if !self.has_hollow && self.sides > 4 && u_index < profile.us.len().saturating_sub(1) {
                u_index += 1;
            }
            let (mut u1, mut u2) = if profile.us.is_empty() {
                (0.0, 0.0)
            } else {
                u_index = u_index.min(profile.us.len() - 1);
                (
                    profile.us[u_index],
                    profile.us.get(u_index + 1).copied().unwrap_or(1.0),
                )
            };
            if which == cut1 || which == cut2 {
                u1 = 0.0;
                u2 = 1.0;
            } else if self.sides < 5 && which < profile.num_outer_verts {
                u1 *= self.sides as f32;
                u2 *= self.sides as f32;
                u2 -= u1 as i32 as f32;
                u1 -= u1 as i32 as f32;
                if u2 < 0.1 {
                    u2 = 1.0;
                }
            }
            if self.sphere_mode && which != cut1 && which != cut2 {
                u1 = u1 * 2.0 - 1.0;
                u2 = u2 * 2.0 - 1.0;
                if which >= profile.num_outer_verts {
                    u1 -= self.hollow;
                    u2 -= self.hollow;
                }
            }
            vf1.uv1 = UVCoord { u: u1, v: this_v };
            vf1.uv2 = UVCoord { u: u1, v: last_v };
            vf1.uv3 = UVCoord { u: u2, v: this_v };
            vf2.uv1 = UVCoord { u: u2, v: this_v };
            vf2.uv2 = UVCoord { u: u1, v: last_v };
            vf2.uv3 = UVCoord { u: u2, v: last_v };
            assign_viewer_coords(&mut vf1, &self.coords, f1)?;
            assign_viewer_coords(&mut vf2, &self.coords, f2)?;
            if which == cut1 {
                let current_normal = Coord::mul_with_coord_quat(profile.cut_normal1, rotation);
                vf1.prim_face_number = cut1_face;
                vf2.prim_face_number = cut1_face;
                vf1.n1 = current_normal;
                vf1.n2 = last_cut1;
                vf1.n3 = last_cut1;
                vf2.n1 = current_normal;
                vf2.n2 = last_cut1;
                vf2.n3 = current_normal;
            } else if which == cut2 {
                let current_normal = Coord::mul_with_coord_quat(profile.cut_normal2, rotation);
                vf1.prim_face_number = cut2_face;
                vf2.prim_face_number = cut2_face;
                vf1.n1 = current_normal;
                vf1.n2 = last_cut2;
                vf1.n3 = last_cut2;
                vf2.n1 = current_normal;
                vf2.n2 = last_cut2;
                vf2.n3 = current_normal;
            } else if (self.sides < 5 && which < profile.num_outer_verts)
                || (self.hollow_sides < 5 && which >= profile.num_outer_verts)
            {
                vf1.calc_surface_normal()?;
                vf2.calc_surface_normal()?;
            } else {
                assign_viewer_normals(&mut vf1, &self.normals, f1)?;
                assign_viewer_normals(&mut vf2, &self.normals, f2)?;
            }
            self.viewer_faces.push(vf1);
            self.viewer_faces.push(vf2);
        }
        Ok(())
    }

    fn validate_parameters(&self) -> Result<(), Error> {
        let values = [
            self.dimple_begin,
            self.dimple_end,
            self.hole_size_x,
            self.hole_size_y,
            self.path_cut_begin,
            self.path_cut_end,
            self.radius,
            self.revolutions,
            self.skew,
            self.taper_x,
            self.taper_y,
            self.top_shear_x,
            self.top_shear_y,
        ];
        if values.iter().any(|v| !v.is_finite()) || self.path_cut_end <= self.path_cut_begin {
            return Err(Error::Argument);
        }
        Ok(())
    }
    fn validate_geometry(&self) -> Result<(), Error> {
        if self
            .coords
            .iter()
            .any(|c| !c.x.is_finite() || !c.y.is_finite() || !c.z.is_finite())
            || self
                .normals
                .iter()
                .any(|c| !c.x.is_finite() || !c.y.is_finite() || !c.z.is_finite())
        {
            return Err(Error::InvalidOperation);
        }
        for face in &self.faces {
            idx(face.v1, self.coords.len())?;
            idx(face.v2, self.coords.len())?;
            idx(face.v3, self.coords.len())?;
        }
        Ok(())
    }
    pub fn surface_normal(&self, face_index: i32) -> Result<Coord, Error> {
        let face = *self
            .faces
            .get(idx(face_index, self.faces.len())?)
            .ok_or(Error::IndexOutOfRange)?;
        surface_normal_for(&self.coords, face)
    }
    pub fn calc_normals(&mut self) -> Result<(), Error> {
        if self.normals_processed {
            return Ok(());
        }
        self.normals_processed = true;
        if !self.calc_vertex_normals {
            self.normals = Vec::with_capacity(self.faces.len());
        }
        for index in 0..self.faces.len() {
            let normal = self.surface_normal(i32::try_from(index).map_err(|_| Error::Argument)?)?;
            self.normals.push(normal);
            let normal_index =
                i32::try_from(self.normals.len() - 1).map_err(|_| Error::Argument)?;
            self.faces[index].n1 = normal_index;
            self.faces[index].n2 = normal_index;
            self.faces[index].n3 = normal_index;
        }
        Ok(())
    }
    pub fn copy(&self) -> Result<Self, Error> {
        Ok(self.clone())
    }
    pub fn add_pos(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> {
        for c in &mut self.coords {
            c.x += x;
            c.y += y;
            c.z += z;
        }
        for face in &mut self.viewer_faces {
            face.add_pos(x, y, z)?;
        }
        Ok(())
    }
    pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> {
        for c in &mut self.coords {
            *c = Coord::mul_with_coord_quat(*c, q);
        }
        for n in &mut self.normals {
            *n = Coord::mul_with_coord_quat(*n, q);
        }
        for face in &mut self.viewer_faces {
            face.add_rot(q)?;
        }
        Ok(())
    }
    pub fn scale(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> {
        let m = Coord { x, y, z };
        for c in &mut self.coords {
            *c = Coord::mul_with_coord_coord(*c, m);
        }
        for face in &mut self.viewer_faces {
            face.scale(x, y, z)?;
        }
        Ok(())
    }
    pub fn get_vertex_indexer(&self) -> Result<Option<VertexIndexer>, Error> {
        if self.viewer_mode && !self.viewer_faces.is_empty() {
            Ok(Some(VertexIndexer::from_viewer_faces(&self.viewer_faces)?))
        } else {
            Ok(None)
        }
    }
    pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), Error> {
        dump_raw_geometry(&self.coords, &self.faces, path, name, title)
    }
    pub fn profile_outer_face_number(&self) -> i32 {
        self.profile_outer_face_number
    }
    pub fn set_profile_outer_face_number(&mut self, value: i32) {
        self.profile_outer_face_number = value;
    }
    pub fn profile_hollow_face_number(&self) -> i32 {
        self.profile_hollow_face_number
    }
    pub fn set_profile_hollow_face_number(&mut self, value: i32) {
        self.profile_hollow_face_number = value;
    }
    pub fn has_profile_cut(&self) -> bool {
        self.has_profile_cut
    }
    pub fn set_has_profile_cut(&mut self, value: bool) {
        self.has_profile_cut = value;
    }
    pub fn has_hollow(&self) -> bool {
        self.has_hollow
    }
    pub fn set_has_hollow(&mut self, value: bool) {
        self.has_hollow = value;
    }
    pub fn params_to_display_string(&self) -> Result<String, Error> {
        Ok(format!(
            "sides..................: {}\nhollowSides..........: {}\nprofileStart.........: {}\nprofileEnd...........: {}\nhollow...............: {}\ntwistBegin...........: {}\ntwistEnd.............: {}\ntopShearX............: {}\ntopShearY............: {}\npathCutBegin.........: {}\npathCutEnd...........: {}\ndimpleBegin..........: {}\ndimpleEnd............: {}\nskew.................: {}\nholeSizeX............: {}\nholeSizeY............: {}\ntaperX...............: {}\ntaperY...............: {}\nradius...............: {}\nrevolutions..........: {}\nstepsPerRevolution...: {}\nsphereMode...........: {}\nhasProfileCut........: {}\nhasHollow............: {}\nviewerMode...........: {}",
            self.sides,
            self.hollow_sides,
            self.profile_start,
            self.profile_end,
            self.hollow,
            self.twist_begin,
            self.twist_end,
            self.top_shear_x,
            self.top_shear_y,
            self.path_cut_begin,
            self.path_cut_end,
            self.dimple_begin,
            self.dimple_end,
            self.skew,
            self.hole_size_x,
            self.hole_size_y,
            self.taper_x,
            self.taper_y,
            self.radius,
            self.revolutions,
            self.steps_per_revolution,
            self.sphere_mode,
            self.has_profile_cut,
            self.has_hollow,
            self.viewer_mode
        ))
    }
}

fn to_i32(value: usize) -> Result<i32, Error> {
    i32::try_from(value).map_err(|_| Error::Argument)
}
fn add_index(value: i32, offset: usize) -> Result<i32, Error> {
    value.checked_add(to_i32(offset)?).ok_or(Error::Argument)
}
fn assign_viewer_coords(
    viewer: &mut ViewerFace,
    coords: &[Coord],
    face: Face,
) -> Result<(), Error> {
    viewer.v1 = coords[idx(face.v1, coords.len())?];
    viewer.v2 = coords[idx(face.v2, coords.len())?];
    viewer.v3 = coords[idx(face.v3, coords.len())?];
    viewer.coord_index1 = face.v1;
    viewer.coord_index2 = face.v2;
    viewer.coord_index3 = face.v3;
    Ok(())
}
fn assign_viewer_normals(
    viewer: &mut ViewerFace,
    normals: &[Coord],
    face: Face,
) -> Result<(), Error> {
    if normals.is_empty() {
        return viewer.calc_surface_normal();
    }
    viewer.n1 = normals[idx(face.n1, normals.len())?];
    viewer.n2 = normals[idx(face.n2, normals.len())?];
    viewer.n3 = normals[idx(face.n3, normals.len())?];
    Ok(())
}
pub(crate) fn dump_raw_geometry(
    coords: &[Coord],
    faces: &[Face],
    path: String,
    name: String,
    title: String,
) -> Result<(), Error> {
    let mut target = PathBuf::from(path);
    target.push(format!("{name}_{title}.raw"));
    let file = File::create(target).map_err(|_| Error::InvalidOperation)?;
    let mut writer = BufWriter::new(file);
    for face in faces {
        let a = coords[idx(face.v1, coords.len())?];
        let b = coords[idx(face.v2, coords.len())?];
        let c = coords[idx(face.v3, coords.len())?];
        writeln!(writer, "{a} {b} {c}").map_err(|_| Error::InvalidOperation)?;
    }
    writer.flush().map_err(|_| Error::InvalidOperation)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn mesh(sides: i32, hollow: f32, viewer: bool) -> PrimMesh {
        let mut mesh = PrimMesh::new(sides, 0.0, 1.0, hollow, 4).expect("valid mesh");
        mesh.viewer_mode = viewer;
        mesh.calc_vertex_normals = viewer;
        mesh
    }

    #[test]
    fn golden_box_and_torus_summaries_match_reference() {
        let mut box_mesh = mesh(4, 0.0, true);
        box_mesh.extrude(PathType::Linear).expect("box extrusion");
        assert_eq!(
            (
                box_mesh.coords.len(),
                box_mesh.faces.len(),
                box_mesh.viewer_faces.len(),
                box_mesh.num_prim_faces,
            ),
            (10, 14, 12, 6)
        );

        let mut torus = mesh(24, 0.0, true);
        torus.hole_size_x = 1.0;
        torus.hole_size_y = 0.25;
        torus.extrude(PathType::Circular).expect("torus extrusion");
        assert_eq!(
            (
                torus.coords.len(),
                torus.faces.len(),
                torus.viewer_faces.len(),
                torus.num_prim_faces,
            ),
            (650, 1248, 1200, 1)
        );
    }

    #[test]
    fn special_profile_tables_keep_normalized_u_coordinates() {
        let profile = Profile::new_with_int32_single_single_single_int32_boolean_boolean(
            4, 0.0, 1.0, 0.0, 4, true, true,
        )
        .expect("square profile");
        assert_eq!(profile.us, vec![0.0, 0.25, 0.5, 0.75, 1.0]);
        assert!((profile.vertex_normals[0].x - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6);
        assert!((profile.vertex_normals[0].y - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6);
    }

    #[test]
    fn complex_circular_extrusion_is_deterministic_finite_and_bounded() {
        fn build() -> PrimMesh {
            let mut mesh = PrimMesh::new(8, 0.1, 0.85, 0.35, 4).expect("valid mesh");
            mesh.viewer_mode = true;
            mesh.calc_vertex_normals = true;
            mesh.twist_begin = -1;
            mesh.twist_end = 2;
            mesh.top_shear_x = 0.2;
            mesh.top_shear_y = -0.15;
            mesh.path_cut_begin = 0.05;
            mesh.path_cut_end = 0.9;
            mesh.skew = 0.15;
            mesh.taper_x = 0.25;
            mesh.taper_y = -0.2;
            mesh.radius = 0.2;
            mesh.revolutions = 1.5;
            mesh.steps_per_revolution = 32;
            mesh.extrude(PathType::Circular).expect("complex extrusion");
            mesh
        }
        let first = build();
        let second = build();
        assert_eq!(first.coords, second.coords);
        assert_eq!(first.faces, second.faces);
        assert_eq!(first.viewer_faces, second.viewer_faces);
        for face in &first.faces {
            for index in [face.v1, face.v2, face.v3] {
                assert!(usize::try_from(index).is_ok_and(|index| index < first.coords.len()));
            }
        }
        for coord in &first.coords {
            assert!(coord.x.is_finite() && coord.y.is_finite() && coord.z.is_finite());
        }
        for face in &first.viewer_faces {
            for value in [
                face.uv1.u, face.uv1.v, face.uv2.u, face.uv2.v, face.uv3.u, face.uv3.v,
            ] {
                assert!(value.is_finite());
            }
        }
    }

    #[test]
    fn excessive_profile_and_path_sizes_are_rejected() {
        assert!(matches!(
            Profile::new_with_int32_single_single_single_int32_boolean_boolean(
                i32::try_from(MAX_PROFILE_VERTICES + 1).expect("test limit"),
                0.0,
                1.0,
                0.0,
                4,
                true,
                false,
            ),
            Err(Error::Argument)
        ));
        let mut path = Path::new().expect("path");
        assert!(matches!(
            path.create(
                PathType::Linear,
                i32::try_from(MAX_PATH_NODES).expect("test limit")
            ),
            Err(Error::Argument)
        ));
    }
}
