//! Shared CRUD layer for every style manager (text / dimension / table /
//! multileader / multiline).
//!
//! The five managers all expose the same list operations — New, Copy, Delete,
//! Rename, Set-Current — over a named collection of styles. Only the *property
//! editor* and the *storage backend* differ, so those are the only parts kept
//! per-manager:
//!
//! * **Table-backed** (text, dim): live in `Table<T>`, keyed by upper-cased
//!   name. Renaming must re-key the entry and rewrite name-based entity
//!   references (TEXT/MTEXT `style`, DIMENSION `style_name`).
//! * **Object-backed** (table, multileader, multiline): live in
//!   `document.objects`, keyed by handle. Renaming only mutates the `name`
//!   field; entities reference these by handle, so nothing else moves.
//!
//! Centralising the flow here is what fixes the bug class that kept recurring
//! when each manager was hand-copied: a dead New, a missing ribbon refresh, a
//! style added without a handle (dropped on DWG save, issue #67).

use super::OpenCADStudio;
use acadrust::objects::{MLineStyle, MultiLeaderStyle, ObjectType, TableStyle};
use acadrust::tables::{DimStyle, TextStyle};
use acadrust::types::Handle;

/// Guarantee the built-in "Standard" style of every kind exists in `doc` —
/// a foreign or damaged file saved without them leaves the style dropdowns
/// broken with no way to recover, and new text/dimensions have nothing to
/// reference (#366). Missing entries are re-seeded with the app defaults.
/// Called on every file open; a no-op for healthy documents.
pub(crate) fn ensure_standard_styles(doc: &mut acadrust::CadDocument) {
    if !doc
        .text_styles
        .iter()
        .any(|s| s.name.eq_ignore_ascii_case("Standard"))
    {
        let mut s = TextStyle::new("Standard");
        s.handle = doc.allocate_handle();
        let _ = doc.text_styles.add(s);
    }
    if !doc
        .dim_styles
        .iter()
        .any(|s| s.name.eq_ignore_ascii_case("Standard"))
    {
        let mut s = DimStyle::new("Standard");
        s.handle = doc.allocate_handle();
        let _ = doc.dim_styles.add(s);
    }
    let has = |doc: &acadrust::CadDocument, pred: fn(&ObjectType) -> Option<&str>| {
        doc.objects
            .values()
            .filter_map(pred)
            .any(|n| n.eq_ignore_ascii_case("Standard"))
    };
    if !has(doc, |o| match o {
        ObjectType::TableStyle(s) => Some(&s.name),
        _ => None,
    }) {
        let mut s = TableStyle::standard();
        s.handle = doc.allocate_handle();
        doc.objects.insert(s.handle, ObjectType::TableStyle(s));
    }
    if !has(doc, |o| match o {
        ObjectType::MultiLeaderStyle(s) => Some(&s.name),
        _ => None,
    }) {
        let mut s = MultiLeaderStyle::standard();
        s.handle = doc.allocate_handle();
        doc.objects.insert(s.handle, ObjectType::MultiLeaderStyle(s));
    }
    if !has(doc, |o| match o {
        ObjectType::MLineStyle(s) => Some(&s.name),
        _ => None,
    }) {
        let mut s = MLineStyle::standard();
        s.handle = doc.allocate_handle();
        doc.objects.insert(s.handle, ObjectType::MLineStyle(s));
    }
}

/// Which style manager an operation targets. Carried by the shared rename
/// messages so one handler can dispatch to the right storage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StyleKind {
    Text,
    Dim,
    Table,
    MLeader,
    MLine,
}

impl StyleKind {
    /// True when this style feeds the ribbon's quick-set dropdown.
    fn in_ribbon(self) -> bool {
        !matches!(self, StyleKind::MLine)
    }
}

impl OpenCADStudio {
    // ── Queries ────────────────────────────────────────────────────────────

    /// All style names for `kind`, in display order (object-backed styles are
    /// sorted by name so the `HashMap` backing them renders stably).
    pub(super) fn style_names(&self, kind: StyleKind) -> Vec<String> {
        let doc = &self.tabs[self.active_tab].scene.document;
        let from_objects = |pick: fn(&ObjectType) -> Option<&str>| -> Vec<String> {
            let mut v: Vec<String> = doc
                .objects
                .values()
                .filter_map(pick)
                .map(str::to_string)
                .collect();
            v.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
            v
        };
        match kind {
            StyleKind::Text => doc.text_styles.iter().map(|s| s.name.clone()).collect(),
            StyleKind::Dim => doc.dim_styles.iter().map(|s| s.name.clone()).collect(),
            StyleKind::Table => from_objects(|o| match o {
                ObjectType::TableStyle(s) => Some(s.name.as_str()),
                _ => None,
            }),
            StyleKind::MLeader => from_objects(|o| match o {
                ObjectType::MultiLeaderStyle(s) => Some(s.name.as_str()),
                _ => None,
            }),
            StyleKind::MLine => from_objects(|o| match o {
                ObjectType::MLineStyle(s) => Some(s.name.as_str()),
                _ => None,
            }),
        }
    }

    pub(super) fn style_selected(&self, kind: StyleKind) -> String {
        match kind {
            StyleKind::Text => self.textstyle_selected.clone(),
            StyleKind::Dim => self.dimstyle_selected.clone(),
            StyleKind::Table => self.tablestyle_selected.clone(),
            StyleKind::MLeader => self.mleaderstyle_selected.clone(),
            StyleKind::MLine => self.mlstyle_selected.clone(),
        }
    }

    fn set_style_selected(&mut self, kind: StyleKind, name: String) {
        match kind {
            StyleKind::Text => self.textstyle_selected = name,
            StyleKind::Dim => self.dimstyle_selected = name,
            StyleKind::Table => self.tablestyle_selected = name,
            StyleKind::MLeader => self.mleaderstyle_selected = name,
            StyleKind::MLine => self.mlstyle_selected = name,
        }
    }

    fn style_exists(&self, kind: StyleKind, name: &str) -> bool {
        self.style_names(kind)
            .iter()
            .any(|n| n.eq_ignore_ascii_case(name))
    }

    /// First free `Style{n}` name for a fresh style.
    fn unique_new_name(&self, kind: StyleKind) -> String {
        (1u32..)
            .map(|n| format!("Style{n}"))
            .find(|c| !self.style_exists(kind, c))
            .unwrap()
    }

    /// First free `{base} ({n})` name for a copy / disambiguated entry.
    fn unique_suffixed_name(&self, kind: StyleKind, base: &str) -> String {
        (1u32..)
            .map(|n| format!("{base} ({n})"))
            .find(|c| !self.style_exists(kind, c))
            .unwrap()
    }

    // ── Per-manager glue (the only kind-specific list code) ────────────────

    /// Reload the property-editor buffers for the kinds that have them.
    fn load_style_bufs(&mut self, kind: StyleKind) {
        let i = self.active_tab;
        match kind {
            StyleKind::Text => self.load_textstyle_bufs(i),
            StyleKind::Dim => self.load_dimstyle_bufs(i),
            StyleKind::MLeader => self.load_mleaderstyle_bufs(i),
            StyleKind::Table | StyleKind::MLine => {}
        }
    }

    /// Refresh anything that mirrors the style list / current style after a
    /// mutation (ribbon dropdowns, geometry that depends on the style).
    fn after_style_change(&mut self, kind: StyleKind) {
        if kind.in_ribbon() {
            self.sync_ribbon_styles();
        }
    }

    fn insert_default_style(&mut self, kind: StyleKind, name: &str, handle: Handle) {
        let doc = &mut self.tabs[self.active_tab].scene.document;
        match kind {
            StyleKind::Text => {
                let mut s = TextStyle::new(name);
                s.handle = handle;
                let _ = doc.text_styles.add(s);
            }
            StyleKind::Dim => {
                let mut s = DimStyle::new(name);
                s.handle = handle;
                let _ = doc.dim_styles.add(s);
            }
            StyleKind::Table => {
                let mut s = TableStyle::standard();
                s.name = name.to_string();
                s.handle = handle;
                doc.objects.insert(handle, ObjectType::TableStyle(s));
            }
            StyleKind::MLeader => {
                let mut s = MultiLeaderStyle::new(name);
                s.handle = handle;
                doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s));
            }
            StyleKind::MLine => {
                let mut s = MLineStyle::standard();
                s.name = name.to_string();
                s.handle = handle;
                doc.objects.insert(handle, ObjectType::MLineStyle(s));
            }
        }
    }

    /// Clone the style named `src` under `name` with a fresh `handle`.
    /// Returns false if `src` no longer exists.
    fn clone_style_as(&mut self, kind: StyleKind, src: &str, name: &str, handle: Handle) -> bool {
        let doc = &mut self.tabs[self.active_tab].scene.document;
        match kind {
            StyleKind::Text => {
                if let Some(mut s) = doc.text_styles.get(src).cloned() {
                    s.name = name.to_string();
                    s.handle = handle;
                    let _ = doc.text_styles.add(s);
                    return true;
                }
            }
            StyleKind::Dim => {
                if let Some(mut s) = doc.dim_styles.get(src).cloned() {
                    s.name = name.to_string();
                    s.handle = handle;
                    let _ = doc.dim_styles.add(s);
                    return true;
                }
            }
            StyleKind::Table => {
                if let Some(mut s) = find_object_style(doc, src, |o| match o {
                    ObjectType::TableStyle(s) => Some((s.name.as_str(), s.clone())),
                    _ => None,
                }) {
                    s.name = name.to_string();
                    s.handle = handle;
                    doc.objects.insert(handle, ObjectType::TableStyle(s));
                    return true;
                }
            }
            StyleKind::MLeader => {
                if let Some(mut s) = find_object_style(doc, src, |o| match o {
                    ObjectType::MultiLeaderStyle(s) => Some((s.name.as_str(), s.clone())),
                    _ => None,
                }) {
                    s.name = name.to_string();
                    s.handle = handle;
                    doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s));
                    return true;
                }
            }
            StyleKind::MLine => {
                if let Some(mut s) = find_object_style(doc, src, |o| match o {
                    ObjectType::MLineStyle(s) => Some((s.name.as_str(), s.clone())),
                    _ => None,
                }) {
                    s.name = name.to_string();
                    s.handle = handle;
                    doc.objects.insert(handle, ObjectType::MLineStyle(s));
                    return true;
                }
            }
        }
        false
    }

    fn remove_style_storage(&mut self, kind: StyleKind, name: &str) -> bool {
        let doc = &mut self.tabs[self.active_tab].scene.document;
        match kind {
            StyleKind::Text => doc.text_styles.remove(name).is_some(),
            StyleKind::Dim => doc.dim_styles.remove(name).is_some(),
            StyleKind::Table | StyleKind::MLeader | StyleKind::MLine => {
                let kind2 = kind;
                if let Some(h) = object_handle(doc, name, kind2) {
                    doc.objects.remove(&h).is_some()
                } else {
                    false
                }
            }
        }
    }

    /// Rename `old`→`new` in the backing store, re-keying table entries and
    /// rewriting name-based references + current-style pointers.
    fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) {
        let i = self.active_tab;
        match kind {
            StyleKind::Text => {
                let doc = &mut self.tabs[i].scene.document;
                if let Some(mut s) = doc.text_styles.get(old).cloned() {
                    s.name = new.to_string();
                    if !s.handle.is_valid() {
                        s.handle = doc.allocate_handle();
                    }
                    let _ = doc.text_styles.add(s);
                }
                doc.text_styles.remove(old);
                if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
                    doc.header.current_text_style_name = new.to_string();
                }
                for e in doc.entities_mut() {
                    match e {
                        acadrust::entities::EntityType::Text(t)
                            if t.style.eq_ignore_ascii_case(old) =>
                        {
                            t.style = new.to_string();
                        }
                        acadrust::entities::EntityType::MText(t)
                            if t.style.eq_ignore_ascii_case(old) =>
                        {
                            t.style = new.to_string();
                        }
                        _ => {}
                    }
                }
            }
            StyleKind::Dim => {
                let doc = &mut self.tabs[i].scene.document;
                if let Some(mut s) = doc.dim_styles.get(old).cloned() {
                    s.name = new.to_string();
                    if !s.handle.is_valid() {
                        s.handle = doc.allocate_handle();
                    }
                    let _ = doc.dim_styles.add(s);
                }
                doc.dim_styles.remove(old);
                if doc.header.current_dimstyle_name.eq_ignore_ascii_case(old) {
                    doc.header.current_dimstyle_name = new.to_string();
                }
                for e in doc.entities_mut() {
                    if let acadrust::entities::EntityType::Dimension(d) = e {
                        if d.base().style_name.eq_ignore_ascii_case(old) {
                            d.base_mut().style_name = new.to_string();
                        }
                    }
                }
            }
            StyleKind::Table => {
                let doc = &mut self.tabs[i].scene.document;
                if let Some(h) = object_handle(doc, old, kind) {
                    if let Some(ObjectType::TableStyle(s)) = doc.objects.get_mut(&h) {
                        s.name = new.to_string();
                    }
                }
                if self.ribbon.active_table_style.eq_ignore_ascii_case(old) {
                    self.ribbon.active_table_style = new.to_string();
                }
            }
            StyleKind::MLeader => {
                let doc = &mut self.tabs[i].scene.document;
                if let Some(h) = object_handle(doc, old, kind) {
                    if let Some(ObjectType::MultiLeaderStyle(s)) = doc.objects.get_mut(&h) {
                        s.name = new.to_string();
                    }
                }
                if self.tabs[i].active_mleader_style.eq_ignore_ascii_case(old) {
                    self.tabs[i].active_mleader_style = new.to_string();
                }
                if self.ribbon.active_mleader_style.eq_ignore_ascii_case(old) {
                    self.ribbon.active_mleader_style = new.to_string();
                }
            }
            StyleKind::MLine => {
                let doc = &mut self.tabs[i].scene.document;
                if let Some(h) = object_handle(doc, old, kind) {
                    if let Some(ObjectType::MLineStyle(s)) = doc.objects.get_mut(&h) {
                        s.name = new.to_string();
                    }
                }
                if doc.header.multiline_style.eq_ignore_ascii_case(old) {
                    doc.header.multiline_style = new.to_string();
                }
            }
        }
    }

    // ── Public operations (called by the message handlers) ─────────────────

    // Note: the structural ops below mutate the document live (so the dialog
    // shows a preview) but do NOT mark the tab dirty, push undo, or rebuild the
    // drawing. Those side effects are deferred to `style_stage_commit` (Apply);
    // closing the window without Apply calls `style_stage_discard`, which
    // restores the snapshot taken when the manager opened.

    pub(super) fn style_new(&mut self, kind: StyleKind) {
        let i = self.active_tab;
        let name = self.unique_new_name(kind);
        let h = self.tabs[i].scene.document.allocate_handle();
        self.insert_default_style(kind, &name, h);
        self.set_style_selected(kind, name.clone());
        self.load_style_bufs(kind);
        self.after_style_change(kind);
        self.command_line
            .push_output(&format!("Style '{name}' created."));
    }

    pub(super) fn style_copy(&mut self, kind: StyleKind) {
        let i = self.active_tab;
        let src = self.style_selected(kind);
        let name = self.unique_suffixed_name(kind, &src);
        let h = self.tabs[i].scene.document.allocate_handle();
        if !self.clone_style_as(kind, &src, &name, h) {
            return;
        }
        self.set_style_selected(kind, name.clone());
        self.load_style_bufs(kind);
        self.after_style_change(kind);
        self.command_line
            .push_output(&format!("Style '{name}' created."));
    }

    pub(super) fn style_delete(&mut self, kind: StyleKind) {
        let name = self.style_selected(kind);
        if name.eq_ignore_ascii_case("Standard") {
            self.command_line
                .push_error("Cannot delete the Standard style.");
            return;
        }
        if !self.remove_style_storage(kind, &name) {
            return;
        }
        let first = self
            .style_names(kind)
            .into_iter()
            .next()
            .unwrap_or_else(|| "Standard".to_string());
        self.set_style_selected(kind, first);
        self.load_style_bufs(kind);
        self.after_style_change(kind);
        self.command_line
            .push_output(&format!("Style '{name}' deleted."));
    }

    /// Begin inline rename of the double-clicked style.
    pub(super) fn style_rename_start(&mut self, kind: StyleKind, name: String) {
        self.set_style_selected(kind, name.clone());
        self.load_style_bufs(kind);
        self.style_rename_buf = name.clone();
        self.style_rename = Some(name);
    }

    /// Commit the inline rename. No-op (with feedback) on empty / unchanged /
    /// colliding names, and the Standard style cannot be renamed.
    pub(super) fn style_rename_commit(&mut self, kind: StyleKind) {
        let Some(old) = self.style_rename.take() else {
            return;
        };
        let new = self.style_rename_buf.trim().to_string();
        self.style_rename_buf.clear();
        if new.is_empty() || new.eq_ignore_ascii_case(&old) {
            return;
        }
        if old.eq_ignore_ascii_case("Standard") {
            self.command_line
                .push_error("Cannot rename the Standard style.");
            return;
        }
        if self.style_exists(kind, &new) {
            self.command_line
                .push_error(&format!("Style '{new}' already exists."));
            return;
        }
        self.rename_style_storage(kind, &old, &new);
        if self.style_selected(kind).eq_ignore_ascii_case(&old) {
            self.set_style_selected(kind, new.clone());
        }
        self.load_style_bufs(kind);
        self.after_style_change(kind);
        self.command_line
            .push_output(&format!("Renamed '{old}' → '{new}'."));
    }

    pub(super) fn style_rename_cancel(&mut self) {
        self.style_rename = None;
        self.style_rename_buf.clear();
    }

    // ── Staging: nothing persists until Apply ──────────────────────────────
    //
    // A style manager is a transaction. When it opens we snapshot the style
    // tables / objects / current pointers; every New / Copy / Delete / Rename /
    // Set Current / property edit mutates the live document for an in-dialog
    // preview, but the tab stays clean and the drawing is not rebuilt. Apply
    // commits (marks dirty, pushes one undo entry, rebuilds); closing the
    // window without Apply discards by restoring the snapshot.

    /// Snapshot the style-related document state into a transferable record.
    fn capture_style_state(&self) -> StyleStateSnapshot {
        let i = self.active_tab;
        let doc = &self.tabs[i].scene.document;
        let style_objects = doc
            .objects
            .iter()
            .filter(|(_, o)| {
                matches!(
                    o,
                    ObjectType::TableStyle(_)
                        | ObjectType::MLineStyle(_)
                        | ObjectType::MultiLeaderStyle(_)
                )
            })
            .map(|(&h, o)| (h, o.clone()))
            .collect();
        StyleStateSnapshot {
            text_styles: doc.text_styles.clone(),
            dim_styles: doc.dim_styles.clone(),
            style_objects,
            current_text: doc.header.current_text_style_name.clone(),
            current_dim: doc.header.current_dimstyle_name.clone(),
            multiline_style: doc.header.multiline_style.clone(),
            current_table: doc.header.current_table_style_name.clone(),
            current_mleader: doc.header.current_mleader_style_name.clone(),
            active_table: self.ribbon.active_table_style.clone(),
            active_mleader: self.ribbon.active_mleader_style.clone(),
            tab_active_mleader: self.tabs[i].active_mleader_style.clone(),
        }
    }

    /// Overwrite the live style state with a snapshot (used by commit's undo
    /// dance and by discard).
    pub(super) fn restore_style_state(&mut self, i: usize, snap: &StyleStateSnapshot) {
        let doc = &mut self.tabs[i].scene.document;
        doc.text_styles = snap.text_styles.clone();
        doc.dim_styles = snap.dim_styles.clone();
        doc.objects.retain(|_, o| {
            !matches!(
                o,
                ObjectType::TableStyle(_)
                    | ObjectType::MLineStyle(_)
                    | ObjectType::MultiLeaderStyle(_)
            )
        });
        for (h, o) in &snap.style_objects {
            doc.objects.insert(*h, o.clone());
        }
        doc.header.current_text_style_name = snap.current_text.clone();
        doc.header.current_dimstyle_name = snap.current_dim.clone();
        doc.header.multiline_style = snap.multiline_style.clone();
        doc.header.current_table_style_name = snap.current_table.clone();
        doc.header.current_mleader_style_name = snap.current_mleader.clone();
        if i == self.active_tab {
            self.ribbon.active_table_style = snap.active_table.clone();
            self.ribbon.active_mleader_style = snap.active_mleader.clone();
        }
        self.tabs[i].active_mleader_style = snap.tab_active_mleader.clone();
    }

    /// Begin a staging transaction for a freshly-opened style manager.
    pub(super) fn style_stage_begin(&mut self) {
        let dirty_at_open = self.tabs[self.active_tab].dirty;
        let baseline = self.capture_style_state();
        self.style_stage = Some(StyleStage {
            dirty_at_open,
            baseline,
        });
    }

    /// Commit the staged changes (Apply): make them permanent with a single
    /// undo entry, mark the tab dirty, and rebuild the drawing.
    pub(super) fn style_stage_commit(&mut self) {
        let i = self.active_tab;
        let Some(stage) = self.style_stage.take() else {
            // No active transaction — still refresh the drawing so a bare Apply
            // is harmless.
            self.tabs[i].dirty = true;
            self.tabs[i].scene.bump_geometry();
            self.sync_ribbon_styles();
            return;
        };
        let edited = self.capture_style_state();
        let changed = edited != stage.baseline;
        if changed {
            self.tabs[i].dirty = true;
            let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
            self.tabs[i].scene.invalidate_text_style_dependencies_many(&text_names);
            self.tabs[i]
                .scene
                .invalidate_dim_style_dependencies_many(&dim_names);
            self.tabs[i]
                .scene
                .invalidate_object_style_dependencies(&object_handles);
            self.commit_style_undo(i, stage.baseline, edited.clone(), stage.dirty_at_open);
        } else {
            self.tabs[i].dirty = stage.dirty_at_open;
        }
        self.sync_ribbon_styles();
        // Re-baseline so further edits in the still-open window stage afresh.
        self.style_stage = Some(StyleStage {
            dirty_at_open: self.tabs[i].dirty,
            baseline: edited,
        });
    }

    /// Discard staged changes (window closed without Apply): restore the
    /// snapshot taken when the manager opened.
    pub(super) fn style_stage_discard(&mut self) {
        let Some(stage) = self.style_stage.take() else {
            return;
        };
        self.restore_style_state(self.active_tab, &stage.baseline);
        self.tabs[self.active_tab].dirty = stage.dirty_at_open;
        self.sync_ribbon_styles();
    }
}

/// Snapshot of every document field a style manager can touch.
#[derive(Clone, PartialEq)]
pub(super) struct StyleStateSnapshot {
    text_styles: acadrust::tables::Table<TextStyle>,
    dim_styles: acadrust::tables::Table<DimStyle>,
    style_objects: Vec<(Handle, ObjectType)>,
    current_text: String,
    current_dim: String,
    multiline_style: String,
    current_table: String,
    current_mleader: String,
    active_table: String,
    active_mleader: String,
    tab_active_mleader: String,
}

impl StyleStateSnapshot {
    pub(super) fn estimated_bytes(&self) -> usize {
        self.text_styles
            .iter()
            .count()
            .saturating_mul(320)
            .saturating_add(self.dim_styles.iter().count().saturating_mul(1024))
            .saturating_add(self.style_objects.len().saturating_mul(512))
    }

    pub(super) fn changed_keys(&self, other: &Self) -> (Vec<String>, Vec<String>, Vec<Handle>) {
        let text_names = self
            .text_styles
            .iter()
            .filter(|style| other.text_styles.get(&style.name) != Some(*style))
            .map(|style| style.name.clone())
            .chain(
                other
                    .text_styles
                    .iter()
                    .filter(|style| self.text_styles.get(&style.name) != Some(*style))
                    .map(|style| style.name.clone()),
            )
            .collect::<rustc_hash::FxHashSet<_>>()
            .into_iter()
            .collect();
        let dim_names = self
            .dim_styles
            .iter()
            .filter(|style| other.dim_styles.get(&style.name) != Some(*style))
            .map(|style| style.name.clone())
            .chain(
                other
                    .dim_styles
                    .iter()
                    .filter(|style| self.dim_styles.get(&style.name) != Some(*style))
                    .map(|style| style.name.clone()),
            )
            .collect::<rustc_hash::FxHashSet<_>>()
            .into_iter()
            .collect();
        let self_objects: rustc_hash::FxHashMap<_, _> =
            self.style_objects.iter().map(|(h, o)| (*h, o)).collect();
        let other_objects: rustc_hash::FxHashMap<_, _> =
            other.style_objects.iter().map(|(h, o)| (*h, o)).collect();
        let object_handles = self
            .style_objects
            .iter()
            .chain(other.style_objects.iter())
            .map(|(handle, _)| *handle)
            .collect::<rustc_hash::FxHashSet<_>>()
            .into_iter()
            .filter(|handle| self_objects.get(handle) != other_objects.get(handle))
            .collect();
        (text_names, dim_names, object_handles)
    }
}

/// An in-progress style-manager transaction.
pub(super) struct StyleStage {
    dirty_at_open: bool,
    baseline: StyleStateSnapshot,
}

// ── Object-store helpers ───────────────────────────────────────────────────

/// Find the object-backed style named `name` and return a clone. `pick` maps a
/// matching variant to `(its name, a clone of the inner style)`.
fn find_object_style<T>(
    doc: &acadrust::CadDocument,
    name: &str,
    pick: impl Fn(&ObjectType) -> Option<(&str, T)>,
) -> Option<T> {
    doc.objects.values().find_map(|o| {
        let (n, val) = pick(o)?;
        n.eq_ignore_ascii_case(name).then_some(val)
    })
}

fn object_handle(doc: &acadrust::CadDocument, name: &str, kind: StyleKind) -> Option<Handle> {
    doc.objects.iter().find_map(|(&h, o)| {
        let matches = match (kind, o) {
            (StyleKind::Table, ObjectType::TableStyle(s)) => s.name.eq_ignore_ascii_case(name),
            (StyleKind::MLeader, ObjectType::MultiLeaderStyle(s)) => {
                s.name.eq_ignore_ascii_case(name)
            }
            (StyleKind::MLine, ObjectType::MLineStyle(s)) => s.name.eq_ignore_ascii_case(name),
            _ => false,
        };
        matches.then_some(h)
    })
}

// ── Annotation-scale manager staging (mirrors the style-manager stage) ──────

/// Snapshot of the drawing's annotation-scale *list* a scale manager can stage:
/// the `Scale` objects and the `ACAD_SCALELIST` dictionary. Set Current takes
/// effect immediately (like the pill) and is *not* reverted; `current` is kept
/// only to repair the header pointer if a staged rename/delete of the current
/// scale is rolled back, leaving it dangling.
pub(super) struct ScaleSnapshot {
    scales: Vec<(Handle, ObjectType)>,
    scalelist: Option<(Handle, ObjectType)>,
    current: String,
}

/// An in-progress scale-manager transaction (baseline restored on discard).
/// `changed` guards against a no-op Apply dirtying the drawing / pushing undo.
pub(super) struct ScaleStage {
    dirty_at_open: bool,
    baseline: ScaleSnapshot,
    changed: bool,
    /// The manager materialised the fallback scale set into real objects on
    /// open. Even with no user edit these must be reverted on close, so the
    /// discard restores the baseline when this is set (not only on `changed`).
    materialized: bool,
}

impl OpenCADStudio {
    fn capture_scale_state(&self) -> ScaleSnapshot {
        let i = self.active_tab;
        let doc = &self.tabs[i].scene.document;
        let scales: Vec<(Handle, ObjectType)> = doc
            .objects
            .iter()
            .filter(|(_, o)| matches!(o, ObjectType::Scale(_)))
            .map(|(h, o)| (*h, o.clone()))
            .collect();
        let scalelist_h = self.tabs[i].scene.scalelist_dict_handle();
        let scalelist = scalelist_h.and_then(|h| doc.objects.get(&h).map(|o| (h, o.clone())));
        let current = doc.header.current_annotation_scale.clone();
        ScaleSnapshot {
            scales,
            scalelist,
            current,
        }
    }

    fn restore_scale_state(&mut self, snap: &ScaleSnapshot) {
        let i = self.active_tab;
        let root_h = self.tabs[i].scene.document.header.named_objects_dict_handle;
        let cur_scalelist_h = self.tabs[i].scene.scalelist_dict_handle();
        let doc = &mut self.tabs[i].scene.document;
        doc.objects.retain(|_, o| !matches!(o, ObjectType::Scale(_)));
        if let Some(h) = cur_scalelist_h {
            doc.objects.remove(&h);
        }
        for (h, o) in &snap.scales {
            doc.objects.insert(*h, o.clone());
        }
        if let Some((h, o)) = &snap.scalelist {
            doc.objects.insert(*h, o.clone());
        }
        // Restore ONLY the ACAD_SCALELIST root entry — leave every other root
        // entry alone (a modal blocks concurrent edits, but be surgical anyway).
        let baseline_scalelist_h = snap.scalelist.as_ref().map(|(h, _)| *h);
        if let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&root_h) {
            root.entries.retain(|(k, _)| k != "ACAD_SCALELIST");
            if let Some(h) = baseline_scalelist_h {
                root.entries.push(("ACAD_SCALELIST".to_string(), h));
            }
        }
        // Repair a dangling current-scale pointer. A staged rename/delete of the
        // current scale rewrote the header outside this snapshot; if the live
        // name no longer resolves against the restored list, fall back to the
        // snapshot's name. A Set Current to a still-valid scale is preserved.
        let cur = self.tabs[i]
            .scene
            .document
            .header
            .current_annotation_scale
            .clone();
        let resolves = self.tabs[i]
            .scene
            .scale_list()
            .iter()
            .any(|(n, _, _)| n.eq_ignore_ascii_case(&cur));
        if !resolves {
            self.tabs[i].scene.document.header.current_annotation_scale = snap.current.clone();
        }
        self.tabs[i].scene.bump_geometry();
    }

    /// Begin a staging transaction for a freshly-opened scale manager.
    pub(super) fn scale_stage_begin(&mut self) {
        let dirty_at_open = self.tabs[self.active_tab].dirty;
        let baseline = self.capture_scale_state();
        self.scale_stage = Some(ScaleStage {
            dirty_at_open,
            baseline,
            changed: false,
            materialized: false,
        });
    }

    /// Flag that the staged scale list has actually been mutated, so a later
    /// Apply commits (and a no-op Apply doesn't dirty the drawing).
    pub(super) fn scale_stage_mark(&mut self) {
        if let Some(s) = self.scale_stage.as_mut() {
            s.changed = true;
        }
    }

    /// Flag that the manager materialised the fallback scale set on open, so
    /// the discard reverts it even when the user made no edit.
    pub(super) fn scale_stage_materialized(&mut self) {
        if let Some(s) = self.scale_stage.as_mut() {
            s.materialized = true;
        }
    }

    /// Commit staged scale changes (Apply): one undo entry, mark dirty, and
    /// re-baseline so the still-open window keeps staging afresh. A no-op Apply
    /// (nothing staged) does nothing.
    pub(super) fn scale_stage_commit(&mut self) {
        let i = self.active_tab;
        let Some(stage) = self.scale_stage.take() else {
            return;
        };
        if !stage.changed {
            self.scale_stage = Some(stage);
            return;
        }
        let edited = self.capture_scale_state();
        self.restore_scale_state(&stage.baseline);
        self.push_undo_snapshot(i, "SCALELISTEDIT");
        self.restore_scale_state(&edited);
        self.tabs[i].dirty = true;
        self.scale_stage = Some(ScaleStage {
            dirty_at_open: true,
            baseline: edited,
            changed: false,
            // The materialised set is now committed as the new baseline, so it
            // no longer needs reverting on close.
            materialized: false,
        });
    }

    /// Discard staged scale changes (window closed without Apply): restore the
    /// snapshot taken when the manager opened. No-op when nothing was staged.
    pub(super) fn scale_stage_discard(&mut self) {
        let Some(stage) = self.scale_stage.take() else {
            return;
        };
        // Revert on a real edit *or* when only the fallback set was materialised
        // (opened but not applied) — otherwise those 12 scales would leak in.
        if stage.changed || stage.materialized {
            self.restore_scale_state(&stage.baseline);
            self.tabs[self.active_tab].dirty = stage.dirty_at_open;
        }
    }
}
