// Array dropdown — ribbon definition + interactive commands.
//
// ARRAYRECT (AR):
//   Rectangular array: row/column counts and spacing collected via text input.
//   1. Row count → 2. Column count → 3. Row spacing → 4. Column spacing
//   → Returns BatchCopy with a grid of Translate transforms.
//
// ARRAYPATH:
//   Path array: copies placed at equal intervals along a curve/line.
//   (pending geometry engine support — stub)
//
// ARRAYPOLAR:
//   Polar array: copies rotated around a center point by a total angle.
//   1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)

use acadrust::Handle;
use glam::DVec3;
use crate::t;

use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
use crate::modules::draw::defaults;
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;

// ── Dropdown constants ─────────────────────────────────────────────────────

pub const DROPDOWN_ID: &str = "array_type";
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg"));

pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
    (
        "ARRAYRECT",
        "Rectangular Array",
        IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg")),
    ),
    (
        "ARRAYPATH",
        "Path Array",
        IconKind::Svg(include_bytes!("../../../../assets/icons/array_path.svg")),
    ),
    (
        "ARRAYPOLAR",
        "Polar Array",
        IconKind::Svg(include_bytes!("../../../../assets/icons/array_polar.svg")),
    ),
];

// ── Rectangular Array ──────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy)]
enum RectStep {
    Rows,
    Cols { rows: u32 },
    RowSp { rows: u32, cols: u32 },
    ColSp { rows: u32, cols: u32, row_sp: f64 },
}

pub struct ArrayRectCommand {
    handles: Vec<Handle>,
    wire_models: Vec<WireModel>,
    step: RectStep,
    default_rows: u32,
    default_cols: u32,
    default_row_sp: f64,
    default_col_sp: f64,
    plane: WorkingPlane,
}

impl ArrayRectCommand {
    pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
        Self {
            handles,
            wire_models,
            step: RectStep::Rows,
            default_rows: defaults::get_array_rows() as u32,
            default_cols: defaults::get_array_cols() as u32,
            default_row_sp: defaults::get_array_row_sp(),
            default_col_sp: defaults::get_array_col_sp(),
            plane: WorkingPlane::default(),
        }
    }

    /// Row/column offsets run along the active coordinate axes.
    fn build_transforms(
        rows: u32,
        cols: u32,
        row_sp: f64,
        col_sp: f64,
        plane: WorkingPlane,
    ) -> Vec<EntityTransform> {
        let mut t = Vec::new();
        for r in 0..rows {
            for c in 0..cols {
                if r == 0 && c == 0 {
                    continue;
                }
                t.push(EntityTransform::Translate(plane.vector_to_world(DVec3::new(
                    col_sp * c as f64,
                    row_sp * r as f64,
                    0.0,
                ))));
            }
        }
        t
    }
}

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

    fn set_working_plane(&mut self, plane: WorkingPlane) {
        self.plane = plane;
    }

    fn prompt(&self) -> String {
        match self.step {
            RectStep::Rows => {
                crate::tf!("ARRAYRECT  Enter row count <{}>:", self.default_rows).into_owned()
            }
            RectStep::Cols { rows } => crate::tf!(
                "ARRAYRECT  Enter column count <{}>  [{rows} rows]:",
                self.default_cols
            )
            .into_owned(),
            RectStep::RowSp { rows, cols } => crate::tf!(
                "ARRAYRECT  Row spacing <{:.0}>  [{rows}×{cols}]:",
                self.default_row_sp
            )
            .into_owned(),
            RectStep::ColSp { rows, cols, row_sp } => crate::tf!(
                "ARRAYRECT  Column spacing <{:.0}>  [{rows}×{cols}, row={row_sp:.0}]:",
                self.default_col_sp
            )
            .into_owned(),
        }
    }

    fn wants_text_input(&self) -> bool {
        true
    }

    fn dyn_field(&self) -> crate::command::DynField {
        crate::command::DynField::Scalar
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        let t = text.trim().replace(',', ".");
        let t = t.as_str();
        match self.step {
            RectStep::Rows => {
                let rows = if t.is_empty() {
                    self.default_rows
                } else {
                    let v = t.parse::<u32>().unwrap_or(self.default_rows).max(1);
                    defaults::set_array_rows(v as f64);
                    self.default_rows = v;
                    v
                };
                self.step = RectStep::Cols { rows };
                None
            }
            RectStep::Cols { rows } => {
                let cols = if t.is_empty() {
                    self.default_cols
                } else {
                    let v = t.parse::<u32>().unwrap_or(self.default_cols).max(1);
                    defaults::set_array_cols(v as f64);
                    self.default_cols = v;
                    v
                };
                self.step = RectStep::RowSp { rows, cols };
                None
            }
            RectStep::RowSp { rows, cols } => {
                let row_sp = if t.is_empty() {
                    self.default_row_sp
                } else {
                    let v = crate::entities::common::parse_typed_length(t)
                        .unwrap_or(self.default_row_sp);
                    defaults::set_array_row_sp(v);
                    self.default_row_sp = v;
                    v
                };
                self.step = RectStep::ColSp { rows, cols, row_sp };
                None
            }
            RectStep::ColSp { rows, cols, row_sp } => {
                let col_sp = if t.is_empty() {
                    self.default_col_sp
                } else {
                    let v = crate::entities::common::parse_typed_length(t)
                        .unwrap_or(self.default_col_sp);
                    defaults::set_array_col_sp(v);
                    v
                };
                Some(CmdResult::BatchCopy(
                    self.handles.clone(),
                    Self::build_transforms(rows, cols, row_sp, col_sp, self.plane),
                ))
            }
        }
    }

    fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
        let (rows, cols, row_sp, col_sp) = match self.step {
            RectStep::Rows => (
                self.default_rows,
                self.default_cols,
                self.default_row_sp,
                self.default_col_sp,
            ),
            RectStep::Cols { rows } => (
                rows,
                self.default_cols,
                self.default_row_sp,
                self.default_col_sp,
            ),
            RectStep::RowSp { rows, cols } => {
                (rows, cols, self.default_row_sp, self.default_col_sp)
            }
            RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
        };
        Self::build_transforms(rows, cols, row_sp, col_sp, self.plane)
            .iter()
            .flat_map(|t| {
                if let EntityTransform::Translate(delta) = t {
                    self.wire_models
                        .iter()
                        .map(|w| w.translated(delta.as_vec3()))
                        .collect::<Vec<_>>()
                } else {
                    vec![]
                }
            })
            .collect()
    }

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

    fn on_enter(&mut self) -> CmdResult {
        // Enter with empty input = use default for current step
        self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
    }

    fn on_escape(&mut self) -> CmdResult {
        CmdResult::Cancel
    }
}

// ── Polar Array ────────────────────────────────────────────────────────────

enum PolarStep {
    Center,
    Count { center: DVec3 },
    Angle { center: DVec3, count: u32 },
}

pub struct ArrayPolarCommand {
    handles: Vec<Handle>,
    wire_models: Vec<WireModel>,
    step: PolarStep,
    default_count: u32,
    default_angle: f64,
    plane: WorkingPlane,
}

impl ArrayPolarCommand {
    pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
        Self {
            handles,
            wire_models,
            step: PolarStep::Center,
            default_count: defaults::get_array_p_count() as u32,
            default_angle: defaults::get_array_p_angle(),
            plane: WorkingPlane::default(),
        }
    }
}

impl CadCommand for ArrayPolarCommand {
    fn set_working_plane(&mut self, plane: WorkingPlane) {
        self.plane = plane;
    }

    fn name(&self) -> &'static str {
        "ARRAYPOLAR"
    }

    fn prompt(&self) -> String {
        match &self.step {
            PolarStep::Center => crate::tf!(
                "ARRAYPOLAR  Specify center point  [{} objects]:",
                self.handles.len()
            )
            .into_owned(),
            PolarStep::Count { .. } => {
                crate::tf!("ARRAYPOLAR  Enter item count <{}>:", self.default_count).into_owned()
            }
            PolarStep::Angle { count, .. } => crate::tf!(
                "ARRAYPOLAR  Enter total angle in degrees <{:.0}>  [{count} items]:",
                self.default_angle
            )
            .into_owned(),
        }
    }

    fn wants_text_input(&self) -> bool {
        matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. })
    }

    fn dyn_field(&self) -> crate::command::DynField {
        if matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. }) {
            crate::command::DynField::Scalar
        } else {
            crate::command::DynField::Point
        }
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        let t = text.trim().replace(',', ".");
        let t = t.as_str();
        match &self.step {
            PolarStep::Count { center } => {
                let center = *center;
                let count = if t.is_empty() {
                    self.default_count
                } else {
                    let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
                    defaults::set_array_p_count(v as f64);
                    self.default_count = v;
                    v
                };
                self.step = PolarStep::Angle { center, count };
                None
            }
            PolarStep::Angle { center, count } => {
                let center = *center;
                let count = *count;
                let total_deg = if t.is_empty() {
                    self.default_angle
                } else {
                    // Held in degrees, which is what the stored default is.
                    let v = crate::entities::common::parse_typed_angle(t)
                        .map(f64::to_degrees)
                        .unwrap_or(self.default_angle);
                    defaults::set_array_p_angle(v);
                    v
                };
                let step_rad = total_deg.to_radians() / count as f64;
                let transforms = (1..count)
                    .map(|n| EntityTransform::Rotate {
                        center,
                        axis: self.plane.z,
                        angle_rad: step_rad * n as f64,
                    })
                    .collect();
                Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
            }
            _ => None,
        }
    }

    fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
        let pt = pt.as_vec3();
        let (center, count, total_deg) = match &self.step {
            PolarStep::Center => (pt, self.default_count, self.default_angle),
            PolarStep::Count { center } => {
                (center.as_vec3(), self.default_count, self.default_angle)
            }
            PolarStep::Angle { center, count } => {
                (center.as_vec3(), *count, self.default_angle)
            }
        };
        let step_rad = total_deg.to_radians() / count as f64;
        let axis = self.plane.z.as_vec3();
        let mut out: Vec<WireModel> = (1..count)
            .flat_map(|n| {
                let angle_rad = (step_rad * n as f64) as f32;
                self.wire_models
                    .iter()
                    .map(move |wire| wire.rotated_about_axis(center, axis, angle_rad))
            })
            .collect();
        // Rubber-band from center to cursor while picking the center point.
        if matches!(self.step, PolarStep::Center) {
            out.push(WireModel::solid(
                "rubber_band".into(),
                vec![[center.x, center.y, center.z], [pt.x, pt.y, pt.z]],
                WireModel::CYAN,
                false,
            ));
        }
        out
    }

    fn on_point(&mut self, pt: DVec3) -> CmdResult {
        if let PolarStep::Center = self.step {
            self.step = PolarStep::Count { center: pt };
        }
        CmdResult::NeedPoint
    }

    fn on_enter(&mut self) -> CmdResult {
        self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
    }

    fn on_escape(&mut self) -> CmdResult {
        CmdResult::Cancel
    }
}

// ── Path Array ─────────────────────────────────────────────────────────────
//
// ARRAYPATH:
//   Copies selected objects at equal arc-length intervals along a path entity.
//   1. Select path entity (Line, Arc, Circle, LwPolyline)
//   2. Enter item count (total, including the original at the path start)
//   → Returns BatchCopy with Translate transforms derived from path samples.

use acadrust::EntityType;
use crate::entities::curve::entity_curve;
use std::f64::consts::PI as FPI;
use std::f64::consts::TAU as FTAU;

// ── Path geometry helpers ──────────────────────────────────────────────────



// ── State machine ──────────────────────────────────────────────────────────

enum PathStep {
    SelectPath,
    Count { path_entity: EntityType },
}

pub struct ArrayPathCommand {
    handles: Vec<Handle>,
    wire_models: Vec<WireModel>,
    all_entities: Vec<EntityType>,
    step: PathStep,
    default_count: u32,
    /// Where the user clicked to select the path. The array starts from the
    /// path end nearest this point, so clicking near either end picks the
    /// travel direction (a stored arc is always CCW regardless of how it was
    /// drawn, so without this the array could run opposite to expectation).
    pick_pt: DVec3,
}

impl ArrayPathCommand {
    pub fn new(
        handles: Vec<Handle>,
        wire_models: Vec<WireModel>,
        all_entities: Vec<EntityType>,
    ) -> Self {
        Self {
            handles,
            wire_models,
            all_entities,
            step: PathStep::SelectPath,
            default_count: defaults::get_array_path_count() as u32,
            pick_pt: DVec3::ZERO,
        }
    }

    /// Sample the path and orient the points so index 0 is the end nearest the
    /// pick point, giving the user control over the array's travel direction.
    fn oriented_samples(&self, entity: &EntityType, count: usize) -> Vec<DVec3> {
        let mut pts = Self::sample_path(entity, count);
        if pts.len() >= 2 {
            let d_first = self.pick_pt.distance_squared(pts[0]);
            let d_last = self.pick_pt.distance_squared(pts[pts.len() - 1]);
            if d_last < d_first {
                pts.reverse();
            }
        }
        pts
    }

    /// Sample `count` evenly-spaced points along `entity`.
    /// `count` points spaced evenly **by distance** along the path.
    ///
    /// By distance, not by parameter. The two agree on a line, a circle and a
    /// circular arc, which is how a per-type version got away with confusing
    /// them; on an ellipse or a spline they do not, and copies laid out at
    /// even parameters bunch up wherever the parameter runs slow. The path's
    /// own OCS is honoured too, so an arc carrying a mirrored normal is
    /// walked along the side it is drawn on.
    fn sample_path(entity: &EntityType, count: usize) -> Vec<DVec3> {
        if count == 0 {
            return vec![];
        }
        let Some(curve) = entity_curve(entity) else {
            return vec![DVec3::ZERO; count];
        };
        let total = curve.length();
        if !total.is_finite() || total <= 0.0 {
            return vec![DVec3::ZERO; count];
        }
        // A closed path has no far end to stop short of, so the last copy
        // must not land on top of the first.
        let steps = if curve.is_closed() {
            count as f64
        } else {
            (count - 1).max(1) as f64
        };
        (0..count)
            .map(|i| {
                let p = curve.point_at_distance(total * (i as f64 / steps));
                DVec3::new(p[0], p[1], p[2])
            })
            .collect()
    }

    /// Continuous path-tangent angle (XY) at each sample, via central
    /// differences and unwrapped so the sequence stays smooth across an arc
    /// that turns more than ±π in total.
    fn tangents(pts: &[DVec3]) -> Vec<f64> {
        let n = pts.len();
        let mut a = vec![0.0f64; n];
        for i in 0..n {
            let prev = if i > 0 { pts[i - 1] } else { pts[i] };
            let next = if i + 1 < n { pts[i + 1] } else { pts[i] };
            let d = next - prev;
            a[i] = if d.x.abs() < 1e-12 && d.y.abs() < 1e-12 {
                if i > 0 {
                    a[i - 1]
                } else {
                    0.0
                }
            } else {
                d.y.atan2(d.x)
            };
        }
        // Unwrap to remove ±2π jumps.
        for i in 1..n {
            while a[i] - a[i - 1] > FPI {
                a[i] -= FTAU;
            }
            while a[i] - a[i - 1] <= -FPI {
                a[i] += FTAU;
            }
        }
        a
    }

    /// Build per-copy transforms that array the selection along the path with
    /// the items aligned to the path tangent (the first item keeps its drawn
    /// orientation; each copy rotates by the tangent change at its sample).
    /// On a straight path the tangent never changes, so every copy is a pure
    /// `Translate` — identical to a non-aligned array.
    fn build_transforms(pts: &[DVec3]) -> Vec<EntityTransform> {
        if pts.len() < 2 {
            return vec![];
        }
        let p0 = pts[0];
        let tans = Self::tangents(pts);
        let t0 = tans[0];
        pts.iter()
            .enumerate()
            .skip(1)
            .map(|(i, &p)| {
                let dth = tans[i] - t0;
                if dth.abs() < 1e-5 {
                    EntityTransform::Translate(p - p0)
                } else {
                    // The aligned copy is the rigid motion x' = p + R(x - p0),
                    // which is a pure rotation by `dth` about its fixed point.
                    let center = Self::rigid_center(p0, p, dth);
                    EntityTransform::Rotate {
                        center,
                        axis: DVec3::Z,
                        angle_rad: dth,
                    }
                }
            })
            .collect()
    }

    /// Fixed point of the rigid motion `x' = pi + R(dth)·(x - p0)`; rotating an
    /// entity by `dth` about this point reproduces that motion exactly.
    fn rigid_center(p0: DVec3, pi: DVec3, dth: f64) -> DVec3 {
        let (s, c) = dth.sin_cos();
        // b = pi - R·p0
        let bx = pi.x - (c * p0.x - s * p0.y);
        let by = pi.y - (s * p0.x + c * p0.y);
        // c = (I - R)^{-1} · b,  with (I-R)^{-1} = 1/(2(1-c)) [[1-c, -s],[s, 1-c]]
        let one_c = 1.0 - c;
        let det = 2.0 * one_c;
        DVec3::new(
            (one_c * bx - s * by) / det,
            (s * bx + one_c * by) / det,
            p0.z,
        )
    }
}

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

    fn prompt(&self) -> String {
        match &self.step {
            PathStep::SelectPath => crate::tf!(
                "ARRAYPATH  Select path entity  [{} objects]:",
                self.handles.len()
            )
            .into_owned(),
            PathStep::Count { .. } => {
                crate::tf!("ARRAYPATH  Enter item count <{}>:", self.default_count).into_owned()
            }
        }
    }

    fn needs_entity_pick(&self) -> bool {
        matches!(self.step, PathStep::SelectPath)
    }

    fn wants_text_input(&self) -> bool {
        matches!(self.step, PathStep::Count { .. })
    }

    fn dyn_field(&self) -> crate::command::DynField {
        if matches!(self.step, PathStep::Count { .. }) {
            crate::command::DynField::Scalar
        } else {
            crate::command::DynField::Point
        }
    }

    fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
        if handle.is_null() || self.handles.contains(&handle) {
            return CmdResult::NeedPoint;
        }
        if let Some(entity) = self
            .all_entities
            .iter()
            .find(|e| e.common().handle == handle)
            .cloned()
        {
            self.pick_pt = pt;
            self.step = PathStep::Count {
                path_entity: entity,
            };
        }
        CmdResult::NeedPoint
    }

    fn on_hover_entity(&mut self, handle: Handle, _pt: DVec3) -> Vec<WireModel> {
        if handle.is_null() || self.handles.contains(&handle) {
            return vec![];
        }
        if !matches!(self.step, PathStep::SelectPath) {
            return vec![];
        }
        if let Some(entity) = self
            .all_entities
            .iter()
            .find(|e| e.common().handle == handle)
        {
            let pts = Self::sample_path(entity, 64);
            if pts.len() >= 2 {
                return vec![WireModel::solid(
                    "arraypath_hover".into(),
                    pts.iter()
                        .map(|p| [p.x as f32, p.y as f32, p.z as f32])
                        .collect(),
                    WireModel::CYAN,
                    false,
                )];
            }
        }
        vec![]
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        let PathStep::Count { path_entity } = &self.step else {
            return None;
        };
        let t = text.trim().replace(',', ".");
        let count = if t.is_empty() {
            self.default_count
        } else {
            let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
            defaults::set_array_path_count(v as f64);
            self.default_count = v;
            v
        };
        let pts = self.oriented_samples(path_entity, count as usize);
        let transforms = Self::build_transforms(&pts);
        if transforms.is_empty() {
            return Some(CmdResult::Cancel);
        }
        Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
    }

    fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
        let PathStep::Count { path_entity } = &self.step else {
            return vec![];
        };
        let pts = self.oriented_samples(path_entity, self.default_count as usize);
        let transforms = Self::build_transforms(&pts);
        transforms
            .iter()
            .flat_map(|t| match t {
                EntityTransform::Translate(delta) => self
                    .wire_models
                    .iter()
                    .map(|w| w.translated(delta.as_vec3()))
                    .collect::<Vec<_>>(),
                EntityTransform::Rotate { center, angle_rad, .. } => self
                    .wire_models
                    .iter()
                    .map(|w| w.rotated(center.as_vec3(), *angle_rad as f32))
                    .collect::<Vec<_>>(),
                _ => vec![],
            })
            .collect()
    }

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

    fn on_enter(&mut self) -> CmdResult {
        self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
    }

    fn on_escape(&mut self) -> CmdResult {
        CmdResult::Cancel
    }
}

// ── 3D Rectangular Array ──────────────────────────────────────────────────

/// ARRAY3D — rectangular array in X (columns), Z (rows in drawing plane), Y (levels up).
/// Prompts: rows → cols → levels → row spacing → col spacing → level spacing
#[derive(Debug, Clone, Copy)]
enum Array3DStep {
    Rows,
    Cols {
        rows: u32,
    },
    Levels {
        rows: u32,
        cols: u32,
    },
    RowSp {
        rows: u32,
        cols: u32,
        levels: u32,
    },
    ColSp {
        rows: u32,
        cols: u32,
        levels: u32,
        row_sp: f64,
    },
    LvlSp {
        rows: u32,
        cols: u32,
        levels: u32,
        row_sp: f64,
        col_sp: f64,
    },
}

pub struct Array3DCommand {
    handles: Vec<Handle>,
    step: Array3DStep,
}

impl Array3DCommand {
    pub fn new(handles: Vec<Handle>) -> Self {
        Self {
            handles,
            step: Array3DStep::Rows,
        }
    }

    fn build_transforms(
        rows: u32,
        cols: u32,
        levels: u32,
        row_sp: f64,
        col_sp: f64,
        lvl_sp: f64,
    ) -> Vec<EntityTransform> {
        let mut t = Vec::new();
        for l in 0..levels {
            for r in 0..rows {
                for c in 0..cols {
                    if l == 0 && r == 0 && c == 0 {
                        continue;
                    }
                    // Drawing plane is world XY: X = col dir, Y = row dir,
                    // Z = level (elevation).
                    t.push(EntityTransform::Translate(DVec3::new(
                        col_sp * c as f64,
                        row_sp * r as f64,
                        lvl_sp * l as f64,
                    )));
                }
            }
        }
        t
    }
}

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

    fn prompt(&self) -> String {
        match self.step {
            Array3DStep::Rows => t!("ARRAY3D  Enter row count:").into_owned(),
            Array3DStep::Cols { rows } => {
                t!("ARRAY3D  Enter column count  [%{rows} rows]:", rows = rows).into_owned()
            }
            Array3DStep::Levels { rows, cols } => t!(
                "ARRAY3D  Enter level count  [%{rows}×%{cols}]:",
                rows = rows,
                cols = cols
            )
            .into_owned(),
            Array3DStep::RowSp { rows, cols, levels } => t!(
                "ARRAY3D  Row spacing  [%{rows}×%{cols}×%{levels}]:",
                rows = rows,
                cols = cols,
                levels = levels
            )
            .into_owned(),
            Array3DStep::ColSp {
                rows,
                cols,
                levels,
                row_sp,
            } => {
                let rs = format!("{:.0}", row_sp);
                t!(
                    "ARRAY3D  Column spacing  [%{rows}×%{cols}×%{levels}, row=%{rs}]:",
                    rows = rows,
                    cols = cols,
                    levels = levels,
                    rs = rs
                )
                .into_owned()
            }
            Array3DStep::LvlSp {
                rows,
                cols,
                levels,
                row_sp,
                col_sp,
            } => {
                let rs = format!("{:.0}", row_sp);
                let cs = format!("{:.0}", col_sp);
                t!(
                    "ARRAY3D  Level spacing  [%{rows}×%{cols}×%{levels}, r=%{rs} c=%{cs}]:",
                    rows = rows,
                    cols = cols,
                    levels = levels,
                    rs = rs,
                    cs = cs
                )
                .into_owned()
            }
        }
    }

    fn wants_text_input(&self) -> bool {
        true
    }

    fn dyn_field(&self) -> crate::command::DynField {
        crate::command::DynField::Scalar
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        let t = text.trim().replace(',', ".");
        let t = t.as_str();
        match self.step {
            Array3DStep::Rows => {
                let v = if t.is_empty() {
                    2
                } else {
                    t.parse::<u32>().unwrap_or(2).max(1)
                };
                self.step = Array3DStep::Cols { rows: v };
                Some(CmdResult::NeedPoint)
            }
            Array3DStep::Cols { rows } => {
                let v = if t.is_empty() {
                    2
                } else {
                    t.parse::<u32>().unwrap_or(2).max(1)
                };
                self.step = Array3DStep::Levels { rows, cols: v };
                Some(CmdResult::NeedPoint)
            }
            Array3DStep::Levels { rows, cols } => {
                let v = if t.is_empty() {
                    2
                } else {
                    t.parse::<u32>().unwrap_or(2).max(1)
                };
                self.step = Array3DStep::RowSp {
                    rows,
                    cols,
                    levels: v,
                };
                Some(CmdResult::NeedPoint)
            }
            Array3DStep::RowSp { rows, cols, levels } => {
                let v: f64 = if t.is_empty() {
                    1.0
                } else {
                    t.parse().unwrap_or(1.0)
                };
                self.step = Array3DStep::ColSp {
                    rows,
                    cols,
                    levels,
                    row_sp: v,
                };
                Some(CmdResult::NeedPoint)
            }
            Array3DStep::ColSp {
                rows,
                cols,
                levels,
                row_sp,
            } => {
                let v: f64 = if t.is_empty() {
                    1.0
                } else {
                    t.parse().unwrap_or(1.0)
                };
                self.step = Array3DStep::LvlSp {
                    rows,
                    cols,
                    levels,
                    row_sp,
                    col_sp: v,
                };
                Some(CmdResult::NeedPoint)
            }
            Array3DStep::LvlSp {
                rows,
                cols,
                levels,
                row_sp,
                col_sp,
            } => {
                let v: f64 = if t.is_empty() {
                    1.0
                } else {
                    t.parse().unwrap_or(1.0)
                };
                let transforms = Self::build_transforms(rows, cols, levels, row_sp, col_sp, v);
                Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
            }
        }
    }

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

    fn on_enter(&mut self) -> CmdResult {
        self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
    }

    fn on_escape(&mut self) -> CmdResult {
        CmdResult::Cancel
    }

    fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
        vec![]
    }
}

// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["ARRAY3D", "3DARRAY"] });
