use super::*;

impl OpenCADStudio {
    pub(super) fn dispatch_display(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
        match cmd {
            // Interactive pan: left-drag pans the view until Esc. The only pan
            // path when there is no middle mouse button (trackpad / web).
            "PAN" => {
                self.tabs[i].pan_mode = true;
                self.clear_navigation_hover(i);
                self.command_line
                    .push_output(crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref());
            }

            // ── TABLE structure and cell editing ───────────────────────────────
            cmd if cmd.starts_with("TABLE ") => {
                let rest = cmd.trim_start_matches("TABLE").trim();
                #[derive(Clone)]
                enum TableAction {
                    Cell(usize, usize, String),
                    InsertRow(usize),
                    DeleteRow(usize),
                    InsertColumn(usize),
                    DeleteColumn(usize),
                    Merge(usize, usize, usize, usize),
                    Unmerge(usize, usize),
                    RowHeight(usize, f64),
                    ColumnWidth(usize, f64),
                    Lock(usize, usize, bool),
                    Block(usize, usize, String),
                    Formula(usize, usize, String),
                    Field(usize, usize, acadrust::Handle),
                }
                let words: Vec<&str> = rest.split_whitespace().collect();
                let integer = |index: usize| {
                    words.get(index).and_then(|value| value.parse::<usize>().ok())
                };
                let real = |index: usize| {
                    words.get(index).and_then(|value| value.parse::<f64>().ok())
                };
                let action = match words.first().map(|word| word.to_ascii_uppercase()).as_deref() {
                    Some("CELL") => {
                        let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
                        match (integer(1), integer(2)) {
                            (Some(row), Some(column)) => Some(TableAction::Cell(
                                row,
                                column,
                                parts.get(3).copied().unwrap_or("").to_string(),
                            )),
                            _ => None,
                        }
                    }
                    Some("INSERTROW") => integer(1).map(TableAction::InsertRow),
                    Some("DELETEROW") => integer(1).map(TableAction::DeleteRow),
                    Some("INSERTCOLUMN") | Some("INSERTCOL") => {
                        integer(1).map(TableAction::InsertColumn)
                    }
                    Some("DELETECOLUMN") | Some("DELETECOL") => {
                        integer(1).map(TableAction::DeleteColumn)
                    }
                    Some("MERGE") => match (integer(1), integer(2), integer(3), integer(4)) {
                        (Some(r1), Some(c1), Some(r2), Some(c2)) => {
                            Some(TableAction::Merge(r1, c1, r2, c2))
                        }
                        _ => None,
                    },
                    Some("UNMERGE") => match (integer(1), integer(2)) {
                        (Some(row), Some(column)) => Some(TableAction::Unmerge(row, column)),
                        _ => None,
                    },
                    Some("ROWHEIGHT") => match (integer(1), real(2)) {
                        (Some(row), Some(height)) if height > 0.0 => {
                            Some(TableAction::RowHeight(row, height))
                        }
                        _ => None,
                    },
                    Some("COLUMNWIDTH") | Some("COLWIDTH") => match (integer(1), real(2)) {
                        (Some(column), Some(width)) if width > 0.0 => {
                            Some(TableAction::ColumnWidth(column, width))
                        }
                        _ => None,
                    },
                    Some("LOCK") => match (integer(1), integer(2), words.get(3)) {
                        (Some(row), Some(column), Some(value)) => Some(TableAction::Lock(
                            row,
                            column,
                            !value.eq_ignore_ascii_case("OFF"),
                        )),
                        _ => None,
                    },
                    Some("BLOCK") => {
                        let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
                        match (integer(1), integer(2), parts.get(3)) {
                            (Some(row), Some(column), Some(name)) if !name.trim().is_empty() => {
                                Some(TableAction::Block(row, column, name.trim().to_string()))
                            }
                            _ => None,
                        }
                    }
                    Some("FORMULA") => {
                        let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
                        match (integer(1), integer(2), parts.get(3)) {
                            (Some(row), Some(column), Some(expression))
                                if !expression.trim().is_empty() =>
                            {
                                Some(TableAction::Formula(
                                    row,
                                    column,
                                    expression.trim().to_string(),
                                ))
                            }
                            _ => None,
                        }
                    }
                    Some("FIELD") => match (integer(1), integer(2), words.get(3)) {
                        (Some(row), Some(column), Some(value)) => {
                            u64::from_str_radix(value.trim_start_matches("0x"), 16)
                                .ok()
                                .map(|handle| {
                                    TableAction::Field(
                                        row,
                                        column,
                                        acadrust::Handle::new(handle),
                                    )
                                })
                        }
                        _ => None,
                    },
                    _ => None,
                };
                let Some(action) = action else {
                    self.command_line.push_info(
                        crate::t!("Usage: TABLE CELL | INSERTROW | DELETEROW | INSERTCOL | DELETECOL | MERGE | UNMERGE | ROWHEIGHT | COLWIDTH | LOCK | BLOCK | FORMULA | FIELD").as_ref(),
                    );
                    return Some(Task::none());
                };
                let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(handle, _)| *handle)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if selected_handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("TABLE: select a table first.").as_ref());
                    return Some(Task::none());
                }
                let block_handle = if let TableAction::Block(_, _, name) = &action {
                    self.tabs[i]
                        .scene
                        .document
                        .block_records
                        .iter()
                        .find(|record| record.name.eq_ignore_ascii_case(name))
                        .map(|record| record.handle)
                } else {
                    None
                };
                if matches!(action, TableAction::Block(_, _, _)) && block_handle.is_none() {
                    self.command_line.push_error(
                        crate::t!("TABLE: block definition not found.").as_ref(),
                    );
                    return Some(Task::none());
                }
                if let TableAction::Field(_, _, field_handle) = &action {
                    if !self.tabs[i].scene.document.fields.contains_key(field_handle) {
                        self.command_line.push_error(
                            crate::t!("TABLE: field handle not found.").as_ref(),
                        );
                        return Some(Task::none());
                    }
                }
                self.push_undo_snapshot(i, "TABLE EDIT");
                let mut changed = false;
                for handle in &selected_handles {
                    let Some(acadrust::EntityType::Table(table)) =
                        self.tabs[i].scene.document.get_entity_mut(*handle)
                    else {
                        continue;
                    };
                    match &action {
                        TableAction::Cell(row, column, text) => {
                            if let Some(cell) = table.cell_mut(*row, *column) {
                                use acadrust::entities::table::CellStateFlags;
                                if !cell.state.intersects(
                                    CellStateFlags::CONTENT_LOCKED
                                        | CellStateFlags::CONTENT_READ_ONLY,
                                ) {
                                    cell.set_text(text);
                                    changed = true;
                                }
                            }
                        }
                        TableAction::InsertRow(row) if *row <= table.row_count() => {
                            let height = table
                                .rows
                                .get((*row).min(table.row_count().saturating_sub(1)))
                                .map(|row| row.height)
                                .unwrap_or(0.5);
                            table.insert_row(*row);
                            table.set_row_height(*row, height);
                            changed = true;
                        }
                        TableAction::DeleteRow(row)
                            if table.row_count() > 1 && *row < table.row_count() =>
                        {
                            table.remove_row(*row);
                            changed = true;
                        }
                        TableAction::InsertColumn(column) if *column <= table.column_count() => {
                            let width = table
                                .columns
                                .get((*column).min(table.column_count().saturating_sub(1)))
                                .map(|column| column.width)
                                .unwrap_or(2.0);
                            table.insert_column(*column, width);
                            changed = true;
                        }
                        TableAction::DeleteColumn(column)
                            if table.column_count() > 1 && *column < table.column_count() =>
                        {
                            table.remove_column(*column);
                            changed = true;
                        }
                        TableAction::Merge(r1, c1, r2, c2)
                            if *r1 < table.row_count()
                                && *r2 < table.row_count()
                                && *c1 < table.column_count()
                                && *c2 < table.column_count() =>
                        {
                            table.merge_cells(acadrust::entities::table::CellRange::new(
                                (*r1).min(*r2),
                                (*c1).min(*c2),
                                (*r1).max(*r2),
                                (*c1).max(*c2),
                            ));
                            changed = true;
                        }
                        TableAction::Unmerge(row, column) => {
                            table.unmerge_cell(*row, *column);
                            changed = true;
                        }
                        TableAction::RowHeight(row, height) if *row < table.row_count() => {
                            table.set_row_height(*row, *height);
                            changed = true;
                        }
                        TableAction::ColumnWidth(column, width)
                            if *column < table.column_count() =>
                        {
                            table.set_column_width(*column, *width);
                            changed = true;
                        }
                        TableAction::Lock(row, column, locked) => {
                            if let Some(cell) = table.cell_mut(*row, *column) {
                                use acadrust::entities::table::CellStateFlags;
                                cell.state.set(CellStateFlags::CONTENT_LOCKED, *locked);
                                cell.state.set(CellStateFlags::FORMAT_LOCKED, *locked);
                                changed = true;
                            }
                        }
                        TableAction::Block(row, column, _) => {
                            if let (Some(handle), Some(cell)) =
                                (block_handle, table.cell_mut(*row, *column))
                            {
                                use acadrust::entities::table::{
                                    CellContent, CellStateFlags, CellType,
                                };
                                if !cell.state.intersects(
                                    CellStateFlags::CONTENT_LOCKED
                                        | CellStateFlags::CONTENT_READ_ONLY,
                                ) {
                                    cell.contents.clear();
                                    cell.contents.push(CellContent::block(handle));
                                    cell.cell_type = CellType::Block;
                                    changed = true;
                                }
                            }
                        }
                        TableAction::Formula(row, column, expression) => {
                            if let Some(cell) = table.cell_mut(*row, *column) {
                                use acadrust::entities::table::CellStateFlags;
                                if !cell.state.intersects(
                                    CellStateFlags::CONTENT_LOCKED
                                        | CellStateFlags::CONTENT_READ_ONLY,
                                ) {
                                    let formula = if expression.starts_with('=') {
                                        expression.clone()
                                    } else {
                                        format!("={expression}")
                                    };
                                    cell.set_text(&formula);
                                    changed = true;
                                }
                            }
                        }
                        TableAction::Field(row, column, field_handle) => {
                            let mut attached = false;
                            if let Some(cell) = table.cell_mut(*row, *column) {
                                use acadrust::entities::table::{
                                    CellContent, CellStateFlags, CellType,
                                };
                                if !cell.state.intersects(
                                    CellStateFlags::CONTENT_LOCKED
                                        | CellStateFlags::CONTENT_READ_ONLY,
                                ) {
                                    let mut content = CellContent::text("");
                                    content.field_handle = Some(*field_handle);
                                    cell.contents.clear();
                                    cell.contents.push(content);
                                    cell.cell_type = CellType::Text;
                                    attached = true;
                                }
                            }
                            if attached {
                                if !table.field_handles.contains(field_handle) {
                                    table.field_handles.push(*field_handle);
                                }
                                changed = true;
                            }
                        }
                        _ => {}
                    }
                }
                if changed {
                    self.invalidate_property_targets(i, &selected_handles);
                    self.tabs[i].dirty = true;
                    self.refresh_properties();
                    self.command_line
                        .push_output(crate::t!("TABLE: edit applied.").as_ref());
                } else {
                    self.command_line
                        .push_error(crate::t!(
                            "TABLE: row/column is out of range or the cell is locked."
                        ).as_ref());
                }
            }

            // ── UCSICON — toggle UCS icon visibility on all viewports ────────────
            // UCSICON ON       — show UCS icon in all viewports
            // UCSICON OFF      — hide UCS icon in all viewports
            // UCSICON NOORIGIN — show icon but not at origin (show at corner)
            // UCSICON ORIGIN   — show icon at UCS origin
            "UCSICON" => {
                use crate::command::KeywordCommand;
                let c = KeywordCommand::new(
                    "UCSICON",
                    "UCSICON  [On / Off / NoOrigin / Origin]:",
                    vec![
                        ("On", "ON", None),
                        ("Off", "OFF", None),
                        ("NoOrigin", "NOORIGIN", None),
                        ("Origin", "ORIGIN", None),
                    ],
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("UCSICON ") => {
                let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
                match sub.as_str() {
                    "ON" | "OFF" | "NOORIGIN" | "ORIGIN" => {
                        self.push_undo_snapshot(i, "UCSICON");
                        let visible = sub != "OFF";
                        let at_origin = sub == "ORIGIN";
                        // Update model-space icon flags.
                        self.show_ucs_icon = visible;
                        self.ribbon.set_ucs_icon(visible);
                        if sub == "NOORIGIN" || sub == "ORIGIN" {
                            self.ucs_icon_at_origin = at_origin;
                        }
                        let mut count = 0usize;
                        for entity in self.tabs[i].scene.document.entities_mut() {
                            if let acadrust::EntityType::Viewport(vp) = entity {
                                vp.status.ucs_icon_visible = visible;
                                if sub == "NOORIGIN" || sub == "ORIGIN" {
                                    vp.status.ucs_icon_at_origin = at_origin;
                                }
                                count += 1;
                            }
                        }
                        self.tabs[i].dirty = true;
                        self.command_line.push_output(crate::tf!(
                            "UCSICON {sub}: updated {count} viewport(s) + model space."
                        ).as_ref());
                    }
                    "" => {
                        // Bare UCSICON toggles visibility.
                        self.push_undo_snapshot(i, "UCSICON");
                        let visible = !self.show_ucs_icon;
                        self.show_ucs_icon = visible;
                        self.ribbon.set_ucs_icon(visible);
                        for entity in self.tabs[i].scene.document.entities_mut() {
                            if let acadrust::EntityType::Viewport(vp) = entity {
                                vp.status.ucs_icon_visible = visible;
                            }
                        }
                        self.tabs[i].dirty = true;
                        let state = if visible { "ON" } else { "OFF" };
                        self.command_line.push_output(crate::tf!("UCSICON {state}").as_ref());
                    }
                    _ => {
                        self.command_line
                            .push_info(crate::t!("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN").as_ref());
                    }
                }
            }

            // ── NAVVCUBE — toggle ViewCube visibility ────────────────────────────
            "NAVVCUBE" => {
                return Some(Task::done(Message::ToggleViewCube));
            }

            // ── LIMITS — drawing/grid boundary for the active space ─────────────
            "LIMITS" => {
                use crate::modules::view::limits::LimitsCommand;
                let (min, max) = self.tabs[i]
                    .scene
                    .current_drawing_limits()
                    .unwrap_or((glam::DVec2::ZERO, glam::DVec2::new(12.0, 9.0)));
                let command = LimitsCommand::new(min, max);
                self.command_line.push_info(&command.prompt());
                self.tabs[i].active_cmd = Some(Box::new(command));
            }
            "LIMITS ON" | "LIMITS OFF" => {
                let enabled = cmd.ends_with("ON");
                if self.tabs[i].scene.drawing_limit_check_enabled() != enabled {
                    self.push_undo_snapshot(i, "LIMITS");
                    self.tabs[i].scene.set_drawing_limit_check(enabled);
                    self.tabs[i].dirty = true;
                }
                self.command_line.push_output(if enabled {
                    "Limits checking ON."
                } else {
                    "Limits checking OFF."
                });
            }
            cmd if cmd.starts_with("LIMITS SET ") => {
                let tokens: Vec<&str> = cmd["LIMITS SET ".len()..].split_whitespace().collect();
                let values: Result<Vec<f64>, _> =
                    tokens.iter().map(|value| value.parse()).collect();
                let Ok(values) = values else {
                    self.command_line
                        .push_error(crate::t!("LIMITS: four numeric coordinates required.").as_ref());
                    return Some(Task::none());
                };
                if tokens.len() != 4 || !values.iter().all(|value| value.is_finite()) {
                    self.command_line
                        .push_error(crate::t!("LIMITS: four finite numeric coordinates required.").as_ref());
                } else {
                    let first = glam::DVec2::new(values[0], values[1]);
                    let opposite = glam::DVec2::new(values[2], values[3]);
                    let min = first.min(opposite);
                    let max = first.max(opposite);
                    if min.x == max.x || min.y == max.y {
                        self.command_line
                            .push_error(crate::t!("LIMITS: corners must define a non-zero area.").as_ref());
                    } else {
                        self.push_undo_snapshot(i, "LIMITS");
                        self.tabs[i].scene.set_current_drawing_limits(min, max);
                        self.tabs[i].dirty = true;
                        self.command_line.push_output(crate::tf!(
                            "Drawing limits: {:.4},{:.4} to {:.4},{:.4}.",
                            min.x, min.y, max.x, max.y
                        ).as_ref());
                    }
                }
            }

            // ── PROPERTIES — toggle Properties panel visibility ──────────────────
            "PROPERTIES" | "PROPS" => {
                return Some(Task::done(Message::ToggleProperties));
            }

            // ── FILETAB — toggle file/document tabs ──────────────────────────────
            "FILETAB" => {
                return Some(Task::done(Message::ToggleFileTabs));
            }

            // ── LAYOUTTAB — toggle layout/paper-space tabs ───────────────────────
            "LAYOUTTAB" => {
                return Some(Task::done(Message::ToggleLayoutTabs));
            }

            // ── REDRAW / REGEN ──────────────────────────────────────────────
            // REDRAW — force a full re-rasterize of the current viewport next
            // frame, WITHOUT touching the DB (never bumps geometry_epoch /
            // block_epoch, never pushes undo). Scope: Active. This arm does not
            // itself clear previews or cancel commands (the normal non-transparent
            // dispatch teardown, commands/mod.rs:90-96, governs that separately);
            // it only queues a per-viewport cache invalidation.
            "REDRAW" => {
                use crate::scene::ViewportRefreshScope;
                self.tabs[i].scene.request_refresh(ViewportRefreshScope::Active);
                self.command_line.push_output("REDRAW: viewport refreshed.");
                return Some(Task::none());
            }
            // REDRAWALL — force re-rasterize of every generated viewport.
            "REDRAWALL" => {
                use crate::scene::ViewportRefreshScope;
                self.tabs[i].scene.request_refresh(ViewportRefreshScope::All);
                self.command_line.push_output("REDRAWALL: viewports refreshed.");
                return Some(Task::none());
            }
            // REGEN — full model regeneration (bump_geometry: geometry_epoch AND
            // block_epoch; C4). No undo, no DB mutation, so do NOT touch
            // self.tabs[i].dirty — a newly opened drawing must not become
            // "modified" merely because tessellation caches were invalidated (C7).
            // REGENALL is functionally identical (C5).
            "REGEN" | "REGENALL" => {
                self.tabs[i].scene.bump_geometry();
                self.command_line.push_output("REGEN: regenerated model.");
                return Some(Task::none());
            }

            // ── Drafting aids — same toggles the status-bar pills drive, also
            //    reachable by name from the command line. ─────────────────────────
            // GRID — show / hide the reference grid.
            "GRID" => {
                return Some(Task::done(Message::ToggleGrid));
            }
            // SNAP — toggle cursor snapping to the grid.
            "SNAP" => {
                return Some(Task::done(Message::ToggleGridSnap));
            }
            // ISOPLANE — cycle the isometric drafting axis pair (F5).
            "ISOPLANE" => {
                return Some(Task::done(Message::CycleIsoPlane));
            }
            cmd if cmd.starts_with("ISOPLANE ") => {
                let plane = match cmd.trim_start_matches("ISOPLANE").trim() {
                    "LEFT" | "L" => Some(crate::app::settings::IsoPlane::Left),
                    "TOP" | "T" => Some(crate::app::settings::IsoPlane::Top),
                    "RIGHT" | "R" => Some(crate::app::settings::IsoPlane::Right),
                    _ => None,
                };
                if let Some(plane) = plane {
                    return Some(Task::done(Message::SetIsoPlane(plane)));
                }
                self.command_line
                    .push_error(crate::t!("ISOPLANE: expected Left, Top, or Right.").as_ref());
            }
            // ISODRAFT — enable or disable isometric drafting.
            "ISODRAFT" => {
                return Some(Task::done(Message::ToggleIsometricDrafting));
            }
            cmd if cmd.starts_with("ISODRAFT ") => {
                let requested = match cmd.trim_start_matches("ISODRAFT").trim() {
                    "1" | "ON" => Some(true),
                    "0" | "OFF" => Some(false),
                    _ => None,
                };
                match requested {
                    Some(value) if value != self.isometric_drafting => {
                        return Some(Task::done(Message::ToggleIsometricDrafting));
                    }
                    Some(_) => {}
                    None => self
                        .command_line
                        .push_error(crate::t!("ISODRAFT: expected On or Off.").as_ref()),
                }
            }
            // POLAR — toggle polar tracking.
            "POLAR" => {
                return Some(Task::done(Message::TogglePolar));
            }
            // DSETTINGS / OSNAP — open the drafting-settings popup, which is OCS's
            // settings surface (the persisted DYN/ORTHO/POLAR/OSNAP prefs).
            "DSETTINGS" | "OSNAP" => {
                return Some(Task::done(Message::ToggleSnapPopup));
            }
            // UNITS — length and angle formats, plus the insertion unit. The
            // status-bar button covers the length format alone; everything else
            // about how this drawing writes numbers is here.
            "UNITS" | "DDUNITS" => {
                return Some(Task::done(Message::OpenDrawingUnits));
            }

            // ── CLEANSCREEN — collapse the surrounding panels for a full canvas ──
            "CLEANSCREEN" => {
                return Some(Task::done(Message::ToggleCleanScreen));
            }
            // ── QUICKPROPERTIES — toggle the floating quick-properties readout ───
            "QUICKPROPERTIES" => {
                return Some(Task::done(Message::ToggleQuickProperties));
            }

            // ── TOOLPALETTES — not yet implemented ───────────────────────────────
            "TOOLPALETTES" => {
                self.command_line
                    .push_info(crate::t!("TOOLPALETTES: Tool Palettes not yet implemented.").as_ref());
            }

            // ── SHEETSET — not yet implemented ───────────────────────────────────
            "SHEETSET" => {
                self.command_line
                    .push_info(crate::t!("SHEETSET: Sheet Set Manager not yet implemented.").as_ref());
            }

            // ── XDATA — read/write extended entity data ──────────────────────────
            // XDATA LIST             — show all xdata records on selected entities
            // XDATA SET <app> <str>  — append a string xdata value for <app>
            // XDATA CLEAR            — remove all xdata from selected entities
            // XDATA CLEAR <app>      — remove xdata for a specific application
            "XDATA" => {
                use crate::command::SelectThenKeywordCommand;
                let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
                let c = SelectThenKeywordCommand::new(
                    "XDATA",
                    "XDATA  [List / Clear]  (SET <app> <value> by typing):",
                    vec![("List", "LIST", None), ("Clear", "CLEAR", None)],
                    has_sel,
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("XDATA ") => {
                use acadrust::xdata::{ExtendedDataRecord, XDataValue};
                let rest = cmd.trim_start_matches("XDATA").trim();
                let parts: Vec<&str> = rest.splitn(3, char::is_whitespace).collect();
                let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
                let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(h, _)| *h)
                    .collect();
                if selected_handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("XDATA: select entities first.").as_ref());
                } else {
                    match sub.as_str() {
                        "LIST" | "" => {
                            for sh in &selected_handles {
                                if let Some(entity) = self.tabs[i].scene.document.get_entity(*sh) {
                                    let xd = &entity.common().extended_data;
                                    if xd.is_empty() {
                                        self.command_line
                                            .push_output(crate::tf!("  {:x}: no xdata.", sh.value()).as_ref());
                                    } else {
                                        for rec in xd.records() {
                                            self.command_line.push_output(crate::tf!(
                                                "  {:x} [{}]: {} value(s)",
                                                sh.value(),
                                                rec.application_name,
                                                rec.values.len()
                                            ).as_ref());
                                            for v in &rec.values {
                                                self.command_line
                                                    .push_output(crate::tf!("    {:?}", v).as_ref());
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        "SET" => {
                            let app = parts.get(1).copied().unwrap_or("OpenCADStudio");
                            let val = parts.get(2).copied().unwrap_or("");
                            let editable: Vec<_> = selected_handles
                                .iter()
                                .copied()
                                .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                                .collect();
                            if editable.is_empty() {
                                self.command_line.push_error(
                                    crate::t!("XDATA: selected entities are on locked layers.")
                                        .as_ref(),
                                );
                                return Some(Task::none());
                            }
                            self.push_undo_snapshot(i, "XDATA SET");
                            for sh in &editable {
                                if let Some(entity) =
                                    self.tabs[i].scene.document.get_entity_mut(*sh)
                                {
                                    let mut rec = ExtendedDataRecord::new(app);
                                    rec.add_value(XDataValue::String(val.to_string()));
                                    entity.common_mut().extended_data.add_record(rec);
                                }
                            }
                            self.tabs[i].dirty = true;
                            self.command_line.push_output(crate::tf!(
                                "XDATA: set [{app}] = \"{val}\" on {} entity/entities.",
                                editable.len()
                            ).as_ref());
                        }
                        "CLEAR" => {
                            let app_filter = parts.get(1).copied();
                            let editable: Vec<_> = selected_handles
                                .iter()
                                .copied()
                                .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                                .collect();
                            if editable.is_empty() {
                                self.command_line.push_error(
                                    crate::t!("XDATA: selected entities are on locked layers.")
                                        .as_ref(),
                                );
                                return Some(Task::none());
                            }
                            self.push_undo_snapshot(i, "XDATA CLEAR");
                            for sh in &editable {
                                if let Some(entity) =
                                    self.tabs[i].scene.document.get_entity_mut(*sh)
                                {
                                    let xd = &mut entity.common_mut().extended_data;
                                    if let Some(app) = app_filter {
                                        // Rebuild without the matching app.
                                        let kept: Vec<_> = xd
                                            .records()
                                            .iter()
                                            .filter(|r| r.application_name != app)
                                            .cloned()
                                            .collect();
                                        xd.clear();
                                        for r in kept {
                                            xd.add_record(r);
                                        }
                                    } else {
                                        xd.clear();
                                    }
                                }
                            }
                            self.tabs[i].dirty = true;
                            self.command_line.push_output(crate::t!("XDATA: cleared.").as_ref());
                        }
                        _ => {
                            self.command_line
                                .push_info(crate::t!("Usage: XDATA LIST | SET <app> <value> | CLEAR [app]").as_ref());
                        }
                    }
                }
            }

            // ── EXTRUDE ────────────────────────────────────────────────────
            "EXTRUDE" | "THICKEN" => {
                use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
                // If a single entity is already selected, skip the pick step.
                let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
                let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
                if selected.len() == 1 {
                    let handle = selected[0].0;
                    let mut cmd = ExtrudeCommand::new_named(cmd, color);
                    if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
                        cmd.set_entity_pick_direction(
                            curve.plane.normal().map(glam::DVec3::from_array),
                        );
                        cmd.on_entity_pick(
                            handle,
                            glam::DVec3::from_array(curve.plane.origin),
                        );
                    }
                    self.command_line.push_info(&cmd.prompt());
                    self.tabs[i].active_cmd = Some(Box::new(cmd));
                } else {
                    let cmd = ExtrudeCommand::new_named(cmd, color);
                    self.command_line.push_info(&cmd.prompt());
                    self.tabs[i].active_cmd = Some(Box::new(cmd));
                }
            }

            "PRESSPULL" => {
                use crate::modules::insert::solid3d_cmds::PresspullCommand;
                let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
                let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
                let mut command = PresspullCommand::new(color);
                if selected.len() == 1
                    && !matches!(selected[0].1, acadrust::EntityType::Solid3D(_))
                {
                    if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
                        command.set_entity_pick_direction(
                            curve.plane.normal().map(glam::DVec3::from_array),
                        );
                        command.on_entity_pick(
                            selected[0].0,
                            glam::DVec3::from_array(curve.plane.origin),
                        );
                    }
                }
                self.command_line.push_info(&command.prompt());
                self.tabs[i].active_cmd = Some(Box::new(command));
            }

            // ── REVOLVE ────────────────────────────────────────────────────
            "REVOLVE" => {
                use crate::modules::insert::solid3d_cmds::RevolveCommand;
                let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
                let cmd = RevolveCommand::new(color);
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            // ── SWEEP ──────────────────────────────────────────────────────
            "SWEEP" => {
                use crate::modules::insert::solid3d_cmds::SweepCommand;
                let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
                let cmd = SweepCommand::new(color);
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            // ── LOFT ───────────────────────────────────────────────────────
            "LOFT" => {
                use crate::modules::insert::solid3d_cmds::LoftCommand;
                let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
                let cmd = LoftCommand::new(color);
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }

            // ── OBJ import ───────────────────────────────────────────────
            "IMPORTOBJ" | "OBJIMPORT" => {
                return Some(Task::done(Message::ObjImport));
            }

            // ── STL export ────────────────────────────────────────────────
            "STLOUT" | "EXPORTSTL" => {
                return Some(Task::done(Message::StlExport));
            }

            // STEPOUT — export 3D meshes to STEP AP203 format
            "STEPOUT" | "EXPORTSTEP" | "STPOUT" => {
                return Some(Task::done(Message::StepExport));
            }

            // ── Plot Style Editor GUI ─────────────────────────────────────
            "PLOTSTYLEPANEL" | "PLOTSTYLEEDITOR" | "STYLESMANAGER" => {
                return Some(Task::done(Message::PlotStylePanelOpen));
            }

            // ── Plot / Page Setup ──────────────────────────────────────────
            // PLOT / PRINT open the full plot dialog (printer, paper, scale,
            // options); EXPORT / EXPORTPDF stay a direct PDF export.
            "PLOT" | "PRINT" => {
                return Some(Task::done(Message::PlotDialogOpen));
            }
            "PRINTALL" => {
                return Some(Task::done(Message::PrintAllOpen));
            }
            "EXPORT" | "EXPORTPDF" => {
                return Some(Task::done(Message::PlotExport));
            }
            // PLOTSTYLE — load or clear CTB/STB plot style table
            cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
                let sub = cmd
                    .split_once(' ')
                    .map(|(_, r)| r.trim().to_uppercase())
                    .unwrap_or_default();
                match sub.as_str() {
                    "CLEAR" | "NONE" => {
                        return Some(Task::done(Message::PlotStyleClear));
                    }
                    "" | "LOAD" => {
                        let active = self
                            .active_plot_style
                            .as_ref()
                            .map(|t| format!("Active: {}", t.name))
                            .unwrap_or_else(|| "No plot style loaded.".into());
                        self.command_line.push_info(&active);
                        return Some(Task::done(Message::PlotStyleLoad));
                    }
                    "?" | "STATUS" => {
                        let msg = self
                            .active_plot_style
                            .as_ref()
                            .map(|t| {
                                format!(
                                    "Plot style: {}  ({} color overrides)",
                                    t.name,
                                    t.aci_entries.iter().filter(|e| e.color.is_some()).count()
                                )
                            })
                            .unwrap_or_else(|| "No plot style table loaded.".into());
                        self.command_line.push_output(&msg);
                    }
                    _ => {
                        self.command_line
                            .push_error(crate::t!("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]").as_ref());
                    }
                }
            }
            // UNDERLAY — edit properties of selected PDF/DWF/DGN underlay entities.
            // Usage:
            //   UNDERLAY FADE <0-80>
            //   UNDERLAY CONTRAST <0-100>
            //   UNDERLAY ON | OFF
            //   UNDERLAY CLIP ON | OFF
            //   UNDERLAY MONO ON | OFF
            "UNDERLAY" => {
                use crate::command::SelectThenKeywordCommand;
                let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
                let c = SelectThenKeywordCommand::new(
                    "UNDERLAY",
                    "UNDERLAY  [Fade / Contrast / On / Off / Mono / Clip]:",
                    vec![
                        ("Fade", "FADE", Some("UNDERLAY  fade 0-100:")),
                        ("Contrast", "CONTRAST", Some("UNDERLAY  contrast 0-100:")),
                        ("On", "ON", None),
                        ("Off", "OFF", None),
                        ("Mono", "MONO", Some("UNDERLAY MONO  [On / Off]:")),
                        ("Clip", "CLIP", Some("UNDERLAY CLIP  [On / Off]:")),
                    ],
                    has_sel,
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("UNDERLAY ") => {
                let sub = cmd
                    .split_once(' ')
                    .map(|(_, r)| r.trim().to_uppercase())
                    .unwrap_or_default();
                let handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(h, _)| *h)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("UNDERLAY: select underlay entities first.").as_ref());
                } else {
                    let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect();
                    let action = parts.first().copied().unwrap_or("");
                    let arg = parts.get(1).copied().unwrap_or("").trim();
                    let mut changed = 0usize;
                    self.push_undo_snapshot(i, "UNDERLAY");
                    for h in &handles {
                        if let Some(acadrust::EntityType::Underlay(ul)) = self.tabs[i]
                            .scene
                            .document
                            .entities_mut()
                            .find(|e| e.common().handle == *h)
                        {
                            match action {
                                "FADE" => {
                                    if let Ok(v) = arg.parse::<u8>() {
                                        ul.set_fade(v);
                                        changed += 1;
                                    }
                                }
                                "CONTRAST" => {
                                    if let Ok(v) = arg.parse::<u8>() {
                                        ul.set_contrast(v);
                                        changed += 1;
                                    }
                                }
                                "ON" => {
                                    ul.set_on(true);
                                    changed += 1;
                                }
                                "OFF" => {
                                    ul.set_on(false);
                                    changed += 1;
                                }
                                "CLIP" => match arg {
                                    "ON" => {
                                        ul.flags |=
                                            acadrust::entities::UnderlayDisplayFlags::CLIPPING;
                                        changed += 1;
                                    }
                                    "OFF" => {
                                        ul.clear_clip();
                                        changed += 1;
                                    }
                                    _ => {}
                                },
                                "MONO" => match arg {
                                    "ON" => {
                                        ul.set_monochrome(true);
                                        changed += 1;
                                    }
                                    "OFF" => {
                                        ul.set_monochrome(false);
                                        changed += 1;
                                    }
                                    _ => {}
                                },
                                _ => {
                                    // No sub-command: print status.
                                    self.command_line.push_output(crate::tf!(
                                        "Underlay {:x}: fade={}, contrast={}, on={}, clip={}, mono={}",
                                        h.value(),
                                        ul.fade,
                                        ul.contrast,
                                        ul.is_on(),
                                        ul.is_clipping(),
                                        ul.is_monochrome(),
                                    ).as_ref());
                                }
                            }
                        }
                    }
                    if changed > 0 {
                        self.tabs[i].dirty = true;
                        self.command_line
                            .push_info(crate::tf!("Updated {changed} underlay(s).").as_ref());
                    } else if !action.is_empty() {
                        self.command_line.push_error(
                            crate::t!("Usage: UNDERLAY [FADE <n>|CONTRAST <n>|ON|OFF|CLIP ON|OFF|MONO ON|OFF]").as_ref()
                        );
                    }
                }
            }

            // PAGESETUP is folded into the unified plot dialog.
            "PAGESETUP" => {
                return Some(Task::done(Message::PlotDialogOpen));
            }

            // ── Recognized commands whose full implementation is pending ─────────
            // These verbs are surfaced by the ribbon / menus but their feature is
            // still being built. Acknowledge them with an honest status so the
            // button responds instead of reporting an unknown command; each is
            // replaced by its real handler as the feature lands.
            // OBJECTSCALE ADD — add the active scale representation to every
            // selected object that supports per-scale context data.
            "OBJECTSCALE ADD" => {
                let handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(h, _)| *h)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("OBJECTSCALE: select objects first.").as_ref());
                    return Some(Task::none());
                }
                self.push_undo_snapshot(i, "OBJECTSCALE");
                let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
                    self.command_line
                        .push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
                    return Some(Task::none());
                };
                let mut n = 0usize;
                for h in &handles {
                    if crate::scene::annotative::create_annotation_context(
                        &mut self.tabs[i].scene.document,
                        *h,
                        scale,
                    ) {
                        crate::scene::annotative::set_entity_annotative(
                            &mut self.tabs[i].scene.document,
                            *h,
                            true,
                        );
                        n += 1;
                    }
                }
                let changes: Vec<_> = handles
                    .into_iter()
                    .map(|handle| (handle, crate::scene::ChangeKind::Modified))
                    .collect();
                self.tabs[i].scene.bump_entities(&changes);
                self.tabs[i].dirty = true;
                self.command_line.push_output(crate::tf!(
                    "OBJECTSCALE: added the active scale to {n} object(s)."
                ).as_ref());
                return Some(Task::none());
            }

            // HYPERLINK <url> — attach a hyperlink to the selected objects, stored
            // in the standard PE_URL XData record so it round-trips in the file.
            "HYPERLINK" => {
                use crate::command::SelectThenValueCommand;
                let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
                let c = SelectThenValueCommand::new(
                    "HYPERLINK",
                    "HYPERLINK  URL to attach:",
                    has_sel,
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("HYPERLINK ") => {
                use acadrust::xdata::{ExtendedDataRecord, XDataValue};
                let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string();
                if url.is_empty() {
                    self.command_line.push_info(crate::t!("Usage: HYPERLINK <url>   (select objects first)").as_ref());
                    return Some(Task::none());
                }
                let handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(h, _)| *h)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if handles.is_empty() {
                    self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref());
                    return Some(Task::none());
                }
                self.push_undo_snapshot(i, "HYPERLINK");
                let mut n = 0usize;
                for h in &handles {
                    if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
                        let xd = &mut e.common_mut().extended_data;
                        let mut rec = ExtendedDataRecord::new("PE_URL");
                        rec.add_value(XDataValue::String(url.clone()));
                        xd.add_record(rec);
                        n += 1;
                    }
                }
                self.tabs[i].dirty = true;
                self.command_line
                    .push_output(crate::tf!("HYPERLINK: attached to {n} object(s).").as_ref());
                return Some(Task::none());
            }

            // ADJUST — set brightness / contrast / fade on selected raster images
            //   ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>
            "ADJUST" => {
                use crate::command::SelectThenKeywordCommand;
                let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
                let c = SelectThenKeywordCommand::new(
                    "ADJUST",
                    "ADJUST  [Brightness / Contrast / Fade]:",
                    vec![
                        ("Brightness", "BRIGHTNESS", Some("ADJUST  brightness 0-100:")),
                        ("Contrast", "CONTRAST", Some("ADJUST  contrast 0-100:")),
                        ("Fade", "FADE", Some("ADJUST  fade 0-100:")),
                    ],
                    has_sel,
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("ADJUST ") => {
                let rest = cmd.trim_start_matches("ADJUST").trim();
                let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
                let action = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
                let arg = parts.get(1).copied().unwrap_or("").trim();
                let handles: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(h, _)| *h)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("ADJUST: select raster image(s) first.").as_ref());
                } else if action.is_empty() {
                    self.command_line
                        .push_info(crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref());
                } else if let Ok(v) = arg.parse::<u8>() {
                    let v = v.min(100);
                    self.push_undo_snapshot(i, "ADJUST");
                    let mut changed = 0usize;
                    let mut changed_handles = Vec::new();
                    for h in &handles {
                        if let Some(acadrust::EntityType::RasterImage(img)) = self.tabs[i]
                            .scene
                            .document
                            .entities_mut()
                            .find(|e| e.common().handle == *h)
                        {
                            match action.as_str() {
                                "BRIGHTNESS" => {
                                    img.brightness = v;
                                    changed += 1;
                                    changed_handles.push(*h);
                                }
                                "CONTRAST" => {
                                    img.contrast = v;
                                    changed += 1;
                                    changed_handles.push(*h);
                                }
                                "FADE" => {
                                    img.fade = v;
                                    changed += 1;
                                    changed_handles.push(*h);
                                }
                                _ => {}
                            }
                        }
                    }
                    if changed > 0 {
                        self.tabs[i].dirty = true;
                        for &handle in &changed_handles {
                            self.tabs[i].scene.reseed_derived_caches(handle);
                        }
                        let changes: Vec<_> = changed_handles
                            .into_iter()
                            .map(|handle| (handle, crate::scene::ChangeKind::Modified))
                            .collect();
                        self.tabs[i].scene.bump_entities(&changes);
                        self.command_line
                            .push_output(crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref());
                    } else {
                        self.command_line.push_error(
                            "ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).",
                        );
                    }
                } else {
                    self.command_line.push_error(crate::t!("ADJUST: value must be 0-100.").as_ref());
                }
            }

            // ANNOSCALE / CANNOSCALE <ratio> — set the current annotation scale
            // (e.g. 1:50, 2:1, or a plain factor). Drives annotative-object size
            // in model space and is written to the drawing header.
            "ANNOSCALE" | "CANNOSCALE" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new(
                    "ANNOSCALE",
                    "ANNOSCALE  new annotation scale  (e.g. 1:50, 2:1, or a factor):",
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            "ANNOALLVISIBLE" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new(
                    "ANNOALLVISIBLE",
                    "ANNOALLVISIBLE  new value [0/1]:",
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("ANNOALLVISIBLE ") => {
                let value = cmd.split_whitespace().nth(1).unwrap_or("");
                match value {
                    "0" | "OFF" | "FALSE" => {
                        self.tabs[i].scene.set_annotation_all_visible(false);
                        self.tabs[i].dirty = true;
                    }
                    "1" | "ON" | "TRUE" => {
                        self.tabs[i].scene.set_annotation_all_visible(true);
                        self.tabs[i].dirty = true;
                    }
                    _ => self
                        .command_line
                        .push_error(crate::t!("ANNOALLVISIBLE: enter 0 or 1.").as_ref()),
                }
            }
            "ANNOAUTOSCALE" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new(
                    "ANNOAUTOSCALE",
                    "ANNOAUTOSCALE  new value [-4..4]:",
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("ANNOAUTOSCALE ") => {
                let value = cmd.split_whitespace().nth(1).unwrap_or("");
                match value.parse::<i8>() {
                    Ok(mode @ -4..=4) => self.annotation_auto_scale = mode,
                    _ => self.command_line.push_error(
                        "ANNOAUTOSCALE: enter an integer from -4 through 4.",
                    ),
                }
            }
            "ANNOUPDATE" => {
                let handles: Vec<_> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .iter()
                    .map(|(handle, _)| *handle)
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if handles.is_empty() {
                    self.command_line
                        .push_error(crate::t!("ANNOUPDATE: select annotation objects first.").as_ref());
                    return Some(Task::none());
                }
                self.push_undo_snapshot(i, "ANNOUPDATE");
                let scale = self.tabs[i].scene.creation_annotation_scale_handle();
                let mut updated = 0usize;
                for handle in &handles {
                    if crate::scene::annotative::update_entity_from_annotation_style(
                        &mut self.tabs[i].scene.document,
                        *handle,
                        scale,
                    ) {
                        updated += 1;
                    }
                }
                if updated > 0 {
                    let changes: Vec<_> = handles
                        .into_iter()
                        .map(|handle| (handle, crate::scene::ChangeKind::Modified))
                        .collect();
                    self.tabs[i].scene.bump_entities(&changes);
                    self.tabs[i].dirty = true;
                }
                self.command_line
                    .push_output(crate::tf!("ANNOUPDATE: updated {updated} object(s).").as_ref());
                return Some(Task::none());
            }
            cmd if cmd.starts_with("ANNOSCALE ") || cmd.starts_with("CANNOSCALE ") => {
                let arg = cmd
                    .split_whitespace()
                    .nth(1)
                    .unwrap_or("")
                    .trim()
                    .to_string();
                if arg.is_empty() {
                    let name = self.tabs[i]
                        .scene
                        .document
                        .header
                        .current_annotation_scale
                        .clone();
                    self.command_line
                        .push_output(crate::tf!("Current annotation scale: {name}").as_ref());
                    return Some(Task::none());
                }
                let previous = self.tabs[i].scene.displayed_annotation_scale_handle();
                match self.tabs[i].scene.set_annotation_scale_named(&arg) {
                    Some(handle) => {
                        if self.annotation_auto_scale > 0 {
                            self.tabs[i].scene.add_annotation_scale_to_objects(
                                handle,
                                previous,
                                self.annotation_auto_scale as u8,
                            );
                        }
                        self.tabs[i].dirty = true;
                        self.command_line
                            .push_output(crate::tf!("Annotation scale: {arg}").as_ref());
                    }
                    None => self
                        .command_line
                        .push_error(crate::t!("Usage: ANNOSCALE <ratio>  e.g. 1:50, 2:1, or a factor").as_ref()),
                }
            }

            // SCALELISTEDIT — list / add / delete the drawing's annotation scales.
            //   SCALELISTEDIT              list
            //   SCALELISTEDIT ADD 1:50     add (name is a paper:drawing ratio)
            //   SCALELISTEDIT DELETE 1:50  remove (not the current scale)
            "SCALELISTEDIT" => {
                use crate::command::KeywordCommand;
                let c = KeywordCommand::new(
                    "SCALELISTEDIT",
                    "SCALELISTEDIT  [Add / Delete]:",
                    vec![
                        ("Add", "ADD", Some("SCALELISTEDIT ADD  new scale ratio (e.g. 1:50):")),
                        ("Delete", "DELETE", Some("SCALELISTEDIT DELETE  scale ratio to remove:")),
                    ],
                );
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("SCALELISTEDIT ") => {
                let rest = cmd.trim_start_matches("SCALELISTEDIT").trim();
                let mut parts = rest.splitn(2, char::is_whitespace);
                let sub = parts.next().unwrap_or("").to_uppercase();
                let arg = parts.next().unwrap_or("").trim();
                match sub.as_str() {
                    "ADD" => match arg.split_once(':') {
                        Some((p, d)) => match (p.trim().parse::<f64>(), d.trim().parse::<f64>()) {
                            (Ok(paper), Ok(drawing)) if paper > 0.0 && drawing > 0.0 => {
                                self.push_undo_snapshot(i, "SCALELISTEDIT");
                                if self.tabs[i].scene.add_scale(arg, paper, drawing) {
                                    self.tabs[i].dirty = true;
                                    self.command_line
                                        .push_output(crate::tf!("Added annotation scale {arg}.").as_ref());
                                } else {
                                    self.command_line
                                        .push_info(crate::tf!("Scale {arg} already exists.").as_ref());
                                }
                            }
                            _ => self
                                .command_line
                                .push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()),
                        },
                        None => self
                            .command_line
                            .push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()),
                    },
                    "DELETE" | "REMOVE" => {
                        let current = self.tabs[i]
                            .scene
                            .document
                            .header
                            .current_annotation_scale
                            .clone();
                        if arg.is_empty() {
                            self.command_line.push_info(crate::t!("Usage: SCALELISTEDIT DELETE <name>").as_ref());
                        } else if arg.eq_ignore_ascii_case(&current) {
                            self.command_line.push_error(crate::tf!(
                                "Cannot delete the current annotation scale ({arg})."
                            ).as_ref());
                        } else {
                            self.push_undo_snapshot(i, "SCALELISTEDIT");
                            if self.tabs[i].scene.remove_scale(arg) {
                                self.tabs[i].dirty = true;
                                self.command_line
                                    .push_output(crate::tf!("Removed annotation scale {arg}.").as_ref());
                            } else {
                                self.command_line
                                    .push_info(crate::tf!("No annotation scale named {arg}.").as_ref());
                            }
                        }
                    }
                    "" => {
                        let names: Vec<String> = self.tabs[i]
                            .scene
                            .scale_list()
                            .into_iter()
                            .map(|(n, _, _)| n)
                            .collect();
                        if names.is_empty() {
                            self.command_line.push_info(crate::t!("No annotation scales defined.").as_ref());
                        } else {
                            self.command_line
                                .push_output(crate::tf!("Annotation scales: {}", names.join(", ")).as_ref());
                        }
                    }
                    _ => self
                        .command_line
                        .push_info(crate::t!("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]").as_ref()),
                }
            }

            // OBJECTSCALE — open the Annotation Object Scale dialog for the
            // selected object (add / remove its per-object scale representations).
            // Reachable now that the immediate "add current scale" quick action
            // moved to the explicit `OBJECTSCALE ADD` keyword above.
            "OBJECTSCALE" => {
                return Some(Task::done(Message::AnnoObjectScaleOpen));
            }

            // DATALINK <path.csv> — create a persistent linked table.
            "DATALINK" => {
                use crate::command::ValuePromptCommand;
                let c = ValuePromptCommand::new("DATALINK", "DATALINK  path to the .csv file:");
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("DATALINK ") => {
                let path = cmd.trim_start_matches("DATALINK").trim();
                if path.is_empty() {
                    self.command_line.push_info(
                        "Usage: DATALINK <path-to-.csv>",
                    );
                    return Some(Task::none());
                }
                match std::fs::read_to_string(path) {
                    Ok(text) => {
                        let rows_data = parse_csv_table(&text);
                        let nrows = rows_data.len();
                        let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
                        if nrows == 0 || ncols == 0 {
                            self.command_line
                                .push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref());
                            return Some(Task::none());
                        }
                        use acadrust::entities::TableBuilder;
                        use acadrust::types::Vector3;
                        let mut table = TableBuilder::new(nrows, ncols)
                            .at(Vector3::new(0.0, 0.0, 0.0))
                            .row_height(0.5)
                            .column_width(2.0)
                            .build();
                        for (r, row) in rows_data.iter().enumerate() {
                            for (c, cell) in row.iter().enumerate() {
                                table.set_cell_text(r, c, cell);
                            }
                        }
                        let doc = &self.tabs[i].scene.document;
                        let current_style = doc.header.current_table_style_name.clone();
                        table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
                            match object {
                                acadrust::objects::ObjectType::TableStyle(style)
                                    if style.name.eq_ignore_ascii_case(&current_style) =>
                                {
                                    Some(*handle)
                                }
                                _ => None,
                            }
                        });
                        let link_path = std::fs::canonicalize(path)
                            .map(|value| value.to_string_lossy().into_owned())
                            .unwrap_or_else(|_| path.to_string());
                        let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::new(
                            table, &link_path,
                        );
                        self.command_line
                            .push_info(&crate::command::CadCommand::prompt(&command));
                        self.tabs[i].active_cmd = Some(Box::new(command));
                    }
                    Err(e) => {
                        self.command_line
                            .push_error(crate::tf!("DATALINK: cannot read \"{path}\": {e}").as_ref());
                    }
                }
            }

            cmd if cmd == "DATALINKUPDATE" || cmd.starts_with("DATALINKUPDATE ") => {
                let write_back = cmd
                    .trim_start_matches("DATALINKUPDATE")
                    .trim()
                    .eq_ignore_ascii_case("WRITE");
                let selected: Vec<acadrust::Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .into_iter()
                    .filter_map(|(handle, entity)| {
                        matches!(entity, acadrust::EntityType::Table(_)).then_some(handle)
                    })
                    .collect();
                let table_handles = if selected.is_empty() {
                    self.tabs[i]
                        .scene
                        .document
                        .entities()
                        .filter_map(|entity| {
                            matches!(entity, acadrust::EntityType::Table(_))
                                .then_some(entity.common().handle)
                        })
                        .collect::<Vec<_>>()
                } else {
                    selected
                };
                let mut jobs = Vec::new();
                for handle in &table_handles {
                    let Some(acadrust::EntityType::Table(table)) =
                        self.tabs[i].scene.document.get_entity(*handle)
                    else {
                        continue;
                    };
                    let link_handle = table
                        .rows
                        .iter()
                        .flat_map(|row| row.cells.iter())
                        .find_map(|cell| cell.data_link_handle);
                    let Some(link_handle) = link_handle else {
                        continue;
                    };
                    let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
                        Some(acadrust::objects::ObjectType::ClassObject(object)) => {
                            match &object.data {
                                acadrust::objects::ClassObjectData::DataLink(link) => {
                                    Some(link.connection_string.clone())
                                }
                                _ => None,
                            }
                        }
                        _ => None,
                    };
                    if let Some(path) = path {
                        jobs.push((*handle, link_handle, path));
                    }
                }
                if jobs.is_empty() {
                    self.command_line
                        .push_error("DATALINKUPDATE: no linked tables found.");
                    return Some(Task::none());
                }
                if write_back {
                    let mut written = 0usize;
                    for (table_handle, _, path) in &jobs {
                        let Some(acadrust::EntityType::Table(table)) =
                            self.tabs[i].scene.document.get_entity(*table_handle)
                        else {
                            continue;
                        };
                        let csv = table_to_csv(table);
                        if std::fs::write(path, csv).is_ok() {
                            written += 1;
                        }
                    }
                    self.command_line.push_output(
                        crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
                    );
                    return Some(Task::none());
                }
                let updates: Vec<_> = jobs
                    .into_iter()
                    .filter_map(|(table_handle, link_handle, path)| {
                        std::fs::read_to_string(&path)
                            .ok()
                            .map(|text| (table_handle, link_handle, parse_csv_table(&text)))
                    })
                    .filter(|(_, _, rows)| !rows.is_empty())
                    .collect();
                if updates.is_empty() {
                    self.command_line
                        .push_error("DATALINKUPDATE: linked sources could not be read.");
                    return Some(Task::none());
                }
                self.push_undo_snapshot(i, "DATALINKUPDATE");
                use acadrust::entities::table::CellStateFlags;
                let mut changed_handles = Vec::new();
                for (table_handle, link_handle, rows) in updates {
                    let columns = rows.iter().map(Vec::len).max().unwrap_or(0);
                    let Some(acadrust::EntityType::Table(table)) =
                        self.tabs[i].scene.document.get_entity_mut(table_handle)
                    else {
                        continue;
                    };
                    while table.row_count() < rows.len() {
                        table.add_row();
                    }
                    while table.row_count() > rows.len() {
                        table.remove_row(table.row_count() - 1);
                    }
                    while table.column_count() < columns {
                        let width = table.columns.last().map(|column| column.width).unwrap_or(2.0);
                        table.add_column(width);
                    }
                    while table.column_count() > columns {
                        table.remove_column(table.column_count() - 1);
                    }
                    for row in 0..rows.len() {
                        for column in 0..columns {
                            if let Some(cell) = table.cell_mut(row, column) {
                                cell.set_text(
                                    rows[row].get(column).map(String::as_str).unwrap_or(""),
                                );
                                cell.has_linked_data = true;
                                cell.data_link_handle = Some(link_handle);
                                cell.data_link_rows = rows.len() as i32;
                                cell.data_link_columns = columns as i32;
                                cell.state.insert(
                                    CellStateFlags::LINKED
                                        | CellStateFlags::CONTENT_LOCKED
                                        | CellStateFlags::FORMAT_LOCKED,
                                );
                            }
                        }
                    }
                    changed_handles.push(table_handle);
                }
                self.invalidate_property_targets(i, &changed_handles);
                self.tabs[i].dirty = true;
                self.refresh_properties();
                self.command_line.push_output(
                    crate::tf!("DATALINKUPDATE: updated {} linked table(s).", changed_handles.len())
                        .as_ref(),
                );
            }

            // LANDXMLIMPORT <path> — import survey points (LandXML <CgPoint>
            // elements) as Point objects. Reads the coordinate text content
            // (northing easting elevation) → Point at (easting, northing, elev).
            "LANDXMLIMPORT" => {
                use crate::command::ValuePromptCommand;
                let c =
                    ValuePromptCommand::new("LANDXMLIMPORT", "LANDXMLIMPORT  path to the .xml file:");
                self.command_line.push_info(&c.prompt());
                self.tabs[i].active_cmd = Some(Box::new(c));
            }
            cmd if cmd.starts_with("LANDXMLIMPORT ") => {
                let path = cmd.trim_start_matches("LANDXMLIMPORT").trim();
                if path.is_empty() {
                    self.command_line.push_info(
                        "Usage: LANDXMLIMPORT <path-to-.xml>  (imports CgPoint survey points)",
                    );
                    return Some(Task::none());
                }
                match std::fs::read_to_string(path) {
                    Ok(xml) => {
                        let pts = parse_landxml_cgpoints(&xml);
                        if pts.is_empty() {
                            self.command_line
                                .push_info(crate::t!("LANDXMLIMPORT: no <CgPoint> survey points found.").as_ref());
                            return Some(Task::none());
                        }
                        self.push_undo_snapshot(i, "LANDXMLIMPORT");
                        for [x, y, z] in &pts {
                            let mut p = acadrust::entities::Point::new();
                            p.location = acadrust::types::Vector3::new(*x, *y, *z);
                            self.tabs[i]
                                .scene
                                .add_entity_clone(acadrust::EntityType::Point(p));
                        }
                        self.tabs[i].dirty = true;
                        self.command_line.push_output(crate::tf!(
                            "LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.",
                            pts.len()
                        ).as_ref());
                    }
                    Err(e) => self
                        .command_line
                        .push_error(crate::tf!("LANDXMLIMPORT: cannot read \"{path}\": {e}").as_ref()),
                }
            }

            "POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS"
            | "UOSNAP" => {
                self.command_line
                    .push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
            }

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

// Scan LandXML text for <CgPoint> survey points. Each element's text content is
// "northing easting elevation"; returned as [easting, northing, elevation] so it
// maps to a Point at (X=easting, Y=northing, Z=elevation). Tolerant manual scan
// (no XML dependency); handles the standard text-content form.
// (landxml cgpoint scan)
fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
    let mut out = Vec::new();
    let mut rest = xml;
    while let Some(open) = rest.find("<CgPoint") {
        let after = &rest[open + "<CgPoint".len()..];
        // Skip the container element "<CgPoints>".
        if !matches!(
            after.chars().next(),
            Some(' ') | Some('>') | Some('\t') | Some('\n') | Some('\r')
        ) {
            rest = after;
            continue;
        }
        let Some(gt) = after.find('>') else { break };
        let body = &after[gt + 1..];
        let Some(close) = body.find("</CgPoint>") else {
            break;
        };
        let text = &body[..close];
        let nums: Vec<f64> = text
            .split_whitespace()
            .filter_map(|s| s.parse().ok())
            .collect();
        if nums.len() >= 3 {
            out.push([nums[1], nums[0], nums[2]]);
        }
        rest = &body[close + "</CgPoint>".len()..];
    }
    out
}

fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
    let mut rows = Vec::new();
    let mut row = Vec::new();
    let mut field = String::new();
    let mut quoted = false;
    let mut chars = text.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            '"' if quoted && chars.peek() == Some(&'"') => {
                field.push('"');
                chars.next();
            }
            '"' => quoted = !quoted,
            ',' if !quoted => row.push(std::mem::take(&mut field)),
            '\n' if !quoted => {
                if field.ends_with('\r') {
                    field.pop();
                }
                row.push(std::mem::take(&mut field));
                if row.iter().any(|value| !value.is_empty()) {
                    rows.push(std::mem::take(&mut row));
                } else {
                    row.clear();
                }
            }
            _ => field.push(ch),
        }
    }
    if field.ends_with('\r') {
        field.pop();
    }
    if !field.is_empty() || !row.is_empty() {
        row.push(field);
        if row.iter().any(|value| !value.is_empty()) {
            rows.push(row);
        }
    }
    rows
}

fn table_to_csv(table: &acadrust::entities::Table) -> String {
    fn escape(value: &str) -> String {
        if value.contains([',', '"', '\r', '\n']) {
            format!("\"{}\"", value.replace('"', "\"\""))
        } else {
            value.to_string()
        }
    }
    table
        .rows
        .iter()
        .map(|row| {
            row.cells
                .iter()
                .map(|cell| escape(cell.text_value()))
                .collect::<Vec<_>>()
                .join(",")
        })
        .collect::<Vec<_>>()
        .join("\r\n")
}

#[cfg(test)]
mod tests {
    use crate::app::OpenCADStudio;

    fn fresh_app() -> OpenCADStudio {
        let mut app = OpenCADStudio::new_for_test();
        app.automation_op(r#"{"op":"new"}"#);
        app
    }

    #[test]
    fn redraw_requests_and_leaves_geometry_untouched() {
        let mut app = fresh_app();
        let i = app.active_tab;
        let geom_before = app.tabs[i].scene.geometry_epoch;
        let block_before = app.tabs[i].scene.block_epoch;
        let _ = app.run_command_line("REDRAW");
        assert!(
            app.tabs[i].scene.refresh_pending_any(),
            "REDRAW must leave a pending force request"
        );
        assert_eq!(app.tabs[i].scene.geometry_epoch, geom_before, "REDRAW must not regen");
        assert_eq!(app.tabs[i].scene.block_epoch, block_before, "REDRAW must not regen blocks");
    }

    #[test]
    fn aliases_route_like_full_verbs() {
        let mut full = fresh_app();
        let mut short = fresh_app();
        let _ = full.run_command_line("REDRAW");
        let _ = short.run_command_line("R");
        let i = full.active_tab;
        assert!(short.tabs[i].scene.refresh_pending_any(), "'R' must trigger REDRAW");
        assert_eq!(
            short.tabs[i].scene.refresh_pending_any(),
            full.tabs[i].scene.refresh_pending_any(),
            "'R' and 'REDRAW' must leave the same refresh state"
        );
    }

    #[test]
    fn redrawall_marks_all_tiles() {
        let mut app = fresh_app();
        let i = app.active_tab;
        let _ = app.run_command_line("REDRAWALL");
        assert!(app.tabs[i].scene.refresh_pending_any(), "REDRAWALL must leave a force request");
    }

    #[test]
    fn regen_rebuilds_but_does_not_dirty_document() {
        let mut app = fresh_app();
        let i = app.active_tab;
        let geom_before = app.tabs[i].scene.geometry_epoch;
        let block_before = app.tabs[i].scene.block_epoch;
        app.tabs[i].dirty = false;
        let _ = app.run_command_line("REGEN");
        assert_ne!(app.tabs[i].scene.geometry_epoch, geom_before, "REGEN must regenerate geometry");
        assert_ne!(app.tabs[i].scene.block_epoch, block_before, "REGEN must regenerate block epoch");
        assert!(!app.tabs[i].dirty, "REGEN must NOT mark the document as modified (no DB change)");
        let _ = app.run_command_line("REGENALL");
        assert!(!app.tabs[i].dirty, "REGENALL must not dirty the document either");
    }
}
