//! Gemeinsame Tabellenauflösung getrennter Modulübersetzungen.
use crate::{
    bytecode::{CompiledModule, Instr},
    codegen,
    library::{Library, LibraryModule, ModuleMetadata},
};
use std::collections::{HashMap, HashSet};
use tb_frontend::{
    ast::{Module, Stmt, TypeName},
    forms::FormCatalog,
    hir::{HProcKind, HTy},
    source::{locate_diagnostics, SourceUnit},
    Diagnostic, SourcePos,
};
use tb_runtime::value::TypeInit;
use tb_ui::frm::FormFile;

fn diagnostic(message: impl Into<String>) -> Diagnostic {
    Diagnostic {
        file: None,
        pos: SourcePos::default(),
        message: message.into(),
    }
}

/// Prozesslokaler Cache: unverknüpfte Produkte, niemals ein bereits gelinktes Programm.
#[derive(Default)]
pub struct ProjectCompiler {
    parsed: Vec<Parsed>,
    products: Vec<Product>,
    pub stats: CompileStats,
    /// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können.
    pub debug_symbols: bool,
    debug_maps: Vec<DebugMap>,
    debug_names: Vec<String>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CompileStats {
    pub parsed: usize,
    pub compiled: usize,
    pub reused: usize,
}
struct Parsed {
    unit: SourceUnit,
    prefix: Vec<tb_frontend::source::SourceFile>,
    sources: Vec<tb_frontend::source::SourceFile>,
    module: Module,
    diagnostics: Vec<Diagnostic>,
    names: HashSet<String>,
}
struct Product {
    module: Module,
    debug_module: Module,
    catalog: FormCatalog,
    code: CompiledModule,
    commons: Vec<tb_frontend::hir::HCommon>,
}

pub fn compile_project(
    name: &str,
    units: &[SourceUnit],
    catalog: &FormCatalog,
    forms: &[FormFile],
) -> Result<CompiledModule, Vec<Diagnostic>> {
    ProjectCompiler::default().compile(name, units, catalog, forms)
}

impl ProjectCompiler {
    /// Bindungen aus den tatsächlich übersetzten Modulen einschließlich ihrer Imports.
    pub fn bound_form_references(
        &self,
    ) -> Result<tb_frontend::sema::BoundFormReferences, Vec<Diagnostic>> {
        let mut result = tb_frontend::sema::BoundFormReferences::default();
        for product in &self.products {
            let bound = tb_frontend::sema::bound_form_references(&product.module, &product.catalog);
            result.objects.extend(bound.objects);
            // Importierte Prototypen im Cache haben keine physische Quellposition.
            result
                .procedures
                .extend(bound.procedures.into_iter().filter(|(pos, _)| pos.line > 0));
            result.diagnostics.extend(bound.diagnostics);
        }
        if result.diagnostics.is_empty() {
            Ok(result)
        } else {
            Err(result.diagnostics)
        }
    }
    pub fn compile(
        &mut self,
        name: &str,
        units: &[SourceUnit],
        catalog: &FormCatalog,
        forms: &[FormFile],
    ) -> Result<CompiledModule, Vec<Diagnostic>> {
        let library = self.compile_library(units, catalog, forms, &[])?;
        self.link_library(name, library, forms.first().map(|f| f.root.name.as_str()))
    }
    pub fn link_library(
        &mut self,
        name: &str,
        library: Library,
        startup: Option<&str>,
    ) -> Result<CompiledModule, Vec<Diagnostic>> {
        self.link_product(name, library, startup, false)
    }
    fn link_product(
        &mut self,
        name: &str,
        library: Library,
        startup: Option<&str>,
        allow_open: bool,
    ) -> Result<CompiledModule, Vec<Diagnostic>> {
        library.validate().map_err(|e| vec![diagnostic(e)])?;
        let source_count = library
            .modules
            .iter()
            .try_fold(0usize, |sum, m| sum.checked_add(m.code.sources.len()));
        if source_count.is_none_or(|n| n > u32::MAX as usize) {
            return Err(vec![diagnostic("Zu viele Quelldateien")]);
        }
        let mut sources = Vec::new();
        let metadata: Vec<_> =
            library
                .modules
                .iter()
                .enumerate()
                .map(|(id, m)| {
                    let offset = sources.len() as u32;
                    sources.extend(m.code.sources.iter().map(|s| {
                        tb_frontend::source::SourceFile {
                            module: id as u16,
                            path: s.path.clone(),
                        }
                    }));
                    let mut meta = m.metadata.clone();
                    meta.pos.source += offset;
                    for (_, p) in &mut meta.declarations {
                        p.source += offset;
                    }
                    for c in &mut meta.commons {
                        c.pos.source += offset;
                    }
                    meta
                })
                .collect();
        let parts = library.modules.into_iter().map(|m| m.code).collect();
        let (mut result, maps) = link(name, parts, &metadata, allow_open).map_err(|e| {
            let mut errors = vec![e];
            locate_diagnostics(&mut errors, &sources);
            errors
        })?;
        self.debug_maps = maps;
        self.debug_names = metadata.iter().map(|m| m.name.clone()).collect();
        result.startup_form = startup.and_then(|name| {
            FormCatalog {
                objects: result.objects.clone(),
            }
            .find(name)
            .map(|(id, _)| id)
        });
        result
            .validate()
            .map_err(|e| vec![diagnostic(e.to_string())])?;
        Ok(result)
    }
    pub fn compile_library(
        &mut self,
        units: &[SourceUnit],
        catalog: &FormCatalog,
        forms: &[FormFile],
        libraries: &[Library],
    ) -> Result<Library, Vec<Diagnostic>> {
        self.stats = CompileStats::default();
        if (units.is_empty() && libraries.is_empty())
            || units.len() + libraries.iter().map(|l| l.modules.len()).sum::<usize>()
                > u16::MAX as usize
        {
            return Err(vec![diagnostic("Projekt ohne Module oder zu viele Module")]);
        }
        let mut sources = Vec::new();
        let mut diagnostics = Vec::new();
        let mut names = HashSet::new();
        let mut parsed: Vec<_> = units
            .iter()
            .enumerate()
            .map(|(id, unit)| {
                let cached = self
                    .parsed
                    .get(id)
                    .filter(|p| p.unit == *unit && p.prefix == sources);
                let (module, errors) = if let Some(p) = cached {
                    sources = p.sources.clone();
                    (p.module.clone(), p.diagnostics.clone())
                } else {
                    let prefix = sources.clone();
                    let (module, errors) = unit.parse(id as u16, &mut sources);
                    let entry = Parsed {
                        unit: unit.clone(),
                        prefix,
                        sources: sources.clone(),
                        module: module.clone(),
                        diagnostics: errors.clone(),
                        names: unit
                            .segments
                            .iter()
                            .flat_map(|s| tb_frontend::lexer::lex(&s.text).tokens)
                            .filter_map(|t| match t.kind {
                                tb_frontend::lexer::TokenKind::Ident { name, .. } => Some(name),
                                _ => None,
                            })
                            .collect(),
                    };
                    if id < self.parsed.len() {
                        self.parsed[id] = entry;
                    } else {
                        self.parsed.push(entry);
                    }
                    self.stats.parsed += 1;
                    (module, errors)
                };
                if !names.insert(unit.name.to_uppercase()) {
                    let mut error =
                        diagnostic(format!("Duplicate definition: module {}", unit.name));
                    error.pos = module_pos(&module);
                    error.file = unit.segments.first().map(|s| s.file.clone());
                    diagnostics.push(error);
                }
                diagnostics.extend(errors);
                module
            })
            .collect();
        locate_diagnostics(&mut diagnostics, &sources);
        if !diagnostics.is_empty() {
            return Err(diagnostics);
        }
        for library in libraries {
            library.validate().map_err(|e| vec![diagnostic(e)])?;
            for m in &library.modules {
                if !names.insert(m.metadata.name.to_uppercase()) {
                    return Err(vec![diagnostic(format!(
                        "Duplicate definition: module {}",
                        m.metadata.name
                    ))]);
                }
                parsed.push(m.metadata.declaration_view());
            }
        }
        let mut catalog = catalog.clone();
        for library in libraries {
            for m in &library.modules {
                merge_objects(&mut catalog.objects, &m.code.objects).map_err(|e| vec![e])?;
            }
        }
        if catalog.find("SCREEN").is_none() {
            catalog.add(
                "SCREEN",
                tb_frontend::forms::ObjectClass::Screen,
                None,
                false,
            );
        }
        for module in &parsed {
            if module
                .body
                .iter()
                .any(|s| matches!(s, Stmt::MetaForm { .. }))
                && catalog.find(&module.name).is_none()
            {
                catalog.add(
                    &module.name,
                    tb_frontend::forms::ObjectClass::Form,
                    None,
                    false,
                );
            }
        }
        let mut exports: Vec<_> = parsed
            .iter()
            .map(|m| tb_frontend::sema::export_declarations(m, &[]))
            .collect();
        // Konstantenabhängigkeiten können beliebig über Module verteilt sein.
        // Jeder erfolgreiche Durchlauf löst mindestens eine weitere Deklaration auf.
        let constant_count: usize = exports
            .iter()
            .flatten()
            .filter(|s| matches!(s, Stmt::ConstDecl { .. }))
            .count();
        for _ in 0..constant_count {
            let mut constants: HashMap<_, Vec<_>> = HashMap::new();
            for stmt in exports.iter().flatten() {
                if let Stmt::ConstDecl { items, .. } = stmt {
                    constants.entry(&items[0].0).or_default().push(stmt.clone());
                }
            }
            let constants: Vec<_> = constants
                .into_values()
                .filter(|s| s.len() == 1)
                .flatten()
                .collect();
            let next: Vec<_> = parsed
                .iter()
                .map(|m| tb_frontend::sema::export_declarations(m, &constants))
                .collect();
            if next == exports {
                break;
            }
            exports = next;
        }
        let mut parts = Vec::new();
        let mut commons = Vec::new();
        let mut products = Vec::new();
        for (index, module) in parsed.iter().take(units.len()).enumerate() {
            let mut key = module.clone();
            import_declarations(&mut key, &parsed, &exports, Some(&self.parsed[index].names));
            let mut module = module.clone();
            import_declarations(&mut module, &parsed, &exports, None);
            if self.debug_symbols {
                key = module.clone();
            }
            if let Some(product) = self
                .products
                .iter()
                .find(|p| p.module == key && p.catalog == catalog)
            {
                parts.push(product.code.clone());
                commons.push(product.commons.clone());
                self.stats.reused += 1;
                continue;
            }
            self.stats.compiled += 1;
            let (hir, mut errors) = tb_frontend::sema::lower_with_forms(&module, &catalog);
            for error in &mut errors {
                if error.message != "Subprogram not defined" {
                    continue;
                }
                // Sema kennt absichtlich nur eindeutige Imports. Ergänze den Konflikt am tatsächlichen Aufruftoken.
                let file = sources
                    .get(error.pos.source as usize)
                    .map(|s| s.path.as_str());
                let called = units[index]
                    .segments
                    .iter()
                    .filter(|s| Some(s.file.as_str()) == file)
                    .find_map(|s| {
                        let line = error.pos.line.checked_sub(s.first_line)? as usize;
                        let text = s.text.lines().nth(line)?;
                        tb_frontend::lexer::lex(text)
                            .tokens
                            .into_iter()
                            .filter(|t| t.pos.column >= error.pos.column)
                            .find_map(|t| match t.kind {
                                tb_frontend::lexer::TokenKind::Ident { name, .. } => Some(name),
                                _ => None,
                            })
                    });
                if let Some(called) = called {
                    let origins: Vec<_> = exports
                        .iter()
                        .enumerate()
                        .filter(|(_, declarations)| {
                            declarations
                                .iter()
                                .any(|s| matches!(s,Stmt::Declare{sig,..} if sig.name==called))
                        })
                        .map(|(id, _)| parsed[id].name.as_str())
                        .collect();
                    if origins.len() > 1 {
                        error.message =
                            format!("Ambiguous subprogram: {called} ({})", origins.join(", "));
                    }
                }
            }
            diagnostics.extend(errors);
            if let Some(hir) = hir {
                let code = codegen::compile(&hir);
                parts.push(code.clone());
                commons.push(hir.commons.clone());
                products.push(Product {
                    module: key,
                    debug_module: module,
                    catalog: catalog.clone(),
                    code,
                    commons: hir.commons,
                });
            }
        }
        locate_diagnostics(&mut diagnostics, &sources);
        if !diagnostics.is_empty() {
            return Err(diagnostics);
        }
        self.parsed.truncate(units.len());
        self.products.retain(|p| {
            parsed.iter().any(|m| m.name == p.module.name)
                && !products.iter().any(|n| n.module.name == p.module.name)
        });
        self.products.extend(products);
        let mut library = Library::default();
        for (index, mut code) in parts.into_iter().enumerate() {
            let mut metadata = ModuleMetadata::from_source(
                &parsed[index],
                exports[index].clone(),
                commons[index].clone(),
            );
            // Die Quell-IDs jedes Produkts werden lokal: ein TBL bleibt unabhängig vom Verbraucher.
            let local: Vec<_> = sources
                .iter()
                .enumerate()
                .filter(|(_, s)| s.module as usize == index)
                .collect();
            let source_id = |id: u32| {
                local
                    .iter()
                    .position(|(old, _)| *old == id as usize)
                    .unwrap_or(0) as u32
            };
            code.sources = local
                .iter()
                .map(|(_, s)| tb_frontend::source::SourceFile {
                    module: 0,
                    path: s.path.clone(),
                })
                .collect();
            if code.sources.is_empty() {
                code.sources.push(tb_frontend::source::SourceFile {
                    module: 0,
                    path: code.name.clone(),
                });
            }
            for proc in &mut code.procs {
                for instruction in &mut proc.code {
                    if let Instr::Source(id, _) = instruction {
                        *id = source_id(*id);
                    }
                }
            }
            metadata.pos.source = source_id(metadata.pos.source);
            for (_, p) in &mut metadata.declarations {
                p.source = source_id(p.source);
            }
            for c in &mut metadata.commons {
                c.pos.source = source_id(c.pos.source);
            }
            for stmt in &mut metadata.exports {
                match stmt {
                    Stmt::Declare { pos, .. }
                    | Stmt::TypeDecl { pos, .. }
                    | Stmt::ConstDecl { pos, .. } => pos.source = source_id(pos.source),
                    _ => {}
                }
            }
            let objects = FormCatalog {
                objects: code.objects.clone(),
            };
            for form in forms
                .iter()
                .filter(|f| f.root.name.eq_ignore_ascii_case(&code.name))
            {
                code.form_initial.extend(
                    form.initial_values(&objects)
                        .map_err(|e| vec![diagnostic(e.to_string())])?,
                );
            }
            library.modules.push(LibraryModule { metadata, code });
        }
        for dependency in libraries {
            library.modules.extend(dependency.modules.clone());
        }
        library.validate().map_err(|e| vec![diagnostic(e)])?;
        // Auch offene Produkte müssen vorhandene Definitionen und COMMON-Verträge konsistent binden.
        self.link_product("LIBRARY", library.clone(), None, true)?;
        Ok(library)
    }
}

/// Lokale Definition gewinnt; außerhalb ihres Moduls muss ein Name eindeutig sein.
fn type_definition<'a>(name: &str, origin: usize, all: &'a [Module]) -> Option<(usize, &'a Stmt)> {
    let mut candidates = all.iter().enumerate().flat_map(|(id, module)| {
        module.body.iter().filter_map(move |stmt| {
            matches!(stmt, Stmt::TypeDecl { name: n, .. } if n == name).then_some((id, stmt))
        })
    });
    if let Some(local) = candidates.clone().find(|(id, _)| *id == origin) {
        return Some(local);
    }
    let first = candidates.next()?;
    candidates.next().is_none().then_some(first)
}

fn same_type(
    name: &str,
    a: usize,
    b: usize,
    all: &[Module],
    visiting: &mut HashSet<(usize, usize, String)>,
) -> bool {
    let (
        Some((a, Stmt::TypeDecl { fields: af, .. })),
        Some((b, Stmt::TypeDecl { fields: bf, .. })),
    ) = (type_definition(name, a, all), type_definition(name, b, all))
    else {
        return false;
    };
    if a == b {
        return true;
    }
    if !visiting.insert((a, b, name.into())) {
        return false;
    }
    let equal = af.len() == bf.len()
        && af.iter().zip(bf).all(|((an, at), (bn, bt))| {
            an == bn
                && match (at, bt) {
                    (TypeName::Udt(an), TypeName::Udt(bn)) => {
                        an == bn && same_type(an, a, b, all, visiting)
                    }
                    _ => at == bt,
                }
        });
    visiting.remove(&(a, b, name.into()));
    equal
}

fn import_type(
    name: &str,
    origin: usize,
    target: usize,
    all: &[Module],
    imported: &mut Vec<Stmt>,
    seen: &mut HashMap<(usize, String), String>,
) -> String {
    let Some((origin, Stmt::TypeDecl { fields, pos, .. })) = type_definition(name, origin, all)
    else {
        return name.into();
    };
    if origin == target
        || (type_definition(name, target, all).is_some_and(|(id, _)| id == target)
            && same_type(name, origin, target, all, &mut HashSet::new()))
    {
        return name.into();
    }
    let key = (origin, name.to_string());
    if let Some(alias) = seen.get(&key) {
        return alias.clone();
    }
    // Öffentliche eindeutige Namen bleiben erhalten. Konfliktbehaftete Abhängigkeiten
    // bekommen einen internen Modulnamen, damit das lokale Layout unangetastet bleibt.
    let alias = if type_definition(name, target, all).is_some_and(|(id, _)| id == origin) {
        name.to_string()
    } else {
        format!("{}!{name}", all[origin].name)
    };
    seen.insert(key, alias.clone());
    let fields = fields
        .iter()
        .map(|(field, ty)| {
            let ty = match ty {
                TypeName::Udt(name) => {
                    TypeName::Udt(import_type(name, origin, target, all, imported, seen))
                }
                _ => ty.clone(),
            };
            (field.clone(), ty)
        })
        .collect();
    imported.push(Stmt::TypeDecl {
        name: alias.clone(),
        fields,
        pos: *pos,
    });
    alias
}

fn import_declarations(
    module: &mut Module,
    all: &[Module],
    exports: &[Vec<Stmt>],
    signature: Option<&HashSet<String>>,
) {
    let target = all.iter().position(|m| m.name == module.name).unwrap();
    let mut procedures: HashSet<_> = module.procs.iter().map(|p| p.sig.name.clone()).collect();
    let mut local_constants = HashSet::new();
    let mut local_types = HashSet::new();
    for stmt in &module.body {
        match stmt {
            Stmt::Declare { sig, .. } => {
                procedures.insert(sig.name.clone());
            }
            Stmt::ConstDecl { items, .. } => {
                local_constants.extend(items.iter().map(|i| i.0.clone()))
            }
            Stmt::TypeDecl { name, .. } => {
                local_types.insert(name.clone());
            }
            _ => {}
        }
    }
    let mut foreign: HashMap<_, Vec<_>> = HashMap::new();
    for (origin, declarations) in exports.iter().enumerate().filter(|(id, _)| *id != target) {
        for stmt in declarations {
            let key = match stmt {
                Stmt::Declare { sig, .. } => (0, sig.name.clone()),
                Stmt::ConstDecl { items, .. } => (1, items[0].0.clone()),
                Stmt::TypeDecl { name, .. } => (2, name.clone()),
                _ => continue,
            };
            foreign.entry(key).or_default().push((origin, stmt));
        }
    }
    let mut names: Vec<_> = foreign.keys().cloned().collect();
    names.sort();
    let mut imported = Vec::new();
    let mut seen_types = HashMap::new();
    for key in names {
        let candidates = &foreign[&key];
        if candidates.len() != 1 {
            continue;
        }
        let (origin, stmt) = candidates[0];
        match stmt {
            Stmt::Declare { sig, pos } if !procedures.contains(&sig.name) => {
                let mut sig = sig.clone();
                for param in &mut sig.params {
                    if let Some(TypeName::Udt(name)) = &param.as_type {
                        param.as_type = Some(TypeName::Udt(import_type(
                            name,
                            origin,
                            target,
                            all,
                            &mut imported,
                            &mut seen_types,
                        )));
                    }
                }
                imported.push(Stmt::Declare { sig, pos: *pos });
            }
            Stmt::ConstDecl { items, .. } if !local_constants.contains(&items[0].0) => {
                imported.push(stmt.clone())
            }
            Stmt::TypeDecl { name, .. } if !local_types.contains(name) => {
                import_type(name, origin, target, all, &mut imported, &mut seen_types);
            }
            _ => {}
        }
    }
    if let Some(names) = signature {
        // Nicht verwendete importierte Konstanten erzeugen weder Slots noch Tabellen.
        // TYPEs und Prozeduren dagegen gehören zu den unverketteten Tabellen.
        imported
            .retain(|s| !matches!(s, Stmt::ConstDecl {items,..} if !names.contains(&items[0].0)));
        // Importverträge enthalten Namen/Typen/Werte, keine verschobenen Rumpfzeilen
        // ihres Ursprungs. Eigene Quellorte bleiben Teil des Modulschlüssels.
        for stmt in &mut imported {
            match stmt {
                Stmt::Declare { pos, .. } | Stmt::TypeDecl { pos, .. } => {
                    *pos = SourcePos::default()
                }
                Stmt::ConstDecl { items, pos } => {
                    *pos = SourcePos::default();
                    for (_, _, expr) in items {
                        match expr {
                            tb_frontend::ast::Expr::DoubleLit(_, p)
                            | tb_frontend::ast::Expr::StrLit(_, p) => *p = SourcePos::default(),
                            _ => {}
                        }
                    }
                }
                _ => {}
            }
        }
    }
    imported.append(&mut module.body);
    let mut types = Vec::new();
    imported.retain(|stmt| {
        if matches!(stmt, Stmt::TypeDecl { .. }) {
            types.push(stmt.clone());
            false
        } else {
            true
        }
    });
    let mut known = HashSet::new();
    let mut ordered = Vec::new();
    while !types.is_empty() {
        let next = types.iter().position(|stmt| match stmt {
            Stmt::TypeDecl { fields, .. } => fields.iter().all(|(_, ty)| match ty {
                TypeName::Udt(name) => known.contains(name),
                _ => true,
            }),
            _ => unreachable!(),
        });
        let Some(index) = next else { break };
        let stmt = types.remove(index);
        if let Stmt::TypeDecl { name, .. } = &stmt {
            known.insert(name.clone());
        }
        ordered.push(stmt);
    }
    ordered.extend(types); // Sema diagnostiziert fehlende oder zyklische Typen.
    ordered.extend(imported);
    module.body = ordered;
}

fn module_pos(module: &Module) -> SourcePos {
    module
        .body
        .iter()
        .map(tb_frontend::sema::stmt_pos)
        .find(|p| p.line > 0)
        .or_else(|| module.procs.first().map(|p| p.pos))
        .unwrap_or_default()
}

fn remap_type(ty: &mut TypeInit, ids: &[u16]) {
    if let TypeInit::Udt(id) = ty {
        *id = ids[*id as usize];
    }
}
fn remap_signature(ty: &mut HTy, ids: &[u16]) {
    if let HTy::Udt(id) = ty {
        *id = ids[*id as usize];
    }
}

fn merge_objects(
    target: &mut Vec<tb_frontend::forms::FormObject>,
    objects: &[tb_frontend::forms::FormObject],
) -> Result<Vec<u16>, Diagnostic> {
    let mut ids = Vec::new();
    for object in objects {
        let mut object = object.clone();
        object.parent = object.parent.map(|id| ids[id as usize]);
        let id = if let Some(id) = target
            .iter()
            .position(|o| o.name == object.name && o.parent == object.parent)
        {
            if target[id] != object {
                return Err(diagnostic(format!(
                    "Incompatible Forms object: {}",
                    object.name
                )));
            }
            id
        } else {
            target.push(object);
            target.len() - 1
        };
        ids.push(u16::try_from(id).map_err(|_| diagnostic("Zu viele Forms-Objekte"))?);
    }
    Ok(ids)
}

fn link(
    name: &str,
    mut parts: Vec<CompiledModule>,
    ast: &[ModuleMetadata],
    allow_open: bool,
) -> Result<(CompiledModule, Vec<DebugMap>), Diagnostic> {
    let at = |pos, message| Diagnostic {
        file: None,
        pos,
        message,
    };
    let mut result = codegen::compile(&tb_frontend::analyze_source(name, "").hir.unwrap());
    result.modules = parts
        .iter()
        .map(|p| (p.name.clone(), p.option_base))
        .collect();
    result.objects.clear();
    result.sources.clear();
    result.strings.clear();
    result.procs.clear();
    result.option_base = parts[0].option_base;
    let mut proc_maps = Vec::new();
    let mut count = 1usize;
    let mut definitions: HashMap<String, Vec<u16>> = HashMap::new();
    for (part, module) in parts.iter().zip(ast) {
        let defined: HashSet<_> = module.defined.iter().map(String::as_str).collect();
        let mut map = vec![0];
        for p in part.procs.iter().skip(1) {
            if count >= u16::MAX as usize {
                return Err(at(module.proc_pos(&p.name), "Zu viele Prozeduren".into()));
            }
            let id = u16::try_from(count)
                .map_err(|_| at(module.proc_pos(&p.name), "Zu viele Prozeduren".into()))?;
            count += 1;
            map.push(id);
            if defined.contains(p.name.as_str()) || p.kind == HProcKind::DefFn {
                definitions.entry(p.name.clone()).or_default().push(id);
            }
        }
        proc_maps.push(map);
    }
    let mut unused_imports = HashSet::new();
    // DECLARE-Platzhalter auf die tatsächliche, eindeutig bestimmte Definition binden.
    for (module_id, part) in parts.iter().enumerate() {
        let defined: HashSet<_> = ast[module_id].defined.iter().map(String::as_str).collect();
        for (id, proc) in part.procs.iter().enumerate().skip(1) {
            if !defined.contains(proc.name.as_str()) && proc.kind != HProcKind::DefFn {
                if let Some(candidates) = definitions.get(&proc.name) {
                    if candidates.len() != 1 {
                        return Err(at(
                            ast[module_id].proc_pos(&proc.name),
                            format!(
                                "Ambiguous subprogram: {} ({})",
                                proc.name,
                                ast.iter()
                                    .filter(|m| m.defined.contains(&proc.name))
                                    .map(|m| m.name.as_str())
                                    .collect::<Vec<_>>()
                                    .join(", ")
                            ),
                        ));
                    }
                    proc_maps[module_id][id] = candidates[0];
                } else if !allow_open && !crate::interp::is_runtime_external(&proc.name) {
                    let referenced = part
                        .procs
                        .iter()
                        .flat_map(|p| &p.code)
                        .any(|i| matches!(i,Instr::Call(target,_) if *target as usize==id))
                        || part.event_procs.iter().any(|e| e.proc as usize == id);
                    if referenced {
                        return Err(at(
                            ast[module_id].proc_pos(&proc.name),
                            format!("Subprogram not defined: {} ({})", proc.name, part.name),
                        ));
                    }
                    // Ein bloßer Prototyp ohne Relokation ist kein offener Import des Endprodukts.
                    unused_imports.insert(proc_maps[module_id][id]);
                    proc_maps[module_id][id] = u16::MAX;
                }
            }
        }
    }
    let splits: Vec<_> = parts
        .iter()
        .map(|p| {
            p.procs[0]
                .code
                .iter()
                .position(|i| matches!(i, Instr::Stmt(0)))
                .map_or(0, |i| i.saturating_sub(1))
        })
        .collect();
    let mut init_starts = Vec::new();
    let mut body_starts = Vec::new();
    let mut pc = 0;
    for split in &splits {
        init_starts.push(pc);
        pc += split;
    }
    for (part, split) in parts.iter().zip(&splits) {
        body_starts.push(pc);
        pc += part.procs[0].code.len() - split - 1;
    }
    if pc >= u32::MAX as usize {
        return Err(diagnostic("Zu viele Hauptprogramminstruktionen"));
    }
    let mut main = parts[0].procs[0].clone();
    main.code.clear();
    main.name = name.into();
    let mut initializers = Vec::new();
    let mut bodies = Vec::new();
    let mut debug_maps = Vec::new();
    let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
    for (module_id, part) in parts.iter_mut().enumerate() {
        let objects = merge_objects(&mut result.objects, &part.objects)?;
        let source_offset =
            u32::try_from(result.sources.len()).map_err(|_| diagnostic("Zu viele Quelldateien"))?;
        if result
            .sources
            .len()
            .checked_add(part.sources.len())
            .is_none_or(|n| n > u32::MAX as usize)
        {
            return Err(diagnostic("Zu viele Quelldateien"));
        }
        result.sources.extend(
            part.sources
                .iter()
                .map(|s| tb_frontend::source::SourceFile {
                    module: module_id as u16,
                    path: s.path.clone(),
                }),
        );
        for mut initial in part.form_initial.drain(..) {
            initial.object = objects[initial.object as usize];
            for value in initial.properties.values_mut() {
                if let tb_ui::forms::PropertyValue::Object(Some((id, _))) = value {
                    *id = objects[*id as usize];
                }
            }
            result.form_initial.push(initial);
        }

        let mut types = Vec::new();
        for udt in &part.udts {
            let mut udt = udt.clone();
            udt.name = udt.name.rsplit('!').next().unwrap().to_string();
            for ty in &mut udt.fields {
                remap_type(ty, &types);
            }
            let id = result
                .udts
                .iter()
                .position(|u| u.name == udt.name && u.fields == udt.fields)
                .unwrap_or_else(|| {
                    result.udts.push(udt);
                    result.udts.len() - 1
                });
            if result.udts.len() > u16::MAX as usize {
                return Err(at(ast[module_id].pos, "Zu viele TYPEs".into()));
            }
            types.push(
                u16::try_from(id).map_err(|_| at(ast[module_id].pos, "Zu viele TYPEs".into()))?,
            );
        }
        let commons: HashMap<_, _> = ast[module_id].commons.iter().map(|c| (c.slot, c)).collect();
        let mut globals = Vec::new();
        for (slot, (ty, name)) in part.globals_init.iter().zip(&part.global_names).enumerate() {
            let mut ty = ty.clone();
            remap_type(&mut ty, &types);
            let declaration = commons.get(&(slot as u16));
            let key = declaration.map(|c| (c.block.clone(), c.key.clone()));
            let mut common_ty = declaration.map(|c| c.ty.clone());
            if let Some(ty) = &mut common_ty {
                remap_signature(ty, &types);
            }
            let id = if let Some((id, previous)) = key.as_ref().and_then(|key| common.get_mut(key))
            {
                let declaration = declaration.unwrap();
                let compatible_dims = match (&previous.dims, &declaration.dims) {
                    (None, None) => true,
                    (Some(a), Some(b)) if a.is_empty() || b.is_empty() => true,
                    (Some(a), Some(b)) => {
                        a.len() == b.len()
                            && a.iter().zip(b).all(|((al, ah), (bl, bh))| {
                                al.zip(*bl).is_none_or(|(a, b)| a == b)
                                    && ah.zip(*bh).is_none_or(|(a, b)| a == b)
                            })
                    }
                    _ => false,
                };
                if Some(&previous.ty) != common_ty.as_ref() || !compatible_dims {
                    return Err(at(
                        declaration.pos,
                        format!("COMMON type or bounds mismatch: {name}"),
                    ));
                }
                if let (Some(previous), Some(current)) = (&mut previous.dims, &declaration.dims) {
                    if previous.is_empty() {
                        *previous = current.clone();
                    } else {
                        for ((lo, hi), (new_lo, new_hi)) in previous.iter_mut().zip(current) {
                            *lo = lo.or(*new_lo);
                            *hi = hi.or(*new_hi);
                        }
                    }
                }
                *id
            } else {
                let id = u16::try_from(result.globals_init.len())
                    .map_err(|_| at(ast[module_id].pos, "Zu viele globale Variablen".into()))?;
                result.globals_init.push(ty);
                result.global_names.push(if ast.len() == 1 {
                    name.clone()
                } else {
                    format!("{}!{name}", part.name)
                });
                if let Some(key) = key {
                    let mut declaration = (*declaration.unwrap()).clone();
                    declaration.ty = common_ty.unwrap();
                    common.insert(key, (id, declaration));
                }
                id
            };
            globals.push(id);
        }
        debug_maps.push(DebugMap {
            globals: globals.clone(),
            types: types.clone(),
            procs: proc_maps[module_id].clone(),
        });
        let string_offset = result.strings.len();
        if string_offset + part.strings.len() >= u16::MAX as usize {
            return Err(at(ast[module_id].pos, "Zu viele Stringkonstanten".into()));
        }
        result.strings.append(&mut part.strings);
        let data_offset =
            u32::try_from(result.data.len()).map_err(|_| diagnostic("Zu viele DATA-Werte"))?;
        if result
            .data
            .len()
            .checked_add(part.data.len())
            .is_none_or(|n| n > u32::MAX as usize)
        {
            return Err(diagnostic("Zu viele DATA-Werte"));
        }
        result.data.append(&mut part.data);
        let jump_offset = result.jump_tables.len();
        if jump_offset + part.jump_tables.len() > u16::MAX as usize {
            return Err(at(ast[module_id].pos, "Zu viele Sprungtabellen".into()));
        }
        let main_pc = |pc: u32| if (pc as usize) < splits[module_id] { init_starts[module_id] + pc as usize } else { body_starts[module_id] + pc as usize - splits[module_id] } as u32;
        for (proc_id, proc) in part.procs.iter_mut().enumerate() {
            proc.module = module_id as u16;
            for ty in &mut proc.locals_init {
                remap_type(ty, &types);
            }
            for param in &mut proc.params {
                remap_signature(&mut param.ty, &types);
            }
            if let Some(ty) = &mut proc.ret_ty {
                remap_signature(ty, &types);
            }
            if ast.len() > 1 {
                proc.name = format!("{}!{}", part.name, proc.name);
            }
            for instruction in &mut proc.code {
                use Instr::*;
                match instruction {
                    Source(id, _) => *id += source_offset,
                    LoadObjectProperty(id, _, _)
                    | StoreObjectProperty(id, _, _)
                    | PushObject(id, _)
                    | ObjectMethod(id, _, _)
                    | ObjectLoad(id, _, _)
                    | LoadObjectIndexedProperty(id, _)
                    | ObjectMethodFn(id, _, _)
                    | StoreObjectIndexedProperty(id, _) => *id = objects[*id as usize],
                    PushStr(id)
                    | Unsupported(id)
                    | LoadDynamicObjectProperty(id)
                    | StoreDynamicObjectProperty(id) => *id += string_offset as u16,
                    Input(_, _, id, _) if *id != u16::MAX => *id += string_offset as u16,
                    LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
                        *id = globals[*id as usize]
                    }
                    LoadArr(global, id, _, ty)
                    | DimArr(global, id, _, ty)
                    | CommonArr(global, id, _, ty)
                    | RedimArr(global, id, _, ty) => {
                        if *global {
                            *id = globals[*id as usize];
                        }
                        remap_type(ty, &types);
                    }
                    EraseSlot(true, id) => *id = globals[*id as usize],
                    GetPut(_, _, 7, id) | PushUdtId(id) => *id = types[*id as usize],
                    Call(id, _) => *id = proc_maps[module_id][*id as usize],
                    Restore(id) => *id += data_offset,
                    OnErrorGoto(pc) => *pc = main_pc(*pc),
                    Jump(pc)
                    | JumpIfFalse(pc)
                    | JumpIfTrue(pc)
                    | Gosub(pc)
                    | RetGosubTo(pc)
                    | OnErrorLocal(pc)
                    | ResumeLabel(pc)
                    | TrapDefine(_, pc)
                        if proc_id == 0 =>
                    {
                        *pc = main_pc(*pc)
                    }
                    OnJump(id, _) => {
                        if proc_id == 0 {
                            for target in &mut part.jump_tables[*id as usize] {
                                *target = main_pc(*target);
                            }
                        }
                        *id += jump_offset as u16;
                    }
                    _ => {}
                }
            }
        }
        initializers.extend_from_slice(&part.procs[0].code[..splits[module_id]]);
        bodies.extend_from_slice(
            &part.procs[0].code[splits[module_id]..part.procs[0].code.len() - 1],
        );
        result.procs.extend(part.procs.iter().skip(1).cloned());
        for mut e in part.event_procs.drain(..) {
            e.proc = proc_maps[module_id][e.proc as usize];
            e.object = objects[e.object as usize];
            result.event_procs.push(e);
        }
        result.jump_tables.append(&mut part.jump_tables);
    }
    initializers.extend(bodies);
    initializers.push(Instr::End);
    main.code = initializers;
    result.procs.insert(0, main);
    // Vollständige Signaturen zwischen DECLARE-Platzhalter und Ziel vergleichen.
    for (module, part) in parts.iter().enumerate() {
        for (id, proc) in part.procs.iter().enumerate().skip(1) {
            if proc_maps[module][id] == u16::MAX {
                continue;
            }
            let target = &result.procs[proc_maps[module][id] as usize];
            if proc.ret_ty != target.ret_ty
                || proc.kind != target.kind
                || proc.params.len() != target.params.len()
                || proc
                    .params
                    .iter()
                    .zip(&target.params)
                    .any(|(a, b)| a.ty != b.ty || a.array != b.array || a.by_ref != b.by_ref)
            {
                return Err(at(
                    ast[module].proc_pos(proc.name.rsplit('!').next().unwrap()),
                    format!("Parameter type mismatch: {}", proc.name),
                ));
            }
        }
    }
    if !unused_imports.is_empty() {
        let mut compact = vec![u16::MAX; result.procs.len()];
        let mut next = 0u16;
        for (old, id) in compact.iter_mut().enumerate() {
            if !unused_imports.contains(&(old as u16)) {
                *id = next;
                next += 1;
            }
        }
        result.procs = std::mem::take(&mut result.procs)
            .into_iter()
            .enumerate()
            .filter_map(|(id, mut proc)| {
                if compact[id] == u16::MAX {
                    return None;
                }
                for instruction in &mut proc.code {
                    if let Instr::Call(id, _) = instruction {
                        *id = compact[*id as usize];
                    }
                }
                Some(proc)
            })
            .collect();
        for e in &mut result.event_procs {
            e.proc = compact[e.proc as usize];
        }
        for map in &mut debug_maps {
            for id in &mut map.procs {
                if *id != u16::MAX {
                    *id = compact[*id as usize];
                }
            }
        }
    }
    Ok((result, debug_maps))
}

// Erst ein ausdrücklich erzeugter DebugCompiler bindet den Quellcompiler ein.
// Die reine Runtime kann dadurch den gleichen VM-Pfad ohne Parser/Codegenerator linken.
type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result<DebugCode, String>;

/// Ephemeral symbol/link context, deliberately not part of TBC serialization.
#[derive(Clone)]
pub struct DebugCompiler {
    compile_fn: DebugCompileFn,
    modules: Vec<(u16, Module, FormCatalog, DebugMap)>,
    symbols: tb_frontend::sema::DebugSymbols,
    slots: Vec<u16>,
    error: Option<String>,
}
#[derive(Clone)]
struct DebugMap {
    globals: Vec<u16>,
    types: Vec<u16>,
    procs: Vec<u16>,
}
#[derive(Clone)]
pub struct DebugCode {
    pub procedure: crate::bytecode::ProcCode,
    pub strings: Vec<std::rc::Rc<str>>,
    pub expression: bool,
}
impl ProjectCompiler {
    pub fn debug_compiler(&self) -> DebugCompiler {
        let modules: Vec<_> = self
            .debug_names
            .iter()
            .zip(&self.debug_maps)
            .enumerate()
            .filter_map(|(id, (name, map))| {
                self.products
                    .iter()
                    .find(|p| p.module.name == *name)
                    .map(|p| {
                        (
                            id as u16,
                            p.debug_module.clone(),
                            p.catalog.clone(),
                            map.clone(),
                        )
                    })
            })
            .collect();
        let mut symbols = tb_frontend::sema::DebugSymbols::default();
        let mut slots = Vec::new();
        let mut error = None;
        for (_, ast, catalog, map) in &modules {
            if let Some(hir) = tb_frontend::sema::lower_with_forms(ast, catalog).0 {
                if symbols.udts.len() + hir.udts.len() >= u16::MAX as usize
                    || symbols.globals.len() + hir.globals.len() >= u16::MAX as usize
                {
                    error = Some("Debug-Symboltabelle überschreitet die 16-Bit-Slotgrenze".into());
                    break;
                }
                let offset = symbols.udts.len() as u16;
                for mut udt in hir.udts {
                    udt.name = format!("<Debug:{}!{}>", ast.name, udt.name);
                    for (_, ty) in &mut udt.fields {
                        if let HTy::Udt(id) = ty {
                            *id += offset;
                        }
                    }
                    symbols.udts.push(udt);
                }
                for (mut var, slot) in hir.globals.into_iter().zip(&map.globals) {
                    var.name = format!("{}!{}", ast.name, var.name);
                    if let HTy::Udt(id) = &mut var.ty {
                        *id += offset;
                    }
                    symbols.globals.push(var);
                    slots.push(*slot);
                }
            }
        }
        DebugCompiler {
            compile_fn: DebugCompiler::compile_impl,
            modules,
            symbols,
            slots,
            error,
        }
    }
}
impl DebugCompiler {
    pub fn compile(
        &self,
        module: u16,
        procedure: &str,
        text: &str,
        expression: bool,
    ) -> Result<DebugCode, String> {
        (self.compile_fn)(self, module, procedure, text, expression)
    }
    fn compile_impl(
        &self,
        module: u16,
        procedure: &str,
        text: &str,
        expression: bool,
    ) -> Result<DebugCode, String> {
        if let Some(error) = &self.error {
            return Err(error.clone());
        }
        let (_, ast, catalog, map) = self
            .modules
            .iter()
            .find(|(id, _, _, _)| *id == module)
            .ok_or("Kein Debug-Quellkontext für dieses Kompilat")?;
        let name = if procedure == "<main>" {
            &ast.name
        } else {
            procedure.rsplit('!').next().unwrap_or(procedure)
        };
        if map.globals.len() + self.symbols.globals.len() >= u16::MAX as usize
            || map.types.len() + self.symbols.udts.len() >= u16::MAX as usize
        {
            return Err("Debug-Kontext überschreitet die 16-Bit-Slotgrenze".into());
        }
        let mut symbols = self.symbols.clone();
        symbols.original_globals = map.globals.len();
        let (mut hir, debug) = tb_frontend::sema::lower_debug(
            ast, catalog, name, text, expression, symbols,
        )
        .map_err(|e| {
            e.iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("\n")
        })?;
        hir.procs.push(debug);
        let mut globals = map.globals.clone();
        globals.extend(&self.slots);
        let mut types = map.types.clone();
        for (_, _, _, mapping) in &self.modules {
            types.extend(&mapping.types);
        }
        let mut compiled = codegen::compile(&hir);
        let mut proc = compiled.procs.pop().unwrap();
        proc.module = module;
        for ty in &mut proc.locals_init {
            remap_type(ty, &types);
        }
        if let Some(ty) = &mut proc.ret_ty {
            remap_signature(ty, &types);
        }
        for i in &mut proc.code {
            use Instr::*;
            match i {
                LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
                    *id = *globals
                        .get(*id as usize)
                        .ok_or("Unbekannter globaler Slot")?
                }
                LoadArr(global, id, _, ty) => {
                    if *global {
                        *id = globals[*id as usize];
                    }
                    remap_type(ty, &types);
                }
                Call(id, _) => {
                    *id = map.procs[*id as usize];
                    if *id == u16::MAX {
                        return Err(
                            "Subprogram not defined: ungebundene DECLARE-Deklaration".into()
                        );
                    }
                }
                PushUdtId(id) => *id = types[*id as usize],
                _ => {}
            }
        }
        Ok(DebugCode {
            procedure: proc,
            strings: compiled.strings,
            expression,
        })
    }
}

impl DebugCompiler {
    pub(crate) fn same_control_context(
        &self,
        module: u16,
        procedure: &str,
        from: SourcePos,
        to: SourcePos,
    ) -> bool {
        let Some((_, ast, _, _)) = self.modules.iter().find(|(id, _, _, _)| *id == module) else {
            return false;
        };
        let body = if procedure == "<main>" {
            &ast.body
        } else {
            let Some(proc) = ast.procs.iter().find(|p| {
                p.sig
                    .name
                    .eq_ignore_ascii_case(procedure.rsplit('!').next().unwrap_or(procedure))
            }) else {
                return false;
            };
            &proc.body
        };
        let a = control_path(body, from);
        let b = control_path(body, to);
        a.is_some() && a == b
    }
}
/// Structured arms supplement bytecode loop ranges: ELSE and CASE are distinct
/// even when their forward jump would otherwise look like a top-level GOTO.
fn control_path(body: &[Stmt], target: SourcePos) -> Option<Vec<(SourcePos, usize)>> {
    for stmt in body {
        let pos = tb_frontend::sema::stmt_pos(stmt);
        if pos == target {
            return Some(vec![]);
        }
        let mut children: Vec<(&[Stmt], Option<SourcePos>)> = Vec::new();
        match stmt {
            Stmt::If {
                then_body,
                elseifs,
                else_body,
                ..
            } => {
                children.push((then_body, None));
                for (_, body, pos) in elseifs {
                    children.push((body, Some(*pos)));
                }
                if let Some(body) = else_body {
                    children.push((body, None));
                }
            }
            Stmt::Select { arms, .. } => {
                for arm in arms {
                    children.push((&arm.body, Some(arm.pos)));
                }
            }
            Stmt::For { body, end_pos, .. }
            | Stmt::DoLoop { body, end_pos, .. }
            | Stmt::While { body, end_pos, .. } => children.push((body, Some(*end_pos))),
            _ => {}
        }
        for (index, (body, entry)) in children.into_iter().enumerate() {
            if let Some(mut path) = if entry == Some(target) {
                Some(vec![])
            } else {
                control_path(body, target)
            } {
                path.insert(0, (pos, index));
                return Some(path);
            }
        }
    }
    None
}
