// LENGTHEN command — extend or trim a Line or Arc by a specified delta or total.
//
// Options (entered as text after the entity pick):
//   DE <value>   — extend by delta (positive extends, negative trims)
//   TO <value>   — set total length (Line) or arc length (Arc)
//   P <pct>      — change by percentage (100 = no change, 150 = +50%)
//
// The entity is modified at whichever end is closest to the pick point.

use crate::modules::draw::modify::spline_ops::{bspline_to_spline, spline_to_bspline};
use acadrust::entities::{
    Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::{DVec3, Vec3};
use truck_modeling::base::{BoundedCurve, Cut};

use crate::command::{CadCommand, CmdResult};

pub struct LengthenCommand {
    state: LenState,
}

enum LenState {
    PickEntity,
    PickOption { handle: Handle, pick_pt: Vec3 },
}

impl LengthenCommand {
    pub fn new() -> Self {
        Self {
            state: LenState::PickEntity,
        }
    }
}

impl CadCommand for LengthenCommand {
    fn name(&self) -> &'static str {
        "LENGTHEN"
    }

    fn prompt(&self) -> String {
        match &self.state {
            LenState::PickEntity => "LENGTHEN  Select object:".into(),
            LenState::PickOption { .. } => {
                "LENGTHEN  Enter option [DE <delta> / TO <total> / P <pct>]:".into()
            }
        }
    }

    fn needs_entity_pick(&self) -> bool {
        matches!(self.state, LenState::PickEntity)
    }

    fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
        if handle.is_null() {
            return CmdResult::NeedPoint;
        }
        self.state = LenState::PickOption {
            handle,
            pick_pt: pt,
        };
        CmdResult::NeedPoint
    }

    fn wants_text_input(&self) -> bool {
        matches!(self.state, LenState::PickOption { .. })
    }

    fn dyn_field(&self) -> crate::command::DynField {
        if matches!(self.state, LenState::PickOption { .. }) {
            crate::command::DynField::Scalar
        } else {
            crate::command::DynField::Point
        }
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        let (handle, pick_pt) = match &self.state {
            LenState::PickOption { handle, pick_pt } => (*handle, *pick_pt),
            _ => return None,
        };
        let pick_pt = pick_pt.as_dvec3();

        let text = text.trim().to_uppercase();
        if let Some(rest) = text.strip_prefix("DE ").or_else(|| text.strip_prefix("DE")) {
            let delta: f64 = rest.trim().replace(',', ".").parse().ok()?;
            Some(CmdResult::LengthenEntity {
                handle,
                pick_pt,
                mode: LenMode::Delta(delta),
            })
        } else if let Some(rest) = text.strip_prefix("TO ").or_else(|| text.strip_prefix("TO")) {
            let total: f64 = rest
                .trim()
                .replace(',', ".")
                .parse()
                .ok()
                .filter(|&v: &f64| v > 0.0)?;
            Some(CmdResult::LengthenEntity {
                handle,
                pick_pt,
                mode: LenMode::Total(total),
            })
        } else if let Some(rest) = text.strip_prefix("P ").or_else(|| text.strip_prefix("P")) {
            let pct: f64 = rest
                .trim()
                .replace(',', ".")
                .parse()
                .ok()
                .filter(|&v: &f64| v > 0.0)?;
            Some(CmdResult::LengthenEntity {
                handle,
                pick_pt,
                mode: LenMode::Percent(pct),
            })
        } else {
            // Try plain number as delta
            let delta: f64 = text.replace(',', ".").parse().ok()?;
            Some(CmdResult::LengthenEntity {
                handle,
                pick_pt,
                mode: LenMode::Delta(delta),
            })
        }
    }

    fn on_point(&mut self, _pt: DVec3) -> CmdResult {
        CmdResult::NeedPoint
    }
    fn on_enter(&mut self) -> CmdResult {
        CmdResult::Cancel
    }
}

// ── Mode enum (also used in CmdResult) ────────────────────────────────────

#[derive(Clone)]
pub enum LenMode {
    Delta(f64),
    Total(f64),
    Percent(f64),
}

// ── Geometry ───────────────────────────────────────────────────────────────

/// Apply LENGTHEN to a Line, Arc, Ellipse, or Spline.
/// `pick_pt` determines which end to extend/trim (closest end is modified).
pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    match entity {
        EntityType::Line(l) => lengthen_line(l, pick_pt, mode),
        EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode),
        EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode),
        EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode),
        EntityType::LwPolyline(p) => lengthen_lwpoly(p, pick_pt, mode),
        _ => None,
    }
}

fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    // Keep the line's own coordinates on the f64 grid; only pick_pt (screen-only,
    // nearest-end selection) widens for the distance compare.
    let sx = line.start.x;
    let sy = line.start.y;
    let ex = line.end.x;
    let ey = line.end.y;

    let dx = ex - sx;
    let dy = ey - sy;
    let current_len = (dx * dx + dy * dy).sqrt();
    if current_len < 1e-10 {
        return None;
    }

    let new_len = apply_mode(current_len, mode)?;
    if new_len < 1e-10 {
        return None;
    }

    let ux = dx / current_len;
    let uy = dy / current_len;

    // Which end is closer to pick?
    let px = pick_pt.x as f64;
    let py = pick_pt.y as f64;
    let dist_to_start = (px - sx).hypot(py - sy);
    let dist_to_end = (px - ex).hypot(py - ey);

    let mut result = line.clone();
    result.common.handle = Handle::NULL;

    if dist_to_end <= dist_to_start {
        // Extend/trim the end
        let new_x = sx + ux * new_len;
        let new_y = sy + uy * new_len;
        result.end = Vector3::new(new_x, new_y, line.end.z);
    } else {
        // Extend/trim the start (move start backward along dir)
        let new_x = ex - ux * new_len;
        let new_y = ey - uy * new_len;
        result.start = Vector3::new(new_x, new_y, line.start.z);
    }
    Some(EntityType::Line(result))
}

fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    let cx = arc.center.x as f32;
    let cy = arc.center.y as f32;

    // Current arc span
    let span = arc_span_rad(arc.start_angle, arc.end_angle);
    let current_arc_len = arc.radius * span;

    let new_arc_len = apply_mode(current_arc_len, mode)?;
    if new_arc_len < 1e-10 {
        return None;
    }
    let new_span = new_arc_len / arc.radius;

    // Which end (start or end angle) is closer to pick?
    let start_rad = arc.start_angle;
    let end_rad = arc.end_angle;

    let start_pt = Vec3::new(
        cx + arc.radius as f32 * start_rad.cos() as f32,
        pick_pt.y,
        cy + arc.radius as f32 * start_rad.sin() as f32,
    );
    let end_pt = Vec3::new(
        cx + arc.radius as f32 * end_rad.cos() as f32,
        pick_pt.y,
        cy + arc.radius as f32 * end_rad.sin() as f32,
    );
    let dist_start = (pick_pt - start_pt).length();
    let dist_end = (pick_pt - end_pt).length();

    let delta_span = new_span - span;

    let mut result = arc.clone();
    result.common.handle = Handle::NULL;

    if dist_end <= dist_start {
        // Extend end angle
        result.end_angle = arc.start_angle + new_span;
    } else {
        // Extend start angle (move start backwards)
        result.start_angle = arc.end_angle - new_span;
    }
    let _ = delta_span;
    Some(EntityType::Arc(result))
}

fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt();
    if a < 1e-9 {
        return None;
    }
    let b = a * ell.minor_axis_ratio;
    let nx = ell.major_axis.x / a;
    let ny = ell.major_axis.y / a;

    let t0 = ell.start_parameter;
    let mut t1 = ell.end_parameter;
    if t1 <= t0 {
        t1 += std::f64::consts::TAU;
    }
    let span = t1 - t0;

    // Approximate arc length via 128-point Gaussian quadrature estimate.
    let arc_len_approx = |span: f64| -> f64 {
        let n = 128usize;
        let mut len = 0.0;
        for i in 0..n {
            let ti = t0 + span * (i as f64 / n as f64);
            let tip = t0 + span * ((i + 1) as f64 / n as f64);
            let xi = a * ti.cos() * nx - b * ti.sin() * ny + ell.center.x;
            let yi = a * ti.cos() * ny + b * ti.sin() * nx + ell.center.y;
            let xip = a * tip.cos() * nx - b * tip.sin() * ny + ell.center.x;
            let yip = a * tip.cos() * ny + b * tip.sin() * nx + ell.center.y;
            len += (xip - xi).hypot(yip - yi);
        }
        len
    };

    let current_len = arc_len_approx(span);
    if current_len < 1e-10 {
        return None;
    }

    let new_len = apply_mode(current_len, mode)?;
    if new_len < 1e-10 {
        return None;
    }

    // Find the new span via bisection so that arc_len_approx(new_span) ≈ new_len.
    let max_span = std::f64::consts::TAU;
    let mut lo = 0.0f64;
    let mut hi = max_span;
    for _ in 0..40 {
        let mid = (lo + hi) * 0.5;
        if arc_len_approx(mid) < new_len {
            lo = mid;
        } else {
            hi = mid;
        }
    }
    let new_span = (lo + hi) * 0.5;

    // Determine which end is closer to pick_pt (use DXF XY plane).
    let p_x = pick_pt.x as f64;
    let p_y = pick_pt.y as f64;
    let pt_start_x = ell.center.x + a * t0.cos() * nx - b * t0.sin() * ny;
    let pt_start_y = ell.center.y + a * t0.cos() * ny + b * t0.sin() * nx;
    let pt_end_x = ell.center.x + a * t1.cos() * nx - b * t1.sin() * ny;
    let pt_end_y = ell.center.y + a * t1.cos() * ny + b * t1.sin() * nx;
    let dist_start = (p_x - pt_start_x).hypot(p_y - pt_start_y);
    let dist_end = (p_x - pt_end_x).hypot(p_y - pt_end_y);

    let mut result = ell.clone();
    result.common.handle = Handle::NULL;

    if dist_end <= dist_start {
        result.end_parameter = t0 + new_span;
    } else {
        result.start_parameter = t1 - new_span;
    }
    Some(EntityType::Ellipse(result))
}

fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
    match mode {
        LenMode::Delta(d) => Some(current + d),
        LenMode::Total(t) => Some(*t),
        LenMode::Percent(p) => Some(current * p / 100.0),
    }
}

fn arc_span_rad(start: f64, end: f64) -> f64 {
    let span = (end - start).rem_euclid(std::f64::consts::TAU);
    if span < 1e-6 {
        std::f64::consts::TAU
    } else {
        span
    }
}

fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    let n = poly.vertices.len();
    if n < 2 {
        return None;
    }

    // Determine which end is closer to the pick point (DXF XY: pick_pt.x, pick_pt.z).
    let px = pick_pt.x as f64;
    let py = pick_pt.y as f64;

    let first = &poly.vertices[0];
    let last = &poly.vertices[n - 1];
    let d_first = (first.location.x - px).hypot(first.location.y - py);
    let d_last = (last.location.x - px).hypot(last.location.y - py);
    let at_end = d_last <= d_first;

    // Terminal segment direction and current length.
    let (sx, sy, ex, ey) = if at_end {
        (
            poly.vertices[n - 2].location.x,
            poly.vertices[n - 2].location.y,
            last.location.x,
            last.location.y,
        )
    } else {
        (
            poly.vertices[1].location.x,
            poly.vertices[1].location.y,
            first.location.x,
            first.location.y,
        )
    };

    let dx = ex - sx;
    let dy = ey - sy;
    let current_len = (dx * dx + dy * dy).sqrt();
    if current_len < 1e-10 {
        return None;
    }

    let new_len = apply_mode(current_len, mode)?;
    if new_len < 1e-10 {
        return None;
    }

    let ux = dx / current_len;
    let uy = dy / current_len;
    let new_x = sx + ux * new_len;
    let new_y = sy + uy * new_len;

    let mut new_poly = poly.clone();
    new_poly.common.handle = Handle::NULL;
    if at_end {
        let v = new_poly.vertices.last_mut()?;
        v.location.x = new_x;
        v.location.y = new_y;
    } else {
        let v = new_poly.vertices.first_mut()?;
        v.location.x = new_x;
        v.location.y = new_y;
    }
    Some(EntityType::LwPolyline(new_poly))
}

fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
    let bs = spline_to_bspline(spl)?;
    let (t0, t1) = bs.range_tuple();
    if (t1 - t0).abs() < 1e-12 {
        return None;
    }

    // Approximate arc length via 64-point numerical integration.
    use truck_modeling::base::ParametricCurve;
    let arc_len = {
        let n = 64usize;
        let mut len = 0.0f64;
        for i in 0..n {
            let ta = t0 + (t1 - t0) * (i as f64 / n as f64);
            let tb = t0 + (t1 - t0) * ((i + 1) as f64 / n as f64);
            let pa = bs.subs(ta);
            let pb = bs.subs(tb);
            len += (pb.x - pa.x).hypot(pb.y - pa.y);
        }
        len
    };
    if arc_len < 1e-10 {
        return None;
    }

    let new_len = apply_mode(arc_len, mode)?;
    if new_len < 1e-10 {
        return None;
    }

    // Determine which end (start or end) is closer to pick_pt.
    let p_start = bs.subs(t0);
    let p_end = bs.subs(t1);
    let dist_start = (p_start.x - pick_pt.x as f64).hypot(p_start.y - pick_pt.y as f64);
    let dist_end = (p_end.x - pick_pt.x as f64).hypot(p_end.y - pick_pt.y as f64);
    let extend_end = dist_end <= dist_start;

    // Find the parameter `t_new` such that the arc length from the fixed end to t_new = new_len.
    // Use bisection on cumulative arc length.
    let fixed_t = if extend_end { t0 } else { t1 };
    let delta_ratio = new_len / arc_len;

    // Find t_new via bisection: cumulative_len(fixed_t..t_new) = new_len.
    let cum_len = |t_end_param: f64| -> f64 {
        let (lo, hi) = if extend_end {
            (t0, t_end_param)
        } else {
            (t_end_param, t1)
        };
        if hi <= lo {
            return 0.0;
        }
        let n = 32usize;
        let mut len = 0.0f64;
        for i in 0..n {
            let ta = lo + (hi - lo) * (i as f64 / n as f64);
            let tb = lo + (hi - lo) * ((i + 1) as f64 / n as f64);
            let pa = bs.subs(ta);
            let pb = bs.subs(tb);
            len += (pb.x - pa.x).hypot(pb.y - pa.y);
        }
        len
    };

    let (mut lo_t, mut hi_t) = if extend_end {
        (t0, t0 + delta_ratio * (t1 - t0) * 2.0)
    } else {
        (t1 - delta_ratio * (t1 - t0) * 2.0, t1)
    };
    // Clamp to valid range with some buffer for extension.
    let buf = (t1 - t0) * 0.5;
    lo_t = lo_t.max(t0 - buf);
    hi_t = hi_t.min(t1 + buf);

    for _ in 0..40 {
        let mid = (lo_t + hi_t) * 0.5;
        if cum_len(mid) < new_len {
            if extend_end {
                hi_t = mid;
            } else {
                lo_t = mid;
            }
        } else {
            if extend_end {
                lo_t = mid;
            } else {
                hi_t = mid;
            }
        }
    }
    let t_new = (lo_t + hi_t) * 0.5;
    let _ = fixed_t;

    // Split the spline at t_new.
    let mut piece = bs.clone();
    if extend_end {
        // Keep [t0, t_new]: cut at t_new
        let _right = piece.cut(t_new.clamp(t0 + 1e-10, t1 * 2.0));
        Some(EntityType::Spline(bspline_to_spline(&piece, spl)))
    } else {
        // Keep [t_new, t1]: cut at t_new, return the right portion
        let right = piece.cut(t_new.clamp(t0 * 0.5, t1 - 1e-10));
        Some(EntityType::Spline(bspline_to_spline(&right, spl)))
    }
}


// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] });  // LengthenCommand
