//! Bytecode-Definition und `.tbc`-Serialisierung.
//!
//! In-Memory führt die VM dekodierte Instruktionen (`Vec<Instr>`, Enum
//! mit eingebetteten Operanden — Wort-Dispatch); die Serialisierung
//! bildet jede Instruktion auf 1 Opcode-Byte + Operanden (little-endian)
//! ab. Opcode-Bytes sind **stabil** und gruppenweise mit Lücken vergeben
//! (Phase 3 ergänzt in den Lücken). Dokumentation: docs/tbvm-design.md.

use std::fmt;
use std::rc::Rc;
use tb_frontend::forms::{FormObject, ObjectClass};
use tb_frontend::hir::{HEventProc, HParam, HProcKind, HTy, NumTy};
use tb_frontend::source::SourceFile;
use tb_runtime::value::{TypeInit, UdtLayout};
use tb_ui::forms::PropertyValue;
use tb_ui::frm::FormInitial;

pub const TBC_MAGIC: &[u8; 4] = b"TBC\0";
pub const TBC_VERSION: u16 = 4;

/// Vergleichsoperator (Operand der `Cmp*`-Instruktionen).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CmpOp {
    Eq = 0,
    Ne = 1,
    Lt = 2,
    Le = 3,
    Gt = 4,
    Ge = 5,
}

impl CmpOp {
    fn from_u8(v: u8) -> Result<Self, LoadError> {
        Ok(match v {
            0 => CmpOp::Eq,
            1 => CmpOp::Ne,
            2 => CmpOp::Lt,
            3 => CmpOp::Le,
            4 => CmpOp::Gt,
            5 => CmpOp::Ge,
            _ => return Err(LoadError::Corrupt("CmpOp")),
        })
    }
}

#[derive(Debug)]
pub enum LoadError {
    BadMagic,
    /// Unbekannte Formatversion (enthaltene Version).
    Version(u16),
    Corrupt(&'static str),
}

impl fmt::Display for LoadError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LoadError::BadMagic => write!(f, "Keine .tbc-Datei (Magic fehlt)"),
            LoadError::Version(v) => {
                write!(
                    f,
                    "Unbekannte .tbc-Formatversion {v} (unterstützt: {TBC_VERSION})"
                )
            }
            LoadError::Corrupt(what) => write!(f, "Beschädigte .tbc-Datei ({what})"),
        }
    }
}

// ---- Encoder/Decoder-Hilfen -------------------------------------------------

pub struct Reader<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> Reader<'a> {
    pub fn new(buf: &'a [u8]) -> Self {
        Reader { buf, pos: 0 }
    }
    fn finish(&self) -> Result<(), LoadError> {
        if self.pos != self.buf.len() {
            Err(LoadError::Corrupt("überzählige Abschnittsdaten"))
        } else {
            Ok(())
        }
    }
    fn take(&mut self, n: usize) -> Result<&'a [u8], LoadError> {
        if n > self.buf.len().saturating_sub(self.pos) {
            return Err(LoadError::Corrupt("unerwartetes Dateiende"));
        }
        let s = &self.buf[self.pos..self.pos + n];
        self.pos += n;
        Ok(s)
    }
    fn u8(&mut self) -> Result<u8, LoadError> {
        Ok(self.take(1)?[0])
    }
    fn u16(&mut self) -> Result<u16, LoadError> {
        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
    }
    fn u32(&mut self) -> Result<u32, LoadError> {
        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
    }
    fn string(&mut self) -> Result<String, LoadError> {
        let n = self.u32()? as usize;
        let b = self.take(n)?;
        String::from_utf8(b.to_vec()).map_err(|_| LoadError::Corrupt("UTF-8"))
    }
}

trait Enc: Sized {
    fn enc(&self, out: &mut Vec<u8>);
    fn dec(r: &mut Reader) -> Result<Self, LoadError>;
}

macro_rules! enc_prim {
    ($t:ty, $n:literal) => {
        impl Enc for $t {
            fn enc(&self, out: &mut Vec<u8>) {
                out.extend_from_slice(&self.to_le_bytes());
            }
            fn dec(r: &mut Reader) -> Result<Self, LoadError> {
                Ok(<$t>::from_le_bytes(r.take($n)?.try_into().unwrap()))
            }
        }
    };
}
enc_prim!(u16, 2);
enc_prim!(u32, 4);
enc_prim!(i16, 2);
enc_prim!(i32, 4);
enc_prim!(i64, 8);
enc_prim!(f32, 4);
enc_prim!(f64, 8);

impl Enc for u8 {
    fn enc(&self, out: &mut Vec<u8>) {
        out.push(*self);
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        r.u8()
    }
}

impl Enc for bool {
    fn enc(&self, out: &mut Vec<u8>) {
        out.push(*self as u8);
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        match r.u8()? {
            0 => Ok(false),
            1 => Ok(true),
            _ => Err(LoadError::Corrupt("bool")),
        }
    }
}

impl Enc for CmpOp {
    fn enc(&self, out: &mut Vec<u8>) {
        out.push(*self as u8);
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        CmpOp::from_u8(r.u8()?)
    }
}

impl Enc for TypeInit {
    fn enc(&self, out: &mut Vec<u8>) {
        let (tag, extra): (u8, u32) = match self {
            TypeInit::Int => (0, 0),
            TypeInit::Lng => (1, 0),
            TypeInit::Sng => (2, 0),
            TypeInit::Dbl => (3, 0),
            TypeInit::Cur => (4, 0),
            TypeInit::Str => (5, 0),
            TypeInit::FixedStr(n) => (6, *n),
            TypeInit::Udt(id) => (7, *id as u32),
            TypeInit::Empty => (8, 0),
        };
        out.push(tag);
        out.extend_from_slice(&extra.to_le_bytes());
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        let tag = r.u8()?;
        let extra = r.u32()?;
        if (tag != 6 && tag != 7 && extra != 0) || (tag == 7 && extra > u16::MAX as u32) {
            return Err(LoadError::Corrupt("TypeInit-Zusatz"));
        }
        Ok(match tag {
            0 => TypeInit::Int,
            1 => TypeInit::Lng,
            2 => TypeInit::Sng,
            3 => TypeInit::Dbl,
            4 => TypeInit::Cur,
            5 => TypeInit::Str,
            6 => TypeInit::FixedStr(extra),
            7 => TypeInit::Udt(extra as u16),
            8 => TypeInit::Empty,
            _ => return Err(LoadError::Corrupt("TypeInit")),
        })
    }
}

// ---- Instruktionssatz ---------------------------------------------------------

macro_rules! instrs {
    ($( $op:literal $name:ident $(( $($fname:ident : $ft:ty),+ ))? ; )+) => {
        /// Eine dekodierte Instruktion. Serialisiert: Opcode-Byte + Operanden.
        #[derive(Debug, Clone, PartialEq)]
        pub enum Instr {
            $( $name $(( $($ft),+ ))? , )+
        }

        impl Instr {
            pub fn encode(&self, out: &mut Vec<u8>) {
                match self {
                    $( Instr::$name $(( $($fname),+ ))? => {
                        out.push($op);
                        $( $( Enc::enc($fname, out); )+ )?
                    } )+
                }
            }

            pub fn decode(r: &mut Reader) -> Result<Instr, LoadError> {
                let op = r.u8()?;
                Ok(match op {
                    $( $op => Instr::$name $(( $( <$ft as Enc>::dec(r)? ),+ ))? , )+
                    _ => return Err(LoadError::Corrupt("Opcode")),
                })
            }
        }
    };
}

instrs! {
    // 0x00 — Anweisungsgrenzen und Kontrolle
    0x00 Stmt(a: u32);              // Quellzeile; Tick-Prüfung, Resume-Punkt
    0x01 SetErl(a: u32);            // numerische Zeilennummer durchlaufen
    0x02 End;
    0x03 StopInstr;
    0x04 SystemInstr;
    0x05 Unsupported(a: u16);       // Name im Stringpool → Fehler 73
    0x06 Source(a: u32, b: u32);   // Quelldatei-ID, physische Spalte der folgenden Stmt-Grenze
    0x07 InitStmt(a: u32);          // globale Initialisierung: Quellort/Debugger, noch keine Ereignisse

    // 0x10 — Konstanten und Stack
    0x10 PushInt(a: i16);
    0x11 PushLng(a: i32);
    0x12 PushSng(a: f32);
    0x13 PushDbl(a: f64);
    0x14 PushCur(a: i64);
    0x15 PushStr(a: u16);
    0x16 Dup;
    0x17 Pop;
    0x18 PushUdtId(a: u16);         // TYPE-Index als LONG auf dem Stack (ISAM)

    // 0x20 — Variablen und Referenzen
    0x20 LoadGlobal(a: u16);
    0x21 StoreGlobal(a: u16);
    0x22 LoadLocal(a: u16);
    0x23 StoreLocal(a: u16);
    0x24 LoadRef(a: u16);           // durch Referenz in lokalem Slot lesen
    0x25 StoreRef(a: u16);
    0x26 MakeRefGlobal(a: u16);
    0x27 MakeRefLocal(a: u16);
    0x28 MakeRefElem(a: u8);        // Handle+Indizes → Elementreferenz
    0x29 MakeRefField(a: u16);      // Rec/Feldreferenz → tiefere Feldreferenz

    // 0x30 — Arrays und Records
    0x30 LoadArr(a: bool, b: u16, c: u8, d: TypeInit); // Slot sichern (Auto-DIM) + Handle
    0x31 LoadElem(a: u8);
    0x32 StoreElem(a: u8);
    0x33 DimArr(a: bool, b: u16, c: u8, d: TypeInit);
    0x34 RedimArr(a: bool, b: u16, c: u8, d: TypeInit);
    0x35 EraseSlot(a: bool, b: u16);
    0x36 LoadField(a: u16);
    0x37 StoreField(a: u16);
    0x38 CopyRec;
    0x39 ArrBound(a: bool);         // true = LBOUND
    0x3A FixStr(a: u32);            // auf feste Länge kürzen/padden

    0x3B CommonArr(a: bool, b: u16, c: u8, d: TypeInit); // gemeinsame Initialisierung/Layoutprüfung

    // 0x40 — Arithmetik (monomorph)
    0x40 AddI2; 0x41 AddI4; 0x42 AddR4; 0x43 AddR8; 0x44 AddCy;
    0x45 SubI2; 0x46 SubI4; 0x47 SubR4; 0x48 SubR8; 0x49 SubCy;
    0x4A MulI2; 0x4B MulI4; 0x4C MulR4; 0x4D MulR8; 0x4E MulCy;
    0x4F NegI2; 0x50 NegI4; 0x51 NegR4; 0x52 NegR8; 0x53 NegCy;
    0x54 DivR4; 0x55 DivR8;
    0x56 IDivI2; 0x57 IDivI4;
    0x58 ModI2; 0x59 ModI4;
    0x5A PowR8;
    0x5B Concat;

    // 0x60 — Konvertierungen (Matrix)
    0x60 ConvI2I4; 0x61 ConvI2R4; 0x62 ConvI2R8; 0x63 ConvI2Cy;
    0x64 ConvI4I2; 0x65 ConvI4R4; 0x66 ConvI4R8; 0x67 ConvI4Cy;
    0x68 ConvR4I2; 0x69 ConvR4I4; 0x6A ConvR4R8; 0x6B ConvR4Cy;
    0x6C ConvR8I2; 0x6D ConvR8I4; 0x6E ConvR8R4; 0x6F ConvR8Cy;
    0x70 ConvCyI2; 0x71 ConvCyI4; 0x72 ConvCyR4; 0x73 ConvCyR8;

    // 0x80 — Logik (bitweise)
    0x80 NotI2; 0x81 NotI4;
    0x82 AndI2; 0x83 AndI4;
    0x84 OrI2;  0x85 OrI4;
    0x86 XorI2; 0x87 XorI4;
    0x88 EqvI2; 0x89 EqvI4;
    0x8A ImpI2; 0x8B ImpI4;

    // 0x90 — Vergleiche (Ergebnis INTEGER −1/0)
    0x90 CmpI2(a: CmpOp);
    0x91 CmpI4(a: CmpOp);
    0x92 CmpR4(a: CmpOp);
    0x93 CmpR8(a: CmpOp);
    0x94 CmpCy(a: CmpOp);
    0x95 CmpStr(a: CmpOp);

    // 0xA0 — Kontrollfluss
    0xA0 Jump(a: u32);
    0xA1 JumpIfFalse(a: u32);
    0xA2 JumpIfTrue(a: u32);
    0xA3 Gosub(a: u32);
    0xA4 RetGosub;
    0xA5 RetGosubTo(a: u32);
    0xA6 OnJump(a: u16, b: bool);      // Sprungtabelle, gosub?
    0xA7 Run(a: u8);                   // 0 = ohne Ziel, 1 = Zeile, 2 = Datei

    // 0xB0 — Prozeduren und Builtins
    0xB0 Call(a: u16, b: u8);
    0xB1 RetProc;
    0xB2 RetFn;
    0xB3 CallBuiltin(a: u16, b: u8);
    0xB4 LoadObjectProperty(a: u16, b: u16, c: bool);
    0xB5 StoreObjectProperty(a: u16, b: u16, c: bool);
    0xB6 PushObject(a: u16, b: bool);
    0xB7 TypeOf(a: u8);
    0xB8 ObjectMethod(a: u16, b: u16, c: u8);
    0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
    0xBA LoadDynamicObjectProperty(a: u16);
    0xBB StoreDynamicObjectProperty(a: u16);
    0xBC LoadObjectIndexedProperty(a: u16, b: u16);
    0xBD ObjectMethodFn(a: u16, b: u16, c: u8);
    0xBE StoreObjectIndexedProperty(a: u16, b: u16);

    // 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
    0xE0 TrapDefine(a: u8, b: u32);    // Quellenart, Sprungziel
    0xE1 TrapDisable(a: u8);           // GOSUB 0
    0xE2 TrapSet(a: u8, b: u8);        // Quellenart, Zustand
    0xE3 EventSwitch(a: bool);
    0xE4 Doevents;                     // Zustellpunkt; legt 0 ab
    0xE5 Sleep(a: bool);               // hat Argument? Dauer vom Stack (Sekunden)

    // 0xC0 — Fehlerbehandlung
    0xC0 OnErrorGoto(a: u32);
    0xC1 OnErrorLocal(a: u32);
    0xC2 OnErrorDisable;
    0xC3 OnErrorLocalDisable;
    0xC4 OnErrorResumeNext(a: bool);
    0xC5 Resume0;
    0xC6 ResumeNext;
    0xC7 ResumeLabel(a: u32);
    0xC8 RaiseError;             // Code vom Stack (ERROR n)
    0xC9 LoadErr;
    0xCA LoadErl;
    0xCB SetErr;                 // ERR = n (setzt den Code, löst nichts aus)

    // 0xD0 — DATA und Eingabe
    0xD0 ReadData(a: u8);        // nächstes DATA-Element; 0 = String, 1 = Zahl (DOUBLE)
    0xD1 Restore(a: u32);
    0xD2 Input(a: u8, b: bool, c: u16, d: bool); // argc, line_mode, prompt (0xFFFF=ohne), '?'
    0xD3 InputFile(a: u8, b: bool); // argc, line_mode — Dateinummer liegt unter den Referenzen
    // put?, mit Recordnummer?, Feldart (0 = ohne Variable, s. `Feldart`),
    // Zusatz (UDT-Index bzw. Länge fester Strings)
    0xD4 GetPut(a: bool, b: bool, c: u8, d: u16);
    0xD5 Field(a: u8);          // Feldzahl; Stack: Dateinummer, dann (Länge, Referenz)*
    0xD6 LsetRset(a: bool);     // rset?; Stack: Referenz, Wert // argc, line_mode — Dateinummer liegt unter den Referenzen
}

impl Enc for HTy {
    fn enc(&self, out: &mut Vec<u8>) {
        let tag = match self {
            HTy::Num(NumTy::Int) => 0,
            HTy::Num(NumTy::Lng) => 1,
            HTy::Num(NumTy::Sng) => 2,
            HTy::Num(NumTy::Dbl) => 3,
            HTy::Num(NumTy::Cur) => 4,
            HTy::Str => 5,
            HTy::FixedStr(_) => 6,
            HTy::Udt(_) => 7,
            HTy::Form => 8,
            HTy::Control => 9,
        };
        out.push(tag);
        match self {
            HTy::FixedStr(n) => n.enc(out),
            HTy::Udt(n) => n.enc(out),
            _ => {}
        }
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        Ok(match r.u8()? {
            0 => HTy::Num(NumTy::Int),
            1 => HTy::Num(NumTy::Lng),
            2 => HTy::Num(NumTy::Sng),
            3 => HTy::Num(NumTy::Dbl),
            4 => HTy::Num(NumTy::Cur),
            5 => HTy::Str,
            6 => HTy::FixedStr(r.u32()?),
            7 => HTy::Udt(r.u16()?),
            8 => HTy::Form,
            9 => HTy::Control,
            _ => return Err(LoadError::Corrupt("Signaturtyp")),
        })
    }
}

impl Enc for PropertyValue {
    fn enc(&self, out: &mut Vec<u8>) {
        match self {
            Self::Integer(v) => {
                out.push(0);
                v.enc(out);
            }
            Self::Single(v) => {
                out.push(1);
                v.enc(out);
            }
            Self::String(v) => {
                out.push(2);
                w_string(out, v);
            }
            Self::Boolean(v) => {
                out.push(3);
                v.enc(out);
            }
            Self::Object(v) => {
                out.push(4);
                v.is_some().enc(out);
                if let Some((object, index)) = v {
                    object.enc(out);
                    index.is_some().enc(out);
                    if let Some(index) = index {
                        index.enc(out);
                    }
                }
            }
            Self::IntegerArray(v) => {
                out.push(5);
                (v.len() as u32).enc(out);
                for v in v {
                    v.enc(out);
                }
            }
        }
    }
    fn dec(r: &mut Reader) -> Result<Self, LoadError> {
        Ok(match r.u8()? {
            0 => Self::Integer(i32::dec(r)?),
            1 => Self::Single(f32::dec(r)?),
            2 => Self::String(r.string()?),
            3 => Self::Boolean(bool::dec(r)?),
            4 => Self::Object(if bool::dec(r)? {
                Some((
                    r.u16()?,
                    if bool::dec(r)? {
                        Some(i32::dec(r)?)
                    } else {
                        None
                    },
                ))
            } else {
                None
            }),
            5 => {
                let n = r.u32()?;
                let mut values = Vec::new();
                for _ in 0..n {
                    values.push(i32::dec(r)?);
                }
                Self::IntegerArray(values)
            }
            _ => return Err(LoadError::Corrupt("Anfangswerttyp")),
        })
    }
}

// ---- Modulstruktur ------------------------------------------------------------

#[derive(Debug, Clone)]
pub struct ProcCode {
    pub module: u16,
    pub kind: HProcKind,
    pub params: Vec<HParam>,
    pub ret_ty: Option<HTy>,
    pub name: String,
    pub n_params: u16,
    /// Initialisierung aller Frame-Slots (Parameter zuerst; deren Init
    /// wird beim Aufruf durch die Argumente ersetzt).
    pub locals_init: Vec<TypeInit>,
    /// Slot-Namen (Debugger-Inspektion).
    pub local_names: Vec<String>,
    pub code: Vec<Instr>,
}

#[derive(Debug, Clone)]
pub struct DataItem {
    pub text: String,
    pub line: u32,
}

/// Übersetztes Modul — Inhalt des `.tbc`-Containers.
#[derive(Debug, Clone)]
pub struct CompiledModule {
    pub modules: Vec<(String, u8)>,
    pub sources: Vec<SourceFile>,
    pub form_initial: Vec<FormInitial>,
    pub startup_form: Option<u16>,
    pub name: String,
    /// `OPTION BASE` (Untergrenze impliziter Arrays).
    pub option_base: u8,
    /// Deduplizierter Stringpool.
    pub strings: Vec<Rc<str>>,
    pub globals_init: Vec<TypeInit>,
    pub global_names: Vec<String>,
    pub udts: Vec<UdtLayout>,
    /// Prozeduren; Index 0 ist das Hauptprogramm (modul-qualifiziert über
    /// `name` des Moduls + Prozedurname).
    pub procs: Vec<ProcCode>,
    pub data: Vec<DataItem>,
    /// Sprungtabellen für `ON n GOTO/GOSUB`.
    pub jump_tables: Vec<Vec<u32>>,
    pub objects: Vec<FormObject>,
    pub event_procs: Vec<HEventProc>,
}

fn w_string(out: &mut Vec<u8>, s: &str) {
    out.extend_from_slice(&(s.len() as u32).to_le_bytes());
    out.extend_from_slice(s.as_bytes());
}

impl CompiledModule {
    pub fn validate(&self) -> Result<(), LoadError> {
        let bad = || LoadError::Corrupt("ungültige Tabellenreferenz oder Anfangsdaten");
        if self.procs.is_empty()
            || self.sources.is_empty()
            || self.modules.is_empty()
            || self.modules.len() > u16::MAX as usize
            || self.procs.len() > u16::MAX as usize
            || self.objects.len() >= u16::MAX as usize
            || self.strings.len() >= u16::MAX as usize
            || self.globals_init.len() > u16::MAX as usize
            || self.global_names.len() != self.globals_init.len()
            || self.modules.iter().any(|(_, base)| *base > 1)
            || self.option_base != self.modules[0].1
            || self
                .sources
                .iter()
                .any(|s| s.module as usize >= self.modules.len())
        {
            return Err(bad());
        }
        let ty_ok =
            |ty: &TypeInit| !matches!(ty, TypeInit::Udt(id) if *id as usize >= self.udts.len());
        let sig_ok = |ty: &HTy| !matches!(ty, HTy::Udt(id) if *id as usize >= self.udts.len());
        if self.globals_init.iter().any(|t| !ty_ok(t)) {
            return Err(bad());
        }
        for (id, udt) in self.udts.iter().enumerate() {
            if udt
                .fields
                .iter()
                .any(|t| matches!(t, TypeInit::Udt(n) if *n as usize >= id))
            {
                return Err(bad());
            }
        }
        for (id, o) in self.objects.iter().enumerate() {
            if o.parent.is_some_and(|parent| parent as usize >= id) {
                return Err(bad());
            }
            if o.parent_form.as_deref()
                != o.parent
                    .map(|parent| self.objects[parent as usize].name.as_str())
            {
                return Err(bad());
            }
        }
        if self.startup_form.is_some_and(|id| {
            self.objects
                .get(id as usize)
                .is_none_or(|o| o.class != ObjectClass::Form)
        }) {
            return Err(bad());
        }
        for e in &self.event_procs {
            if e.object as usize >= self.objects.len() || e.proc as usize >= self.procs.len() {
                return Err(bad());
            }
        }
        let mut initials = std::collections::HashSet::new();
        for initial in &self.form_initial {
            let object = self.objects.get(initial.object as usize).ok_or_else(bad)?;
            if !initials.insert((initial.object, initial.index))
                || (initial.index != 0 && !object.array)
            {
                return Err(bad());
            }
            for (id, value) in &initial.properties {
                use tb_frontend::forms::PropertyType as T;
                let spec = tb_frontend::forms::properties(object.class)
                    .get(*id as usize)
                    .copied()
                    .ok_or_else(bad)?;
                let valid = match (value, spec.ty) {
                    (PropertyValue::Integer(v), T::Integer) => {
                        !spec.min.is_some_and(|min| *v < min)
                            && !spec.max.is_some_and(|max| *v > max)
                            && (spec.name != "INDEX" || *v == initial.index)
                    }
                    (PropertyValue::Single(v), T::Single) => v.is_finite(),
                    (PropertyValue::String(_), T::String)
                    | (PropertyValue::Boolean(_), T::Boolean)
                    | (PropertyValue::IntegerArray(_), T::IntegerArray) => true,
                    (PropertyValue::Object(v), T::Object) => {
                        v.is_none_or(|(id, _)| (id as usize) < self.objects.len())
                    }
                    _ => false,
                };
                if !valid {
                    return Err(bad());
                }
            }
        }
        // PARENT overrides carry design-array indices in the existing TBC4 property format.
        let mut parents: std::collections::HashMap<_, _> = self
            .objects
            .iter()
            .enumerate()
            .map(|(id, object)| ((id as u16, 0), object.parent.map(|id| (id, 0))))
            .collect();
        for initial in &self.form_initial {
            let object = &self.objects[initial.object as usize];
            let parent = tb_frontend::forms::property(object.class, "PARENT")
                .and_then(|(id, _)| initial.properties.get(&id));
            parents.insert(
                (initial.object, initial.index),
                match parent {
                    Some(PropertyValue::Object(parent)) => {
                        parent.map(|(id, index)| (id, index.unwrap_or(0)))
                    }
                    _ => parents[&(initial.object, 0)],
                },
            );
        }
        for key in parents.keys() {
            let mut seen = std::collections::HashSet::new();
            let mut current = Some(*key);
            while let Some(key) = current {
                if !seen.insert(key) {
                    return Err(bad());
                }
                current = *parents.get(&key).ok_or_else(bad)?;
            }
        }
        for p in &self.procs {
            if p.module as usize >= self.modules.len()
                || p.n_params as usize != p.params.len()
                || p.params.len() > p.locals_init.len()
                || p.locals_init.len() != p.local_names.len()
                || p.locals_init.iter().any(|t| !ty_ok(t))
                || p.params.iter().any(|p| !sig_ok(&p.ty))
                || p.ret_ty.as_ref().is_some_and(|t| !sig_ok(t))
                || matches!(p.kind, HProcKind::Function | HProcKind::DefFn) != p.ret_ty.is_some()
                || p.params.iter().zip(&p.locals_init).any(|(param, init)| {
                    *init
                        != if param.array {
                            TypeInit::Empty
                        } else {
                            crate::codegen::type_init(&param.ty)
                        }
                })
            {
                return Err(bad());
            }
            for instruction in &p.code {
                use Instr::*;
                let valid = match instruction {
                    Source(id, _) => (*id as usize) < self.sources.len(),
                    PushUdtId(id) => (*id as usize) < self.udts.len(),
                    PushStr(id) | Unsupported(id) => (*id as usize) < self.strings.len(),
                    LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
                        (*id as usize) < self.globals_init.len()
                    }
                    LoadLocal(id) | StoreLocal(id) | MakeRefLocal(id) | LoadRef(id)
                    | StoreRef(id) => (*id as usize) < p.locals_init.len(),
                    LoadArr(global, id, _, ty)
                    | DimArr(global, id, _, ty)
                    | CommonArr(global, id, _, ty)
                    | RedimArr(global, id, _, ty) => {
                        ty_ok(ty)
                            && (*id as usize)
                                < if *global {
                                    self.globals_init.len()
                                } else {
                                    p.locals_init.len()
                                }
                    }
                    EraseSlot(global, id) => {
                        (*id as usize)
                            < if *global {
                                self.globals_init.len()
                            } else {
                                p.locals_init.len()
                            }
                    }
                    Call(id, argc) => self
                        .procs
                        .get(*id as usize)
                        .is_some_and(|p| p.n_params == *argc as u16),
                    Jump(pc) | JumpIfFalse(pc) | JumpIfTrue(pc) | Gosub(pc) | RetGosubTo(pc)
                    | OnErrorLocal(pc) | ResumeLabel(pc) => (*pc as usize) < p.code.len(),
                    OnErrorGoto(pc) => self
                        .procs
                        .first()
                        .is_some_and(|p| (*pc as usize) < p.code.len()),
                    OnJump(id, _) => self
                        .jump_tables
                        .get(*id as usize)
                        .is_some_and(|t| t.iter().all(|pc| (*pc as usize) < p.code.len())),
                    Restore(id) => (*id as usize) <= self.data.len(),
                    Input(_, _, id, _) => *id == u16::MAX || (*id as usize) < self.strings.len(),
                    LoadObjectProperty(id, prop, _)
                    | StoreObjectProperty(id, prop, _)
                    | LoadObjectIndexedProperty(id, prop)
                    | StoreObjectIndexedProperty(id, prop) => {
                        self.objects.get(*id as usize).is_some_and(|o| {
                            ((*prop & 0x7fff) as usize)
                                < tb_frontend::forms::properties(o.class).len()
                        })
                    }
                    PushObject(id, _) | ObjectLoad(id, _, _) => (*id as usize) < self.objects.len(),
                    ObjectMethod(id, method, _) | ObjectMethodFn(id, method, _) => {
                        self.objects.get(*id as usize).is_some_and(|o| {
                            (*method as usize) < tb_frontend::forms::methods(o.class).len()
                        })
                    }
                    LoadDynamicObjectProperty(name) | StoreDynamicObjectProperty(name) => {
                        (*name as usize) < self.strings.len()
                    }
                    GetPut(_, _, 7, id) => (*id as usize) < self.udts.len(),
                    GetPut(_, _, kind, _) => *kind <= 8,
                    TypeOf(class) => ObjectClass::from_id(*class).is_some(),
                    TrapDefine(kind, pc) => *kind <= 3 && (*pc as usize) < p.code.len(),
                    TrapDisable(kind) => *kind <= 3,
                    TrapSet(kind, state) => *kind <= 3 && *state <= 2,
                    Run(kind) => *kind <= 2,
                    ReadData(kind) => *kind <= 1,
                    _ => true,
                };
                if !valid {
                    return Err(bad());
                }
            }
        }
        Ok(())
    }

    /// `.tbc`-Container schreiben: Magic, Version, Flags, Abschnittstabelle
    /// (Kennung/Offset/Länge), Abschnitte MODN, SRCS, CONS, TYPS, GLOB, PROC
    /// (mit eingebettetem Code und Quellorten), DATA, JMPT, OBJS.
    pub fn to_tbc(&self) -> Vec<u8> {
        let mut sections: Vec<([u8; 4], Vec<u8>)> = Vec::new();

        let mut modn = Vec::new();
        w_string(&mut modn, &self.name);
        modn.push(self.option_base);
        sections.push((*b"MODN", modn));

        let mut srcs = Vec::new();
        (self.modules.len() as u32).enc(&mut srcs);
        for (name, base) in &self.modules {
            w_string(&mut srcs, name);
            base.enc(&mut srcs);
        }
        (self.sources.len() as u32).enc(&mut srcs);
        for source in &self.sources {
            source.module.enc(&mut srcs);
            w_string(&mut srcs, &source.path);
        }
        sections.push((*b"SRCS", srcs));

        let mut cons = Vec::new();
        cons.extend_from_slice(&(self.strings.len() as u32).to_le_bytes());
        for s in &self.strings {
            w_string(&mut cons, s);
        }
        sections.push((*b"CONS", cons));

        let mut typs = Vec::new();
        typs.extend_from_slice(&(self.udts.len() as u32).to_le_bytes());
        for u in &self.udts {
            w_string(&mut typs, &u.name);
            typs.extend_from_slice(&(u.fields.len() as u32).to_le_bytes());
            for f in &u.fields {
                f.enc(&mut typs);
            }
        }
        sections.push((*b"TYPS", typs));

        let mut glob = Vec::new();
        glob.extend_from_slice(&(self.globals_init.len() as u32).to_le_bytes());
        for (init, name) in self.globals_init.iter().zip(&self.global_names) {
            init.enc(&mut glob);
            w_string(&mut glob, name);
        }
        sections.push((*b"GLOB", glob));

        let mut proc = Vec::new();
        proc.extend_from_slice(&(self.procs.len() as u32).to_le_bytes());
        for p in &self.procs {
            w_string(&mut proc, &p.name);
            p.module.enc(&mut proc);
            (p.kind as u8).enc(&mut proc);
            (p.params.len() as u32).enc(&mut proc);
            for param in &p.params {
                w_string(&mut proc, &param.name);
                param.ty.enc(&mut proc);
                param.array.enc(&mut proc);
                param.by_ref.enc(&mut proc);
            }
            p.ret_ty.is_some().enc(&mut proc);
            if let Some(ty) = &p.ret_ty {
                ty.enc(&mut proc);
            }
            proc.extend_from_slice(&p.n_params.to_le_bytes());
            proc.extend_from_slice(&(p.locals_init.len() as u32).to_le_bytes());
            for (init, name) in p.locals_init.iter().zip(&p.local_names) {
                init.enc(&mut proc);
                w_string(&mut proc, name);
            }
            let mut code = Vec::new();
            for i in &p.code {
                i.encode(&mut code);
            }
            proc.extend_from_slice(&(p.code.len() as u32).to_le_bytes());
            proc.extend_from_slice(&(code.len() as u32).to_le_bytes());
            proc.extend_from_slice(&code);
        }
        sections.push((*b"PROC", proc));

        let mut data = Vec::new();
        data.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
        for d in &self.data {
            w_string(&mut data, &d.text);
            data.extend_from_slice(&d.line.to_le_bytes());
        }
        sections.push((*b"DATA", data));

        let mut jmpt = Vec::new();
        jmpt.extend_from_slice(&(self.jump_tables.len() as u32).to_le_bytes());
        for t in &self.jump_tables {
            jmpt.extend_from_slice(&(t.len() as u32).to_le_bytes());
            for target in t {
                jmpt.extend_from_slice(&target.to_le_bytes());
            }
        }
        sections.push((*b"JMPT", jmpt));

        let mut objs = Vec::new();
        objs.extend_from_slice(&(self.objects.len() as u32).to_le_bytes());
        for o in &self.objects {
            w_string(&mut objs, &o.name);
            objs.push(o.class.id());
            w_string(&mut objs, o.parent_form.as_deref().unwrap_or(""));
            objs.push(o.array as u8);
            o.parent.unwrap_or(u16::MAX).enc(&mut objs);
        }
        objs.extend_from_slice(&(self.event_procs.len() as u32).to_le_bytes());
        for e in &self.event_procs {
            objs.extend_from_slice(&e.object.to_le_bytes());
            w_string(&mut objs, &e.event);
            objs.extend_from_slice(&e.proc.to_le_bytes());
        }
        self.startup_form.unwrap_or(u16::MAX).enc(&mut objs);
        (self.form_initial.len() as u32).enc(&mut objs);
        for initial in &self.form_initial {
            initial.object.enc(&mut objs);
            initial.index.enc(&mut objs);
            (initial.properties.len() as u32).enc(&mut objs);
            for (id, value) in &initial.properties {
                id.enc(&mut objs);
                value.enc(&mut objs);
            }
        }
        sections.push((*b"OBJS", objs));

        // Header + Abschnittstabelle
        let mut out = Vec::new();
        out.extend_from_slice(TBC_MAGIC);
        out.extend_from_slice(&TBC_VERSION.to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes()); // Flags
        out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
        let table_start = out.len();
        // Platzhalter für Tabelle
        for _ in 0..sections.len() {
            out.extend_from_slice(&[0u8; 12]);
        }
        let mut offsets = Vec::new();
        for (_, payload) in &sections {
            offsets.push((out.len() as u32, payload.len() as u32));
            out.extend_from_slice(payload);
        }
        for (i, ((id, _), (off, len))) in sections.iter().zip(&offsets).enumerate() {
            let at = table_start + i * 12;
            out[at..at + 4].copy_from_slice(id);
            out[at + 4..at + 8].copy_from_slice(&off.to_le_bytes());
            out[at + 8..at + 12].copy_from_slice(&len.to_le_bytes());
        }
        out
    }

    pub fn from_tbc(buf: &[u8]) -> Result<CompiledModule, LoadError> {
        let mut r = Reader::new(buf);
        if r.take(4)? != TBC_MAGIC {
            return Err(LoadError::BadMagic);
        }
        let version = r.u16()?;
        if version != TBC_VERSION {
            return Err(LoadError::Version(version));
        }
        if r.u16()? != 0 {
            return Err(LoadError::Corrupt("Header-Flags"));
        }
        let n_sections = r.u32()? as usize;
        if n_sections != 9 {
            return Err(LoadError::Corrupt("Abschnittsanzahl"));
        }
        let mut table = Vec::new();
        for _ in 0..n_sections {
            let id: [u8; 4] = r.take(4)?.try_into().unwrap();
            let off = r.u32()? as usize;
            let len = r.u32()? as usize;
            table.push((id, off, len));
        }
        let expected = [
            *b"MODN", *b"SRCS", *b"CONS", *b"TYPS", *b"GLOB", *b"PROC", *b"DATA", *b"JMPT",
            *b"OBJS",
        ];
        let mut seen = std::collections::HashSet::new();
        let mut ranges = Vec::new();
        for (id, off, len) in &table {
            if !expected.contains(id)
                || !seen.insert(*id)
                || *off < r.pos
                || *off > buf.len()
                || *len > buf.len() - *off
            {
                return Err(LoadError::Corrupt("Abschnittstabelle"));
            }
            ranges.push((*off, off + len));
        }
        ranges.sort_unstable();
        if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
            return Err(LoadError::Corrupt("überlappende Abschnitte"));
        }
        let section = |id: &[u8; 4]| -> Result<Reader, LoadError> {
            for (sid, off, len) in &table {
                if sid == id {
                    if off + len > buf.len() {
                        return Err(LoadError::Corrupt("Abschnittstabelle"));
                    }
                    return Ok(Reader::new(&buf[*off..*off + *len]));
                }
            }
            Err(LoadError::Corrupt("Abschnitt fehlt"))
        };

        let mut r = section(b"MODN")?;
        let name = r.string()?;
        let option_base = r.u8()?;

        r.finish()?;
        let mut r = section(b"SRCS")?;
        let n = r.u32()?;
        let mut modules = Vec::new();
        for _ in 0..n {
            modules.push((r.string()?, r.u8()?));
        }
        let n = r.u32()?;
        let mut sources = Vec::new();
        for _ in 0..n {
            sources.push(SourceFile {
                module: r.u16()?,
                path: r.string()?,
            });
        }
        r.finish()?;
        let mut r = section(b"CONS")?;
        let n = r.u32()? as usize;
        let mut strings = Vec::new();
        for _ in 0..n {
            strings.push(Rc::from(r.string()?.as_str()));
        }

        r.finish()?;
        let mut r = section(b"TYPS")?;
        let n = r.u32()? as usize;
        let mut udts = Vec::new();
        for _ in 0..n {
            let name = r.string()?;
            let nf = r.u32()? as usize;
            let mut fields = Vec::new();
            for _ in 0..nf {
                fields.push(TypeInit::dec(&mut r)?);
            }
            udts.push(UdtLayout { name, fields });
        }

        r.finish()?;
        let mut r = section(b"GLOB")?;
        let n = r.u32()? as usize;
        let mut globals_init = Vec::new();
        let mut global_names = Vec::new();
        for _ in 0..n {
            globals_init.push(TypeInit::dec(&mut r)?);
            global_names.push(r.string()?);
        }

        r.finish()?;
        let mut r = section(b"PROC")?;
        let n = r.u32()? as usize;
        let mut procs = Vec::new();
        for _ in 0..n {
            let name = r.string()?;
            let module = r.u16()?;
            let kind = match r.u8()? {
                0 => HProcKind::Main,
                1 => HProcKind::Sub,
                2 => HProcKind::Function,
                3 => HProcKind::DefFn,
                _ => return Err(LoadError::Corrupt("Prozedurart")),
            };
            let np = r.u32()?;
            let mut params = Vec::new();
            for _ in 0..np {
                params.push(HParam {
                    name: r.string()?,
                    ty: HTy::dec(&mut r)?,
                    array: bool::dec(&mut r)?,
                    by_ref: bool::dec(&mut r)?,
                });
            }
            let ret_ty = if bool::dec(&mut r)? {
                Some(HTy::dec(&mut r)?)
            } else {
                None
            };
            let n_params = r.u16()?;
            let nl = r.u32()? as usize;
            let mut locals_init = Vec::new();
            let mut local_names = Vec::new();
            for _ in 0..nl {
                locals_init.push(TypeInit::dec(&mut r)?);
                local_names.push(r.string()?);
            }
            let n_instr = r.u32()? as usize;
            let code_len = r.u32()? as usize;
            let code_bytes = r.take(code_len)?;
            let mut cr = Reader::new(code_bytes);
            let mut code = Vec::new();
            for _ in 0..n_instr {
                code.push(Instr::decode(&mut cr)?);
            }
            cr.finish()?;
            procs.push(ProcCode {
                module,
                kind,
                params,
                ret_ty,
                name,
                n_params,
                locals_init,
                local_names,
                code,
            });
        }

        r.finish()?;
        let mut r = section(b"DATA")?;
        let n = r.u32()? as usize;
        let mut data = Vec::new();
        for _ in 0..n {
            let text = r.string()?;
            let line = r.u32()?;
            data.push(DataItem { text, line });
        }

        r.finish()?;
        let mut r = section(b"JMPT")?;
        let n = r.u32()? as usize;
        let mut jump_tables = Vec::new();
        for _ in 0..n {
            let m = r.u32()? as usize;
            let mut t = Vec::new();
            for _ in 0..m {
                t.push(r.u32()?);
            }
            jump_tables.push(t);
        }

        r.finish()?;
        let mut r = section(b"OBJS")?;
        let n = r.u32()? as usize;
        let mut objects = Vec::new();
        for _ in 0..n {
            let name = r.string()?;
            let class = ObjectClass::from_id(r.u8()?).ok_or(LoadError::Corrupt("Objektklasse"))?;
            let parent = r.string()?;
            let array = bool::dec(&mut r)?;
            let parent_id = r.u16()?;
            objects.push(FormObject {
                name,
                class,
                parent_form: (!parent.is_empty()).then_some(parent),
                parent: (parent_id != u16::MAX).then_some(parent_id),
                array,
            });
        }
        let n = r.u32()? as usize;
        let mut event_procs = Vec::new();
        for _ in 0..n {
            event_procs.push(HEventProc {
                object: r.u16()?,
                event: r.string()?,
                proc: r.u16()?,
            });
        }

        let startup = r.u16()?;
        let startup_form = (startup != u16::MAX).then_some(startup);
        let n = r.u32()?;
        let mut form_initial = Vec::new();
        for _ in 0..n {
            let object = r.u16()?;
            let index = i32::dec(&mut r)?;
            let n = r.u32()?;
            let mut properties = std::collections::BTreeMap::new();
            for _ in 0..n {
                if properties
                    .insert(r.u16()?, PropertyValue::dec(&mut r)?)
                    .is_some()
                {
                    return Err(LoadError::Corrupt("doppelte Anfangseigenschaft"));
                }
            }
            form_initial.push(FormInitial {
                object,
                index,
                properties,
            });
        }
        r.finish()?;
        let module = CompiledModule {
            modules,
            sources,
            form_initial,
            startup_form,
            name,
            option_base,
            strings,
            globals_init,
            global_names,
            udts,
            procs,
            data,
            jump_tables,
            objects,
            event_procs,
        };
        module.validate()?;
        Ok(module)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn instr_roundtrip() {
        let samples = vec![
            Instr::Stmt(42),
            Instr::PushInt(-7),
            Instr::PushDbl(1.5),
            Instr::PushCur(-12_345),
            Instr::LoadArr(true, 3, 2, TypeInit::FixedStr(30)),
            Instr::CmpR8(CmpOp::Le),
            Instr::OnJump(1, true),
            Instr::Call(2, 3),
            Instr::Input(2, false, 0xFFFF, true),
            Instr::ConvCyR8,
            Instr::TrapDefine(1, 4242),
            Instr::TrapDisable(3),
            Instr::TrapSet(0, 2),
            Instr::EventSwitch(false),
            Instr::Doevents,
            Instr::Sleep(true),
            Instr::RetFn,
        ];
        let mut buf = Vec::new();
        for i in &samples {
            i.encode(&mut buf);
        }
        let mut r = Reader::new(&buf);
        for want in &samples {
            let got = Instr::decode(&mut r).unwrap();
            assert_eq!(&got, want);
        }
    }

    #[test]
    fn tbc_roundtrip() {
        let m = CompiledModule {
            modules: vec![("TEST".into(), 1)],
            sources: vec![SourceFile {
                module: 0,
                path: "test.bas".into(),
            }],
            form_initial: vec![],
            startup_form: None,
            name: "TEST".into(),
            option_base: 1,
            strings: vec![Rc::from("Hallo"), Rc::from("Welt")],
            globals_init: vec![TypeInit::Int, TypeInit::Str],
            global_names: vec!["a".into(), "s".into()],
            udts: vec![UdtLayout {
                name: "Kunde".into(),
                fields: vec![TypeInit::FixedStr(30), TypeInit::Dbl],
            }],
            procs: vec![ProcCode {
                module: 0,
                kind: HProcKind::Main,
                params: vec![],
                ret_ty: None,
                name: "TEST".into(),
                n_params: 0,
                locals_init: vec![],
                local_names: vec![],
                code: vec![Instr::Stmt(1), Instr::PushStr(0), Instr::End],
            }],
            data: vec![DataItem {
                text: "1.5".into(),
                line: 3,
            }],
            jump_tables: vec![vec![4, 9]],
            objects: vec![],
            event_procs: vec![],
        };
        let bytes = m.to_tbc();
        let back = CompiledModule::from_tbc(&bytes).unwrap();
        assert_eq!(back.name, "TEST");
        assert_eq!(back.strings.len(), 2);
        assert_eq!(&*back.strings[0], "Hallo");
        assert_eq!(back.globals_init, m.globals_init);
        assert_eq!(back.udts[0].fields, m.udts[0].fields);
        assert_eq!(back.procs[0].code, m.procs[0].code);
        assert_eq!(back.data[0].text, "1.5");
        assert_eq!(back.jump_tables, m.jump_tables);
    }

    #[test]
    fn unbekannte_version_wird_abgelehnt() {
        let m = CompiledModule {
            modules: vec![("TEST".into(), 0)],
            sources: vec![SourceFile {
                module: 0,
                path: "test.bas".into(),
            }],
            form_initial: vec![],
            startup_form: None,
            name: "T".into(),
            option_base: 0,
            strings: vec![],
            globals_init: vec![],
            global_names: vec![],
            udts: vec![],
            procs: vec![],
            data: vec![],
            jump_tables: vec![],
            objects: vec![],
            event_procs: vec![],
        };
        let mut bytes = m.to_tbc();
        bytes[4] = 0xFF; // Version hochsetzen
        bytes[5] = 0x7F;
        match CompiledModule::from_tbc(&bytes) {
            Err(LoadError::Version(v)) => assert_eq!(v, 0x7FFF),
            other => panic!("Version-Fehler erwartet, war {other:?}"),
        }
        let msg = LoadError::Version(0x7FFF).to_string();
        assert!(msg.contains("32767"), "Meldung nennt die Version: {msg}");
    }
}
