use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation};
use bone_render::SketchPreview;
use bone_types::{Length, Point2, SketchEntityId};

use crate::sketch_mode::{ClickAnchor, Pending, SketchTool};
use crate::snap::{SnapHit, SnapKind};

mod geometry;
mod place;
mod preview;

#[cfg(test)]
mod tests;

pub(super) type PlaceResult = (Option<Sketch>, Option<Pending>);

fn add_entity(sketch: Sketch, entity: SketchEntity) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(entity)) else {
        unreachable!("AddEntity yields Entity outcome on a well-formed sketch");
    };
    (next, id)
}

pub fn add_point(sketch: Sketch, at: Point2) -> (Sketch, SketchEntityId) {
    add_entity(sketch, SketchEntity::point(at))
}

pub fn add_line(
    sketch: Sketch,
    a: SketchEntityId,
    b: SketchEntityId,
    for_construction: bool,
) -> (Sketch, SketchEntityId) {
    add_entity(sketch, SketchEntity::line(a, b, for_construction))
}

pub fn add_circle(
    sketch: Sketch,
    center: SketchEntityId,
    radius: Length,
    for_construction: bool,
) -> (Sketch, SketchEntityId) {
    add_entity(
        sketch,
        SketchEntity::circle(center, radius, for_construction),
    )
}

pub fn add_arc(
    sketch: Sketch,
    center: SketchEntityId,
    start: SketchEntityId,
    end: SketchEntityId,
    for_construction: bool,
) -> (Sketch, SketchEntityId) {
    add_entity(
        sketch,
        SketchEntity::arc(center, start, end, for_construction),
    )
}

pub fn add_ellipse(
    sketch: Sketch,
    center: SketchEntityId,
    major: SketchEntityId,
    minor: SketchEntityId,
    for_construction: bool,
) -> (Sketch, SketchEntityId) {
    add_entity(
        sketch,
        SketchEntity::ellipse(center, major, minor, for_construction),
    )
}

pub fn add_relation(sketch: Sketch, relation: SketchRelation) -> Sketch {
    match sketch.clone().apply(SketchEdit::AddRelation(relation)) {
        Ok((next, _)) => next,
        Err(e) => {
            tracing::warn!(error = %e, ?relation, "auto-relation rejected; skipping");
            sketch
        }
    }
}

pub fn click_position(sketch: &Sketch, click: ClickAnchor) -> Option<Point2> {
    match click {
        ClickAnchor::Position(p)
        | ClickAnchor::MidpointOf { position: p, .. }
        | ClickAnchor::OnEdge { position: p, .. } => Some(p),
        ClickAnchor::Endpoint(id) => geometry::endpoint_position(sketch, id),
    }
}

pub(super) fn materialize(sketch: Sketch, click: ClickAnchor) -> (Sketch, SketchEntityId) {
    match click {
        ClickAnchor::Position(p) => add_point(sketch, p),
        ClickAnchor::Endpoint(id) => (sketch, id),
        ClickAnchor::MidpointOf { line, position } => {
            let (sketch, id) = add_point(sketch, position);
            let sketch = add_relation(sketch, SketchRelation::Midpoint { point: id, line });
            (sketch, id)
        }
        ClickAnchor::OnEdge { entity, position } => {
            let (sketch, id) = add_point(sketch, position);
            let sketch = add_relation(sketch, SketchRelation::Coincident(id, entity));
            (sketch, id)
        }
    }
}

pub(super) fn anchor_at(pos: Point2, snap: Option<SnapHit>) -> ClickAnchor {
    match snap.map(|s| s.kind) {
        Some(SnapKind::Endpoint(id)) => ClickAnchor::Endpoint(id),
        Some(SnapKind::Midpoint(line)) => ClickAnchor::MidpointOf {
            line,
            position: pos,
        },
        Some(SnapKind::OnEdge(entity)) => ClickAnchor::OnEdge {
            entity,
            position: pos,
        },
        _ => ClickAnchor::Position(pos),
    }
}

pub(super) fn anchor_from(world: Point2, snap: Option<SnapHit>) -> ClickAnchor {
    anchor_at(snap.map_or(world, |s| s.world), snap)
}

pub(super) fn anchors_collide(a: ClickAnchor, b: ClickAnchor) -> bool {
    matches!((a, b), (ClickAnchor::Endpoint(x), ClickAnchor::Endpoint(y)) if x == y)
}

pub(super) fn snap_world(world: Point2, snap: Option<SnapHit>) -> Point2 {
    snap.map_or(world, |s| s.world)
}

pub fn place(
    sketch: Sketch,
    tool: SketchTool,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    match tool {
        SketchTool::Point => place::place_point(sketch, world, snap),
        SketchTool::Line => place::place_line(sketch, world, pending, snap, false),
        SketchTool::Centerline => place::place_line(sketch, world, pending, snap, true),
        SketchTool::Circle => place::place_circle(sketch, world, pending, snap),
        SketchTool::PerimeterCircle => place::place_perimeter_circle(sketch, world, pending, snap),
        SketchTool::Ellipse => place::place_ellipse(sketch, world, pending, snap),
        SketchTool::CenterpointArc => place::place_centerpoint_arc(sketch, world, pending, snap),
        SketchTool::TangentArc => place::place_tangent_arc(sketch, world, pending, snap),
        SketchTool::ThreePointArc => place::place_three_point_arc(sketch, world, pending, snap),
        SketchTool::CornerRectangle => place::place_corner_rectangle(sketch, world, pending, snap),
        SketchTool::CenterRectangle => place::place_center_rectangle(sketch, world, pending, snap),
        SketchTool::ThreePointCornerRectangle => {
            place::place_three_point_corner_rectangle(sketch, world, pending, snap)
        }
        SketchTool::ThreePointCenterRectangle => {
            place::place_three_point_center_rectangle(sketch, world, pending, snap)
        }
        SketchTool::Parallelogram => place::place_parallelogram(sketch, world, pending, snap),
        SketchTool::Spline => (None, pending),
    }
}

#[must_use]
pub fn preview_anchors_only(sketch: &Sketch, pending: Option<Pending>) -> SketchPreview {
    let anchors: Vec<Point2> = match pending {
        None => Vec::new(),
        Some(Pending::First(a)) => click_position(sketch, a).into_iter().collect(),
        Some(Pending::Second(a, b)) => [a, b]
            .into_iter()
            .filter_map(|c| click_position(sketch, c))
            .collect(),
    };
    SketchPreview {
        anchors,
        ..SketchPreview::empty()
    }
}

#[must_use]
pub fn preview(
    sketch: &Sketch,
    tool: SketchTool,
    cursor: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> SketchPreview {
    preview::preview(sketch, tool, cursor, pending, snap)
}
