use bone_kernel::{BrepFace, BrepSolid};
use bone_types::{
    BrepFaceId, EntityRef, FaceFingerprint, FaceLabel, FaceRef, FeatureId, Resolution,
    SketchPlaneBasis,
};

use crate::recompute::{EvaluatedModel, RebuildPass};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ResolvedFace {
    body: FeatureId,
    face: BrepFaceId,
    built_at: RebuildPass,
}

impl ResolvedFace {
    #[must_use]
    pub const fn body(self) -> FeatureId {
        self.body
    }

    #[must_use]
    pub const fn face(self) -> BrepFaceId {
        self.face
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ResolvedEntity {
    Face(ResolvedFace),
}

#[must_use]
pub fn resolve(model: &EvaluatedModel, reference: EntityRef) -> Resolution<ResolvedEntity> {
    match reference {
        EntityRef::Face(label, fingerprint) => {
            resolve_face(model, label, fingerprint).map(ResolvedEntity::Face)
        }
        EntityRef::Edge(..) | EntityRef::Vertex(..) => Resolution::Dangling {
            last_known: reference,
        },
    }
}

#[must_use]
pub fn resolve_face(
    model: &EvaluatedModel,
    label: FaceLabel,
    fingerprint: FaceFingerprint,
) -> Resolution<ResolvedFace> {
    let dangling = Resolution::Dangling {
        last_known: EntityRef::Face(label, fingerprint),
    };
    let (Some(solid), Some(built_at)) = (model.body(label.feature), model.built_at(label.feature))
    else {
        return dangling;
    };
    match unique_face(solid, label) {
        Some(face) => Resolution::Resolved(ResolvedFace {
            body: label.feature,
            face: face.id(),
            built_at,
        }),
        None => dangling,
    }
}

#[must_use]
pub(crate) fn unique_face(solid: &BrepSolid, label: FaceLabel) -> Option<&BrepFace> {
    let mut matches = solid.iter_faces().filter(|face| face.label() == label);
    let first = matches.next()?;
    if matches.next().is_some() {
        debug_assert!(false, "face label {label} matched multiple faces");
        return None;
    }
    Some(first)
}

impl EvaluatedModel {
    #[must_use]
    pub fn face(&self, resolved: ResolvedFace) -> Option<&BrepFace> {
        (self.built_at(resolved.body) == Some(resolved.built_at))
            .then(|| self.body(resolved.body))
            .flatten()
            .and_then(|solid| solid.iter_faces().find(|face| face.id() == resolved.face))
    }

    #[must_use]
    pub fn face_ref(&self, resolved: ResolvedFace) -> Option<FaceRef> {
        let label = self.face(resolved)?.label();
        let fingerprint = self.body(resolved.body)?.face_fingerprint(resolved.face)?;
        Some(FaceRef::new(label, fingerprint))
    }

    #[must_use]
    pub fn face_ref_at(&self, body: FeatureId, face: BrepFaceId) -> Option<FaceRef> {
        let solid = self.body(body)?;
        let label = solid
            .iter_faces()
            .find(|candidate| candidate.id() == face)?
            .label();
        let fingerprint = solid.face_fingerprint(face)?;
        Some(FaceRef::new(label, fingerprint))
    }

    #[must_use]
    pub fn face_ref_any(&self, face: BrepFaceId) -> Option<FaceRef> {
        self.bodies()
            .find_map(|(body, _)| self.face_ref_at(body, face))
    }

    #[must_use]
    pub fn face_for_sketch(&self, face: BrepFaceId) -> Option<(FaceRef, SketchPlaneBasis)> {
        self.bodies().find_map(|(body, solid)| {
            let basis = solid.face_plane_basis(face)?;
            let face_ref = self.face_ref_at(body, face)?;
            Some((face_ref, basis))
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use bone_kernel::BrepFace;
    use bone_types::{
        DocumentId, EdgeFingerprint, EdgeLabel, EdgeRole, EntityRef, FaceFingerprint, FaceLabel,
        FaceRole, FeatureId, Length, LoopIndex, Plane3, Point3, Resolution, RollbackMarker,
        UnitVec3, millimeter,
    };
    use proptest::prelude::*;

    use super::{ResolvedEntity, ResolvedFace, resolve, resolve_face};
    use crate::document::Document;
    use crate::recompute::{EvaluatedModel, RecomputeScope};
    use crate::sketch::{DimensionValue, SketchEdit};
    use crate::test_support::{
        blind_extrude, chain_handles, circle_with_radius_dim, full_model, push_chain,
    };

    fn placeholder_fingerprint() -> FaceFingerprint {
        FaceFingerprint {
            plane: Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()),
            centroid: Point3::origin(),
        }
    }

    fn face_labels(model: &EvaluatedModel, body: FeatureId) -> Vec<FaceLabel> {
        let Some(solid) = model.body(body) else {
            panic!("body is built");
        };
        solid.iter_faces().map(BrepFace::label).collect()
    }

    fn resolved_label(model: &EvaluatedModel, resolved: ResolvedFace) -> FaceLabel {
        let Some(face) = model.face(resolved) else {
            panic!("a resolved face is present in its model");
        };
        face.label()
    }

    #[test]
    fn face_ref_at_round_trips_a_picked_face() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_chain(&mut document, "Body", 6.0, 4.0);
        let model = full_model(&document);
        let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else {
            panic!("the body has faces");
        };
        let Resolution::Resolved(resolved) = resolve_face(&model, label, placeholder_fingerprint())
        else {
            panic!("the label resolves on its own model");
        };
        let Some(face_ref) = model.face_ref_at(resolved.body(), resolved.face()) else {
            panic!("a picked face yields a face reference");
        };
        let Resolution::Resolved(ResolvedEntity::Face(again)) =
            resolve(&model, face_ref.entity_ref())
        else {
            panic!("the reattach reference resolves back to a face");
        };
        assert_eq!(
            again.face(),
            resolved.face(),
            "reattach targets the same face"
        );
        assert_eq!(
            model
                .face_ref_any(resolved.face())
                .map(|reference| reference.label),
            Some(face_ref.label),
        );
    }

    #[test]
    fn face_for_sketch_yields_a_basis_for_a_planar_cap() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_chain(&mut document, "Body", 6.0, 4.0);
        let model = full_model(&document);
        let Some(cap) = face_labels(&model, chain.extrude)
            .into_iter()
            .find(|label| matches!(label.role, FaceRole::EndCap))
        else {
            panic!("the cylinder has a planar cap");
        };
        let Resolution::Resolved(resolved) = resolve_face(&model, cap, placeholder_fingerprint())
        else {
            panic!("the cap resolves on its own model");
        };
        assert!(
            model.face_for_sketch(resolved.face()).is_some(),
            "a planar cap yields a face reference and a sketch basis",
        );
    }

    proptest! {
        #[test]
        fn a_surviving_ref_resolves_and_a_deleted_target_dangles(
            kept_radius in 2.0f64..18.0,
            doomed_radius in 2.0f64..18.0,
        ) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let kept = push_chain(&mut document, "Keep", kept_radius, 4.0);
            let doomed = push_chain(&mut document, "Drop", doomed_radius, 4.0);
            let mut model = full_model(&document);

            let Some(survivor) = face_labels(&model, kept.extrude).into_iter().next() else {
                return Err(TestCaseError::fail("the kept body has faces"));
            };
            let Some(lost) = face_labels(&model, doomed.extrude).into_iter().next() else {
                return Err(TestCaseError::fail("the doomed body has faces"));
            };

            document.remove_extrude(doomed.extrude_id);
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Full,
            );

            let Resolution::Resolved(resolved) =
                resolve_face(&model, survivor, placeholder_fingerprint())
            else {
                return Err(TestCaseError::fail(format!(
                    "{survivor} must survive a sibling deletion"
                )));
            };
            prop_assert_eq!(resolved.body(), kept.extrude);
            prop_assert_eq!(resolved_label(&model, resolved), survivor);

            prop_assert_eq!(
                resolve_face(&model, lost, placeholder_fingerprint()),
                Resolution::Dangling {
                    last_known: EntityRef::Face(lost, placeholder_fingerprint()),
                },
                "a deleted target dangles with its last-known reference",
            );
        }
    }

    #[test]
    fn a_no_match_label_never_binds_to_a_nearest_face() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, circle, _dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        let absent = FaceLabel {
            feature: chain.extrude,
            role: FaceRole::Side {
                loop_index: LoopIndex::new(u16::MAX),
                from: circle,
            },
        };
        let resolution = resolve_face(&model, absent, placeholder_fingerprint());
        assert!(resolution.is_dangling());
        assert_eq!(resolution.id(), None, "no nearest-guess id is returned");
    }

    #[test]
    fn resolve_routes_a_face_ref_and_dangles_unimplemented_kinds() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, circle, _dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else {
            panic!("the body has faces");
        };
        let Resolution::Resolved(ResolvedEntity::Face(resolved)) =
            resolve(&model, EntityRef::Face(label, placeholder_fingerprint()))
        else {
            panic!("a face reference resolves through the dispatcher");
        };
        assert_eq!(resolved.body(), chain.extrude);

        let edge_ref = EntityRef::Edge(
            EdgeLabel {
                feature: chain.extrude,
                role: EdgeRole::StartCapEdge { from: circle },
            },
            EdgeFingerprint {
                sample: Point3::origin(),
                direction: UnitVec3::x_axis(),
            },
        );
        assert!(
            resolve(&model, edge_ref).is_dangling(),
            "edge resolution is unimplemented, so it dangles rather than guessing",
        );
    }

    #[test]
    fn a_resolved_face_goes_stale_when_its_body_rebuilds() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        let (sketch, _circle, radius_dim) = circle_with_radius_dim(5.0);
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let mut model = full_model(&document);

        let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else {
            panic!("the body has faces");
        };
        let Resolution::Resolved(stale) = resolve_face(&model, label, placeholder_fingerprint())
        else {
            panic!("the face resolves before the edit");
        };
        assert!(
            model.face(stale).is_some(),
            "a fresh handle reads back on the model that produced it",
        );

        let Some(original) = document.sketch(sketch_id).cloned() else {
            panic!("the sketch is present");
        };
        let Ok((edited, _)) = original.apply(SketchEdit::UpdateDimensionValue {
            id: radius_dim,
            value: DimensionValue::Length(Length::new::<millimeter>(9.0)),
        }) else {
            panic!("the driving radius dimension accepts a new value");
        };
        document.replace_sketch(sketch_id, edited);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(chain.sketch),
        );

        assert!(
            model.face(stale).is_none(),
            "a handle from before the rebuild must not read a face out of the new arena",
        );
        assert!(
            resolve_face(&model, label, placeholder_fingerprint())
                .id()
                .is_some(),
            "re-resolving against the rebuilt model yields a usable handle",
        );
    }

    proptest! {
        #[test]
        fn a_dimension_edit_keeps_every_face_ref_resolved(
            radius in 2.0f64..18.0,
            edited in 2.0f64..18.0,
        ) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let sketch_id = document.allocate_sketch();
            let (sketch, _circle, radius_dim) = circle_with_radius_dim(radius);
            document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch);
            let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
            let chain = chain_handles(&document, sketch_id, extrude_id);
            let mut model = full_model(&document);

            let labels = face_labels(&model, chain.extrude);

            let Some(original) = document.sketch(sketch_id).cloned() else {
                prop_assert!(false, "the sketch is present");
                return Ok(());
            };
            let Ok((edited_sketch, _)) = original.apply(SketchEdit::UpdateDimensionValue {
                id: radius_dim,
                value: DimensionValue::Length(Length::new::<millimeter>(edited)),
            }) else {
                prop_assert!(false, "a driving radius dimension accepts a new value");
                return Ok(());
            };
            document.replace_sketch(sketch_id, edited_sketch);
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Edited(chain.sketch),
            );

            let unresolved: Vec<FaceLabel> = labels
                .iter()
                .copied()
                .filter(|&label| {
                    match resolve_face(&model, label, placeholder_fingerprint()) {
                        Resolution::Resolved(resolved) => resolved_label(&model, resolved) != label,
                        Resolution::Repaired { .. } | Resolution::Dangling { .. } => true,
                    }
                })
                .collect();
            prop_assert!(
                unresolved.is_empty(),
                "every face ref must resolve to its identical label across a dimension edit, failed: {unresolved:?}",
            );
        }
    }
}
