//! Designeraktionen auf dem gemeinsamen FRM-Dokument, ohne VM oder Ereignisschleife.
use crate::{
    app::{App, Dialog, DialogKind, Field, Hit, Mode, WindowKind},
    commands::Command,
    documents::DocumentId,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use crossterm::event::{
    KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{
    layout::Rect,
    style::{Modifier, Style},
    widgets::Paragraph,
    Frame,
};
use std::collections::{BTreeMap, BTreeSet};
use tb_frontend::{
    forms::{self, ObjectClass as Class, PropertyType},
    lexer::{self, TokenKind},
};
use tb_runtime::screen::TextScreen;
use tb_ui::{
    forms::{FormsModel, PropertyValue as Value},
    frm::{self, FormFile, FormNode},
};
use tb_vm::project_io::Content;

pub type DesignId = u64;
type Key = (String, Option<i32>);
type Drag = (u16, u16, Option<(i8, i8)>);
#[derive(Default)]
pub struct Designer {
    pub document: Option<DocumentId>,
    pub selections: BTreeMap<DocumentId, Vec<DesignId>>,
    pub grid: bool,
    pub tool: Option<(Class, i32)>,
    pub property: usize,
    pub value: Option<Field>,
    clipboard: Vec<FormNode>,
    pub viewport: Rect,
    pub objects: Vec<(DesignId, Rect)>,
    drag: Option<Drag>,
    paint: Option<(&'static str, u8)>,
    last_tool_click: Option<(usize, std::time::Instant)>,
}
pub fn node_key(n: &FormNode) -> Key {
    (
        n.name.to_uppercase(),
        forms::property(n.class, "INDEX")
            .and_then(|(p, _)| n.properties.get(&p))
            .and_then(|v| {
                if let Value::Integer(n) = v {
                    Some(*n)
                } else {
                    None
                }
            }),
    )
}
pub fn nodes(root: &FormNode) -> Vec<&FormNode> {
    let mut out = vec![root];
    for n in &root.children {
        out.extend(nodes(n));
    }
    out
}
fn find_mut<'a>(n: &'a mut FormNode, key: &Key) -> Option<&'a mut FormNode> {
    if node_key(n) == *key {
        return Some(n);
    }
    n.children.iter_mut().find_map(|n| find_mut(n, key))
}
pub fn value(n: &FormNode, name: &str) -> Option<Value> {
    let (p, s) = forms::property(n.class, name)?;
    Some(
        n.properties
            .get(&p)
            .cloned()
            .unwrap_or_else(|| Value::from_default(s.default, s.ty)),
    )
}
fn integer(n: &FormNode, name: &str) -> i32 {
    match value(n, name) {
        Some(Value::Integer(n)) => n,
        _ => 0,
    }
}
fn set(n: &mut FormNode, name: &str, v: Value) -> Result<()> {
    let (p, _) =
        forms::property(n.class, name).ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?;
    n.properties.insert(p, v);
    Ok(())
}
fn text(v: &Value) -> String {
    match v {
        Value::Integer(v) => v.to_string(),
        Value::Single(v) => v.to_string(),
        Value::String(v) => v.clone(),
        Value::Boolean(v) => v.to_string(),
        _ => format!("{v:?}"),
    }
}
fn container(c: Class) -> bool {
    matches!(c, Class::Form | Class::Frame | Class::PictureBox)
}
/// Beide Spin-Stile sind Varianten derselben Metadatenklasse.
pub fn tools() -> Vec<(&'static str, Class, i32)> {
    let mut out: Vec<_> = Class::ALL
        .into_iter()
        .filter(|c| !matches!(c, Class::Form | Class::Screen | Class::Menu | Class::Spin))
        .map(|c| (c.display_name(), c, 0))
        .collect();
    out.extend([("VSpin", Class::Spin, 0), ("HSpin", Class::Spin, 1)]);
    out
}
pub fn preview(form: &FormFile, size: (u16, u16)) -> Result<(FormsModel, TextScreen)> {
    let mut model = FormsModel::new(form.catalog().objects, size.0 as usize, size.1 as usize);
    form.apply(&mut model)?;
    // Timer bleiben ausschließlich im Dokument konfiguriert, nie in der Vorschau aktiv.
    let enabled = forms::property(Class::Timer, "ENABLED").unwrap().0;
    let catalog = form.catalog();
    for node in nodes(&form.root)
        .into_iter()
        .filter(|n| n.class == Class::Timer)
    {
        let object = catalog.find(&node.name).unwrap().0;
        model.set_initial_at(object, node_key(node).1, enabled, Value::Boolean(false))?;
    }
    model.show(0, false)?;
    // Es gibt weder eine VM noch einen Timer-/Event-Pump im Designer.
    model.events.clear();
    let mut screen = TextScreen::with_size(size.0 as usize, size.1 as usize);
    model.render(&mut screen);
    Ok((model, screen))
}
fn validate(form: &FormFile) -> Result<()> {
    let parsed = frm::read_text("Designer", &frm::write_text(form))?;
    let catalog = parsed.catalog();
    parsed.initial_values(&catalog)?;
    fn visit(n: &FormNode, parent: Option<&FormNode>, depth: usize) -> Result<()> {
        if let Some(p) = parent {
            ensure!(
                if n.class == Class::Menu {
                    matches!(p.class, Class::Menu | Class::Form)
                } else {
                    container(p.class)
                },
                "{}: ungültiger Container {}",
                n.name,
                p.name
            );
            if !matches!(n.class, Class::Timer | Class::Menu) {
                ensure!(
                    integer(n, "LEFT") + integer(n, "WIDTH") <= integer(p, "WIDTH")
                        && integer(n, "TOP") + integer(n, "HEIGHT") <= integer(p, "HEIGHT"),
                    "{}: Geometrie außerhalb von {}",
                    n.name,
                    p.name
                );
            }
        }
        if n.class == Class::Menu {
            ensure!(depth <= 6, "{}: höchstens sechs Menüebenen", n.name);
            let shortcut = text(&value(n, "SHORTCUT").unwrap());
            ensure!(
                shortcut.is_empty()
                    || forms::menu_shortcuts()
                        .iter()
                        .any(|s| s.eq_ignore_ascii_case(&shortcut.replace(' ', ""))),
                "{}.Shortcut: nicht unterstützt",
                n.name
            );
            ensure!(
                shortcut.is_empty() || parent.is_some_and(|p| p.class == Class::Menu),
                "{}.Shortcut: kein Shortcut auf Menütitel",
                n.name
            );
            if value(n, "SEPARATOR") == Some(Value::Boolean(true))
                || value(n, "CAPTION") == Some(Value::String("-".into()))
            {
                ensure!(
                    n.children.is_empty()
                        && shortcut.is_empty()
                        && value(n, "CHECKED") == Some(Value::Boolean(false))
                        && value(n, "ENABLED") == Some(Value::Boolean(true)),
                    "{}: ungültiger Separator",
                    n.name
                );
            }
        }
        for child in &n.children {
            visit(
                child,
                Some(n),
                depth + usize::from(child.class == Class::Menu),
            )?;
        }
        Ok(())
    }
    visit(&parsed.root, None, 0)
}
impl App {
    pub fn design_document(&self) -> Result<DocumentId> {
        self.active_window()
            .and_then(|w| {
                if let WindowKind::Code(v) = w.kind {
                    self.project.view(v).ok().map(|v| v.document())
                } else {
                    None
                }
            })
            .filter(|id| {
                self.project
                    .document(*id)
                    .is_ok_and(|d| matches!(d.content(), Content::Form(_)))
            })
            .or(self.designer.document)
            .ok_or_else(|| anyhow!("Kein Formular ausgewählt"))
    }
    pub fn design_form(&self) -> Result<&FormFile> {
        let Content::Form(f) = self.project.document(self.design_document()?)?.content() else {
            bail!("Kein Formular")
        };
        Ok(f)
    }
    pub fn design_enter(&mut self, id: DocumentId) -> Result<()> {
        self.designer.document = Some(id);
        let ids = self.project.design_ids(id)?.clone();
        let root = ids[&node_key(&self.design_form()?.root)];
        let selection = self.designer.selections.entry(id).or_default();
        selection.retain(|i| ids.values().any(|v| i == v));
        if selection.is_empty() {
            selection.push(root);
        }
        self.designer.value = None;
        Ok(())
    }
    pub fn design_objects(&mut self) -> Result<Vec<(DesignId, FormNode)>> {
        let id = self.design_document()?;
        let ids = self.project.design_ids(id)?.clone();
        Ok(nodes(&self.design_form()?.root)
            .into_iter()
            .map(|n| (ids[&node_key(n)], n.clone()))
            .collect())
    }
    pub fn design_select(&mut self, id: DesignId, multiple: bool) -> Result<()> {
        ensure!(
            self.design_objects()?.iter().any(|(i, _)| *i == id),
            "Unbekanntes Designobjekt"
        );
        let doc = self.design_document()?;
        let selection = self.designer.selections.entry(doc).or_default();
        if !multiple {
            selection.clear();
        }
        if multiple && selection.contains(&id) {
            selection.retain(|i| *i != id);
        } else {
            selection.push(id);
        }
        self.designer.property = 0;
        self.designer.value = None;
        self.value_focus = false;
        Ok(())
    }
    fn selected_keys(&mut self) -> Result<Vec<Key>> {
        let doc = self.design_document()?;
        let selected = self
            .designer
            .selections
            .get(&doc)
            .cloned()
            .unwrap_or_default();
        Ok(self
            .design_objects()?
            .into_iter()
            .filter(|(id, _)| selected.contains(id))
            .map(|(_, n)| node_key(&n))
            .collect())
    }
    fn selected_node(&mut self) -> Result<FormNode> {
        let key = self
            .selected_keys()?
            .last()
            .cloned()
            .ok_or_else(|| anyhow!("Kein Control ausgewählt"))?;
        Ok(nodes(&self.design_form()?.root)
            .into_iter()
            .find(|n| node_key(n) == key)
            .unwrap()
            .clone())
    }
    fn design_edit(&mut self, edit: impl FnOnce(&mut FormFile) -> Result<()>) -> Result<()> {
        let id = self.design_document()?;
        self.project.edit_form(id, |f| {
            edit(f)?;
            validate(f)
        })?;
        self.designer.value = None;
        Ok(())
    }
    pub fn design_place(
        &mut self,
        class: Class,
        style: i32,
        parent: DesignId,
        rect: Rect,
    ) -> Result<DesignId> {
        ensure!(
            !matches!(class, Class::Form | Class::Screen),
            "Keine platzierbare Klasse"
        );
        let objects = self.design_objects()?;
        let p = objects
            .iter()
            .find(|(id, _)| *id == parent)
            .ok_or_else(|| anyhow!("Container fehlt"))?
            .1
            .clone();
        ensure!(
            if class == Class::Menu {
                matches!(p.class, Class::Menu | Class::Form)
            } else {
                container(p.class)
            },
            "{} ist kein passender Container",
            p.name
        );
        let names: BTreeSet<_> = objects.iter().map(|(_, n)| n.name.to_uppercase()).collect();
        let name = (1..)
            .map(|n| format!("{}{n}", class.display_name()))
            .find(|n| !names.contains(&n.to_uppercase()))
            .unwrap();
        let mut node = FormNode {
            class,
            name: name.clone(),
            properties: BTreeMap::new(),
            children: vec![],
        };
        if !matches!(class, Class::Timer | Class::Menu) {
            for (name, n) in [
                ("LEFT", rect.x),
                ("TOP", rect.y),
                ("WIDTH", rect.width),
                ("HEIGHT", rect.height),
            ] {
                set(&mut node, name, Value::Integer(n as i32))?;
            }
        }
        if class == Class::Spin {
            ensure!((0..=1).contains(&style), "Spin.Style ungültig");
            set(&mut node, "STYLE", Value::Integer(style))?;
            set(
                &mut node,
                "WIDTH",
                Value::Integer(if style == 1 { 2 } else { 1 }),
            )?;
            set(
                &mut node,
                "HEIGHT",
                Value::Integer(if style == 1 { 1 } else { 2 }),
            )?;
        }
        if forms::property(class, "CAPTION").is_some() {
            set(&mut node, "CAPTION", Value::String(name.clone()))?;
        }
        if forms::property(class, "TABINDEX").is_some() {
            set(
                &mut node,
                "TABINDEX",
                Value::Integer(
                    objects
                        .iter()
                        .map(|(_, n)| integer(n, "TABINDEX"))
                        .max()
                        .unwrap_or(0)
                        + 1,
                ),
            )?;
        }
        let key = node_key(&node);
        self.design_edit(|f| {
            find_mut(&mut f.root, &node_key(&p))
                .unwrap()
                .children
                .push(node);
            Ok(())
        })?;
        let doc = self.design_document()?;
        let id = self.project.design_ids(doc)?[&key];
        self.design_select(id, false)?;
        Ok(id)
    }
    pub fn design_geometry(&mut self, dx: i32, dy: i32, sizing: bool) -> Result<()> {
        let keys = self.selected_keys()?;
        self.design_edit(|f| {
            // Ausgewählte Kinder eines ausgewählten Containers nicht doppelt verschieben.
            fn edit(n: &mut FormNode, keys: &[Key], dx: i32, dy: i32, sizing: bool) -> Result<()> {
                if keys.contains(&node_key(n)) && !matches!(n.class, Class::Timer | Class::Menu) {
                    for (name, delta) in [
                        (if sizing { "WIDTH" } else { "LEFT" }, dx),
                        (if sizing { "HEIGHT" } else { "TOP" }, dy),
                    ] {
                        if delta == 0 {
                            continue;
                        }
                        let (_, spec) = forms::property(n.class, name).unwrap();
                        ensure!(spec.writable, "{}.{} ist schreibgeschützt", n.name, name);
                        set(n, name, Value::Integer(integer(n, name) + delta))?;
                    }
                    if !sizing {
                        return Ok(());
                    }
                }
                for c in &mut n.children {
                    edit(c, keys, dx, dy, sizing)?;
                }
                Ok(())
            }
            edit(&mut f.root, &keys, dx, dy, sizing)
        })
    }
    fn design_resize(&mut self, dx: i32, dy: i32, edges: (i8, i8)) -> Result<()> {
        let keys = self.selected_keys()?;
        self.design_edit(|f| {
            for key in keys {
                let n = find_mut(&mut f.root, &key).ok_or_else(|| anyhow!("Control fehlt"))?;
                if matches!(n.class, Class::Timer | Class::Menu) {
                    continue;
                }
                for (position, size, edge, delta) in [
                    ("LEFT", "WIDTH", edges.0, dx),
                    ("TOP", "HEIGHT", edges.1, dy),
                ] {
                    if edge == 0 || delta == 0 {
                        continue;
                    }
                    ensure!(
                        forms::property(n.class, size).unwrap().1.writable,
                        "{}.{} ist schreibgeschützt",
                        n.name,
                        size
                    );
                    if edge < 0 {
                        set(n, position, Value::Integer(integer(n, position) + delta))?;
                    }
                    set(
                        n,
                        size,
                        Value::Integer(integer(n, size) + delta * i32::from(edge)),
                    )?;
                }
            }
            Ok(())
        })
    }
    pub fn design_property(&mut self, name: &str, input: &str) -> Result<()> {
        let n = self.selected_node()?;
        if name.eq_ignore_ascii_case("CTLNAME") || name.eq_ignore_ascii_case("FORMNAME") {
            return self.design_rename(input);
        }
        let (p, s) = forms::property(n.class, name)
            .ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?;
        let edit = || -> Result<Value> {
            ensure!(s.writable, "schreibgeschützt");
            Ok(match s.ty {
                PropertyType::Integer => {
                    let v = input.parse::<i32>()?;
                    ensure!(
                        s.min.is_none_or(|min| v >= min) && s.max.is_none_or(|max| v <= max),
                        "außerhalb des Wertebereichs"
                    );
                    Value::Integer(v)
                }
                PropertyType::Single => {
                    let v = input.parse::<f32>()?;
                    ensure!(v.is_finite(), "nicht endlich");
                    Value::Single(v)
                }
                PropertyType::Boolean => {
                    Value::Boolean(match input.to_ascii_lowercase().as_str() {
                        "true" | "-1" | "1" => true,
                        "false" | "0" => false,
                        _ => bail!("Boolean erwartet: true/false"),
                    })
                }
                PropertyType::String => Value::String(input.into()),
                _ => bail!("nicht editierbarer Propertytyp"),
            })
        };
        let v = edit().with_context(|| format!("{}.{}", n.name, s.name))?;
        let (mut preview_model, _) = preview(self.design_form()?, self.size)?;
        let object = self.design_form()?.catalog().find(&n.name).unwrap().0;
        preview_model
            .set_at(object, node_key(&n).1, p, v.clone())
            .with_context(|| format!("{}.{}", n.name, s.name))?;
        let old = node_key(&n);
        let doc = self.design_document()?;
        let stable = self.project.design_ids(doc)?[&old];
        let mut changed = n.clone();
        changed.properties.insert(p, v.clone());
        if n.class == Class::Spin && s.name == "STYLE" {
            set(
                &mut changed,
                "WIDTH",
                Value::Integer(if input == "1" { 2 } else { 1 }),
            )?;
            set(
                &mut changed,
                "HEIGHT",
                Value::Integer(if input == "1" { 1 } else { 2 }),
            )?;
        }
        let new = node_key(&changed);
        self.design_edit(|f| {
            *find_mut(&mut f.root, &old).unwrap() = changed;
            Ok(())
        })
        .with_context(|| format!("{}.{}", n.name, s.name))?;
        if old != new {
            self.project.rekey_design_id(doc, &old, new, stable);
        }
        Ok(())
    }
    pub fn design_copy(&mut self, cut: bool) -> Result<()> {
        let keys = self.selected_keys()?;
        let mut copied = Vec::new();
        fn collect(n: &FormNode, keys: &[Key], out: &mut Vec<FormNode>) {
            if n.class != Class::Form && keys.contains(&node_key(n)) {
                out.push(n.clone());
            } else {
                for c in &n.children {
                    collect(c, keys, out);
                }
            }
        }
        collect(&self.design_form()?.root, &keys, &mut copied);
        ensure!(!copied.is_empty(), "Keine Controls ausgewählt");
        if cut {
            self.design_delete()?;
        }
        self.designer.clipboard = copied;
        Ok(())
    }
    pub fn design_delete(&mut self) -> Result<()> {
        let keys = self.selected_keys()?;
        ensure!(
            !keys.contains(&node_key(&self.design_form()?.root)),
            "Formular über Remove File entfernen"
        );
        let names: Vec<_> = nodes(&self.design_form()?.root)
            .into_iter()
            .filter(|n| keys.contains(&node_key(n)))
            .flat_map(nodes)
            .map(|n| n.name.clone())
            .collect();
        self.design_edit(|f| {
            fn remove(n: &mut FormNode, keys: &[Key]) {
                n.children.retain(|c| !keys.contains(&node_key(c)));
                for c in &mut n.children {
                    remove(c, keys);
                }
            }
            remove(&mut f.root, &keys);
            Ok(())
        })?;
        let id = self.design_document()?;
        self.design_enter(id)?;
        self.message=format!("Entfernt: {}. Vorhandener Code bleibt erhalten; zugehörige Handler sind jetzt ungebunden.",names.join(", "));
        self.open_dialog(
            "Löschfolgen",
            DialogKind::Message,
            vec![Field::text("Hinweis", self.message.clone())],
        );
        Ok(())
    }
    pub fn design_paste(&mut self) -> Result<()> {
        ensure!(
            !self.designer.clipboard.is_empty(),
            "Control-Zwischenablage leer"
        );
        let parent = self.selected_node()?;
        ensure!(
            container(parent.class) || parent.class == Class::Menu,
            "Zum Einfügen einen Container auswählen"
        );
        let mut copies = self.designer.clipboard.clone();
        let mut names: BTreeSet<_> = nodes(&self.design_form()?.root)
            .iter()
            .map(|n| n.name.to_uppercase())
            .collect();
        let mut renamed = BTreeMap::new();
        fn rename(
            n: &mut FormNode,
            names: &mut BTreeSet<String>,
            renamed: &mut BTreeMap<String, String>,
        ) -> Result<()> {
            let old = n.name.to_uppercase();
            let name = renamed
                .entry(old.clone())
                .or_insert_with(|| {
                    let chosen = (1..)
                        .map(|i| format!("{old}Copy{i}"))
                        .find(|n| !names.contains(n))
                        .unwrap();
                    names.insert(chosen.clone());
                    chosen
                })
                .clone();
            n.name = name.clone();
            if let Some((p, _)) = forms::property(n.class, "CTLNAME") {
                if n.properties.contains_key(&p) {
                    n.properties.insert(p, Value::String(name));
                }
            }
            for c in &mut n.children {
                rename(c, names, renamed)?;
            }
            Ok(())
        }
        for n in &mut copies {
            rename(n, &mut names, &mut renamed)?;
        }
        let pasted: Vec<_> = copies.iter().map(node_key).collect();
        self.design_edit(|f| {
            find_mut(&mut f.root, &node_key(&parent))
                .unwrap()
                .children
                .extend(copies);
            Ok(())
        })?;
        let doc = self.design_document()?;
        let ids = self.project.design_ids(doc)?;
        self.designer
            .selections
            .insert(doc, pasted.iter().map(|k| ids[k]).collect());
        Ok(())
    }
    pub fn design_menu_move(&mut self, direction: &str) -> Result<()> {
        let key = node_key(&self.selected_node()?);
        self.design_edit(|f| {
            fn change(n: &mut FormNode, key: &Key, d: &str) -> Result<bool> {
                if let Some(i) = n.children.iter().position(|c| node_key(c) == *key) {
                    ensure!(n.children[i].class == Class::Menu, "Kein Menüelement");
                    match d {
                        "up" => {
                            ensure!(
                                i > 0 && n.children[i - 1].class == Class::Menu,
                                "Kein Vorgänger"
                            );
                            n.children.swap(i, i - 1);
                        }
                        "down" => {
                            ensure!(
                                i + 1 < n.children.len() && n.children[i + 1].class == Class::Menu,
                                "Kein Nachfolger"
                            );
                            n.children.swap(i, i + 1);
                        }
                        "in" => {
                            ensure!(
                                i > 0 && n.children[i - 1].class == Class::Menu,
                                "Kein Menüvorgänger"
                            );
                            let c = n.children.remove(i);
                            n.children[i - 1].children.push(c);
                        }
                        _ => bail!("Keine äußere Menüebene"),
                    }
                    return Ok(true);
                }
                for i in 0..n.children.len() {
                    if d == "out" {
                        if let Some(j) = n.children[i]
                            .children
                            .iter()
                            .position(|c| node_key(c) == *key)
                        {
                            let c = n.children[i].children.remove(j);
                            n.children.insert(i + 1, c);
                            return Ok(true);
                        }
                    }
                    if change(&mut n.children[i], key, d)? {
                        return Ok(true);
                    }
                }
                Ok(false)
            }
            ensure!(change(&mut f.root, &key, direction)?, "Menüelement fehlt");
            Ok(())
        })
    }
    pub fn design_rename(&mut self, new: &str) -> Result<()> {
        self.design_rename_with(new, None)
    }
    fn design_rename_with(&mut self, new: &str, extra: Option<FormFile>) -> Result<()> {
        let selected = self.selected_node()?;
        let old = selected.name.to_uppercase();
        if old == new.to_uppercase() {
            return Ok(());
        }
        // Benutzt dieselbe Namensprüfung wie die Prozedurerzeugung.
        tb_frontend::editing::new_procedure("", 0, new, false).map_err(|e| anyhow!(e))?;
        let doc = self.design_document()?;
        let ids = self.project.design_ids(doc)?.clone();
        let input = self.project.sources()?;
        let mut compiler = tb_vm::project::ProjectCompiler::default();
        let compiled = input.compile(&mut compiler, "RENAME").map_err(|d| {
            anyhow!(
                "Umbenennen erfordert eindeutig auflösbare Quellen: {}",
                d.iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("; ")
            )
        })?;
        let mut catalog = forms::FormCatalog::default();
        for f in &input.forms {
            catalog.append(&f.catalog());
        }
        ensure!(
            !catalog
                .objects
                .iter()
                .any(|o| o.name.eq_ignore_ascii_case(new)),
            "Name {new} bereits vorhanden"
        );
        let form_name = self.design_form()?.root.name.clone();
        let target = if selected.class == Class::Form {
            catalog.find(&old)
        } else {
            catalog.find_in(&old, &form_name)
        }
        .ok_or_else(|| anyhow!("Objektbindung fehlt"))?
        .0;
        let mut changes: BTreeMap<DocumentId, Content> = BTreeMap::new();
        let mut edits: BTreeMap<DocumentId, BTreeMap<usize, (usize, String)>> = BTreeMap::new();
        let mut handlers = BTreeMap::new();
        let mut references = Vec::new();
        let mut procedures = Vec::new();
        let source_files = compiled.sources;
        let bound = compiler.bound_form_references().map_err(|d| {
            anyhow!(
                "Unklare Bindungen: {}",
                d.iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("; ")
            )
        })?;
        procedures.extend(bound.procedures);
        for (pos, name, object) in &bound.objects {
            if *object != target {
                continue;
            }
            if *name == old {
                references.push((*pos, name.clone(), new.to_owned()));
            } else if let Some(event) = name.strip_prefix(&format!("{old}_")) {
                let replacement = format!("{new}_{event}");
                handlers.insert(name.clone(), replacement.clone());
                references.push((*pos, name.clone(), replacement));
            }
        }
        for (pos, name) in procedures {
            if let Some(replacement) = handlers.get(&name) {
                references.push((pos, name, replacement.clone()));
            }
        }
        for (pos, name, replacement) in references {
            let path = &source_files[pos.source as usize].path;
            let id = self
                .project
                .find_document(std::path::Path::new(path))
                .ok_or_else(|| anyhow!("Betroffene Include-Datei nicht geöffnet: {path}"))?;
            let d = self.project.document(id)?;
            let first = match d.content() {
                Content::Form(f) => frm::read_text(path, &frm::write_text(f))?.code_line(),
                _ => 1,
            };
            let line = pos
                .line
                .checked_sub(first)
                .ok_or_else(|| anyhow!("Referenz außerhalb des Codebereichs"))?
                + 1;
            let lexed = lexer::lex(d.code());
            let t = lexed
                .tokens
                .iter()
                .find(|t| {
                    t.pos.line == line
                        && t.pos.column >= pos.column
                        && matches!(&t.kind,TokenKind::Ident{name:n,..} if n.split(['.','!']).any(|part|part==name))
                })
                .ok_or_else(|| anyhow!("{path}:{line}: {name} nicht sicher lokalisierbar"))?;
            let token_start = token_offset(d.code(), t.pos.line, t.pos.column)?;
            let TokenKind::Ident {
                name: path_name, ..
            } = &t.kind
            else {
                unreachable!()
            };
            let mut component = 0;
            for part in path_name.split(['.', '!']) {
                if part == name {
                    break;
                }
                component += part.len() + 1;
            }
            let start = token_start + component;
            ensure!(
                d.code()
                    .get(start..start + name.len())
                    .is_some_and(|s| s.eq_ignore_ascii_case(&name)),
                "Referenz nicht bytegenau auflösbar"
            );
            edits
                .entry(id)
                .or_default()
                .insert(start, (start + name.len(), replacement));
        }
        for (id, replacements) in edits {
            let mut content = self.project.document(id)?.content().clone();
            for (start, (end, replacement)) in replacements.into_iter().rev() {
                content.code_mut().replace_range(start..end, &replacement);
            }
            changes.insert(id, content);
        }
        let content = changes
            .entry(doc)
            .or_insert_with(|| self.project.document(doc).unwrap().content().clone());
        let Content::Form(f) = content else {
            unreachable!()
        };
        fn rename(n: &mut FormNode, old: &str, new: &str) {
            if n.name.eq_ignore_ascii_case(old) {
                n.name = new.into();
                let property = if n.class == Class::Form {
                    "FORMNAME"
                } else {
                    "CTLNAME"
                };
                if let Some((p, _)) = forms::property(n.class, property) {
                    if n.properties.contains_key(&p) {
                        n.properties.insert(p, Value::String(new.into()));
                    }
                }
            }
            for c in &mut n.children {
                rename(c, old, new);
            }
        }
        if let Some(extra) = extra {
            f.root = extra.root;
        }
        rename(&mut f.root, &old, new);
        validate(f)?;
        let mut loader = self.project.loader()?;
        for (id, c) in &changes {
            loader
                .insert(self.project.document(*id)?.source_path(), c.clone())
                .map_err(|e| anyhow!(e))?;
        }
        let candidate = loader
            .load_manifest(self.project.manifest().clone())
            .map_err(|e| anyhow!(e))?;
        let mut candidate_compiler = tb_vm::project::ProjectCompiler::default();
        let candidate_compiled = candidate
            .compile(&mut candidate_compiler, "RENAME")
            .map_err(|d| {
                anyhow!(
                    "Namenskonflikt; nichts geändert: {}",
                    d.iter()
                        .map(ToString::to_string)
                        .collect::<Vec<_>>()
                        .join("; ")
                )
            })?;
        // Dieselben vollständig aufgelösten Importkontexte vor und nach der Änderung.
        let candidate_bound = candidate_compiler.bound_form_references().map_err(|d| {
            anyhow!(
                "Unklare neue Bindungen: {}",
                d.iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("; ")
            )
        })?;
        let bindings = |references: &[(tb_frontend::SourcePos, String, u16)],
                        files: &[tb_frontend::source::SourceFile],
                        rename_old: bool| {
            let mut result = Vec::new();
            for (p, n, o) in references {
                let mut n = n.clone();
                if rename_old && *o == target {
                    if n == old {
                        n = new.to_uppercase();
                    } else if let Some(r) = handlers.get(&n) {
                        n = r.to_uppercase();
                    }
                }
                result.push((files[p.source as usize].path.clone(), n, *o));
            }
            result.sort();
            result
        };
        ensure!(
            bindings(&bound.objects, &source_files, true)
                == bindings(&candidate_bound.objects, &candidate_compiled.sources, false),
            "Unklare oder verdeckte Objektverweise; nichts geändert"
        );
        self.project
            .commit_transaction(changes.into_iter().collect())?;
        self.project.rename_design_ids(doc, &old, new, &ids);
        self.designer.value = None;
        Ok(())
    }
    pub fn design_event(&mut self, object: DesignId, event: &str) -> Result<()> {
        let doc = self.design_document()?;
        let n = self
            .design_objects()?
            .into_iter()
            .find(|(id, _)| *id == object)
            .ok_or_else(|| anyhow!("Objekt fehlt"))?
            .1;
        let event = event.to_uppercase();
        ensure!(
            forms::events(n.class).contains(&event.as_str()),
            "{}.{} ist kein Ereignis",
            n.name,
            event
        );
        let prefix = if n.class == Class::Form {
            "Form"
        } else {
            &n.name
        };
        let name = format!("{prefix}_{event}");
        let input = self.project.sources()?;
        let mut catalog = forms::FormCatalog::default();
        for f in &input.forms {
            catalog.append(&f.catalog());
        }
        let target_object = if n.class == Class::Form {
            catalog.find(&n.name)
        } else {
            catalog.find_in(&n.name, &self.design_form()?.root.name)
        }
        .ok_or_else(|| anyhow!("Objektbindung fehlt"))?
        .0;
        let mut found = None;
        for unit in &input.units {
            let mut files = Vec::new();
            let (module, diags) = unit.parse(0, &mut files);
            ensure!(
                diags.is_empty(),
                "Unvollständiger Quelltext vor Ereigniserzeugung"
            );
            let bound = tb_frontend::sema::bound_form_references(&module, &catalog);
            let exports = tb_frontend::sema::export_declarations(&module, &[]);
            for p in module.procs.iter().filter(|p| {
                p.sig.name.rsplit_once('_').is_some_and(|(_, e)| e == event)
                    && bound.objects.iter().any(|(pos, name, object)| {
                        *pos == p.pos && *name == p.sig.name && *object == target_object
                    })
            }) {
                let sig = exports
                    .iter()
                    .find_map(|s| match s {
                        tb_frontend::ast::Stmt::Declare { sig, .. } if sig.name == p.sig.name => {
                            Some(sig)
                        }
                        _ => None,
                    })
                    .unwrap();
                ensure!(found.is_none(), "Mehrdeutige Ereignisprozedur {name}");
                let target = self
                    .project
                    .find_document(std::path::Path::new(&files[p.pos.source as usize].path))
                    .ok_or_else(|| anyhow!("Ereignisdatei fehlt"))?;
                let params = event_parameters(&n, &event);
                ensure!(
                    sig.kind == tb_frontend::ast::ProcKind::Sub
                        && sig.params.len() == params.len()
                        && sig
                            .params
                            .iter()
                            .zip(&params)
                            .all(
                                |(actual, (name, ty))| actual.name.eq_ignore_ascii_case(name)
                                    && actual
                                        .as_type
                                        .as_ref()
                                        .map(tb_frontend::editing::type_text)
                                        .or_else(|| actual.suffix.map(|s| match s {
                                            lexer::Suffix::Integer => "INTEGER",
                                            lexer::Suffix::Single => "SINGLE",
                                            _ => "",
                                        }
                                        .into()))
                                        .is_some_and(|t| t == *ty)
                            ),
                    "{name}: vorhandene Ereignissignatur stimmt nicht"
                );
                let first = match self.project.document(target)?.content() {
                    Content::Form(f) => frm::read_text("Event", &frm::write_text(f))?.code_line(),
                    _ => 1,
                };
                found = Some((
                    target,
                    crate::editor::line_offset(
                        self.project.document(target)?.code(),
                        p.pos.line - first + 1,
                    ),
                ));
            }
        }
        let (target, at) = if let Some(found) = found {
            found
        } else {
            let code = self.project.document(doc)?.code();
            let mut addition = self.new_procedure_text(doc, code.len(), &name, false, true)?;
            let params = event_parameters(&n, &event)
                .iter()
                .map(|(name, ty)| format!("{name} AS {ty}"))
                .collect::<Vec<_>>()
                .join(", ");
            // BASIC-Parameter sind BYREF; derselbe Prozedurrumpf wie New Sub.
            addition = addition.replacen(" ()", &format!(" ({params})"), 1);
            let at = code.len();
            self.project.replace_text(doc, at..at, &addition)?;
            (doc, at + 1)
        };
        self.designer.document = Some(doc);
        self.designer.selections.insert(doc, vec![object]);
        self.show_document(target, false)?;
        let v = self.editor_view()?;
        self.project.view_mut(v)?.cursor = at;
        self.editor_scroll()?;
        Ok(())
    }
    pub(crate) fn designer_command(&mut self, command: Command) -> Result<bool> {
        use Command::*;
        match command {
            Grid => self.designer.grid = !self.designer.grid,
            Toolbox | Palette | MenuDesign => {
                self.design_document()?;
                self.show_tool(match command {
                    Toolbox => WindowKind::Toolbox,
                    Palette => WindowKind::Palette,
                    _ => WindowKind::MenuDesign,
                });
            }
            Tool(name) => {
                let (_, class, style) = tools()
                    .into_iter()
                    .find(|(n, _, _)| n.eq_ignore_ascii_case(name))
                    .ok_or_else(|| anyhow!("Unbekanntes Werkzeug"))?;
                let doc = self.design_document()?;
                self.show_document(doc, true)?;
                self.designer.tool = Some((class, style));
                self.value_focus = false;
            }
            Events => {
                let objects = self.design_objects()?;
                let selected = self.selected_keys()?.last().cloned();
                let at = objects
                    .iter()
                    .position(|(_, n)| Some(node_key(n)) == selected)
                    .unwrap_or(0);
                self.open_dialog(
                    "Event Procedures · Objekt",
                    DialogKind::DesignObject(objects.iter().map(|(id, _)| *id).collect()),
                    vec![Field::choice(
                        "Objekt",
                        objects
                            .iter()
                            .map(|(_, n)| format!("{} {:?}", n.name, node_key(n).1))
                            .collect(),
                        at,
                    )],
                );
            }
            Cut | Copy | Paste | Clear if self.mode == Mode::Designer => match command {
                Cut => self.design_copy(true)?,
                Copy => self.design_copy(false)?,
                Paste => self.design_paste()?,
                _ => self.design_delete()?,
            },
            Undo if self.mode == Mode::Designer => {
                let doc = self.design_document()?;
                self.project.undo(doc)?;
                self.design_enter(doc)?;
            }
            Form if self.designer.document.is_some() && self.mode == Mode::Environment => {
                self.show_document(self.designer.document.unwrap(), true)?;
            }
            Code if self.mode == Mode::Designer => {
                let doc = self.design_document()?;
                self.show_document(doc, false)?;
            }
            _ => return Ok(false),
        }
        Ok(true)
    }
    pub(crate) fn design_value_focus(&mut self) -> Result<()> {
        let node = self.selected_node()?;
        let specs = forms::properties(node.class);
        self.designer.property = self.designer.property.min(specs.len() - 1);
        let name = specs[self.designer.property].name;
        let v = if matches!(name, "CTLNAME" | "FORMNAME") {
            node.name.clone()
        } else {
            text(&value(&node, name).unwrap())
        };
        self.designer.value = Some(Field::text(name, v));
        self.properties = true;
        self.value_focus = true;
        Ok(())
    }
    pub(crate) fn design_action(&mut self, action: &str) -> Result<()> {
        match action {
            "property" => {
                let n = self.selected_node()?;
                let specs = forms::properties(n.class);
                self.open_dialog(
                    "Properties",
                    DialogKind::DesignProperty,
                    vec![Field::choice(
                        "Property",
                        specs.iter().map(|p| p.name.into()).collect(),
                        self.designer.property.min(specs.len() - 1),
                    )],
                );
            }
            "value" => self.design_value_focus()?,
            "palette" => self.open_dialog(
                "Color Palette",
                DialogKind::DesignPalette,
                vec![
                    Field::choice("ForeColor", (0..16).map(|n| n.to_string()).collect(), 0),
                    Field::choice("BackColor", (0..16).map(|n| n.to_string()).collect(), 7),
                    Field::toggle("Auf Formular auftragen", false),
                ],
            ),
            "insert" => {
                let objects = self.design_objects()?;
                let selected = self.selected_node()?;
                let parent = if selected.class == Class::Menu {
                    objects
                        .iter()
                        .find(|(_, n)| node_key(n) == node_key(&selected))
                        .unwrap()
                        .0
                } else {
                    objects[0].0
                };
                self.design_place(Class::Menu, 0, parent, Rect::default())?;
            }
            "delete" => self.design_delete()?,
            "up" | "down" | "in" | "out" => self.design_menu_move(action)?,
            "menu" => {
                let n = self.selected_node()?;
                ensure!(n.class == Class::Menu, "Menüelement auswählen");
                let shortcuts = forms::menu_shortcuts();
                let current = text(&value(&n, "SHORTCUT").unwrap());
                let at = shortcuts
                    .iter()
                    .position(|s| s.eq_ignore_ascii_case(&current.replace(' ', "")))
                    .unwrap_or(0);
                self.open_dialog(
                    "Menu Design",
                    DialogKind::DesignMenu(node_key(&n)),
                    vec![
                        Field::text("CtlName", n.name.clone()),
                        Field::text("Caption", text(&value(&n, "CAPTION").unwrap())),
                        Field::text("Tag", text(&value(&n, "TAG").unwrap())),
                        Field::text(
                            "Index (leer: skalar)",
                            node_key(&n).1.map(|i| i.to_string()).unwrap_or_default(),
                        ),
                        Field::toggle(
                            "Checked",
                            value(&n, "CHECKED") == Some(Value::Boolean(true)),
                        ),
                        Field::toggle(
                            "Enabled",
                            value(&n, "ENABLED") == Some(Value::Boolean(true)),
                        ),
                        Field::toggle(
                            "Visible",
                            value(&n, "VISIBLE") == Some(Value::Boolean(true)),
                        ),
                        Field::toggle(
                            "Separator",
                            value(&n, "SEPARATOR") == Some(Value::Boolean(true)),
                        ),
                        Field::choice("Shortcut", shortcuts, at),
                    ],
                );
            }
            _ => bail!("Unbekannte Designeraktion"),
        }
        Ok(())
    }
    pub(crate) fn design_submit(&mut self, d: &mut Dialog) -> Result<bool> {
        match &d.kind {
            DialogKind::DesignProperty => {
                self.designer.property = d.fields[0].index();
                self.design_value_focus()?;
            }
            DialogKind::DesignObject(ids) => {
                let id = ids[d.fields[0].index()];
                let n = self
                    .design_objects()?
                    .into_iter()
                    .find(|(i, _)| *i == id)
                    .unwrap()
                    .1;
                self.open_dialog(
                    "Event Procedures · Ereignis",
                    DialogKind::DesignEvent(id),
                    vec![Field::choice(
                        "Ereignis",
                        forms::events(n.class)
                            .iter()
                            .map(|s| s.to_string())
                            .collect(),
                        0,
                    )],
                );
            }
            DialogKind::DesignEvent(id) => self.design_event(*id, &d.fields[0].string())?,
            DialogKind::DesignPalette => {
                let keys = if d.fields[2].flag() {
                    vec![node_key(&self.design_form()?.root)]
                } else {
                    self.selected_keys()?
                };
                let fg = d.fields[0].index() as i32;
                let bg = d.fields[1].index() as i32;
                self.design_edit(|f| {
                    for key in keys {
                        let n = find_mut(&mut f.root, &key).unwrap();
                        for (name, v) in [("FORECOLOR", fg), ("BACKCOLOR", bg)] {
                            if forms::property(n.class, name).is_some() {
                                set(n, name, Value::Integer(v))?;
                            }
                        }
                    }
                    Ok(())
                })?;
            }
            DialogKind::DesignMenu(key) => {
                let key = key.clone();
                let new = d.fields[0].string();
                let mut candidate = self.design_form()?.clone();
                let n = find_mut(&mut candidate.root, &key)
                    .ok_or_else(|| anyhow!("Menüelement fehlt"))?;
                for (name, field) in [("CAPTION", 1), ("TAG", 2), ("SHORTCUT", 8)] {
                    set(n, name, Value::String(d.fields[field].string()))?;
                }
                for (name, field) in [
                    ("CHECKED", 4),
                    ("ENABLED", 5),
                    ("VISIBLE", 6),
                    ("SEPARATOR", 7),
                ] {
                    set(n, name, Value::Boolean(d.fields[field].flag()))?;
                }
                let p = forms::property(Class::Menu, "INDEX").unwrap().0;
                if d.fields[3].string().is_empty() {
                    n.properties.remove(&p);
                } else {
                    n.properties.insert(
                        p,
                        Value::Integer(
                            d.fields[3]
                                .string()
                                .parse()
                                .context("Menu.Index: Integer erwartet")?,
                        ),
                    );
                }
                let final_key = node_key(n);
                let updated = n.clone();
                validate(&candidate)?;
                // Eine Namensänderung und die übrigen Felder werden zusammen validiert.
                // Die Rename-Transaktion muss vor jedem Commit alle Kandidaten kennen.
                self.design_menu_commit(&key, &new, updated, final_key)?;
            }
            _ => return Ok(false),
        }
        Ok(true)
    }
    pub(crate) fn design_key(&mut self, key: KeyEvent) -> Result<bool> {
        if self.mode != Mode::Designer {
            return Ok(false);
        }
        if self.value_focus {
            if key.code == K::Esc {
                self.value_focus = false;
                self.designer.value = None;
                return Ok(true);
            }
            if key.code == K::Enter {
                let field = self
                    .designer
                    .value
                    .clone()
                    .ok_or_else(|| anyhow!("Keine Property ausgewählt"))?;
                self.design_property(&field.label, &field.string())?;
                self.value_focus = false;
                return Ok(true);
            }
            if let Some(v) = &mut self.designer.value {
                v.key(key);
            }
            return Ok(true);
        }
        match self.active_window().map(|w| w.kind) {
            Some(WindowKind::Palette) if key.code == K::Enter => {
                self.design_action("palette")?;
                return Ok(true);
            }
            Some(WindowKind::Toolbox) => {
                let list = tools();
                let at = self
                    .designer
                    .tool
                    .and_then(|t| list.iter().position(|(_, c, s)| (*c, *s) == t))
                    .unwrap_or(0);
                match key.code {
                    K::Up | K::Down => {
                        let i = (at
                            + list.len()
                            + if key.code == K::Down {
                                1
                            } else {
                                list.len() - 1
                            })
                            % list.len();
                        self.designer.tool = Some((list[i].1, list[i].2));
                    }
                    K::Enter => {
                        self.design_tool_double(at)?;
                    }
                    _ => return Ok(false),
                }
                return Ok(true);
            }
            Some(WindowKind::MenuDesign) => {
                let control = key.modifiers.contains(M::CONTROL);
                match key.code {
                    K::Insert => self.design_action("insert")?,
                    K::Delete => self.design_action("delete")?,
                    K::Enter => self.design_action("menu")?,
                    K::Left if control => self.design_action("out")?,
                    K::Right if control => self.design_action("in")?,
                    K::Up if control => self.design_action("up")?,
                    K::Down if control => self.design_action("down")?,
                    K::Up | K::Down => self.design_tab(key.code == K::Down, true)?,
                    _ => return Ok(false),
                }
                return Ok(true);
            }
            _ => {}
        }
        match key.code {
            K::Tab | K::BackTab => self.design_tab(
                key.code == K::Tab && !key.modifiers.contains(M::SHIFT),
                false,
            )?,
            K::PageUp | K::PageDown => {
                let n = forms::properties(self.selected_node()?.class).len();
                self.designer.property =
                    (self.designer.property + n + if key.code == K::PageDown { 1 } else { n - 1 })
                        % n;
            }
            K::Left | K::Right | K::Up | K::Down => {
                let step = if key.modifiers.contains(M::CONTROL) {
                    5
                } else {
                    1
                };
                let (dx, dy) = match key.code {
                    K::Left => (-step, 0),
                    K::Right => (step, 0),
                    K::Up => (0, -step),
                    _ => (0, step),
                };
                self.design_geometry(dx, dy, key.modifiers.contains(M::SHIFT))?;
            }
            K::Delete => self.design_delete()?,
            K::Esc => self.designer.tool = None,
            _ => return Ok(false),
        }
        Ok(true)
    }
    fn design_tab(&mut self, forward: bool, menus: bool) -> Result<()> {
        let mut objects = self.design_objects()?;
        objects.retain(|(_, n)| {
            if menus {
                n.class == Class::Menu
            } else {
                !matches!(n.class, Class::Form | Class::Menu)
            }
        });
        if objects.is_empty() {
            return Ok(());
        }
        if !menus {
            objects.sort_by_key(|(_, n)| integer(n, "TABINDEX"));
        }
        let selected = self.selected_keys()?.last().cloned();
        let i = objects
            .iter()
            .position(|(_, n)| Some(node_key(n)) == selected);
        let at = i.map_or(if forward { 0 } else { objects.len() - 1 }, |i| {
            (i + objects.len() + if forward { 1 } else { objects.len() - 1 }) % objects.len()
        });
        self.design_select(objects[at].0, false)
    }
    fn design_tool_double(&mut self, index: usize) -> Result<()> {
        let (_, class, style) = tools()[index];
        let objects = self.design_objects()?;
        let selected = self.selected_node()?;
        let parent = objects
            .iter()
            .find(|(_, n)| node_key(n) == node_key(&selected) && container(n.class))
            .unwrap_or(&objects[0])
            .0;
        self.design_place(
            class,
            style,
            parent,
            Rect::new(
                1,
                1,
                if class == Class::VScrollBar { 1 } else { 8 },
                if class == Class::VScrollBar { 5 } else { 1 },
            ),
        )?;
        self.designer.tool = None;
        Ok(())
    }
    pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result<bool> {
        if self.mode != Mode::Designer || self.dialog.is_some() || self.menu.is_some() {
            return Ok(false);
        }
        if let Some((property, color)) = self.designer.paint {
            if m.kind == MouseEventKind::Up(MouseButton::Left) {
                self.designer.paint = None;
                let id = self
                    .designer
                    .objects
                    .iter()
                    .rev()
                    .find(|(_, r)| r.contains((m.column, m.row).into()))
                    .map(|(id, _)| *id)
                    .ok_or_else(|| anyhow!("Farbe auf die Designfläche ziehen"))?;
                let n = self
                    .design_objects()?
                    .into_iter()
                    .find(|(i, _)| *i == id)
                    .unwrap()
                    .1;
                ensure!(
                    forms::property(n.class, property).is_some(),
                    "{}.{} nicht vorhanden",
                    n.name,
                    property
                );
                self.design_edit(|f| {
                    set(
                        find_mut(&mut f.root, &node_key(&n)).unwrap(),
                        property,
                        Value::Integer(color as i32),
                    )
                })?;
            }
            return Ok(true);
        }
        if m.kind == MouseEventKind::Down(MouseButton::Left) {
            if let Some((_, Hit::DesignPaint(property, color))) = self
                .hits
                .iter()
                .rev()
                .find(|(r, _)| r.contains((m.column, m.row).into()))
            {
                self.designer.paint = Some((*property, *color));
                return Ok(true);
            }
        }
        if m.kind == MouseEventKind::Down(MouseButton::Left) {
            if let Some((_, Hit::DesignTool(index))) = self
                .hits
                .iter()
                .rev()
                .find(|(r, _)| r.contains((m.column, m.row).into()))
            {
                let index = *index;
                let double = self
                    .designer
                    .last_tool_click
                    .is_some_and(|(i, t)| i == index && t.elapsed().as_millis() < 500);
                self.designer.last_tool_click = Some((index, std::time::Instant::now()));
                let (_, class, style) = tools()[index];
                self.designer.tool = Some((class, style));
                if double {
                    self.designer.last_tool_click = None;
                    self.design_tool_double(index)?;
                }
                return Ok(true);
            }
        }
        if self.designer.drag.is_none() {
            if let Some((_, hit)) = self
                .hits
                .iter()
                .rev()
                .find(|(r, _)| r.contains((m.column, m.row).into()))
            {
                match hit {
                    Hit::Window(id)
                        if self
                            .windows
                            .iter()
                            .any(|w| w.id == *id && matches!(w.kind, WindowKind::Code(_))) => {}
                    _ => return Ok(false),
                }
            }
        }
        let inside = self.designer.viewport.contains((m.column, m.row).into());
        if !inside && self.designer.drag.is_none() {
            return Ok(false);
        }
        match m.kind {
            MouseEventKind::Down(MouseButton::Left) => {
                if let Some(doc) = self.designer.document {
                    if let Some(w)=self.windows.iter().find(|w|matches!(w.kind,WindowKind::Code(v) if self.project.view(v).is_ok_and(|v|v.document()==doc))) {self.active=w.id;}
                }
                let hit = self
                    .designer
                    .objects
                    .iter()
                    .rev()
                    .find(|(_, r)| r.contains((m.column, m.row).into()))
                    .copied();
                if let Some((id, r)) = hit {
                    let selected = self
                        .designer
                        .selections
                        .get(&self.design_document()?)
                        .is_some_and(|s| s.contains(&id));
                    let resize = if selected && self.designer.tool.is_none() {
                        handle_at(r, m.column, m.row)
                    } else {
                        None
                    };
                    if !selected || m.modifiers.contains(M::CONTROL) {
                        self.design_select(id, m.modifiers.contains(M::CONTROL))?;
                    }
                    self.designer.drag = Some((m.column, m.row, resize));
                } else if self.designer.tool.is_some() {
                    self.designer.drag = Some((m.column, m.row, None));
                }
                self.value_focus = false;
            }
            MouseEventKind::Up(MouseButton::Left) => {
                if let Some((x, y, handle)) = self.designer.drag.take() {
                    if let Some((class, style)) = self.designer.tool.take() {
                        let objects = self.design_objects()?;
                        let parent = self
                            .designer
                            .objects
                            .iter()
                            .rev()
                            .find(|(id, r)| {
                                r.contains((x, y).into())
                                    && objects.iter().any(|(i, n)| i == id && container(n.class))
                            })
                            .copied()
                            .ok_or_else(|| anyhow!("Innerhalb eines Containers aufziehen"))?;
                        let left = x
                            .min(m.column)
                            .checked_sub(parent.1.x)
                            .ok_or_else(|| anyhow!("Außerhalb des Containers"))?;
                        let top = y
                            .min(m.row)
                            .checked_sub(parent.1.y)
                            .ok_or_else(|| anyhow!("Außerhalb des Containers"))?;
                        self.design_place(
                            class,
                            style,
                            parent.0,
                            Rect::new(left, top, x.abs_diff(m.column) + 1, y.abs_diff(m.row) + 1),
                        )?;
                    } else {
                        let dx = m.column as i32 - x as i32;
                        let dy = m.row as i32 - y as i32;
                        if dx != 0 || dy != 0 {
                            if let Some(edges) = handle {
                                self.design_resize(dx, dy, edges)?;
                            } else {
                                self.design_geometry(dx, dy, false)?;
                            }
                        }
                    }
                }
            }
            MouseEventKind::Drag(MouseButton::Left) => {}
            _ => return Ok(false),
        }
        Ok(true)
    }
    fn design_menu_commit(
        &mut self,
        key: &Key,
        new: &str,
        updated: FormNode,
        final_key: Key,
    ) -> Result<()> {
        let doc = self.design_document()?;
        let stable = self.project.design_ids(doc)?[key];
        let mut candidate = self.design_form()?.clone();
        *find_mut(&mut candidate.root, key).unwrap() = updated;
        if !key.0.eq_ignore_ascii_case(new) {
            self.design_rename_with(new, Some(candidate))?;
        } else {
            self.design_edit(|f| {
                f.root = candidate.root;
                Ok(())
            })?;
        }
        self.project
            .rekey_design_id(doc, key, (new.to_uppercase(), final_key.1), stable);
        Ok(())
    }
    pub(crate) fn design_render(&mut self, f: &mut Frame, area: Rect) {
        self.designer.viewport = area;
        self.designer.objects.clear();
        let render = || -> Result<_> {
            let form = self.design_form()?.clone();
            let (model, screen) = preview(&form, (area.width, area.height))?;
            Ok((form, model, screen))
        };
        let Ok((form, model, screen)) = render() else {
            return;
        };
        f.render_widget(tb_ui::screen::ScreenWidget(&screen), area);
        let Ok(objects) = self.design_objects() else {
            return;
        };
        let catalog = form.catalog();
        let mut timers = 0;
        for (id, n) in objects {
            if n.class == Class::Menu {
                continue;
            }
            let object = catalog.find(&n.name).unwrap().0;
            let Some((x, y, w, h)) = model.rect((object, node_key(&n).1)) else {
                continue;
            };
            let r = if n.class == Class::Timer {
                let r = Rect::new(area.x + timers * 4, area.bottom().saturating_sub(1), 3, 1);
                timers += 1;
                f.render_widget(
                    Paragraph::new("[T]").style(Style::default().fg(crate::render::dos(14))),
                    r.intersection(area),
                );
                r
            } else {
                Rect::new(
                    area.x + x.saturating_sub(1) as u16,
                    area.y + y.saturating_sub(1) as u16,
                    w as u16,
                    h as u16,
                )
            };
            let r = r.intersection(area);
            if r.is_empty() {
                continue;
            }
            if self.designer.grid && n.class == Class::Form {
                for y in r.y..r.bottom() {
                    for x in r.x..r.right() {
                        let cell = &mut f.buffer_mut()[(x, y)];
                        if cell.symbol() == " " {
                            cell.set_symbol("·");
                        }
                    }
                }
            }
            self.designer.objects.push((id, r));
        }
        let selection = self
            .design_document()
            .ok()
            .and_then(|id| self.designer.selections.get(&id))
            .cloned()
            .unwrap_or_default();
        for (id, r) in &self.designer.objects {
            if selection.contains(id) {
                for (x, y) in [
                    (r.x, r.y),
                    (r.right() - 1, r.y),
                    (r.x, r.bottom() - 1),
                    (r.right() - 1, r.bottom() - 1),
                    (r.x + r.width / 2, r.y),
                    (r.x + r.width / 2, r.bottom() - 1),
                    (r.x, r.y + r.height / 2),
                    (r.right() - 1, r.y + r.height / 2),
                ] {
                    f.render_widget(
                        Paragraph::new("■").style(Style::default().fg(crate::render::dos(14))),
                        Rect::new(x, y, 1, 1),
                    );
                }
            }
        }
    }
    pub(crate) fn design_tool_render(&mut self, f: &mut Frame, kind: WindowKind, area: Rect) {
        match kind {
            WindowKind::Toolbox => {
                for (i, (name, class, style)) in
                    tools().iter().enumerate().take(area.height as usize)
                {
                    let r = Rect::new(area.x, area.y + i as u16, area.width, 1);
                    f.render_widget(
                        Paragraph::new(format!(
                            "{} {name}",
                            if self.designer.tool == Some((*class, *style)) {
                                ">"
                            } else {
                                " "
                            }
                        )),
                        r,
                    );
                    self.hits.push((r, Hit::DesignTool(i)));
                }
            }
            WindowKind::Palette => {
                for (row, property) in [(0, "FORECOLOR"), (2, "BACKCOLOR")] {
                    f.render_widget(
                        Paragraph::new(property),
                        Rect::new(area.x, area.y + row, area.width, 1).intersection(area),
                    );
                    for n in 0..16u16 {
                        let r =
                            Rect::new(area.x + n * 2, area.y + row + 1, 2, 1).intersection(area);
                        if !r.is_empty() {
                            f.render_widget(
                                Paragraph::new(format!("{n:X} ")).style(
                                    Style::default()
                                        .bg(crate::render::dos(n as u8))
                                        .fg(crate::render::dos(if n < 8 { 15 } else { 0 })),
                                ),
                                r,
                            );
                            self.hits.push((r, Hit::DesignPaint(property, n as u8)));
                        }
                    }
                }
                let r = Rect::new(area.x, area.y + 4, area.width, 1).intersection(area);
                f.render_widget(Paragraph::new("Enter: Fore/Back auftragen"), r);
                self.hits.push((r, Hit::DesignAction("palette")));
            }
            WindowKind::MenuDesign => {
                let labels = [
                    ("+", "insert"),
                    ("Edit", "menu"),
                    ("Del", "delete"),
                    ("↑", "up"),
                    ("↓", "down"),
                    ("→", "in"),
                    ("←", "out"),
                ];
                let mut x = area.x;
                for (label, action) in labels {
                    let r = Rect::new(x, area.y, label.len() as u16 + 1, 1).intersection(area);
                    f.render_widget(Paragraph::new(label), r);
                    self.hits.push((r, Hit::DesignAction(action)));
                    x += r.width;
                }
                if let Ok(objects) = self.design_objects() {
                    let selected = self
                        .design_document()
                        .ok()
                        .and_then(|d| self.designer.selections.get(&d))
                        .cloned()
                        .unwrap_or_default();
                    let menus: Vec<_> = objects
                        .into_iter()
                        .filter(|(_, n)| n.class == Class::Menu)
                        .collect();
                    let current = menus
                        .iter()
                        .position(|(id, _)| selected.contains(id))
                        .unwrap_or(0);
                    let start = current.saturating_sub(area.height.saturating_sub(3) as usize);
                    for (row, (id, n)) in menus
                        .iter()
                        .skip(start)
                        .take(area.height.saturating_sub(1) as usize)
                        .enumerate()
                    {
                        fn depth(n: &FormNode, key: &Key) -> Option<usize> {
                            if node_key(n) == *key {
                                Some(0)
                            } else {
                                n.children.iter().find_map(|c| depth(c, key).map(|d| d + 1))
                            }
                        }
                        let depth = self
                            .design_form()
                            .ok()
                            .and_then(|f| depth(&f.root, &node_key(n)))
                            .unwrap_or(1);
                        let r = Rect::new(area.x, area.y + 1 + row as u16, area.width, 1);
                        f.render_widget(
                            Paragraph::new(format!(
                                "{}{} {}{}",
                                " ".repeat(depth.saturating_sub(1) * 2),
                                text(&value(n, "CAPTION").unwrap()),
                                n.name,
                                node_key(n).1.map(|i| format!("({i})")).unwrap_or_default()
                            ))
                            .style(if selected.contains(id) {
                                Style::default().add_modifier(Modifier::REVERSED)
                            } else {
                                Style::default()
                            }),
                            r,
                        );
                        self.hits.push((r, Hit::DesignObject(*id)));
                    }
                }
            }
            _ => {}
        }
    }
    pub(crate) fn design_bar(&mut self) -> (String, String) {
        let Ok(n) = self.selected_node() else {
            return ("Properties: kein Control ausgewählt".into(), String::new());
        };
        let specs = forms::properties(n.class);
        self.designer.property = self.designer.property.min(specs.len() - 1);
        let name = specs[self.designer.property].name;
        let v = self
            .designer
            .value
            .as_ref()
            .filter(|_| self.value_focus)
            .map(Field::string)
            .unwrap_or_else(|| {
                if matches!(name, "CTLNAME" | "FORMNAME") {
                    n.name.clone()
                } else {
                    text(&value(&n, name).unwrap())
                }
            });
        (
            format!(
                "Property: {} [{}]↓ Value: [{}]{}",
                n.name,
                name,
                v,
                if self.value_focus { " *" } else { "" }
            ),
            format!(
                "{},{} │ {}×{}",
                integer(&n, "LEFT"),
                integer(&n, "TOP"),
                integer(&n, "WIDTH"),
                integer(&n, "HEIGHT")
            ),
        )
    }
}
fn token_offset(code: &str, line: u32, col: u32) -> Result<usize> {
    let start = crate::editor::line_offset(code, line);
    let offset = code[start..]
        .char_indices()
        .nth(col.saturating_sub(1) as usize)
        .map(|(i, _)| i)
        .ok_or_else(|| anyhow!("Ungültige Quellposition"))?;
    Ok(start + offset)
}
fn event_parameters(n: &FormNode, event: &str) -> Vec<(String, String)> {
    let mut params = Vec::new();
    if node_key(n).1.is_some() {
        params.push(("Index".into(), "INTEGER".into()));
    }
    for (name, ty) in forms::event_params(event).unwrap_or_default() {
        params.push((
            (*name).into(),
            match ty {
                forms::EventParamType::Integer => "INTEGER",
                forms::EventParamType::Single => "SINGLE",
                forms::EventParamType::Control => "CONTROL",
            }
            .into(),
        ));
    }
    params
}

fn handle_at(r: Rect, x: u16, y: u16) -> Option<(i8, i8)> {
    let columns = [(r.x, -1), (r.right() - 1, 1), (r.x + r.width / 2, 0)];
    let rows = [(r.y, -1), (r.bottom() - 1, 1), (r.y + r.height / 2, 0)];
    for (col, horizontal) in columns {
        for (row, vertical) in rows {
            if (horizontal != 0 || vertical != 0) && x == col && y == row {
                return Some((
                    if r.width == 1 { 0 } else { horizontal },
                    if r.height == 1 { 0 } else { vertical },
                ));
            }
        }
    }
    None
}
