use bone_kernel::{
    BooleanOp, BrepError, BrepSolid, ExtrudeFeature, ExtrudeProfile, ExtrudeScene, RevolveFeature,
    boolean, evaluate_extrude_plan, next_face_distance, resolve_sweep,
};
use bone_solver::SolverError;
use bone_types::{
    Aabb3, FaceLabel, FeatureId, GeometryGeneration, Plane3, Point3, RebuildError, Tolerance,
    VertexLabel,
};

use crate::profile::profile_from_sketch3;
use crate::sketch3::Sketch3;

#[derive(Clone, Debug, PartialEq)]
pub enum EvaluatedSketch {
    Solved(Sketch3),
    Failed(SolverError),
    PlaneUnresolved(RebuildError),
}

#[must_use]
pub fn evaluate_sketch(input: &Sketch3) -> EvaluatedSketch {
    match input.solve() {
        Ok(solved) => EvaluatedSketch::Solved(solved),
        Err(error) => EvaluatedSketch::Failed(error),
    }
}

#[derive(Clone, Debug, thiserror::Error)]
pub enum ExtrudeError {
    #[error(transparent)]
    UnsolvedSketch(SolverError),
    #[error(transparent)]
    Kernel(BrepError),
    #[error("{0}")]
    PlaneUnresolved(Box<RebuildError>),
    #[error("extruded cut has no body to remove material from")]
    CutWithoutBody,
    #[error("flip side to cut could not be built for this geometry")]
    FlipSideUnsupported,
    #[error("a referenced profile or path could not be resolved")]
    UnresolvedReference,
}

#[derive(Clone)]
pub struct EvaluatedExtrude {
    result: Result<BrepSolid, ExtrudeError>,
    generation: Option<GeometryGeneration>,
}

impl EvaluatedExtrude {
    #[must_use]
    pub fn ok(solid: BrepSolid) -> Self {
        let generation = Some(GeometryGeneration::from_solid_key(solid.content_key()));
        Self {
            result: Ok(solid),
            generation,
        }
    }

    #[must_use]
    pub fn failed(error: ExtrudeError) -> Self {
        Self {
            result: Err(error),
            generation: None,
        }
    }

    pub fn result(&self) -> &Result<BrepSolid, ExtrudeError> {
        &self.result
    }

    #[must_use]
    pub fn solid(&self) -> Option<&BrepSolid> {
        self.result.as_ref().ok()
    }

    #[must_use]
    pub fn generation(&self) -> Option<GeometryGeneration> {
        self.generation
    }
}

impl core::fmt::Debug for EvaluatedExtrude {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let mut state = f.debug_struct("EvaluatedExtrude");
        state.field("generation", &self.generation);
        match &self.result {
            Ok(solid) => state.field("faces", &solid.iter_faces().count()),
            Err(error) => state.field("error", error),
        };
        state.finish()
    }
}

#[derive(Clone)]
pub struct EvaluatedFeature {
    result: Result<Vec<BrepSolid>, ExtrudeError>,
    generation: Option<GeometryGeneration>,
}

impl EvaluatedFeature {
    #[must_use]
    pub fn bodies(mut solids: Vec<BrepSolid>) -> Self {
        solids.sort_by(cmp_body);
        let generation = combine_generations(&solids);
        Self {
            result: Ok(solids),
            generation,
        }
    }

    #[must_use]
    pub fn failed(error: ExtrudeError) -> Self {
        Self {
            result: Err(error),
            generation: None,
        }
    }

    #[must_use]
    pub fn from_tool(tool: EvaluatedExtrude) -> Self {
        match tool.result {
            Ok(solid) => Self {
                result: Ok(vec![solid]),
                generation: tool.generation,
            },
            Err(error) => Self::failed(error),
        }
    }

    #[must_use]
    pub fn solids(&self) -> &[BrepSolid] {
        match &self.result {
            Ok(solids) => solids,
            Err(_) => &[],
        }
    }

    #[must_use]
    pub fn error(&self) -> Option<&ExtrudeError> {
        self.result.as_ref().err()
    }

    #[must_use]
    pub fn generation(&self) -> Option<GeometryGeneration> {
        self.generation
    }
}

impl core::fmt::Debug for EvaluatedFeature {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let mut state = f.debug_struct("EvaluatedFeature");
        state.field("generation", &self.generation);
        match &self.result {
            Ok(solids) => state.field("bodies", &solids.len()),
            Err(error) => state.field("error", error),
        };
        state.finish()
    }
}

fn combine_generations(solids: &[BrepSolid]) -> Option<GeometryGeneration> {
    solids
        .iter()
        .map(|solid| GeometryGeneration::from_solid_key(solid.content_key()))
        .reduce(mix_generations)
}

fn mix_generations(
    accumulated: GeometryGeneration,
    next: GeometryGeneration,
) -> GeometryGeneration {
    GeometryGeneration::from_bits(
        accumulated
            .value()
            .wrapping_mul(0x0100_0000_01b3)
            .wrapping_add(next.value()),
    )
}

fn cmp_body(a: &BrepSolid, b: &BrepSolid) -> core::cmp::Ordering {
    let (center_a, center_b) = (body_center(a), body_center(b));
    center_a
        .iter()
        .zip(center_b.iter())
        .map(|(left, right)| left.total_cmp(right))
        .find(|ordering| ordering.is_ne())
        .unwrap_or_else(|| a.content_key().bytes().cmp(&b.content_key().bytes()))
}

fn body_center(solid: &BrepSolid) -> [f64; 3] {
    solid.bounding_box().map_or([0.0; 3], |bounds| {
        let (x, y, z) = bounds.center().coords_mm();
        [x, y, z]
    })
}

const MERGE_TOLERANCE: Tolerance = Tolerance::new(1.0e-7);

#[must_use]
pub fn merge_into(boss: EvaluatedExtrude, base: &BrepSolid) -> (EvaluatedExtrude, bool) {
    let Ok(boss_solid) = &boss.result else {
        return (boss, false);
    };
    match boolean(base, boss_solid, BooleanOp::Union, MERGE_TOLERANCE) {
        Ok(union) => {
            let generation = Some(GeometryGeneration::from_solid_key(union.content_key()));
            (
                EvaluatedExtrude {
                    result: Ok(union),
                    generation,
                },
                true,
            )
        }
        Err(error) if is_unmergeable_gap(&error) => (boss, false),
        Err(error) => (
            EvaluatedExtrude {
                result: Err(ExtrudeError::Kernel(error)),
                generation: None,
            },
            false,
        ),
    }
}

const fn is_unmergeable_gap(error: &BrepError) -> bool {
    matches!(
        error,
        BrepError::BooleanUnsupported { .. } | BrepError::BooleanDisjoint
    )
}

fn carve(
    tool: EvaluatedExtrude,
    base: Option<&BrepSolid>,
    op: BooleanOp,
    on_error: impl Fn(BrepError) -> ExtrudeError,
) -> (EvaluatedExtrude, bool) {
    let Ok(tool_solid) = &tool.result else {
        return (tool, false);
    };
    let Some(base) = base else {
        return (
            EvaluatedExtrude::failed(ExtrudeError::CutWithoutBody),
            false,
        );
    };
    match boolean(base, tool_solid, op, MERGE_TOLERANCE) {
        Ok(result) => (EvaluatedExtrude::ok(result), true),
        Err(error) => (EvaluatedExtrude::failed(on_error(error)), false),
    }
}

#[must_use]
pub fn cut_from(tool: EvaluatedExtrude, base: Option<&BrepSolid>) -> (EvaluatedExtrude, bool) {
    carve(tool, base, BooleanOp::Difference, ExtrudeError::Kernel)
}

#[must_use]
pub fn intersect_from(
    tool: EvaluatedExtrude,
    base: Option<&BrepSolid>,
) -> (EvaluatedExtrude, bool) {
    carve(tool, base, BooleanOp::Intersection, |_| {
        ExtrudeError::FlipSideUnsupported
    })
}

fn evaluate_profile(
    sketch: &EvaluatedSketch,
    feature: &ExtrudeFeature,
    build: impl FnOnce(&ExtrudeProfile) -> Result<BrepSolid, BrepError>,
) -> EvaluatedExtrude {
    let result = match sketch {
        EvaluatedSketch::Solved(solved) => profile_from_sketch3(solved, feature.contours, None)
            .and_then(|profile| build(&profile))
            .map_err(ExtrudeError::Kernel),
        EvaluatedSketch::Failed(error) => Err(ExtrudeError::UnsolvedSketch(error.clone())),
        EvaluatedSketch::PlaneUnresolved(error) => {
            Err(ExtrudeError::PlaneUnresolved(Box::new(*error)))
        }
    };
    let generation = result
        .as_ref()
        .ok()
        .map(|solid| GeometryGeneration::from_solid_key(solid.content_key()));
    EvaluatedExtrude { result, generation }
}

fn obstacle_bbox(solids: &[&BrepSolid]) -> Option<Aabb3> {
    solids
        .iter()
        .filter_map(|solid| solid.bounding_box())
        .reduce(Aabb3::union)
}

#[must_use]
pub fn evaluate_extrude(
    extrude: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &ExtrudeFeature,
) -> EvaluatedExtrude {
    evaluate_profile(sketch, feature, |profile| {
        bone_kernel::evaluate_extrude(extrude, profile, feature)
    })
}

#[must_use]
pub fn evaluate_extrude_reaching(
    extrude: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &ExtrudeFeature,
    obstacles: &[&BrepSolid],
) -> EvaluatedExtrude {
    evaluate_profile(sketch, feature, |profile| {
        let no_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| None;
        let no_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| None;
        let no_body: &dyn Fn(FeatureId) -> Option<Aabb3> = &|_| None;
        let next_reach = |sign: f64, base_offset_mm: f64| {
            next_face_distance(profile, obstacles, sign, base_offset_mm)
        };
        let scene = ExtrudeScene::new(
            profile.plane(),
            obstacle_bbox(obstacles),
            no_face,
            no_vertex,
            no_body,
            &next_reach,
        );
        let plan = resolve_sweep(feature, &scene)?;
        evaluate_extrude_plan(extrude, profile, &plan)
    })
}

pub fn evaluate_revolve_reaching(
    revolve: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &RevolveFeature,
) -> EvaluatedExtrude {
    let solved = match sketch {
        EvaluatedSketch::Solved(solved) => solved,
        EvaluatedSketch::Failed(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::UnsolvedSketch(error.clone()));
        }
        EvaluatedSketch::PlaneUnresolved(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::PlaneUnresolved(Box::new(*error)));
        }
    };
    let no_face: &dyn Fn(FaceLabel) -> Option<Plane3> = &|_| None;
    let no_vertex: &dyn Fn(VertexLabel) -> Option<Point3> = &|_| None;
    let result = crate::recompute::preview_revolve_axis(feature, solved)
        .and_then(|axis| {
            crate::recompute::revolve_solid(revolve, solved, feature, axis, no_face, no_vertex)
        })
        .map_err(ExtrudeError::Kernel);
    match result {
        Ok(solid) => EvaluatedExtrude::ok(solid),
        Err(error) => EvaluatedExtrude::failed(error),
    }
}
