use super::*;

impl OpenCADStudio {
    pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
        match cmd {
            "3DORBIT" => {
                self.command_line
                    .push_info("3D Orbit: drag with right mouse button.");
            }

            // ── Selection utilities ───────────────────────────────────────
            "SELECTALL" => {
                use crate::scene::Scene;
                let handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .entity_wires()
                    .iter()
                    .filter_map(|w| Scene::handle_from_wire_name(&w.name))
                    // Objects on a locked layer aren't selectable.
                    .filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
                    .collect();
                let count = handles.len();
                for h in handles {
                    self.tabs[i].scene.select_entity(h, false);
                }
                self.command_line
                    .push_output(&format!("SELECTALL: {} object(s) selected.", count));
                self.refresh_properties();
            }

            "DESELECT" | "DESELALL" => {
                self.tabs[i].scene.deselect_all();
                self.command_line.push_output("Deselected.");
                self.refresh_properties();
            }

            "SELECTSIMILAR" | "SELSIM" => {
                let added = self.tabs[i].scene.select_similar();
                self.command_line
                    .push_output(&format!("Select Similar: {} added.", added));
                self.refresh_properties();
            }

            // ADDSELECTED — draw a new object of the same type as the selected
            // one, inheriting its general properties. (#239)
            "ADDSELECTED" => {
                return Some(self.cmd_add_selected(i));
            }

            // QSELECT builds a selection set by object type / property; FILTER is
            // the same criteria-based selection.
            "QSELECT" | "FILTER" => {
                return Some(Task::done(Message::QSelectOpen));
            }

            // CAL / QUICKCALC <expression> — evaluate an arithmetic expression
            //   (+ - * /, parentheses, unary minus, decimals).
            "CAL" | "QUICKCALC" | "QC" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new("CAL", "CAL  expression  (e.g. (2+3)*4):");
                self.command_line.push_info(&c.prompt());
                self.tabs[self.active_tab].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("CAL ")
                || cmd.starts_with("QUICKCALC ")
                || cmd.starts_with("QC ") =>
            {
                let expr = cmd.splitn(2, char::is_whitespace).nth(1).unwrap_or("").trim();
                if expr.is_empty() {
                    self.command_line
                        .push_info("Usage: CAL <expression>   e.g. CAL (2+3)*4");
                } else {
                    match arith_eval(expr) {
                        Ok(v) => self.command_line.push_output(&format!("= {v}")),
                        Err(e) => self.command_line.push_error(&format!("CAL: {e}")),
                    }
                }
            }

            // ── LIST — entity info ────────────────────────────────────────
            "LIST" => {
                let selected: Vec<_> = self.tabs[i].scene.selected_entities();
                if selected.is_empty() {
                    self.command_line
                        .push_error("LIST: no entities selected. Select entities first.");
                } else {
                    for (handle, _) in &selected {
                        if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
                            let type_name = crate::entities::names::dxf_name(entity);
                            let common = entity.common();
                            let color_str = common
                                .color
                                .index()
                                .map(|c| c.to_string())
                                .unwrap_or_else(|| "ByLayer".to_string());
                            let linetype =
                                if common.linetype.is_empty() || common.linetype == "ByLayer" {
                                    "ByLayer".to_string()
                                } else {
                                    common.linetype.clone()
                                };
                            // Entity-specific details
                            let details = entity_list_details(entity);
                            self.command_line.push_output(&format!(
                                "{type_name}  Handle:{:X}  Layer:{}  Color:{}  LT:{}{}",
                                handle.value(),
                                common.layer,
                                color_str,
                                linetype,
                                if details.is_empty() {
                                    String::new()
                                } else {
                                    format!("\n    {details}")
                                }
                            ));
                        }
                    }
                }
            }

            // SELHANDLES — report the current selection for diagnostics: the
            // active space (Model / which paper layout), a per-type and
            // per-block breakdown, and the raw comma-separated hex handle list
            // (so what renders on screen can be compared against the file).
            "SELHANDLES" | "SELH" => {
                let scene = &self.tabs[i].scene;
                let selected = scene.selected_entities();
                if selected.is_empty() {
                    self.command_line
                        .push_error("SELHANDLES: no entities selected. Select entities first.");
                } else {
                    use std::collections::BTreeMap;
                    let space = if scene.current_layout == "Model" {
                        "Model space".to_string()
                    } else {
                        format!("Paper space '{}'", scene.current_layout)
                    };
                    let mut handles: Vec<u64> = Vec::with_capacity(selected.len());
                    let mut type_counts: BTreeMap<&'static str, usize> = BTreeMap::new();
                    let mut block_counts: BTreeMap<String, usize> = BTreeMap::new();
                    for (h, e) in &selected {
                        handles.push(h.value());
                        *type_counts
                            .entry(crate::entities::names::dxf_name(e))
                            .or_default() += 1;
                        if let acadrust::EntityType::Insert(ins) = e {
                            *block_counts.entry(ins.block_name.clone()).or_default() += 1;
                        }
                    }
                    handles.sort_unstable();
                    let types: Vec<String> =
                        type_counts.iter().map(|(t, n)| format!("{t}×{n}")).collect();
                    let list: Vec<String> = handles.iter().map(|h| format!("{:X}", h)).collect();
                    let mut msg = format!(
                        "SELHANDLES: {} selected in {}\n  Types: {}",
                        handles.len(),
                        space,
                        types.join(", ")
                    );
                    if !block_counts.is_empty() {
                        let blocks: Vec<String> =
                            block_counts.iter().map(|(b, n)| format!("{b}×{n}")).collect();
                        msg.push_str(&format!("\n  Blocks: {}", blocks.join(", ")));
                    }
                    msg.push_str(&format!("\n  Handles: {}", list.join(",")));
                    self.command_line.push_output(&msg);
                }
            }

            // DBLIST — list data for every entity in the drawing (LIST over the
            // whole database rather than the current selection).
            "DBLIST" => {
                // Format every entity first so the immutable document borrow is
                // released before writing to the command line.
                let lines: Vec<String> = self.tabs[i]
                    .scene
                    .document
                    .entities()
                    .map(|entity| {
                        let type_name = crate::entities::names::dxf_name(entity);
                        let common = entity.common();
                        let color_str = common
                            .color
                            .index()
                            .map(|c| c.to_string())
                            .unwrap_or_else(|| "ByLayer".to_string());
                        let linetype = if common.linetype.is_empty() || common.linetype == "ByLayer"
                        {
                            "ByLayer".to_string()
                        } else {
                            common.linetype.clone()
                        };
                        let details = entity_list_details(entity);
                        format!(
                            "{type_name}  Handle:{:X}  Layer:{}  Color:{}  LT:{}{}",
                            common.handle.value(),
                            common.layer,
                            color_str,
                            linetype,
                            if details.is_empty() {
                                String::new()
                            } else {
                                format!("\n    {details}")
                            }
                        )
                    })
                    .collect();
                if lines.is_empty() {
                    self.command_line
                        .push_info("DBLIST: drawing has no entities.");
                } else {
                    let count = lines.len();
                    for l in lines {
                        self.command_line.push_output(&l);
                    }
                    self.command_line.push_output(&format!(
                        "DBLIST: {count} entit{}.",
                        if count == 1 { "y" } else { "ies" }
                    ));
                }
            }

            // ── Break / Join ─────────────────────────────────────────────────
            "JOIN" => {
                use crate::modules::draw::modify::join::JoinCommand;
                // Pickfirst: with objects already selected, join them right
                // away instead of asking for a selection again.
                let selected: Vec<acadrust::Handle> =
                    self.tabs[i].scene.selected.iter().copied().collect();
                if selected.len() >= 2 {
                    let task =
                        self.apply_cmd_result(crate::command::CmdResult::JoinEntities(selected));
                    return Some(task);
                }
                let cmd = JoinCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "BREAK" => {
                use crate::modules::draw::modify::break_cmd::BreakInteractiveCommand;
                let cmd = BreakInteractiveCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "BREAKATPOINT" => {
                use crate::modules::draw::modify::break_cmd::BreakAtPointCommand;
                let cmd = BreakAtPointCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "PEDIT" => {
                use crate::modules::draw::modify::pedit::{PeditCommand, PeditTarget};
                let info = self.tabs[i]
                    .scene
                    .document
                    .entities()
                    .filter_map(|e| {
                        let h = e.common().handle.value();
                        match e {
                            acadrust::EntityType::LwPolyline(_)
                            | acadrust::EntityType::Polyline2D(_) => Some((
                                h,
                                PeditTarget {
                                    is_poly: true,
                                    convertible: false,
                                },
                            )),
                            acadrust::EntityType::Line(_) | acadrust::EntityType::Arc(_) => {
                                Some((
                                    h,
                                    PeditTarget {
                                        is_poly: false,
                                        convertible: true,
                                    },
                                ))
                            }
                            _ => None,
                        }
                    })
                    .collect();
                // Pickfirst: an already-selected polyline (or line/arc, via
                // the convert prompt) skips the select step.
                let preselected: Vec<acadrust::Handle> =
                    self.tabs[i].scene.selected.iter().copied().collect();
                let cmd_obj = PeditCommand::new(info).with_preselection(&preselected);
                self.command_line.push_info(&cmd_obj.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
            }

            "SPLINEDIT" => {
                use crate::modules::draw::modify::splinedit::SplineditCommand;
                let cmd_obj = SplineditCommand::new();
                self.command_line.push_info(&cmd_obj.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
            }

            // Bare ATTEDIT (and the ATE alias) open the attribute editor dialog.
            // If a single block with attributes is already selected it opens on
            // that block; otherwise the pick command runs and the editor opens
            // once a block is chosen (see `command_driver`).
            "ATTEDIT" => {
                self.open_attedit_dialog();
            }

            // ── REFEDIT — in-place block editing ─────────────────────────────
            "REFEDIT" => {
                use crate::modules::draw::modify::refedit::RefEditPickCommand;
                // If a session is already active, tell the user.
                if self.tabs[i].refedit_session.is_some() {
                    self.command_line
                        .push_error("REFEDIT: a session is already active. Use REFCLOSE first.");
                } else {
                    // Check if a single INSERT is already selected.
                    let selected: Vec<_> =
                        self.tabs[i].scene.selected_entities().into_iter().collect();
                    if selected.len() == 1 {
                        if let Some(acadrust::EntityType::Insert(_)) =
                            selected.first().map(|(_, e)| e)
                        {
                            let handle = selected[0].0;
                            // Skip pick phase — jump straight to begin.
                            let _ =
                                self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value()));
                            return Some(Task::none());
                        }
                    }
                    let cmd_obj = RefEditPickCommand::new();
                    self.command_line.push_info(&cmd_obj.prompt());
                    self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
                }
            }

            "BEDIT" => {
                use crate::modules::draw::modify::block_edit::BlockEditPickCommand;
                if self.tabs[i].block_edit.is_some() {
                    self.command_line.push_error(
                        "BEDIT: a block editor is already open. Save or discard it first.",
                    );
                } else if self.tabs[i].refedit_session.is_some() {
                    self.command_line
                        .push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first.");
                } else {
                    // Jump straight to begin when a single INSERT is preselected.
                    let selected: Vec<_> =
                        self.tabs[i].scene.selected_entities().into_iter().collect();
                    if selected.len() == 1 {
                        if let Some(acadrust::EntityType::Insert(_)) =
                            selected.first().map(|(_, e)| e)
                        {
                            let handle = selected[0].0;
                            let _ =
                                self.dispatch_command(&format!("BEDIT_BEGIN:{}", handle.value()));
                            return Some(Task::none());
                        }
                    }
                    let cmd_obj = BlockEditPickCommand::new();
                    self.command_line.push_info(&cmd_obj.prompt());
                    self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
                }
            }

            cmd if cmd.starts_with("BEDIT_BEGIN:") => {
                use crate::modules::draw::modify::block_edit::BlockEditSession;
                use acadrust::Handle;

                let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
                let insert_handle = Handle::new(handle_u64);

                let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
                    Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
                    _ => {
                        self.command_line
                            .push_error("BEDIT: selected object is not a block reference.");
                        return Some(Task::none());
                    }
                };

                // Resolve the block record; reject external references (xrefs).
                let (br_handle, is_xref) = match self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get(&insert.block_name)
                {
                    Some(br) => (br.handle, br.flags.is_xref),
                    None => {
                        self.command_line.push_error(&format!(
                            "BEDIT: block \"{}\" not found.",
                            insert.block_name
                        ));
                        return Some(Task::none());
                    }
                };
                if is_xref {
                    self.command_line
                        .push_error("BEDIT: cannot edit an external reference (xref).");
                    return Some(Task::none());
                }

                // Snapshot the block's block-local entities so Discard can restore
                // them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT).
                let snapshot: Vec<_> = {
                    let br = self.tabs[i]
                        .scene
                        .document
                        .block_records
                        .get(&insert.block_name)
                        .unwrap();
                    br.entity_handles
                        .iter()
                        .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
                        .filter(|e| {
                            !matches!(
                                e,
                                acadrust::EntityType::Block(_)
                                    | acadrust::EntityType::BlockEnd(_)
                                    | acadrust::EntityType::AttributeDefinition(_)
                            )
                        })
                        .collect()
                };

                self.push_undo_snapshot(i, "BEDIT");

                let return_layout = self.tabs[i].scene.current_layout.clone();
                // Capture the camera before the editor reframes it, so leaving
                // the block editor returns the view exactly where it was (#425).
                let return_camera = self.tabs[i].scene.camera.borrow().clone();
                // A block editor renders in model style; switch to Model first so
                // the paper-space code paths stay off, then scope to the block.
                if return_layout != "Model" {
                    self.tabs[i].scene.set_current_layout("Model".to_string());
                }
                self.tabs[i].scene.block_edit_block = Some(br_handle);
                self.tabs[i].block_edit = Some(BlockEditSession {
                    block_name: insert.block_name.clone(),
                    br_handle,
                    return_layout,
                    snapshot,
                    return_camera,
                });

                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.bump_geometry();
                // Frame the camera on the block's own geometry (block-local, near
                // origin) — fit_all() goes through current_layout_block_handle so
                // it already scopes to the edited block. Without this the view
                // stays wherever model/paper space was. (#261)
                self.tabs[i].scene.fit_all();
                self.tabs[i].active_cmd = None;
                self.tabs[i].dirty = true;
                self.command_line.push_info(&format!(
                    "BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
                    insert.block_name
                ));
            }

            "BEDIT_SAVE" => {
                let session = match self.tabs[i].block_edit.take() {
                    Some(s) => s,
                    None => {
                        self.command_line
                            .push_error("BEDIT_SAVE: no block editor is open.");
                        return Some(Task::none());
                    }
                };
                // Edits are live on the block record — just leave the block space.
                self.tabs[i].scene.block_edit_block = None;
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.set_current_layout(session.return_layout.clone());
                // Return the view to where it was when BEDIT began (#425).
                *self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
                self.tabs[i].scene.camera_generation += 1;
                self.tabs[i].scene.rebuild_derived_caches();
                self.tabs[i].dirty = true;
                self.command_line.push_output(&format!(
                    "BEDIT: Block \"{}\" saved. All references updated.",
                    session.block_name
                ));
            }

            "BEDIT_DISCARD" => {
                let session = match self.tabs[i].block_edit.take() {
                    Some(s) => s,
                    None => {
                        self.command_line
                            .push_error("BEDIT_DISCARD: no block editor is open.");
                        return Some(Task::none());
                    }
                };
                // Restore the block definition to its on-entry snapshot: remove the
                // block's current entities, then re-add the snapshot ones (mirrors
                // the REFCLOSE_SAVE write-back).
                let old_handles: Vec<_> = match self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get(&session.block_name)
                {
                    Some(br) => br.entity_handles.clone(),
                    None => vec![],
                };
                for h in &old_handles {
                    self.tabs[i].scene.document.remove_entity(*h);
                }
                if let Some(br) = self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get_mut(&session.block_name)
                {
                    br.entity_handles.clear();
                }
                for mut entity in session.snapshot {
                    entity.common_mut().handle = acadrust::Handle::NULL;
                    entity.common_mut().owner_handle = session.br_handle;
                    let _ = self.tabs[i].scene.document.add_entity(entity);
                }
                self.tabs[i].scene.block_edit_block = None;
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.set_current_layout(session.return_layout.clone());
                // Return the view to where it was when BEDIT began (#425).
                *self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
                self.tabs[i].scene.camera_generation += 1;
                self.tabs[i].scene.rebuild_derived_caches();
                self.tabs[i].dirty = true;
                self.command_line.push_output(&format!(
                    "BEDIT: Block \"{}\" edit discarded.",
                    session.block_name
                ));
            }

            cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
                use crate::modules::draw::modify::refedit::{
                    apply_insert_transform, RefEditSession,
                };
                use acadrust::Handle;

                let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0);
                let insert_handle = Handle::new(handle_u64);

                // Get INSERT entity.
                let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
                    Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
                    _ => {
                        self.command_line
                            .push_error("REFEDIT: selected object is not an INSERT.");
                        return Some(Task::none());
                    }
                };

                // Build the INSERT's full placement transform (OCS + rotation +
                // scale, including non-uniform / mirrored) and its inverse, so
                // edits round-trip back to block-local coordinates on SAVE.
                let sx = insert.x_scale();
                let sy = insert.y_scale();
                let sz = insert.z_scale();
                let forward = insert.get_transform();
                let inverse = {
                    use acadrust::types::{Matrix3, Matrix4, Transform};
                    let ocs_t =
                        Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose());
                    let t_inv = Matrix4::translation(
                        -insert.insert_point.x,
                        -insert.insert_point.y,
                        -insert.insert_point.z,
                    );
                    let r_inv = Matrix4::rotation_z(-insert.rotation);
                    let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
                    // inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ
                    Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t)
                };

                // Find the block record.
                let br_handle = match self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get(&insert.block_name)
                {
                    Some(br) => br.handle,
                    None => {
                        self.command_line.push_error(&format!(
                            "REFEDIT: block \"{}\" not found.",
                            insert.block_name
                        ));
                        return Some(Task::none());
                    }
                };

                // Collect block-local entities (skip structural Block/BlockEnd/AttDef).
                let block_entities: Vec<_> = {
                    let br = self.tabs[i]
                        .scene
                        .document
                        .block_records
                        .get(&insert.block_name)
                        .unwrap();
                    br.entity_handles
                        .iter()
                        .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
                        .filter(|e| {
                            !matches!(
                                e,
                                acadrust::EntityType::Block(_)
                                    | acadrust::EntityType::BlockEnd(_)
                                    | acadrust::EntityType::AttributeDefinition(_)
                            )
                        })
                        .collect()
                };

                if block_entities.is_empty() {
                    self.command_line.push_error("REFEDIT: block is empty.");
                    return Some(Task::none());
                }

                let session = RefEditSession {
                    block_name: insert.block_name.clone(),
                    br_handle,
                    temp_handles: vec![],
                    forward,
                    inverse,
                    // Everything allocated from here on is part of the session.
                    handle_watermark: self
                        .tabs[i]
                        .scene
                        .document
                        .next_handle()
                        .max(self.tabs[i].scene.document.header.handle_seed),
                };

                self.push_undo_snapshot(i, "REFEDIT");
                self.tabs[i].refedit_session = Some(session.clone());

                // Add block entities to model space with INSERT transform applied.
                let mut temp_handles = Vec::new();
                for mut entity in block_entities {
                    apply_insert_transform(&mut entity, &session);
                    entity.common_mut().handle = acadrust::Handle::NULL;
                    entity.common_mut().owner_handle = acadrust::Handle::NULL;
                    let h = self.tabs[i].scene.add_entity(entity);
                    temp_handles.push(h);
                }
                self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone();

                // Fade everything except the entities being edited, so the
                // surrounding drawing stays visible for context but the block's
                // geometry stands out. (#136)
                self.tabs[i]
                    .scene
                    .set_refedit_keep(Some(temp_handles.iter().copied().collect()));

                // Select the temp entities so user can see what they're editing.
                self.tabs[i].scene.deselect_all();
                for h in &temp_handles {
                    self.tabs[i].scene.select_entity(*h, false);
                }
                self.tabs[i].dirty = true;

                // No active command — the user edits the block's geometry freely
                // (move, grips, draw, erase…) and runs REFCLOSE when done. (#136)
                self.tabs[i].active_cmd = None;
                self.command_line.push_info(&format!(
                    "REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.",
                    insert.block_name
                ));
            }

            "REFCLOSE" => {
                if self.tabs[i].refedit_session.is_some() {
                    use crate::modules::draw::modify::refedit::RefCloseCommand;
                    let cmd_obj = RefCloseCommand::new();
                    self.command_line.push_info(&cmd_obj.prompt());
                    self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
                } else {
                    self.command_line
                        .push_error("REFCLOSE: no REFEDIT session active.");
                }
            }

            "REFCLOSE_SAVE" => {
                use crate::modules::draw::modify::explode::normalize_entity_for_block;
                use crate::modules::draw::modify::refedit::apply_insert_inverse_transform;

                let session = match self.tabs[i].refedit_session.take() {
                    Some(s) => s,
                    None => {
                        self.command_line
                            .push_error("REFCLOSE: no REFEDIT session active.");
                        return Some(Task::none());
                    }
                };

                self.push_undo_snapshot(i, "REFCLOSE");

                // The working set: the temp copies PLUS everything the user
                // created in this space during the session (drawn / pasted /
                // offset entities carry handles at or above the watermark) —
                // those must fold into the block too, not stay behind in
                // model space (#423).
                let working: Vec<acadrust::Handle> = {
                    let space = self.tabs[i].scene.current_layout_block_handle_pub();
                    let drawn = self.tabs[i].scene.document.entities().filter(|e| {
                        let c = e.common();
                        c.owner_handle == space
                            && c.handle.value() >= session.handle_watermark
                            && !session.temp_handles.contains(&c.handle)
                    });
                    session
                        .temp_handles
                        .iter()
                        .copied()
                        .chain(drawn.map(|e| e.common().handle))
                        .collect()
                };

                // Collect the edited temp entities.
                let new_entities: Vec<acadrust::EntityType> = working
                    .iter()
                    .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
                    .collect();

                // Remove temp entities from model space.
                self.tabs[i].scene.erase_entities(&working);

                // Apply inverse INSERT transform → block-local coordinates.
                let new_entities: Vec<_> = new_entities
                    .into_iter()
                    .map(|mut entity| {
                        apply_insert_inverse_transform(&mut entity, &session);
                        let mut entity = normalize_entity_for_block(entity);
                        entity.common_mut().handle = acadrust::Handle::NULL;
                        entity.common_mut().owner_handle = session.br_handle;
                        entity
                    })
                    .collect();

                // Remove old block entities from the document.
                let old_handles: Vec<_> = match self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get(&session.block_name)
                {
                    Some(br) => br.entity_handles.clone(),
                    None => vec![],
                };
                for h in &old_handles {
                    self.tabs[i].scene.document.remove_entity(*h);
                }
                // Flush the entity_handles list from the block record.
                if let Some(br) = self.tabs[i]
                    .scene
                    .document
                    .block_records
                    .get_mut(&session.block_name)
                {
                    br.entity_handles.clear();
                }

                // Add the new block entities.
                for entity in new_entities {
                    let _ = self.tabs[i].scene.document.add_entity(entity);
                }

                self.tabs[i].dirty = true;
                self.command_line.push_output(&format!(
                    "REFCLOSE: Block \"{}\" saved. All references updated.",
                    session.block_name
                ));
                // End the edit fade before rebuilding, so the restored geometry
                // recolours bright. (#136)
                self.tabs[i].scene.set_refedit_keep(None);
                // Rebuild hatch/image/mesh caches since block content changed.
                self.tabs[i].scene.rebuild_derived_caches();
            }

            "REFCLOSE_DISCARD" => {
                let session = match self.tabs[i].refedit_session.take() {
                    Some(s) => s,
                    None => {
                        self.command_line
                            .push_error("REFCLOSE: no REFEDIT session active.");
                        return Some(Task::none());
                    }
                };
                // Remove the working set without modifying the block — the
                // temp copies plus anything created during the session (#423).
                let working: Vec<acadrust::Handle> = {
                    let space = self.tabs[i].scene.current_layout_block_handle_pub();
                    let drawn = self.tabs[i].scene.document.entities().filter(|e| {
                        let c = e.common();
                        c.owner_handle == space
                            && c.handle.value() >= session.handle_watermark
                            && !session.temp_handles.contains(&c.handle)
                    });
                    session
                        .temp_handles
                        .iter()
                        .copied()
                        .chain(drawn.map(|e| e.common().handle))
                        .collect()
                };
                self.tabs[i].scene.erase_entities(&working);
                self.tabs[i].scene.deselect_all();
                // End the edit fade — restore the drawing to full brightness.
                self.tabs[i].scene.set_refedit_keep(None);
                self.command_line
                    .push_output("REFCLOSE: Changes discarded.");
            }

            "ALIGN" => {
                use crate::modules::draw::modify::align::AlignCommand;
                let cmd = AlignCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "LENGTHEN" => {
                use crate::modules::draw::modify::lengthen::LengthenCommand;
                let cmd = LengthenCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "DIVIDE" => {
                use crate::modules::draw::inquiry::divide::DivideCommand;
                let cmd = DivideCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "MEASURE" => {
                use crate::modules::draw::inquiry::divide::MeasureCommand;
                let cmd = MeasureCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            // ── Inquiry ──────────────────────────────────────────────────────
            "DIST" => {
                use crate::modules::draw::inquiry::dist::DistCommand;
                let cmd = DistCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "ID" => {
                use crate::modules::draw::inquiry::id::IdCommand;
                let cmd = IdCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            "AREA" => {
                use crate::modules::draw::inquiry::area::AreaCommand;
                let cmd = AreaCommand::new();
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            // ── MASSPROP — area, perimeter, centroid of selected entities ────
            "MASSPROP" => {
                let selected = self.tabs[i].scene.selected_entities();
                if selected.is_empty() {
                    self.command_line
                        .push_error("MASSPROP: no entities selected. Select entities first.");
                } else {
                    for (handle, _) in &selected {
                        if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
                            use crate::entities::traits::EntityTypeOps;
                            if let Some(props) = entity.mass_props() {
                                self.command_line.push_output(&format!(
                                    "{}  Area={:.4}  Perimeter={:.4}  Centroid=({:.4},{:.4})",
                                    crate::entities::names::dxf_name(entity),
                                    props.area,
                                    props.perimeter,
                                    props.cx,
                                    props.cy,
                                ));
                            }
                        }
                    }
                }
            }

            // ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
            "FLATTEN" => {
                let handles: Vec<acadrust::Handle> = {
                    let sel = self.tabs[i].scene.selected_entities();
                    if sel.is_empty() {
                        // Flatten all entities
                        self.tabs[i]
                            .scene
                            .document
                            .entities()
                            .map(|e| e.common().handle)
                            .collect()
                    } else {
                        sel.into_iter().map(|(h, _)| h).collect()
                    }
                };
                if handles.is_empty() {
                    self.command_line.push_error("FLATTEN: no entities.");
                } else {
                    self.push_undo_snapshot(i, "FLATTEN");
                    for h in &handles {
                        if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
                            flatten_entity_z(e);
                        }
                    }
                    self.tabs[i].dirty = true;
                    self.command_line.push_output(&format!(
                        "FLATTEN: {} entity(ies) moved to Z=0.",
                        handles.len()
                    ));
                    self.refresh_properties();
                }
            }

            // ── QSELECT — quick-select entities by property ───────────────────
            // QSELECT TYPE <type>          — select all entities of given type
            // QSELECT LAYER <name>         — select all entities on layer
            // QSELECT COLOR <n>            — select all entities with color index n
            // QSELECT LINETYPE <name>      — select all entities with linetype
            cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => {
                let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
                let parts: Vec<&str> = rest.splitn(2, ' ').collect();
                let prop = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
                let val = parts.get(1).map(|s| s.trim()).unwrap_or("").to_uppercase();

                let matched: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .document
                    .entities()
                    .filter(|e| {
                        let c = e.common();
                        match prop.as_str() {
                            "TYPE" => crate::entities::names::dxf_name(e).to_uppercase() == val,
                            "LAYER" => c.layer.to_uppercase() == val,
                            "COLOR" => c
                                .color
                                .index()
                                .map(|n| n.to_string() == val)
                                .unwrap_or(val == "BYLAYER"),
                            "LINETYPE" => c.linetype.to_uppercase() == val,
                            _ => false,
                        }
                    })
                    .map(|e| e.common().handle)
                    .collect();

                if prop.is_empty() {
                    self.command_line
                        .push_info("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE <value>");
                } else if matched.is_empty() {
                    self.command_line
                        .push_output("QSELECT: no matching entities.");
                } else {
                    self.tabs[i].scene.deselect_all();
                    for h in &matched {
                        self.tabs[i].scene.select_entity(*h, false);
                    }
                    self.command_line
                        .push_output(&format!("QSELECT: {} entity(ies) selected.", matched.len()));
                    self.refresh_properties();
                }
            }

            // ── COUNT — entity statistics ─────────────────────────────────────
            "COUNT" => {
                use crate::command::KeywordCommand;
                let c = KeywordCommand::new(
                    "COUNT",
                    "COUNT  tally  [All (by type) / by Layer]:",
                    vec![("All", "TYPE", None), ("By layer", "LAYER", None)],
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[self.active_tab].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("COUNT ") => {
                let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase());
                let mut counts: std::collections::BTreeMap<String, usize> = Default::default();
                for e in self.tabs[i].scene.document.entities() {
                    let layer = &e.common().layer;
                    let type_name = crate::entities::names::dxf_name(e);
                    let key = match &filter {
                        Some(f) if f == "LAYER" => layer.clone(),
                        Some(f) if f == "TYPE" => type_name.to_string(),
                        Some(f) => {
                            // Filter by layer name
                            if layer.to_uppercase() != *f {
                                continue;
                            }
                            type_name.to_string()
                        }
                        None => type_name.to_string(),
                    };
                    *counts.entry(key).or_default() += 1;
                }
                let total: usize = counts.values().sum();
                for (k, n) in &counts {
                    self.command_line.push_output(&format!("  {k}: {n}"));
                }
                self.command_line
                    .push_output(&format!("COUNT: {total} entity(ies) total."));
            }

            "DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
                let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
                return Some(Task::done(Message::DataExtractionSave(csv)));
            }

            // ── Find / Replace ────────────────────────────────────────────────
            // FIND <search>              — list all Text/MText/Dimension containing <search>
            // FIND <search> REPLACE <rep> — replace first occurrence (case-insensitive)
            // FINDALL <search> REPLACE <rep> — replace all occurrences
            "FIND" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new(
                    "FIND",
                    "FIND  text to find  (add REPLACE <text> by typing):",
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[self.active_tab].active_cmd = Some(Box::new(c));
            }
            "FINDALL" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new(
                    "FINDALL",
                    "FINDALL  text to find  (add REPLACE <text> by typing):",
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[self.active_tab].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("FIND ") || cmd.starts_with("FINDALL ") => {
                let all_mode = cmd.starts_with("FINDALL");
                let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");

                // Split at " REPLACE " keyword (case-insensitive)
                let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ")
                {
                    (&rest[..pos], Some(rest[pos + 9..].trim()))
                } else {
                    (rest, None)
                };

                if search.is_empty() {
                    self.command_line.push_error("FIND: specify search text.");
                } else {
                    let search_lc = search.to_lowercase();
                    let mut count = 0usize;
                    let handles: Vec<acadrust::Handle> = self.tabs[i]
                        .scene
                        .document
                        .entities()
                        .filter_map(|e| {
                            use crate::entities::traits::EntityTypeOps;
                            let txt = e.text_content()?;
                            if txt.to_lowercase().contains(&search_lc) {
                                Some(e.common().handle)
                            } else {
                                None
                            }
                        })
                        .collect();

                    if let Some(rep) = replacement {
                        // Replace mode
                        let targets: Vec<_> = if all_mode {
                            handles.clone()
                        } else {
                            handles.iter().copied().take(1).collect()
                        };
                        if targets.is_empty() {
                            self.command_line
                                .push_output(&format!("FIND: \"{}\" not found.", search));
                        } else {
                            self.push_undo_snapshot(i, "FIND/REPLACE");
                            for h in &targets {
                                if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
                                    crate::entities::traits::EntityTypeOps::replace_text(
                                        e, search, rep,
                                    );
                                    count += 1;
                                }
                            }
                            self.tabs[i].dirty = true;
                            self.command_line.push_output(&format!(
                                "FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".",
                                count, search, rep
                            ));
                            self.refresh_properties();
                        }
                    } else {
                        // List mode
                        if handles.is_empty() {
                            self.command_line
                                .push_output(&format!("FIND: \"{}\" not found.", search));
                        } else {
                            for h in &handles {
                                if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
                                    use crate::entities::traits::EntityTypeOps;
                                    let txt = e.text_content().unwrap_or_default();
                                    self.command_line.push_output(&format!(
                                        "  Handle {:X}: \"{}\"",
                                        h.value(),
                                        txt
                                    ));
                                }
                            }
                            self.command_line.push_output(&format!(
                                "FIND: {} match(es) for \"{}\".",
                                handles.len(),
                                search
                            ));
                        }
                    }
                }
            }

            _ => return None,
        }
        Some(self.finish_dispatch(cmd))
    }

    /// ADDSELECTED — start the draw command that creates the same kind of
    /// object as the currently-selected one, adopting its general properties
    /// (layer, colour, linetype, lineweight, linetype scale) as the current
    /// defaults so the new object is drawn to match. Issue #239.
    pub(super) fn cmd_add_selected(&mut self, i: usize) -> Task<Message> {
        // Use the first selected object as the template.
        let Some(handle) = self.tabs[i].scene.selected.iter().next().copied() else {
            self.command_line.push_info(
                "ADDSELECTED: select an object first, then run ADDSELECTED to draw a new one like it.",
            );
            return Task::none();
        };
        // Pull the template's type + general properties into owned values, then
        // drop the borrow so the document can be mutated below.
        let info = self.tabs[i].scene.document.get_entity(handle).map(|e| {
            let c = e.common();
            (
                add_selected_verb(e),
                crate::entities::names::dxf_name(e).to_string(),
                c.layer.clone(),
                c.color,
                c.linetype.clone(),
                c.linetype_scale,
                c.line_weight,
                // A dimension template also carries its dimension style, adopted
                // as the current DIMSTYLE so the cloned dimension matches (#239).
                match e {
                    acadrust::EntityType::Dimension(d) => Some(d.base().style_name.clone()),
                    _ => None,
                },
            )
        });
        let Some((verb, kind, layer, color, linetype, lt_scale, lw, template_dimstyle)) = info
        else {
            self.command_line
                .push_error("ADDSELECTED: selected object not found.");
            return Task::none();
        };
        let Some(verb) = verb else {
            self.command_line
                .push_error(&format!("ADDSELECTED: creating a new {kind} is not supported."));
            return Task::none();
        };

        // Clear the selection so adopting the properties as defaults doesn't
        // rewrite the template, and so the draw starts on a clean slate.
        self.tabs[i].scene.deselect_all();

        // Snapshot the current drawing defaults so the override below reverts
        // once the launched draw command ends — ADDSELECTED must adopt the
        // template's properties for the new object without permanently changing
        // CLAYER / CECOLOR / CELTYPE / CELWEIGHT (issue #239).
        let restore = crate::app::AddSelectedRestore {
            layer_name: self.tabs[i].scene.document.header.current_layer_name.clone(),
            layer_handle: self.tabs[i].scene.document.header.current_layer_handle,
            color: self.tabs[i].scene.document.header.current_entity_color,
            linetype_name: self.tabs[i].scene.document.header.current_linetype_name.clone(),
            linetype_handle: self.tabs[i].scene.document.header.current_linetype_handle,
            line_weight: self.tabs[i].scene.document.header.current_line_weight,
            lt_scale: self.tabs[i].scene.document.header.current_entity_linetype_scale,
            dimstyle_name: self.tabs[i].scene.document.header.current_dimstyle_name.clone(),
            dimstyle_handle: self.tabs[i].scene.document.header.current_dimstyle_handle,
            tab_active_layer: self.tabs[i].active_layer.clone(),
            tab_layers_current: self.tabs[i].layers.current_layer.clone(),
            ribbon_layer: self.ribbon.active_layer.clone(),
            ribbon_color: self.ribbon.active_color,
            ribbon_linetype: self.ribbon.active_linetype.clone(),
            ribbon_lineweight: self.ribbon.active_lineweight,
        };
        self.add_selected_restore = Some(restore);

        // Adopt the template's general properties as the current defaults. The
        // entity-creation path stamps new objects from the tab's active layer
        // and the ribbon's active colour / linetype / lineweight, mirrored into
        // the header so they persist (CLAYER / CECOLOR / CELTYPE / CELWEIGHT /
        // CELTSCALE).
        let layer_handle = self.tabs[i]
            .scene
            .document
            .layers
            .get(&layer)
            .map(|l| l.handle)
            .unwrap_or(acadrust::types::Handle::NULL);
        let lt_handle = self.tabs[i]
            .scene
            .document
            .line_types
            .iter()
            .find(|x| x.name.eq_ignore_ascii_case(&linetype))
            .map(|x| x.handle)
            .unwrap_or(acadrust::types::Handle::NULL);
        {
            let header = &mut self.tabs[i].scene.document.header;
            header.current_layer_name = layer.clone();
            header.current_layer_handle = layer_handle;
            header.current_entity_color = color;
            header.current_linetype_name = linetype.clone();
            header.current_linetype_handle = lt_handle;
            header.current_line_weight = lw.value();
            header.current_entity_linetype_scale = lt_scale;
        }
        // A dimension template also sets the current DIMSTYLE so the cloned
        // dimension inherits the template's dimension style (#239).
        if let Some(ds) = &template_dimstyle {
            let ds_handle = self.tabs[i]
                .scene
                .document
                .dim_styles
                .iter()
                .find(|s| s.name.eq_ignore_ascii_case(ds))
                .map(|s| s.handle)
                .unwrap_or(acadrust::types::Handle::NULL);
            let header = &mut self.tabs[i].scene.document.header;
            header.current_dimstyle_name = ds.clone();
            header.current_dimstyle_handle = ds_handle;
        }
        self.tabs[i].active_layer = layer.clone();
        self.tabs[i].layers.current_layer = layer.clone();
        self.tabs[i].dirty = true;
        self.ribbon.active_layer = layer.clone();
        self.ribbon.active_color = color;
        self.ribbon.active_linetype = linetype;
        self.ribbon.active_lineweight = lw;
        self.refresh_properties();

        self.command_line.push_output(&format!(
            "Add Selected: drawing a new {kind} on layer \"{layer}\"."
        ));
        // Launch the matching draw command (installs its interactive step).
        self.dispatch_command(verb)
    }

    /// Restore the drawing defaults ADDSELECTED overrode, once the draw command
    /// it launched ends (commit / cancel / interrupt). No-op unless an
    /// ADDSELECTED override is pending. Issue #239.
    pub(crate) fn restore_add_selected_defaults(&mut self) {
        let Some(r) = self.add_selected_restore.take() else {
            return;
        };
        let i = self.active_tab;
        {
            let h = &mut self.tabs[i].scene.document.header;
            h.current_layer_name = r.layer_name;
            h.current_layer_handle = r.layer_handle;
            h.current_entity_color = r.color;
            h.current_linetype_name = r.linetype_name;
            h.current_linetype_handle = r.linetype_handle;
            h.current_line_weight = r.line_weight;
            h.current_entity_linetype_scale = r.lt_scale;
            h.current_dimstyle_name = r.dimstyle_name;
            h.current_dimstyle_handle = r.dimstyle_handle;
        }
        self.tabs[i].active_layer = r.tab_active_layer;
        self.tabs[i].layers.current_layer = r.tab_layers_current;
        self.ribbon.active_layer = r.ribbon_layer;
        self.ribbon.active_color = r.ribbon_color;
        self.ribbon.active_linetype = r.ribbon_linetype;
        self.ribbon.active_lineweight = r.ribbon_lineweight;
        self.refresh_properties();
    }
}

/// Map a template entity to the draw-command verb that creates the same kind of
/// object, or `None` when there is no interactive creator for it. Used by
/// ADDSELECTED (issue #239).
fn add_selected_verb(entity: &acadrust::EntityType) -> Option<&'static str> {
    use acadrust::EntityType;
    Some(match entity {
        EntityType::Point(_) => "POINT",
        EntityType::Line(_) => "LINE",
        EntityType::Circle(_) => "CIRCLE",
        EntityType::Arc(_) => "ARC",
        EntityType::Ellipse(_) => "ELLIPSE",
        EntityType::LwPolyline(_)
        | EntityType::Polyline(_)
        | EntityType::Polyline2D(_)
        | EntityType::Polyline3D(_) => "PLINE",
        EntityType::Text(_) => "TEXT",
        EntityType::MText(_) => "MTEXT",
        EntityType::Spline(_) => "SPLINE",
        EntityType::Hatch(_) => "HATCH",
        EntityType::Solid(_) => "SOLID",
        EntityType::Ray(_) => "RAY",
        EntityType::XLine(_) => "XLINE",
        // Dimensions launch the matching dimension command by their stored type
        // (issue #239). Angular 2-line and 3-point both use DIMANGULAR.
        EntityType::Dimension(d) => {
            use acadrust::entities::DimensionType;
            match d.base().dimension_type {
                DimensionType::Linear => "DIMLINEAR",
                DimensionType::Aligned => "DIMALIGNED",
                DimensionType::Angular | DimensionType::Angular3Point => "DIMANGULAR",
                DimensionType::Diameter => "DIMDIAMETER",
                DimensionType::Radius => "DIMRADIUS",
                DimensionType::Ordinate => "DIMORDINATE",
                DimensionType::ArcLength => "DIMARC",
                DimensionType::LargeRadial => "DIMJOGGED",
            }
        }
        _ => return None,
    })
}

fn entity_list_details(entity: &acadrust::EntityType) -> String {
    use std::f64::consts::PI;
    match entity {
        acadrust::EntityType::Line(l) => format!(
            "from ({:.4},{:.4},{:.4}) to ({:.4},{:.4},{:.4})  len={:.4}",
            l.start.x,
            l.start.y,
            l.start.z,
            l.end.x,
            l.end.y,
            l.end.z,
            ((l.end.x - l.start.x).powi(2)
                + (l.end.y - l.start.y).powi(2)
                + (l.end.z - l.start.z).powi(2))
            .sqrt()
        ),
        acadrust::EntityType::Circle(c) => format!(
            "center ({:.4},{:.4},{:.4})  r={:.4}  area={:.4}",
            c.center.x,
            c.center.y,
            c.center.z,
            c.radius,
            PI * c.radius * c.radius
        ),
        acadrust::EntityType::Arc(a) => format!(
            "center ({:.4},{:.4},{:.4})  r={:.4}  start={:.2}° end={:.2}°",
            a.center.x,
            a.center.y,
            a.center.z,
            a.radius,
            a.start_angle.to_degrees(),
            a.end_angle.to_degrees()
        ),
        acadrust::EntityType::LwPolyline(p) => format!(
            "{} vertices  closed={}  elevation={:.4}",
            p.vertices.len(),
            p.is_closed,
            p.elevation
        ),
        acadrust::EntityType::Text(t) => format!(
            "\"{}\"  h={:.4}  at ({:.4},{:.4})",
            t.value, t.height, t.insertion_point.x, t.insertion_point.y
        ),
        acadrust::EntityType::MText(t) => format!(
            "\"{}\"  h={:.4}  at ({:.4},{:.4})",
            t.value.chars().take(40).collect::<String>(),
            t.height,
            t.insertion_point.x,
            t.insertion_point.y
        ),
        acadrust::EntityType::Insert(ins) => format!(
            "block=\"{}\"  at ({:.4},{:.4},{:.4})  scale=({:.4},{:.4},{:.4})  rot={:.2}°",
            ins.block_name,
            ins.insert_point.x,
            ins.insert_point.y,
            ins.insert_point.z,
            ins.x_scale(),
            ins.y_scale(),
            ins.z_scale(),
            ins.rotation.to_degrees()
        ),
        acadrust::EntityType::Spline(s) => format!(
            "{} ctrl pts  degree={}  closed={}",
            s.control_points.len(),
            s.degree,
            s.flags.closed
        ),
        acadrust::EntityType::Ellipse(e) => format!(
            "center ({:.4},{:.4})  major_len={:.4}  ratio={:.4}",
            e.center.x,
            e.center.y,
            e.major_axis_length(),
            e.minor_axis_ratio
        ),
        _ => String::new(),
    }
}

fn flatten_entity_z(entity: &mut acadrust::EntityType) {
    match entity {
        acadrust::EntityType::Line(l) => {
            l.start.z = 0.0;
            l.end.z = 0.0;
        }
        acadrust::EntityType::Circle(c) => {
            c.center.z = 0.0;
        }
        acadrust::EntityType::Arc(a) => {
            a.center.z = 0.0;
        }
        acadrust::EntityType::LwPolyline(p) => {
            p.elevation = 0.0;
        }
        acadrust::EntityType::Text(t) => {
            t.insertion_point.z = 0.0;
        }
        acadrust::EntityType::MText(t) => {
            t.insertion_point.z = 0.0;
        }
        acadrust::EntityType::Insert(ins) => {
            ins.insert_point.z = 0.0;
        }
        acadrust::EntityType::Point(p) => {
            p.location.z = 0.0;
        }
        acadrust::EntityType::Spline(s) => {
            for cp in &mut s.control_points {
                cp.z = 0.0;
            }
            for fp in &mut s.fit_points {
                fp.z = 0.0;
            }
        }
        acadrust::EntityType::Ellipse(e) => {
            e.center.z = 0.0;
        }
        _ => {}
    }
}

// ── DATAEXTRACTION ─────────────────────────────────────────────────────────

/// Build a CSV string with one row per entity in model space.
/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo
fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
    use acadrust::EntityType;

    let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n");

    let ms_handle = doc.header.model_space_block_handle;
    for e in doc.entities() {
        // Skip Block/EndBlock sentinels and paper-space entities.
        if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
            continue;
        }
        if !ms_handle.is_null() && e.common().owner_handle != ms_handle {
            continue;
        }
        let type_name = crate::entities::names::dxf_name(e);
        let handle = format!("{:X}", e.common().handle.value());
        let layer = csv_escape(&e.common().layer);
        let color = format!("{}", e.common().color);
        let lt = csv_escape(&e.common().linetype);
        let extra = csv_escape(&entity_extra_info(e));
        out.push_str(&format!(
            "{type_name},{handle},{layer},{color},{lt},{extra}\n"
        ));
    }
    out
}

/// Return a short geometry summary for CSV ExtraInfo column.
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
    use acadrust::EntityType;
    match entity {
        EntityType::Line(e) => format!(
            "({:.3},{:.3})-({:.3},{:.3})",
            e.start.x, e.start.y, e.end.x, e.end.y
        ),
        EntityType::Circle(e) => {
            format!("C({:.3},{:.3}) R={:.3}", e.center.x, e.center.y, e.radius)
        }
        EntityType::Arc(e) => format!(
            "C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°",
            e.center.x,
            e.center.y,
            e.radius,
            e.start_angle.to_degrees(),
            e.end_angle.to_degrees()
        ),
        EntityType::Text(e) => e.value.clone(),
        EntityType::MText(e) => e.value.chars().take(60).collect(),
        EntityType::Insert(e) => format!(
            "BLK={} @({:.3},{:.3})",
            e.block_name, e.insert_point.x, e.insert_point.y
        ),
        EntityType::LwPolyline(e) => format!("{} vertices", e.vertices.len()),
        EntityType::Polyline(e) => format!("{} vertices", e.vertices.len()),
        EntityType::Polyline2D(e) => format!("{} vertices", e.vertices.len()),
        EntityType::Polyline3D(e) => format!("{} vertices", e.vertices.len()),
        EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
        EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
        EntityType::Spline(e) => format!("{} ctrl pts", e.control_points.len()),
        _ => String::new(),
    }
}

/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline).
fn csv_escape(s: &str) -> String {
    if s.contains(',') || s.contains('"') || s.contains('\n') {
        format!("\"{}\"", s.replace('"', "\"\""))
    } else {
        s.to_string()
    }
}

// ── CAL — arithmetic expression evaluator ──────────────────────────────────
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
// decimal numbers. Self-contained (no external dependency).
fn arith_eval(expr: &str) -> Result<f64, String> {
    let mut p = ArithParser {
        chars: expr.chars().filter(|c| !c.is_whitespace()).collect(),
        pos: 0,
    };
    let v = p.expr()?;
    if p.pos != p.chars.len() {
        return Err(format!("unexpected '{}'", p.chars[p.pos]));
    }
    Ok(v)
}

impl OpenCADStudio {
    /// Open the attribute editor. If a single block with attributes is already
    /// selected it opens directly on that block; otherwise it starts the
    /// block-pick command and opens once a block is chosen (see
    /// `command_driver`). Shared by ATTEDIT and its command aliases
    /// ATTMAN / BATTMAN.
    pub(in crate::app) fn open_attedit_dialog(&mut self) {
        let i = self.active_tab;
        let selected_attr_insert = {
            let sel = self.tabs[i].scene.selected_entities();
            if sel.len() == 1 {
                let (h, e) = sel[0];
                match e {
                    acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => Some(h),
                    _ => None,
                }
            } else {
                None
            }
        };
        if let Some(handle) = selected_attr_insert {
            self.open_attribute_editor(handle);
        } else {
            use crate::modules::draw::modify::attedit::AtteditCommand;
            let cmd_obj = AtteditCommand::new();
            self.command_line.push_info(&cmd_obj.prompt());
            self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
        }
    }
}

struct ArithParser {
    chars: Vec<char>,
    pos: usize,
}

impl ArithParser {
    fn peek(&self) -> Option<char> {
        self.chars.get(self.pos).copied()
    }

    // expr = term (('+' | '-') term)*
    fn expr(&mut self) -> Result<f64, String> {
        let mut v = self.term()?;
        while let Some(op) = self.peek() {
            if op == '+' || op == '-' {
                self.pos += 1;
                let rhs = self.term()?;
                v = if op == '+' { v + rhs } else { v - rhs };
            } else {
                break;
            }
        }
        Ok(v)
    }

    // term = factor (('*' | '/') factor)*
    fn term(&mut self) -> Result<f64, String> {
        let mut v = self.factor()?;
        while let Some(op) = self.peek() {
            if op == '*' || op == '/' {
                self.pos += 1;
                let rhs = self.factor()?;
                if op == '/' {
                    if rhs == 0.0 {
                        return Err("division by zero".into());
                    }
                    v /= rhs;
                } else {
                    v *= rhs;
                }
            } else {
                break;
            }
        }
        Ok(v)
    }

    // factor = ('+' | '-') factor | '(' expr ')' | number
    fn factor(&mut self) -> Result<f64, String> {
        match self.peek() {
            Some('+') => {
                self.pos += 1;
                self.factor()
            }
            Some('-') => {
                self.pos += 1;
                Ok(-self.factor()?)
            }
            Some('(') => {
                self.pos += 1;
                let v = self.expr()?;
                if self.peek() != Some(')') {
                    return Err("missing ')'".into());
                }
                self.pos += 1;
                Ok(v)
            }
            Some(c) if c.is_ascii_digit() || c == '.' => self.number(),
            Some(c) => Err(format!("unexpected '{c}'")),
            None => Err("unexpected end of expression".into()),
        }
    }

    fn number(&mut self) -> Result<f64, String> {
        let start = self.pos;
        while let Some(c) = self.peek() {
            if c.is_ascii_digit() || c == '.' {
                self.pos += 1;
            } else {
                break;
            }
        }
        let s: String = self.chars[start..self.pos].iter().collect();
        s.parse::<f64>().map_err(|_| format!("bad number '{s}'"))
    }
}
