use bone_document::{Sketch, SketchRelation};
use bone_types::{Length, Point2};
use uom::si::length::millimeter;

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

use super::geometry::{
    axes_distinct, ccw_through, circumcircle, degenerate_triangle, distance, minor_arc_ccw,
    on_circle_endpoint, project_to_circle, tangent_arc_ccw_natural, tangent_arc_center,
    tangent_at_point, three_point_center_corners, three_point_corner_extents,
};
use super::{
    PlaceResult, add_arc, add_circle, add_line, add_point, add_relation, anchor_at, anchor_from,
    anchors_collide, click_position, materialize, snap_world,
};

pub(super) fn dispatch_two<F>(
    pending: Option<Pending>,
    world: Point2,
    snap: Option<SnapHit>,
    commit: F,
) -> PlaceResult
where
    F: FnOnce(ClickAnchor) -> PlaceResult,
{
    match pending {
        None => (None, Some(Pending::First(anchor_from(world, snap)))),
        Some(Pending::First(a)) => commit(a),
        Some(Pending::Second(_, _)) => (None, pending),
    }
}

pub(super) fn dispatch_three<F>(
    pending: Option<Pending>,
    world: Point2,
    snap: Option<SnapHit>,
    commit: F,
) -> PlaceResult
where
    F: FnOnce(ClickAnchor, ClickAnchor) -> PlaceResult,
{
    match pending {
        None => (None, Some(Pending::First(anchor_from(world, snap)))),
        Some(Pending::First(a)) => (None, Some(Pending::Second(a, anchor_from(world, snap)))),
        Some(Pending::Second(a, b)) => commit(a, b),
    }
}

pub(super) fn place_point(sketch: Sketch, world: Point2, snap: Option<SnapHit>) -> PlaceResult {
    if matches!(snap.map(|s| s.kind), Some(SnapKind::Endpoint(_))) {
        return (None, None);
    }
    let (next, _) = add_point(sketch, snap_world(world, snap));
    (Some(next), None)
}

pub(super) fn place_line(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_two(pending, world, snap, move |prev| {
        commit_line(sketch, world, prev, snap)
    })
}

fn commit_line(
    sketch: Sketch,
    world: Point2,
    prev: ClickAnchor,
    snap: Option<SnapHit>,
) -> PlaceResult {
    if click_position(&sketch, prev).is_none() {
        return (None, None);
    }
    let end_anchor = anchor_from(world, snap);
    if anchors_collide(prev, end_anchor) {
        return (None, Some(Pending::First(prev)));
    }
    let (sketch, prev_id) = materialize(sketch, prev);
    let (sketch, end_id) = materialize(sketch, end_anchor);
    let (sketch, line_id) = add_line(sketch, prev_id, end_id, false);
    let sketch = match snap.map(|s| s.kind) {
        Some(SnapKind::Tangent(curve)) => {
            add_relation(sketch, SketchRelation::Tangent(line_id, curve))
        }
        Some(SnapKind::Horizontal) => add_relation(sketch, SketchRelation::Horizontal(line_id)),
        Some(SnapKind::Vertical) => add_relation(sketch, SketchRelation::Vertical(line_id)),
        Some(SnapKind::Midpoint(_) | SnapKind::Endpoint(_)) | None => sketch,
    };
    (
        Some(sketch),
        Some(Pending::First(ClickAnchor::Endpoint(end_id))),
    )
}

pub(super) fn place_circle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_two(pending, world, snap, move |center_anchor| {
        let Some(center_pos) = click_position(&sketch, center_anchor) else {
            return (None, None);
        };
        let radius_mm = distance(center_pos, snap_world(world, snap));
        if !(radius_mm.is_finite() && radius_mm > 0.0) {
            return (None, Some(Pending::First(center_anchor)));
        }
        let (sketch, center_id) = materialize(sketch, center_anchor);
        let (sketch, _) = add_circle(
            sketch,
            center_id,
            Length::new::<millimeter>(radius_mm),
            false,
        );
        (Some(sketch), None)
    })
}

pub(super) fn place_perimeter_circle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |a1, a2| {
        let Some(p1) = click_position(&sketch, a1) else {
            return (None, None);
        };
        let Some(p2) = click_position(&sketch, a2) else {
            return (None, None);
        };
        let Some((center, radius_mm)) = circumcircle(p1, p2, snap_world(world, snap)) else {
            return (None, Some(Pending::Second(a1, a2)));
        };
        let (sketch, center_id) = add_point(sketch, center);
        let (sketch, _) = add_circle(
            sketch,
            center_id,
            Length::new::<millimeter>(radius_mm),
            false,
        );
        (Some(sketch), None)
    })
}

pub(super) fn place_centerpoint_arc(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |center, start| {
        let Some(center_pos) = click_position(&sketch, center) else {
            return (None, None);
        };
        let Some(start_pos) = click_position(&sketch, start) else {
            return (None, None);
        };
        let radius_mm = distance(center_pos, start_pos);
        if !(radius_mm.is_finite() && radius_mm > 0.0) {
            return (None, Some(Pending::Second(center, start)));
        }
        let end_pos = project_to_circle(center_pos, radius_mm, snap_world(world, snap));
        let end_anchor = on_circle_endpoint(snap, center_pos, radius_mm)
            .map_or(ClickAnchor::Position(end_pos), ClickAnchor::Endpoint);
        if anchors_collide(center, start)
            || anchors_collide(center, end_anchor)
            || anchors_collide(start, end_anchor)
            || distance(start_pos, end_pos) < 1e-9
        {
            return (None, Some(Pending::Second(center, start)));
        }
        let (sketch, center_id) = materialize(sketch, center);
        let (sketch, start_id) = materialize(sketch, start);
        let (sketch, end_id) = materialize(sketch, end_anchor);
        let (start_arc, end_arc) = if minor_arc_ccw(center_pos, start_pos, end_pos) {
            (start_id, end_id)
        } else {
            (end_id, start_id)
        };
        let (sketch, _) = add_arc(sketch, center_id, start_arc, end_arc, false);
        (Some(sketch), None)
    })
}

pub(super) fn place_tangent_arc(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    match pending {
        None => match snap.map(|s| s.kind) {
            Some(SnapKind::Endpoint(id)) if tangent_at_point(&sketch, id).is_some() => {
                (None, Some(Pending::First(ClickAnchor::Endpoint(id))))
            }
            _ => (None, None),
        },
        Some(Pending::First(start)) => commit_tangent_arc(sketch, world, start, snap),
        Some(Pending::Second(_, _)) => (None, pending),
    }
}

fn commit_tangent_arc(
    sketch: Sketch,
    world: Point2,
    start: ClickAnchor,
    snap: Option<SnapHit>,
) -> PlaceResult {
    let ClickAnchor::Endpoint(start_id) = start else {
        return (None, None);
    };
    let Some(start_pos) = click_position(&sketch, start) else {
        return (None, None);
    };
    let Some((host, tangent)) = tangent_at_point(&sketch, start_id) else {
        return (None, Some(Pending::First(start)));
    };
    let end_pos = snap_world(world, snap);
    let Some(center_pos) = tangent_arc_center(start_pos, end_pos, tangent) else {
        return (None, Some(Pending::First(start)));
    };
    let end_anchor = anchor_at(end_pos, snap);
    if anchors_collide(start, end_anchor) {
        return (None, Some(Pending::First(start)));
    }
    let (sketch, end_id) = materialize(sketch, end_anchor);
    let (sketch, center_id) = add_point(sketch, center_pos);
    let (start_arc, end_arc) = if tangent_arc_ccw_natural(start_pos, center_pos, tangent) {
        (start_id, end_id)
    } else {
        (end_id, start_id)
    };
    let (sketch, arc_id) = add_arc(sketch, center_id, start_arc, end_arc, false);
    let sketch = add_relation(sketch, SketchRelation::Tangent(arc_id, host));
    (Some(sketch), None)
}

pub(super) fn place_three_point_arc(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |start, end| {
        let Some(start_pos) = click_position(&sketch, start) else {
            return (None, None);
        };
        let Some(end_pos) = click_position(&sketch, end) else {
            return (None, None);
        };
        let mid_pos = snap_world(world, snap);
        let Some((center_pos, _)) = circumcircle(start_pos, end_pos, mid_pos) else {
            return (None, Some(Pending::Second(start, end)));
        };
        let (start, end) = if ccw_through(center_pos, start_pos, end_pos, mid_pos) {
            (start, end)
        } else {
            (end, start)
        };
        let (sketch, center_id) = add_point(sketch, center_pos);
        let (sketch, start_id) = materialize(sketch, start);
        let (sketch, end_id) = materialize(sketch, end);
        let (sketch, _) = add_arc(sketch, center_id, start_id, end_id, false);
        (Some(sketch), None)
    })
}

pub(super) fn place_corner_rectangle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_two(pending, world, snap, move |corner| {
        let Some(corner_pos) = click_position(&sketch, corner) else {
            return (None, None);
        };
        let opposite = anchor_from(world, snap);
        let Some(opposite_pos) = click_position(&sketch, opposite) else {
            return (None, Some(Pending::First(corner)));
        };
        let (cx, cy) = corner_pos.coords_mm();
        let (ox, oy) = opposite_pos.coords_mm();
        if !axes_distinct((cx, cy), (ox, oy)) || anchors_collide(corner, opposite) {
            return (None, Some(Pending::First(corner)));
        }
        let (sketch, p1_id) = materialize(sketch, corner);
        let (sketch, p2_id) = add_point(sketch, Point2::from_mm(ox, cy));
        let (sketch, p3_id) = materialize(sketch, opposite);
        let (sketch, p4_id) = add_point(sketch, Point2::from_mm(cx, oy));
        let (sketch, bottom) = add_line(sketch, p1_id, p2_id, false);
        let (sketch, right) = add_line(sketch, p2_id, p3_id, false);
        let (sketch, top) = add_line(sketch, p3_id, p4_id, false);
        let (sketch, left) = add_line(sketch, p4_id, p1_id, false);
        let sketch = [
            SketchRelation::Horizontal(bottom),
            SketchRelation::Horizontal(top),
            SketchRelation::Vertical(right),
            SketchRelation::Vertical(left),
        ]
        .into_iter()
        .fold(sketch, add_relation);
        (Some(sketch), None)
    })
}

pub(super) fn place_center_rectangle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_two(pending, world, snap, move |center| {
        let Some(center_pos) = click_position(&sketch, center) else {
            return (None, None);
        };
        let corner = anchor_from(world, snap);
        let Some(corner_pos) = click_position(&sketch, corner) else {
            return (None, Some(Pending::First(center)));
        };
        let (cx, cy) = center_pos.coords_mm();
        let (ox, oy) = corner_pos.coords_mm();
        if !axes_distinct((cx, cy), (ox, oy)) || anchors_collide(center, corner) {
            return (None, Some(Pending::First(center)));
        }
        let dx = ox - cx;
        let dy = oy - cy;
        let (sketch, center_id) = materialize(sketch, center);
        let (sketch, p1_id) = materialize(sketch, corner);
        let (sketch, p2_id) = add_point(sketch, Point2::from_mm(cx - dx, cy + dy));
        let (sketch, p3_id) = add_point(sketch, Point2::from_mm(cx - dx, cy - dy));
        let (sketch, p4_id) = add_point(sketch, Point2::from_mm(cx + dx, cy - dy));
        let (sketch, top) = add_line(sketch, p1_id, p2_id, false);
        let (sketch, left) = add_line(sketch, p2_id, p3_id, false);
        let (sketch, bottom) = add_line(sketch, p3_id, p4_id, false);
        let (sketch, right) = add_line(sketch, p4_id, p1_id, false);
        let (sketch, diag1) = add_line(sketch, p1_id, p3_id, true);
        let (sketch, diag2) = add_line(sketch, p2_id, p4_id, true);
        let sketch = [
            SketchRelation::Horizontal(top),
            SketchRelation::Horizontal(bottom),
            SketchRelation::Vertical(left),
            SketchRelation::Vertical(right),
            SketchRelation::Midpoint {
                point: center_id,
                line: diag1,
            },
            SketchRelation::Midpoint {
                point: center_id,
                line: diag2,
            },
        ]
        .into_iter()
        .fold(sketch, add_relation);
        (Some(sketch), None)
    })
}

pub(super) fn place_three_point_corner_rectangle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |c1, c2| {
        let Some(p1) = click_position(&sketch, c1) else {
            return (None, None);
        };
        let Some(p2) = click_position(&sketch, c2) else {
            return (None, None);
        };
        let Some((p3_pos, p4_pos)) = three_point_corner_extents(p1, p2, snap_world(world, snap))
        else {
            return (None, Some(Pending::Second(c1, c2)));
        };
        let (sketch, p1_id) = materialize(sketch, c1);
        let (sketch, p2_id) = materialize(sketch, c2);
        let (sketch, p3_id) = add_point(sketch, p3_pos);
        let (sketch, p4_id) = add_point(sketch, p4_pos);
        let (sketch, base) = add_line(sketch, p1_id, p2_id, false);
        let (sketch, side1) = add_line(sketch, p2_id, p3_id, false);
        let (sketch, top) = add_line(sketch, p3_id, p4_id, false);
        let (sketch, side2) = add_line(sketch, p4_id, p1_id, false);
        let sketch = [
            SketchRelation::Parallel(base, top),
            SketchRelation::Parallel(side1, side2),
            SketchRelation::Perpendicular(base, side1),
        ]
        .into_iter()
        .fold(sketch, add_relation);
        (Some(sketch), None)
    })
}

pub(super) fn place_three_point_center_rectangle(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |center, mid| {
        let Some(center_pos) = click_position(&sketch, center) else {
            return (None, None);
        };
        let Some(mid_pos) = click_position(&sketch, mid) else {
            return (None, None);
        };
        let Some((p1_pos, p2_pos, p3_pos, p4_pos)) =
            three_point_center_corners(center_pos, mid_pos, snap_world(world, snap))
        else {
            return (None, Some(Pending::Second(center, mid)));
        };
        let (sketch, center_id) = materialize(sketch, center);
        let (sketch, mid_id) = materialize(sketch, mid);
        let (sketch, p1_id) = add_point(sketch, p1_pos);
        let (sketch, p2_id) = add_point(sketch, p2_pos);
        let (sketch, p3_id) = add_point(sketch, p3_pos);
        let (sketch, p4_id) = add_point(sketch, p4_pos);
        let (sketch, edge1) = add_line(sketch, p1_id, p2_id, false);
        let (sketch, edge2) = add_line(sketch, p2_id, p3_id, false);
        let (sketch, _edge3) = add_line(sketch, p3_id, p4_id, false);
        let (sketch, edge4) = add_line(sketch, p4_id, p1_id, false);
        let (sketch, diag1) = add_line(sketch, p1_id, p3_id, true);
        let (sketch, diag2) = add_line(sketch, p2_id, p4_id, true);
        let sketch = [
            SketchRelation::Perpendicular(edge1, edge2),
            SketchRelation::Midpoint {
                point: center_id,
                line: diag1,
            },
            SketchRelation::Midpoint {
                point: center_id,
                line: diag2,
            },
            SketchRelation::Midpoint {
                point: mid_id,
                line: edge4,
            },
        ]
        .into_iter()
        .fold(sketch, add_relation);
        (Some(sketch), None)
    })
}

pub(super) fn place_parallelogram(
    sketch: Sketch,
    world: Point2,
    pending: Option<Pending>,
    snap: Option<SnapHit>,
) -> PlaceResult {
    dispatch_three(pending, world, snap, move |c1, c2| {
        let Some(p1) = click_position(&sketch, c1) else {
            return (None, None);
        };
        let Some(p2) = click_position(&sketch, c2) else {
            return (None, None);
        };
        let c3 = anchor_from(world, snap);
        let Some(p3) = click_position(&sketch, c3) else {
            return (None, Some(Pending::Second(c1, c2)));
        };
        if degenerate_triangle(p1, p2, p3)
            || anchors_collide(c1, c2)
            || anchors_collide(c1, c3)
            || anchors_collide(c2, c3)
        {
            return (None, Some(Pending::Second(c1, c2)));
        }
        let (p1x, p1y) = p1.coords_mm();
        let (p2x, p2y) = p2.coords_mm();
        let (p3x, p3y) = p3.coords_mm();
        let p4_pos = Point2::from_mm(p1x + (p3x - p2x), p1y + (p3y - p2y));
        let (sketch, p1_id) = materialize(sketch, c1);
        let (sketch, p2_id) = materialize(sketch, c2);
        let (sketch, p3_id) = materialize(sketch, c3);
        let (sketch, p4_id) = add_point(sketch, p4_pos);
        let (sketch, edge_a) = add_line(sketch, p1_id, p2_id, false);
        let (sketch, edge_b) = add_line(sketch, p2_id, p3_id, false);
        let (sketch, edge_c) = add_line(sketch, p3_id, p4_id, false);
        let (sketch, edge_d) = add_line(sketch, p4_id, p1_id, false);
        let sketch = [
            SketchRelation::Parallel(edge_a, edge_c),
            SketchRelation::Parallel(edge_b, edge_d),
        ]
        .into_iter()
        .fold(sketch, add_relation);
        (Some(sketch), None)
    })
}
