use bone_document::{Document, PrincipalPlane};
use bone_render::{EdgeScene, GenuineEdge, PickId, SolidScene};
use bone_types::{
    Aabb3, CreaseAngle, FeatureId, LinearRgba, Plane3, Point3, SketchPlaneBasis, UnitVec3,
};

pub(crate) const PLANE_HALF_DEFAULT_MM: f64 = 45.0;
const PLANE_MODEL_MARGIN: f64 = 1.35;

const KINDS: [PrincipalPlane; 3] = [PrincipalPlane::Xy, PrincipalPlane::Zx, PrincipalPlane::Yz];

pub(crate) enum PlaneLabel {
    Principal(PrincipalPlane),
    Named(String),
    Preview,
}

pub(crate) struct DatumPlaneInput<'a> {
    pub feature: FeatureId,
    pub plane: Plane3,
    pub name: &'a str,
}

pub(crate) struct ReferencePlane {
    pub label: PlaneLabel,
    pub normal: UnitVec3,
    pub corners: [Point3; 4],
    pub highlighted: bool,
}

impl ReferencePlane {
    pub fn border_edges(&self) -> Vec<GenuineEdge> {
        (0..4)
            .map(|i| {
                GenuineEdge::new(
                    self.corners[i],
                    self.corners[(i + 1) % 4],
                    PickId::NONE,
                    CreaseAngle::FLAT,
                )
            })
            .collect()
    }

    pub fn border(&self) -> EdgeScene {
        EdgeScene::from_genuine(self.border_edges())
    }

    pub fn label_anchor(&self) -> Point3 {
        let (ax, ay, az) = self.corners[3].coords_mm();
        let (bx, by, bz) = self.corners[2].coords_mm();
        Point3::from_mm(
            f64::midpoint(ax, bx),
            f64::midpoint(ay, by),
            f64::midpoint(az, bz),
        )
    }
}

pub(crate) struct ReferencePlaneScene {
    pub planes: Vec<ReferencePlane>,
    pub aabb: Aabb3,
}

fn plane3_corners(plane: Plane3, half: f64) -> [Point3; 4] {
    [
        plane.point_at_local(-half, -half, 0.0),
        plane.point_at_local(half, -half, 0.0),
        plane.point_at_local(half, half, 0.0),
        plane.point_at_local(-half, half, 0.0),
    ]
}

fn plane_corners(basis: SketchPlaneBasis, half: f64) -> [Point3; 4] {
    plane3_corners(Plane3::from(basis), half)
}

pub(crate) fn plane_half_extent(model_aabb: Option<Aabb3>) -> f64 {
    model_aabb.map_or(PLANE_HALF_DEFAULT_MM, |aabb| {
        (PLANE_MODEL_MARGIN * 0.5 * aabb.extent().norm_mm()).max(PLANE_HALF_DEFAULT_MM)
    })
}

pub(crate) fn build(
    document: &Document,
    datums: &[DatumPlaneInput<'_>],
    preview: Option<Plane3>,
    highlighted: Option<FeatureId>,
    half: f64,
    visible: impl Fn(FeatureId) -> bool,
) -> Option<ReferencePlaneScene> {
    let tree = document.feature_tree();
    let principals = KINDS.into_iter().filter_map(|kind| {
        let feature = tree.principal_plane_feature(kind)?;
        if !visible(feature) {
            return None;
        }
        let basis = kind.basis();
        Some(ReferencePlane {
            label: PlaneLabel::Principal(kind),
            normal: basis.normal(),
            corners: plane_corners(basis, half),
            highlighted: Some(feature) == highlighted,
        })
    });
    let committed = datums
        .iter()
        .filter(|input| visible(input.feature))
        .map(|input| ReferencePlane {
            label: PlaneLabel::Named(input.name.to_owned()),
            normal: input.plane.normal(),
            corners: plane3_corners(input.plane, half),
            highlighted: Some(input.feature) == highlighted,
        });
    let staged = preview.map(|plane| ReferencePlane {
        label: PlaneLabel::Preview,
        normal: plane.normal(),
        corners: plane3_corners(plane, half),
        highlighted: true,
    });
    let planes: Vec<ReferencePlane> = principals.chain(committed).chain(staged).collect();
    let aabb = Aabb3::from_points(planes.iter().flat_map(|plane| plane.corners))?;
    Some(ReferencePlaneScene { planes, aabb })
}

pub(crate) fn fill_scene(
    scene: &ReferencePlaneScene,
    color_for: impl Fn(&ReferencePlane) -> LinearRgba,
) -> SolidScene {
    let positions: Vec<Point3> = scene
        .planes
        .iter()
        .flat_map(|plane| plane.corners)
        .collect();
    let normals: Vec<UnitVec3> = scene
        .planes
        .iter()
        .flat_map(|plane| [plane.normal; 4])
        .collect();
    let colors: Vec<LinearRgba> = scene
        .planes
        .iter()
        .flat_map(|plane| [color_for(plane); 4])
        .collect();
    let triangles: Vec<[u32; 3]> = scene
        .planes
        .iter()
        .enumerate()
        .flat_map(|(index, _)| {
            let base = u32::try_from(index).unwrap_or(0) * 4;
            [[base, base + 1, base + 2], [base, base + 2, base + 3]]
        })
        .collect();
    SolidScene::from_parts(positions, normals, colors, triangles)
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_document::Document;
    use bone_types::DocumentId;

    fn scene() -> ReferencePlaneScene {
        let document = Document::new(DocumentId::default(), "Part1".to_owned());
        let Some(scene) = build(&document, &[], None, None, PLANE_HALF_DEFAULT_MM, |_| true) else {
            panic!("seeded document yields three planes");
        };
        scene
    }

    #[test]
    fn three_principal_planes_lie_on_their_axes() {
        let scene = scene();
        assert_eq!(scene.planes.len(), 3, "front, top and right planes");
        scene.planes.iter().for_each(|plane| {
            let PlaneLabel::Principal(kind) = &plane.label else {
                panic!("seeded document yields only principal planes");
            };
            let normal_axis = match kind {
                PrincipalPlane::Xy => (0.0, 0.0, 1.0),
                PrincipalPlane::Yz => (1.0, 0.0, 0.0),
                PrincipalPlane::Zx => (0.0, 1.0, 0.0),
            };
            plane.corners.iter().for_each(|corner| {
                let (x, y, z) = corner.coords_mm();
                let along_normal = x * normal_axis.0 + y * normal_axis.1 + z * normal_axis.2;
                assert!(
                    along_normal.abs() < 1.0e-9,
                    "{kind:?} corner sits in its own plane",
                );
            });
        });
    }
}
