use bone_types::{
    Angle, EdgeLabel, FaceLabel, Length, Plane3, Point3, Tolerance, UnitVec3, VertexLabel,
};
use uom::si::angle::radian;
use uom::si::length::millimeter;

use super::super::edges::{EdgeCurve3, line_between};
use super::ssi::AnalyticSurface;
use super::{as_vec, basis, unit};
use crate::arc3::Arc3;
use crate::circle3::Circle3;

#[derive(Clone, Copy)]
pub(crate) struct PlanarVertex {
    pub position: Point3,
    pub label: VertexLabel,
}

#[derive(Clone)]
pub(crate) struct PlanarLoop {
    pub verts: Vec<PlanarVertex>,
    pub edges: Vec<EdgeLabel>,
    pub curves: Vec<EdgeCurve3>,
}

#[derive(Clone)]
pub(crate) struct PlanarFace {
    pub label: FaceLabel,
    pub plane: Plane3,
    pub polygon: Vec<PlanarVertex>,
    pub edges: Vec<EdgeLabel>,
    pub curves: Vec<EdgeCurve3>,
    pub holes: Vec<PlanarLoop>,
}

impl PlanarFace {
    pub(crate) fn straight(
        label: FaceLabel,
        plane: Plane3,
        polygon: Vec<PlanarVertex>,
        edges: Vec<EdgeLabel>,
    ) -> Self {
        let count = polygon.len();
        let curves = (0..count)
            .map(|i| line_between(polygon[i].position, polygon[(i + 1) % count].position))
            .collect();
        Self {
            label,
            plane,
            polygon,
            edges,
            curves,
            holes: Vec::new(),
        }
    }

    pub(crate) fn analytic(&self) -> AnalyticSurface {
        AnalyticSurface::Plane {
            origin: self.plane.origin(),
            normal: self.plane.normal(),
        }
    }
}

pub(crate) type PlanarBody = Vec<PlanarFace>;

#[derive(Clone, Copy)]
pub(crate) struct Cyl {
    pub axis_origin: Point3,
    pub axis: UnitVec3,
    pub radius: Length,
    pub rx: UnitVec3,
    pub ry: UnitVec3,
}

impl Cyl {
    pub(crate) fn new(
        axis_origin: Point3,
        axis: UnitVec3,
        radius: Length,
        tolerance: Tolerance,
    ) -> Option<Self> {
        let (rx, ry) = basis(axis, tolerance)?;
        Some(Self {
            axis_origin,
            axis,
            radius,
            rx,
            ry,
        })
    }

    pub(crate) fn radius_mm(&self) -> f64 {
        self.radius.get::<millimeter>()
    }

    pub(crate) fn theta_z(&self, point: Point3) -> (f64, f64) {
        let delta = point - self.axis_origin;
        let x = delta.dot_mm2(as_vec(self.rx));
        let y = delta.dot_mm2(as_vec(self.ry));
        let z = delta.dot_mm2(as_vec(self.axis));
        (y.atan2(x), z)
    }

    pub(crate) fn point(&self, theta: f64, z: f64) -> Point3 {
        let r = self.radius_mm();
        self.axis_origin
            + as_vec(self.rx) * (r * theta.cos())
            + as_vec(self.ry) * (r * theta.sin())
            + as_vec(self.axis) * z
    }

    pub(crate) fn radial(&self, theta: f64, tolerance: Tolerance) -> Option<UnitVec3> {
        unit(
            as_vec(self.rx) * theta.cos() + as_vec(self.ry) * theta.sin(),
            tolerance,
        )
    }

    pub(crate) fn analytic(&self) -> AnalyticSurface {
        AnalyticSurface::Cylinder {
            axis_origin: self.axis_origin,
            axis: self.axis,
            radius: self.radius,
        }
    }
}

#[derive(Clone)]
pub(crate) struct Tube {
    pub label: FaceLabel,
    pub cyl: Cyl,
    pub theta0: f64,
    pub theta1: f64,
    pub z0: f64,
    pub z1: f64,
    pub full: bool,
    pub rim_bottom: EdgeLabel,
    pub rim_top: EdgeLabel,
    pub seam: EdgeLabel,
    pub vbot: VertexLabel,
    pub vtop: VertexLabel,
    pub outward: bool,
}

impl Tube {
    pub(crate) fn center(&self) -> Point3 {
        let theta = f64::midpoint(self.theta0, self.theta1);
        let z = f64::midpoint(self.z0, self.z1);
        self.cyl.point(theta, z)
    }

    pub(crate) fn surface_normal(&self, tolerance: Tolerance) -> Option<UnitVec3> {
        let theta = f64::midpoint(self.theta0, self.theta1);
        let radial = self.cyl.radial(theta, tolerance)?;
        Some(if self.outward {
            radial
        } else {
            radial.reversed()
        })
    }
}

#[derive(Clone)]
pub(crate) enum Face {
    Planar(Box<PlanarFace>),
    Tube(Box<Tube>),
}

impl Face {
    pub(crate) fn planar(face: PlanarFace) -> Self {
        Self::Planar(Box::new(face))
    }

    pub(crate) fn tube(tube: Tube) -> Self {
        Self::Tube(Box::new(tube))
    }

    pub(crate) fn label(&self) -> FaceLabel {
        match self {
            Self::Planar(face) => face.label,
            Self::Tube(tube) => tube.label,
        }
    }
}

pub(crate) type Body = Vec<Face>;

pub(crate) fn canonical_axis(u: UnitVec3) -> UnitVec3 {
    let (x, y, z) = u.components();
    let positive = if x.abs() >= y.abs() && x.abs() >= z.abs() {
        x > 0.0
    } else if y.abs() >= z.abs() {
        y > 0.0
    } else {
        z > 0.0
    };
    if positive { u } else { u.reversed() }
}

pub(crate) fn canonical_basis(u: UnitVec3, tolerance: Tolerance) -> Option<(UnitVec3, UnitVec3)> {
    basis(canonical_axis(u), tolerance)
}

pub(crate) fn dot(a: UnitVec3, b: UnitVec3) -> f64 {
    as_vec(a).dot_mm2(as_vec(b))
}

pub(crate) fn project_onto_axis(origin: Point3, axis: UnitVec3, p: Point3) -> f64 {
    (p - origin).dot_mm2(as_vec(axis))
}

pub(crate) fn radial_distance(origin: Point3, axis: UnitVec3, p: Point3) -> f64 {
    let d = p - origin;
    let along = d.dot_mm2(as_vec(axis));
    (d - as_vec(axis) * along).norm_mm()
}

pub(crate) fn vlabel_intersection(faces: [FaceLabel; 3]) -> VertexLabel {
    VertexLabel {
        feature: faces[0].feature,
        role: bone_types::VertexRole::Intersection { faces },
    }
}

pub(crate) fn elabel_intersection(a: FaceLabel, b: FaceLabel) -> EdgeLabel {
    EdgeLabel {
        feature: a.feature.min(b.feature),
        role: bone_types::EdgeRole::intersection(a, b),
    }
}

pub(crate) fn orient_curve(
    curve: EdgeCurve3,
    front: Point3,
    _back: Point3,
    tolerance: Tolerance,
) -> EdgeCurve3 {
    use crate::curve3::Curve3;
    let start = curve.evaluate(bone_types::Parameter::new(0.0));
    let keep =
        (start - front).norm_mm() <= tolerance.value() || matches!(curve, EdgeCurve3::Circle(_));
    if keep { curve } else { reverse_curve(&curve) }
}

pub(crate) fn reverse_curve(curve: &EdgeCurve3) -> EdgeCurve3 {
    use crate::line3::Line3;
    match curve {
        EdgeCurve3::Line(line) => EdgeCurve3::Line(Line3::new_unchecked(line.end(), line.start())),
        EdgeCurve3::Arc(arc) => {
            let start = arc.start_angle().get::<radian>();
            let sweep = arc.sweep_angle().get::<radian>();
            Arc3::new(
                arc.plane(),
                arc.radius(),
                Angle::new::<radian>(start + sweep),
                Angle::new::<radian>(-sweep),
                Tolerance::new(1.0e-9),
            )
            .map_or_else(|_| EdgeCurve3::Arc(*arc), EdgeCurve3::Arc)
        }
        EdgeCurve3::Circle(circle) => {
            let plane = circle.plane();
            let flipped = Plane3::new_unchecked(plane.origin(), plane.y_axis(), plane.x_axis());
            EdgeCurve3::Circle(Circle3::from_raw(flipped, circle.radius()))
        }
    }
}
