use std::collections::BTreeSet;

use bone_kernel::{
    BrepFace, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
};
use bone_types::{
    ExtrudeId, FaceFingerprint, FaceLabel, FaceRef, FaceRole, FeatureId, Length, Plane3, Point2,
    Point3, PositiveLength, Resolution, RollbackMarker, SketchDimensionId, SketchEntityId,
    SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
};

use crate::Sketch;
use crate::document::Document;
use crate::matcher::resolve_face;
use crate::recompute::{EvaluatedModel, RecomputeScope};
use crate::sketch::{DimensionKind, EditOutcome, SketchDimension, SketchEdit, SketchEntity};

pub(crate) fn xy_basis() -> SketchPlaneBasis {
    let Ok(basis) = SketchPlaneBasis::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        Tolerance::new(1e-9),
    ) else {
        panic!("xy plane basis is orthogonal");
    };
    basis
}

pub(crate) fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
        SketchEntity::point(Point2::from_mm(x, y)),
    )) else {
        panic!("add point");
    };
    (next, id)
}

fn circle_entities(radius_mm: f64) -> (Sketch, SketchEntityId) {
    let (sketch, center) = add_point(Sketch::new(xy_basis()), 0.0, 0.0);
    let Ok((next, EditOutcome::Entity(circle))) = sketch.apply(SketchEdit::AddEntity(
        SketchEntity::circle(center, Length::new::<millimeter>(radius_mm), false),
    )) else {
        panic!("add circle");
    };
    (next, circle)
}

pub(crate) fn circle_sketch(radius_mm: f64) -> Sketch {
    circle_entities(radius_mm).0
}

pub(crate) fn circle_with_radius_dim(
    radius_mm: f64,
) -> (Sketch, SketchEntityId, SketchDimensionId) {
    let (sketch, circle) = circle_entities(radius_mm);
    let Ok((next, EditOutcome::Dimension(dim))) =
        sketch.apply(SketchEdit::AddDimension(SketchDimension::Radius {
            target: circle,
            value: Length::new::<millimeter>(radius_mm),
            kind: DimensionKind::Driving,
        }))
    else {
        panic!("add radius dimension");
    };
    (next, circle, dim)
}

pub(crate) fn open_chain_sketch() -> Sketch {
    let add_line = |sketch: Sketch, from: SketchEntityId, to: SketchEntityId| {
        let Ok((next, _)) =
            sketch.apply(SketchEdit::AddEntity(SketchEntity::line(from, to, false)))
        else {
            panic!("add line");
        };
        next
    };
    let (sketch, a) = add_point(Sketch::new(xy_basis()), 0.0, 0.0);
    let (sketch, b) = add_point(sketch, 10.0, 0.0);
    let (sketch, c) = add_point(sketch, 10.0, 5.0);
    add_line(add_line(sketch, a, b), b, c)
}

pub(crate) fn blind_extrude(sketch: SketchId, depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
        panic!("{depth_mm} mm is a positive depth");
    };
    ExtrudeFeature {
        sketch,
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Separate,
    }
}

pub(crate) struct Chain {
    pub(crate) sketch: FeatureId,
    pub(crate) extrude: FeatureId,
    pub(crate) sketch_id: SketchId,
    pub(crate) extrude_id: ExtrudeId,
}

pub(crate) fn chain_handles(
    document: &Document,
    sketch_id: SketchId,
    extrude_id: ExtrudeId,
) -> Chain {
    let tree = document.feature_tree();
    let (Some(sketch), Some(extrude)) = (
        tree.feature_of_sketch(sketch_id),
        tree.feature_of_extrude(extrude_id),
    ) else {
        panic!("inserted chain resolves to feature ids");
    };
    Chain {
        sketch,
        extrude,
        sketch_id,
        extrude_id,
    }
}

pub(crate) fn push_chain(
    document: &mut Document,
    label: &str,
    radius_mm: f64,
    depth_mm: f64,
) -> Chain {
    let sketch_id = document.allocate_sketch();
    document.insert_sketch(sketch_id, label.to_owned(), circle_sketch(radius_mm));
    let extrude_id = document.commit_extrude(blind_extrude(sketch_id, depth_mm));
    chain_handles(document, sketch_id, extrude_id)
}

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

fn face_label_of(
    model: &EvaluatedModel,
    body: FeatureId,
    matches_role: impl Fn(FaceRole) -> bool,
) -> FaceLabel {
    let Some(solid) = model.body(body) else {
        panic!("the body is built");
    };
    let Some(label) = solid
        .iter_faces()
        .map(BrepFace::label)
        .find(|label| matches_role(label.role))
    else {
        panic!("the body carries the requested face role");
    };
    label
}

pub(crate) fn end_cap_ref(model: &EvaluatedModel, body: FeatureId) -> FaceRef {
    let label = face_label_of(model, body, |role| matches!(role, FaceRole::EndCap));
    let Resolution::Resolved(resolved) = resolve_face(model, label, placeholder_fingerprint())
    else {
        panic!("the end cap resolves on the model that built it");
    };
    let Some(face_ref) = model.face_ref(resolved) else {
        panic!("a planar cap yields a face reference");
    };
    face_ref
}

pub(crate) fn side_face_label(model: &EvaluatedModel, body: FeatureId) -> FaceLabel {
    face_label_of(model, body, |role| matches!(role, FaceRole::Side { .. }))
}

pub(crate) fn push_face_bound_chain(
    document: &mut Document,
    label: &str,
    face: FaceRef,
    radius_mm: f64,
    depth_mm: f64,
) -> Chain {
    let sketch_id = document.allocate_sketch();
    document.insert_sketch(sketch_id, label.to_owned(), circle_sketch(radius_mm));
    let Ok(()) = document.bind_sketch_to_face(sketch_id, face) else {
        panic!("the face binding is acyclic");
    };
    let extrude_id = document.commit_extrude(blind_extrude(sketch_id, depth_mm));
    chain_handles(document, sketch_id, extrude_id)
}

pub(crate) fn full_model(document: &Document) -> EvaluatedModel {
    let mut model = EvaluatedModel::new();
    model.recompute(
        document,
        &BTreeSet::new(),
        RollbackMarker::AtEnd,
        RecomputeScope::Full,
    );
    model
}
