//! Leser fuer den tokenisierten BASIC-Codeteil binaerer VBDOS-Formulare.

use crate::frm::{decode_cp437, error, read_u16, FormError};

fn decode_pcode_bytes(bytes: &[u8]) -> String {
    bytes
        .iter()
        .map(|byte| {
            if *byte < 0x80 {
                char::from(*byte)
            } else {
                decode_cp437(&[*byte]).chars().next().unwrap()
            }
        })
        .collect()
}

fn letter_ranges(mask: u32) -> String {
    let mut ranges = Vec::new();
    let mut first = None;
    for index in 0..=26 {
        let set = index < 26 && mask & (1 << (31 - index)) != 0;
        if set && first.is_none() {
            first = Some(index);
        } else if !set && first.is_some() {
            let start = first.take().unwrap();
            let mut range = char::from(b'A' + start as u8).to_string();
            if index - start > 1 {
                range.push('-');
                range.push(char::from(b'A' + index as u8 - 1));
            }
            ranges.push(range);
        }
    }
    ranges.join(", ")
}

#[derive(Clone, Copy)]
struct TokenDef {
    len: i16,
    rules: &'static [&'static str],
}

fn token_def(code: u16) -> Option<TokenDef> {
    match code {
        0x000 => Some(TokenDef {
            len: 0,
            rules: &["newline::=.{#newline}{#tabh}"],
        }),
        0x001 => Some(TokenDef {
            len: 2,
            rules: &["newline::=.{#newline}{#tabi}"],
        }),
        0x002 => Some(TokenDef {
            len: 2,
            rules: &["newline::=.{#newline-include}"],
        }),
        0x003 => Some(TokenDef {
            len: 4,
            rules: &["newline::=.{#newline-include}{#indent:2} "],
        }),
        0x004 => Some(TokenDef {
            len: 4,
            rules: &[".{#newline}{#thaddr:0}{#label:2}"],
        }),
        0x005 => Some(TokenDef {
            len: 6,
            rules: &[".{#newline}{#thaddr:0}{#label:2} {#indent:4}"],
        }),
        0x006 => Some(TokenDef {
            len: 0,
            rules: &[": "],
        }),
        0x007 => Some(TokenDef {
            len: 2,
            rules: &[":{#tabi}"],
        }),
        0x008 => Some(TokenDef {
            len: 0,
            rules: &["."],
        }),
        0x009 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x00a => Some(TokenDef {
            len: -1,
            rules: &["{#raw:2}"],
        }),
        0x00b => Some(TokenDef {
            len: 2,
            rules: &["expr::={#id+}"],
        }),
        0x00c => Some(TokenDef {
            len: 2,
            rules: &[
                "consts::={const:1} {#id+} = {0}",
                "consts::={consts:1}, {#id+} = {0}",
                "{#id+} = {0}",
            ],
        }),
        0x00d => Some(TokenDef {
            len: 2,
            rules: &[
                "decls::={decls:1}, {#id+:0} {astype:0}",
                "decls::={decls:0}, {#id+:0}",
                "decls::={decl:1} {#id+:0} {astype:0}",
                "decls::={decl:0} {#id+:0}",
                "{#id+:0} {astype:0}",
                "{#id+:0}",
            ],
        }),
        0x00e => Some(TokenDef {
            len: 4,
            rules: &["expr::={##id(expr)}"],
        }),
        0x00f => Some(TokenDef {
            len: 4,
            rules: &["{##id(expr)} = {$+0}"],
        }),
        0x010 => Some(TokenDef {
            len: 4,
            rules: &["decls::={##id(decl)}"],
        }),
        0x011 => Some(TokenDef {
            len: 2,
            rules: &["expr::={0}.{#id}"],
        }),
        0x012 => Some(TokenDef {
            len: 2,
            rules: &["{0}.{#id} = {1}"],
        }),
        0x013 => Some(TokenDef {
            len: 4,
            rules: &["expr::={0}.{#id:2}({1})"],
        }),
        0x014 => Some(TokenDef {
            len: 4,
            rules: &["{0}.{#id:2}({1}) = {2}"],
        }),
        0x015 => Some(TokenDef {
            len: 4,
            rules: &["astype::={#tabi:2}AS {#type:0}"],
        }),
        0x016 => Some(TokenDef {
            len: 4,
            rules: &["astype::={#tabi:2}AS {#type:0}"],
        }),
        0x017 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x018 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x019 => Some(TokenDef {
            len: 2,
            rules: &["{#id}"],
        }),
        0x01a => Some(TokenDef {
            len: 0,
            rules: &["declmod::=SHARED"],
        }),
        0x01b => Some(TokenDef {
            len: 6,
            rules: &["deftype::={#thaddr:0}{#DEFxxx}"],
        }),
        0x01c => Some(TokenDef {
            len: 0,
            rules: &["{self:1}, {0}", "REDIM {declmod:1} {0}", "REDIM {0}"],
        }),
        0x01d => Some(TokenDef {
            len: 2,
            rules: &["END TYPE"],
        }),
        0x01e => Some(TokenDef {
            len: 2,
            rules: &["decl::=SHARED"],
        }),
        0x01f => Some(TokenDef {
            len: 2,
            rules: &["decl::=STATIC"],
        }),
        0x020 => Some(TokenDef {
            len: 4,
            rules: &["TYPE {#id:2}"],
        }),
        0x021 => Some(TokenDef {
            len: -1,
            rules: &["$STATIC{#raw}"],
        }),
        0x022 => Some(TokenDef {
            len: -1,
            rules: &["$DYNAMIC{#raw}"],
        }),
        0x023 => Some(TokenDef {
            len: 0,
            rules: &["const::=CONST"],
        }),
        0x024 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x025 => Some(TokenDef {
            len: 0,
            rules: &["BYVAL {0}"],
        }),
        0x026 => Some(TokenDef {
            len: -1,
            rules: &["{deffn:1} = {0}"],
        }),
        0x027 => Some(TokenDef {
            len: 0,
            rules: &["COM({0})"],
        }),
        0x028 => Some(TokenDef {
            len: 2,
            rules: &["ON {0} GOSUB {#id}"],
        }),
        0x029 => Some(TokenDef {
            len: 0,
            rules: &["KEY({0})"],
        }),
        0x02a => Some(TokenDef {
            len: 0,
            rules: &["{0} OFF"],
        }),
        0x02b => Some(TokenDef {
            len: 0,
            rules: &["{0} ON"],
        }),
        0x02c => Some(TokenDef {
            len: 0,
            rules: &["{0} STOP"],
        }),
        0x02d => Some(TokenDef {
            len: 0,
            rules: &["PEN"],
        }),
        0x02e => Some(TokenDef {
            len: 0,
            rules: &["PLAY"],
        }),
        0x02f => Some(TokenDef {
            len: 0,
            rules: &["PLAY({0})"],
        }),
        0x030 => Some(TokenDef {
            len: 0,
            rules: &["SIGNAL({0})"],
        }),
        0x031 => Some(TokenDef {
            len: 0,
            rules: &["STRIG({0})"],
        }),
        0x032 => Some(TokenDef {
            len: 0,
            rules: &["TIMER"],
        }),
        0x033 => Some(TokenDef {
            len: 0,
            rules: &["TIMER({0})"],
        }),
        0x034 => Some(TokenDef {
            len: 4,
            rules: &["newline::={#thaddr:0}{#label:2} "],
        }),
        0x035 => Some(TokenDef {
            len: 6,
            rules: &["newline::={#thaddr:0}{#label:2} {#indent:4}"],
        }),
        0x037 => Some(TokenDef {
            len: 4,
            rules: &["CALL {#id:2}{##call()}"],
        }),
        0x038 => Some(TokenDef {
            len: 4,
            rules: &["{#id:2}{##call}"],
        }),
        0x039 => Some(TokenDef {
            len: 4,
            rules: &["CALLS {#id:2}{##call()}"],
        }),
        0x03a => Some(TokenDef {
            len: 0,
            rules: &["CASE ELSE"],
        }),
        0x03b => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, {0}", "case::=CASE {0}"],
        }),
        0x03c => Some(TokenDef {
            len: 0,
            rules: &["case::={case:2}, {1} TO {0}", "case::=CASE {1} TO {0}"],
        }),
        0x03d => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS = {0}", "case::=CASE IS = {0}"],
        }),
        0x03e => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS < {0}", "case::=CASE IS < {0}"],
        }),
        0x03f => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS > {0}", "case::=CASE IS > {0}"],
        }),
        0x040 => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS <= {0}", "case::=CASE IS <= {0}"],
        }),
        0x041 => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS >= {0}", "case::=CASE IS >= {0}"],
        }),
        0x042 => Some(TokenDef {
            len: 0,
            rules: &["case::={case:1}, IS <> {0}", "case::=CASE IS <> {0}"],
        }),
        0x043 => Some(TokenDef {
            len: 0,
            rules: &["ON"],
        }),
        0x044 => Some(TokenDef {
            len: -1,
            rules: &["DECLARE {#procdecl()}"],
        }),
        0x045 => Some(TokenDef {
            len: -1,
            rules: &["deffn::={#procdecl:2}"],
        }),
        0x046 => Some(TokenDef {
            len: 0,
            rules: &["DO"],
        }),
        0x047 => Some(TokenDef {
            len: 0,
            rules: &["DO UNTIL {0}"],
        }),
        0x048 => Some(TokenDef {
            len: 2,
            rules: &["DO WHILE {0}"],
        }),
        0x049 => Some(TokenDef {
            len: 2,
            rules: &["{newline:0}ELSE ", " ELSE "],
        }),
        0x04a => Some(TokenDef {
            len: 2,
            rules: &["{#id}"],
        }),
        0x04c => Some(TokenDef {
            len: 0,
            rules: &[" ELSE "],
        }),
        0x04d => Some(TokenDef {
            len: 2,
            rules: &["ELSEIF {0} THEN"],
        }),
        0x04e => Some(TokenDef {
            len: 0,
            rules: &["END"],
        }),
        0x04f => Some(TokenDef {
            len: -1,
            rules: &["END DEF"],
        }),
        0x050 => Some(TokenDef {
            len: 0,
            rules: &["END IF"],
        }),
        0x051 => Some(TokenDef {
            len: 0,
            rules: &["END {#proctype}"],
        }),
        0x052 => Some(TokenDef {
            len: 0,
            rules: &["END SELECT"],
        }),
        0x053 => Some(TokenDef {
            len: 2,
            rules: &["EXIT DO"],
        }),
        0x054 => Some(TokenDef {
            len: 2,
            rules: &["EXIT FOR"],
        }),
        0x055 => Some(TokenDef {
            len: 2,
            rules: &["EXIT {#proctype}"],
        }),
        0x056 => Some(TokenDef {
            len: 4,
            rules: &["FOR {2} = {1} TO {0}"],
        }),
        0x057 => Some(TokenDef {
            len: 4,
            rules: &["FOR {3} = {2} TO {1} STEP {0}"],
        }),
        0x058 => Some(TokenDef {
            len: -1,
            rules: &["funcdef::={#procdecl}"],
        }),
        0x059 => Some(TokenDef {
            len: 2,
            rules: &["GOSUB {#id}"],
        }),
        0x05b => Some(TokenDef {
            len: 2,
            rules: &["GOTO {#id}"],
        }),
        0x05d => Some(TokenDef {
            len: 2,
            rules: &["IF {0} THEN "],
        }),
        0x05e => Some(TokenDef {
            len: 2,
            rules: &["IF {0} THEN {#id}"],
        }),
        0x060 => Some(TokenDef {
            len: 2,
            rules: &["IF {0} GOTO {#id}"],
        }),
        0x061 => Some(TokenDef {
            len: 2,
            rules: &["IF {0} THEN"],
        }),
        0x062 => Some(TokenDef {
            len: 2,
            rules: &["LOOP"],
        }),
        0x063 => Some(TokenDef {
            len: 2,
            rules: &["LOOP UNTIL {0}"],
        }),
        0x064 => Some(TokenDef {
            len: 2,
            rules: &["LOOP WHILE {0}"],
        }),
        0x065 => Some(TokenDef {
            len: 4,
            rules: &["NEXT"],
        }),
        0x066 => Some(TokenDef {
            len: 4,
            rules: &["{self:1}, {0}", "NEXT {0}"],
        }),
        0x067 => Some(TokenDef {
            len: 2,
            rules: &["ON ERROR GOTO {#id}"],
        }),
        0x068 => Some(TokenDef {
            len: -1,
            rules: &["ON {0} GOSUB {#id-list}"],
        }),
        0x069 => Some(TokenDef {
            len: -1,
            rules: &["ON {0} GOTO {#id-list}"],
        }),
        0x06a => Some(TokenDef {
            len: 0,
            rules: &["RESTORE"],
        }),
        0x06b => Some(TokenDef {
            len: 2,
            rules: &["RESTORE {#id}"],
        }),
        0x06c => Some(TokenDef {
            len: 0,
            rules: &["RESUME"],
        }),
        0x06d => Some(TokenDef {
            len: 2,
            rules: &["RESUME {#id}"],
        }),
        0x06e => Some(TokenDef {
            len: 0,
            rules: &["RESUME NEXT"],
        }),
        0x06f => Some(TokenDef {
            len: 0,
            rules: &["RETURN"],
        }),
        0x070 => Some(TokenDef {
            len: 2,
            rules: &["RETURN {#id}"],
        }),
        0x071 => Some(TokenDef {
            len: 0,
            rules: &["RUN {0}"],
        }),
        0x072 => Some(TokenDef {
            len: 2,
            rules: &["RUN {#id}"],
        }),
        0x073 => Some(TokenDef {
            len: 0,
            rules: &["RUN"],
        }),
        0x074 => Some(TokenDef {
            len: 2,
            rules: &["SELECT CASE {0}"],
        }),
        0x075 => Some(TokenDef {
            len: 2,
            rules: &["STOP"],
        }),
        0x076 => Some(TokenDef {
            len: -1,
            rules: &["subdef::={#procdecl}"],
        }),
        0x077 => Some(TokenDef {
            len: 0,
            rules: &["WAIT {1}, {0}"],
        }),
        0x078 => Some(TokenDef {
            len: 0,
            rules: &["WAIT {2}, {1}, {0}"],
        }),
        0x079 => Some(TokenDef {
            len: 2,
            rules: &["WEND"],
        }),
        0x07a => Some(TokenDef {
            len: 2,
            rules: &["WHILE {0}"],
        }),
        0x07b => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x07c => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x07d => Some(TokenDef {
            len: 0,
            rules: &["prnmod::={prnmod:1} {0},", "prnmod::=PRINT {0},"],
        }),
        0x07e => Some(TokenDef {
            len: 0,
            rules: &["{0}"],
        }),
        0x07f => Some(TokenDef {
            len: 0,
            rules: &["{0}"],
        }),
        0x080 => Some(TokenDef {
            len: 0,
            rules: &["{0}"],
        }),
        0x081 => Some(TokenDef {
            len: 0,
            rules: &["1st-coord::=({1}, {0})"],
        }),
        0x082 => Some(TokenDef {
            len: 0,
            rules: &["1st-coord::=STEP({1}, {0})"],
        }),
        0x083 => Some(TokenDef {
            len: 0,
            rules: &["{1st-coord:2}-({1}, {0})", "({1}, {0})"],
        }),
        0x084 => Some(TokenDef {
            len: 0,
            rules: &["{1st-coord:2}-STEP({1}, {0})", "-STEP({1}, {0})"],
        }),
        0x085 => Some(TokenDef {
            len: 0,
            rules: &["FIELD {0}"],
        }),
        0x086 => Some(TokenDef {
            len: 0,
            rules: &[", {1} AS {0}"],
        }),
        0x087 => Some(TokenDef {
            len: 0,
            rules: &["finput::=INPUT {0},"],
        }),
        0x088 => Some(TokenDef {
            len: 0,
            rules: &["{input|finput:1} {inputs:0}"],
        }),
        0x089 => Some(TokenDef {
            len: -1,
            rules: &["input::=INPUT {##input-args}"],
        }),
        0x08a => Some(TokenDef {
            len: 0,
            rules: &["#{0}"],
        }),
        0x08c => Some(TokenDef {
            len: 2,
            rules: &[""],
        }),
        0x08f => Some(TokenDef {
            len: 0,
            rules: &[
                "prnsmc::={self|prncma|prnsrl:1} SPC({0});",
                "prnsmc::=SPC({0});",
            ],
        }),
        0x090 => Some(TokenDef {
            len: 0,
            rules: &[
                "prnsmc::={self|prncma|prnsrl:1} TAB({0});",
                "prnsmc::=TAB({0});",
            ],
        }),
        0x091 => Some(TokenDef {
            len: 0,
            rules: &["prncma::={self|prnsmc|prnsrl:0} ,", "prncma::=,"],
        }),
        0x092 => Some(TokenDef {
            len: 0,
            rules: &[
                "prnsmc::={self:0}",
                "prnsmc::={prncma|prnsrl:0} ;",
                "prnsmc::=;",
            ],
        }),
        0x093 => Some(TokenDef {
            len: 0,
            rules: &[
                "{prnmod:2} {prnuse:1} {prnsrl|prnsmc|prncma:0}",
                "{prnmod:1} {prnsrl|prnsmc|prncma:0}",
                "{prnmod:1} {prnuse:0}",
                "{prnmod:1}",
                "PRINT {prnuse:1} {prnsrl|prnsmc|prncma:0}",
                "PRINT {prnsrl|prnsmc|prncma:0}",
                "PRINT {prnuse:0}",
                "PRINT",
            ],
        }),
        0x094 => Some(TokenDef {
            len: 0,
            rules: &[
                "prnsrl::={prncma|prnsmc|self:1} {expr:0},",
                "prnsrl::={expr:0},",
            ],
        }),
        0x095 => Some(TokenDef {
            len: 0,
            rules: &[
                "prnsrl::={prncma|prnsmc|self:1} {expr:0};",
                "prnsrl::={expr:0};",
            ],
        }),
        0x096 => Some(TokenDef {
            len: 0,
            rules: &[
                "{prnmod:3} {prnuse:2} {prnsmc|prncma|prnsrl:1} {expr:0}",
                "{prnmod:2} {prnsmc|prncma|prnsrl:1} {expr:0}",
                "{prnmod:1} {prnsmc|prncma|prnsrl|expr:0}",
                "PRINT {prnuse:2} {prnsmc|prncma|prnsrl:1} {expr:0}",
                "PRINT {prnsmc|prncma|prnsrl:1} {expr:0}",
                "PRINT {prnsmc|prncma|prnsrl|expr:0}",
            ],
        }),
        0x097 => Some(TokenDef {
            len: -1,
            rules: &["{#tabi:0}'{#raw:2}"],
        }),
        0x099 => Some(TokenDef {
            len: -1,
            rules: &["$INCLUDE: '{#raw:0}"],
        }),
        0x09a => Some(TokenDef {
            len: 0,
            rules: &["BEEP"],
        }),
        0x09b => Some(TokenDef {
            len: 0,
            rules: &["BLOAD {0}"],
        }),
        0x09c => Some(TokenDef {
            len: 0,
            rules: &["BLOAD {1}, {0}"],
        }),
        0x09d => Some(TokenDef {
            len: 0,
            rules: &["BSAVE {2}, {1}, {0}"],
        }),
        0x09e => Some(TokenDef {
            len: 0,
            rules: &["CHDIR {0}"],
        }),
        0x09f => Some(TokenDef {
            len: 0,
            rules: &["CIRCLE {##circle-args}"],
        }),
        0x0a0 => Some(TokenDef {
            len: 0,
            rules: &["CIRCLE {##circle-args}"],
        }),
        0x0a1 => Some(TokenDef {
            len: 2,
            rules: &["CLEAR{##varargs}"],
        }),
        0x0a2 => Some(TokenDef {
            len: 2,
            rules: &["CLOSE{##varargs}"],
        }),
        0x0a3 => Some(TokenDef {
            len: 0,
            rules: &["CLS {expr:0}", "CLS "],
        }),
        0x0a4 => Some(TokenDef {
            len: 2,
            rules: &["COLOR{##varargs}"],
        }),
        0x0a5 => Some(TokenDef {
            len: 4,
            rules: &[
                "decl::=COMMON {declmod:0}{#blockname:2}",
                "decl::=COMMON{#blockname:2}",
            ],
        }),
        0x0a6 => Some(TokenDef {
            len: -1,
            rules: &["DATA{#cstr:2}"],
        }),
        0x0a7 => Some(TokenDef {
            len: 0,
            rules: &["DATE$ = {0}"],
        }),
        0x0a8 => Some(TokenDef {
            len: 0,
            rules: &["DEF SEG"],
        }),
        0x0a9 => Some(TokenDef {
            len: 0,
            rules: &["DEF SEG = {0}"],
        }),
        0x0aa => Some(TokenDef {
            len: 0,
            rules: &["DRAW {0}"],
        }),
        0x0ab => Some(TokenDef {
            len: 0,
            rules: &["ENVIRON {0}"],
        }),
        0x0ac => Some(TokenDef {
            len: 2,
            rules: &["ERASE{##varargs}"],
        }),
        0x0ad => Some(TokenDef {
            len: 0,
            rules: &["ERROR {0}"],
        }),
        0x0ae => Some(TokenDef {
            len: 0,
            rules: &["FILES"],
        }),
        0x0af => Some(TokenDef {
            len: 0,
            rules: &["FILES {0}"],
        }),
        0x0b0 => Some(TokenDef {
            len: 0,
            rules: &["GET {0}"],
        }),
        0x0b1 => Some(TokenDef {
            len: 0,
            rules: &["GET {1}, {0}"],
        }),
        0x0b2 => Some(TokenDef {
            len: 2,
            rules: &["GET {1}, , {0}"],
        }),
        0x0b3 => Some(TokenDef {
            len: 2,
            rules: &["GET {2}, {1}, {0}"],
        }),
        0x0b4 => Some(TokenDef {
            len: 0,
            rules: &["GET {1}, {0}"],
        }),
        0x0b5 => Some(TokenDef {
            len: 2,
            rules: &["PUT {1}, {0}, {#action-verb}"],
        }),
        0x0b6 => Some(TokenDef {
            len: 0,
            rules: &["inputs::={inputs:1}, {0}", "inputs::={0}"],
        }),
        0x0b7 => Some(TokenDef {
            len: 0,
            rules: &["IOCTL {1}, {0}"],
        }),
        0x0b8 => Some(TokenDef {
            len: 2,
            rules: &["KEY {#keymode}"],
        }),
        0x0b9 => Some(TokenDef {
            len: 0,
            rules: &["KEY {1}, {0}"],
        }),
        0x0ba => Some(TokenDef {
            len: 0,
            rules: &["KILL {0}"],
        }),
        0x0bb => Some(TokenDef {
            len: 2,
            rules: &["LINE {##line-args}"],
        }),
        0x0bc => Some(TokenDef {
            len: 2,
            rules: &["LINE {##line-args}"],
        }),
        0x0bd => Some(TokenDef {
            len: 2,
            rules: &["LINE {##line-args}"],
        }),
        0x0be => Some(TokenDef {
            len: 2,
            rules: &["LINE {##line-args}"],
        }),
        0x0bf => Some(TokenDef {
            len: 0,
            rules: &["LET "],
        }),
        0x0c0 => Some(TokenDef {
            len: 2,
            rules: &[
                "input::=LINE {finput:1} {0}",
                "input::=LINE INPUT {##input-args} {0}",
            ],
        }),
        0x0c1 => Some(TokenDef {
            len: 2,
            rules: &["LOCATE{##varargs}"],
        }),
        0x0c2 => Some(TokenDef {
            len: 2,
            rules: &["LOCK {##lock-args}"],
        }),
        0x0c3 => Some(TokenDef {
            len: 0,
            rules: &["prnmod::=LPRINT"],
        }),
        0x0c4 => Some(TokenDef {
            len: 0,
            rules: &["LSET {0} = {1}"],
        }),
        0x0c5 => Some(TokenDef {
            len: 0,
            rules: &["MID$({0}, {2}) = {1}"],
        }),
        0x0c6 => Some(TokenDef {
            len: 0,
            rules: &["MID$({0}, {3}, {2}) = {1}"],
        }),
        0x0c7 => Some(TokenDef {
            len: 0,
            rules: &["MKDIR {0}"],
        }),
        0x0c8 => Some(TokenDef {
            len: 0,
            rules: &["NAME {1} AS {0}"],
        }),
        0x0c9 => Some(TokenDef {
            len: 2,
            rules: &["OPEN {1} {#open-args} AS {0}"],
        }),
        0x0ca => Some(TokenDef {
            len: 2,
            rules: &["OPEN {2} {#open-args} AS {1} LEN = {0}"],
        }),
        0x0cb => Some(TokenDef {
            len: 0,
            rules: &["OPEN {2}, {1}, {0}"],
        }),
        0x0cc => Some(TokenDef {
            len: 0,
            rules: &["OPEN {3}, {2}, {1}, {0}"],
        }),
        0x0cd => Some(TokenDef {
            len: 0,
            rules: &["OPTION BASE 0"],
        }),
        0x0ce => Some(TokenDef {
            len: 0,
            rules: &["OPTION BASE 1"],
        }),
        0x0cf => Some(TokenDef {
            len: 0,
            rules: &["OUT {1}, {0}"],
        }),
        0x0d0 => Some(TokenDef {
            len: 0,
            rules: &[
                "PAINT {2}{nularg:1}{nularg:0}",
                "PAINT {2}, {nularg:1}, {0}",
                "PAINT {2}, {1}{nularg:0}",
                "PAINT {2}, {1}, {0}",
            ],
        }),
        0x0d1 => Some(TokenDef {
            len: 0,
            rules: &["PAINT {3}, {2}, {1}, {0}"],
        }),
        0x0d2 => Some(TokenDef {
            len: 0,
            rules: &["PALETTE"],
        }),
        0x0d3 => Some(TokenDef {
            len: 0,
            rules: &["PALETTE {1}, {0}"],
        }),
        0x0d4 => Some(TokenDef {
            len: 0,
            rules: &["PALETTE {0}"],
        }),
        0x0d5 => Some(TokenDef {
            len: 0,
            rules: &["PCOPY {1}, {0}"],
        }),
        0x0d6 => Some(TokenDef {
            len: 0,
            rules: &["PLAY {0}"],
        }),
        0x0d7 => Some(TokenDef {
            len: 0,
            rules: &["POKE {1}, {0}"],
        }),
        0x0d8 => Some(TokenDef {
            len: 0,
            rules: &["PRESET {0}"],
        }),
        0x0d9 => Some(TokenDef {
            len: 0,
            rules: &["PRESET {0}, {1}"],
        }),
        0x0da => Some(TokenDef {
            len: 0,
            rules: &["PSET {0}"],
        }),
        0x0db => Some(TokenDef {
            len: 0,
            rules: &["PSET {1}, {0}"],
        }),
        0x0dd => Some(TokenDef {
            len: 0,
            rules: &["PUT {1}, {0}"],
        }),
        0x0de => Some(TokenDef {
            len: 2,
            rules: &["PUT {1}, , {0}"],
        }),
        0x0df => Some(TokenDef {
            len: 2,
            rules: &["PUT {2}, {1}, {0}"],
        }),
        0x0e0 => Some(TokenDef {
            len: 0,
            rules: &["RANDOMIZE"],
        }),
        0x0e1 => Some(TokenDef {
            len: 0,
            rules: &["RANDOMIZE {0}"],
        }),
        0x0e2 => Some(TokenDef {
            len: 0,
            rules: &["{self:1}, {0}", "READ {0}"],
        }),
        0x0e3 => Some(TokenDef {
            len: -1,
            rules: &["REM{#raw}"],
        }),
        0x0e4 => Some(TokenDef {
            len: 0,
            rules: &["RESET"],
        }),
        0x0e5 => Some(TokenDef {
            len: 0,
            rules: &["RMDIR {0}"],
        }),
        0x0e6 => Some(TokenDef {
            len: 0,
            rules: &["RSET {0} = {1}"],
        }),
        0x0e7 => Some(TokenDef {
            len: 2,
            rules: &["SCREEN{##varargs}"],
        }),
        0x0e8 => Some(TokenDef {
            len: 0,
            rules: &["SEEK {1}, {0}"],
        }),
        0x0e9 => Some(TokenDef {
            len: 0,
            rules: &["SHELL"],
        }),
        0x0ea => Some(TokenDef {
            len: 0,
            rules: &["SHELL {0}"],
        }),
        0x0eb => Some(TokenDef {
            len: 0,
            rules: &["SLEEP"],
        }),
        0x0ec => Some(TokenDef {
            len: 0,
            rules: &["SOUND {1}, {0}"],
        }),
        0x0ed => Some(TokenDef {
            len: 2,
            rules: &["SWAP {1}, {0}"],
        }),
        0x0ee => Some(TokenDef {
            len: 0,
            rules: &["SYSTEM"],
        }),
        0x0ef => Some(TokenDef {
            len: 0,
            rules: &["TIME$ = {0}"],
        }),
        0x0f0 => Some(TokenDef {
            len: 0,
            rules: &["TROFF"],
        }),
        0x0f1 => Some(TokenDef {
            len: 0,
            rules: &["TRON"],
        }),
        0x0f2 => Some(TokenDef {
            len: 2,
            rules: &["UNLOCK {##lock-args}"],
        }),
        0x0f3 => Some(TokenDef {
            len: 0,
            rules: &[
                "VIEW ({5}, {4})-({3}, {2}){nularg:1}{nularg:0}",
                "VIEW ({5}, {4})-({3}, {2}), {nularg:1}, {0}",
                "VIEW ({5}, {4})-({3}, {2}), {1}{nularg:0}",
                "VIEW ({5}, {4})-({3}, {2})",
            ],
        }),
        0x0f4 => Some(TokenDef {
            len: 0,
            rules: &["VIEW"],
        }),
        0x0f5 => Some(TokenDef {
            len: 0,
            rules: &["VIEW PRINT"],
        }),
        0x0f6 => Some(TokenDef {
            len: 0,
            rules: &["VIEW PRINT {1} TO {0}"],
        }),
        0x0f7 => Some(TokenDef {
            len: 0,
            rules: &[
                "VIEW SCREEN ({5}, {4})-({3}, {2}){nularg:1}{nularg:0}",
                "VIEW SCREEN ({5}, {4})-({3}, {2}), {nularg:1}, {0}",
                "VIEW SCREEN ({5}, {4})-({3}, {2}), {1}{nularg:0}",
                "VIEW SCREEN ({5}, {4})-({3}, {2})",
            ],
        }),
        0x0f8 => Some(TokenDef {
            len: 0,
            rules: &["WIDTH {1}{nularg:0}", "WIDTH {1}, {0}"],
        }),
        0x0f9 => Some(TokenDef {
            len: 0,
            rules: &["WIDTH LPRINT {0}"],
        }),
        0x0fa => Some(TokenDef {
            len: 0,
            rules: &["WIDTH {1}, {0}"],
        }),
        0x0fb => Some(TokenDef {
            len: 0,
            rules: &["WINDOW ({3}, {2})-({1}, {0})"],
        }),
        0x0fc => Some(TokenDef {
            len: 0,
            rules: &["WINDOW"],
        }),
        0x0fd => Some(TokenDef {
            len: 0,
            rules: &["WINDOW SCREEN ({3}, {2})-({1}, {0})"],
        }),
        0x0fe => Some(TokenDef {
            len: 0,
            rules: &["prnmod::=WRITE"],
        }),
        0x0ff => Some(TokenDef {
            len: 0,
            rules: &["prnuse::=USING {0};"],
        }),
        0x100 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} + {0}"],
        }),
        0x101 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} AND {0}"],
        }),
        0x102 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} / {0}"],
        }),
        0x103 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} = {0}"],
        }),
        0x104 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} EQV {0}"],
        }),
        0x105 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ABS({0})"],
        }),
        0x106 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ASC({0})"],
        }),
        0x107 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ATN({0})"],
        }),
        0x108 => Some(TokenDef {
            len: 0,
            rules: &["expr::=C{#type-abbr}({0})"],
        }),
        0x109 => Some(TokenDef {
            len: 0,
            rules: &["expr::=CHR$({0})"],
        }),
        0x10a => Some(TokenDef {
            len: 0,
            rules: &["expr::=COMMAND$"],
        }),
        0x10b => Some(TokenDef {
            len: 0,
            rules: &["expr::=COS({0})"],
        }),
        0x10c => Some(TokenDef {
            len: 0,
            rules: &["expr::=CSRLIN"],
        }),
        0x10d => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVD({0})"],
        }),
        0x10e => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVDMBD({0})"],
        }),
        0x10f => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVI({0})"],
        }),
        0x110 => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVL({0})"],
        }),
        0x111 => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVS({0})"],
        }),
        0x112 => Some(TokenDef {
            len: 0,
            rules: &["expr::=CVSMBF({0})"],
        }),
        0x113 => Some(TokenDef {
            len: 0,
            rules: &["expr::=DATE$"],
        }),
        0x114 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ENVIRON$({0})"],
        }),
        0x115 => Some(TokenDef {
            len: 0,
            rules: &["expr::=EOF({0})"],
        }),
        0x116 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERDEV"],
        }),
        0x117 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERDEV$"],
        }),
        0x118 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERL"],
        }),
        0x119 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERR"],
        }),
        0x11a => Some(TokenDef {
            len: 0,
            rules: &["expr::=EXP({0})"],
        }),
        0x11b => Some(TokenDef {
            len: 0,
            rules: &["expr::=FILEATTR({1}, {0})"],
        }),
        0x11c => Some(TokenDef {
            len: 0,
            rules: &["expr::=FIX({0})"],
        }),
        0x11d => Some(TokenDef {
            len: 0,
            rules: &["expr::=FRE({0})"],
        }),
        0x11e => Some(TokenDef {
            len: 0,
            rules: &["expr::=FREEFILE"],
        }),
        0x11f => Some(TokenDef {
            len: 0,
            rules: &["expr::=HEX$({0})"],
        }),
        0x120 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INKEY$"],
        }),
        0x121 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INP({0})"],
        }),
        0x122 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INPUT$({0})"],
        }),
        0x123 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INPUT$({1}, {0})"],
        }),
        0x124 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INSTR({1}, {0})"],
        }),
        0x125 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INSTR({2}, {1}, {0})"],
        }),
        0x126 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INT({0})"],
        }),
        0x127 => Some(TokenDef {
            len: 0,
            rules: &["expr::=IOCTL$({0})"],
        }),
        0x128 => Some(TokenDef {
            len: 0,
            rules: &["expr::=LBOUND({0})"],
        }),
        0x129 => Some(TokenDef {
            len: 0,
            rules: &["expr::=LBOUND({1}, {0})"],
        }),
        0x12a => Some(TokenDef {
            len: 0,
            rules: &["expr::=LCASE$({0})"],
        }),
        0x12b => Some(TokenDef {
            len: 0,
            rules: &["expr::=LTRIM$({0})"],
        }),
        0x12c => Some(TokenDef {
            len: 0,
            rules: &["expr::=LEFT$({1}, {0})"],
        }),
        0x12d => Some(TokenDef {
            len: 2,
            rules: &["expr::=LEN({0})"],
        }),
        0x12e => Some(TokenDef {
            len: 0,
            rules: &["expr::=LOC({0})"],
        }),
        0x12f => Some(TokenDef {
            len: 0,
            rules: &["expr::=LOF({0})"],
        }),
        0x130 => Some(TokenDef {
            len: 0,
            rules: &["expr::=LOG({0})"],
        }),
        0x131 => Some(TokenDef {
            len: 0,
            rules: &["expr::=LPOS({0})"],
        }),
        0x132 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MID$({1}, {0})"],
        }),
        0x133 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MID$({2}, {1}, {0})"],
        }),
        0x134 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKD$({0})"],
        }),
        0x135 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKDMBF$({0})"],
        }),
        0x136 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKI$({0})"],
        }),
        0x137 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKL$({0})"],
        }),
        0x138 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKS$({expr:0})", "{0}"],
        }),
        0x139 => Some(TokenDef {
            len: 0,
            rules: &["expr::=MKSMBF({0})"],
        }),
        0x13a => Some(TokenDef {
            len: 0,
            rules: &["expr::=OCT$({0})"],
        }),
        0x13b => Some(TokenDef {
            len: 0,
            rules: &["expr::=PEEK({0})"],
        }),
        0x13c => Some(TokenDef {
            len: 0,
            rules: &["expr::=PEN"],
        }),
        0x13d => Some(TokenDef {
            len: 0,
            rules: &["expr::=PLAY"],
        }),
        0x13e => Some(TokenDef {
            len: 0,
            rules: &["expr::=PMAP({1}, {0})"],
        }),
        0x13f => Some(TokenDef {
            len: 0,
            rules: &["expr::=POINT({0})"],
        }),
        0x140 => Some(TokenDef {
            len: 0,
            rules: &["expr::=POINT({1}, {0})"],
        }),
        0x141 => Some(TokenDef {
            len: 0,
            rules: &["expr::=POS({0})"],
        }),
        0x142 => Some(TokenDef {
            len: 0,
            rules: &["expr::=RIGHT$({1}, {0})"],
        }),
        0x143 => Some(TokenDef {
            len: 0,
            rules: &["expr::=RND"],
        }),
        0x144 => Some(TokenDef {
            len: 0,
            rules: &["expr::=RND({0})"],
        }),
        0x145 => Some(TokenDef {
            len: 0,
            rules: &["expr::=RTRIM$({0})"],
        }),
        0x146 => Some(TokenDef {
            len: 0,
            rules: &["expr::=SADD({0})"],
        }),
        0x147 => Some(TokenDef {
            len: 0,
            rules: &["expr::=SCREEN({1}, {0})"],
        }),
        0x148 => Some(TokenDef {
            len: 0,
            rules: &["expr::=SCREEN({2}, {1}, {0})"],
        }),
        0x149 => Some(TokenDef {
            len: 0,
            rules: &["expr::=SEEK({0})"],
        }),
        0x14a => Some(TokenDef {
            len: 0,
            rules: &["expr::=SETMEM({0})"],
        }),
        0x14b => Some(TokenDef {
            len: 0,
            rules: &["expr::=SGN({0})"],
        }),
        0x14c => Some(TokenDef {
            len: 0,
            rules: &["expr::=SHELL({0})"],
        }),
        0x14d => Some(TokenDef {
            len: 0,
            rules: &["expr::=SIN({0})"],
        }),
        0x14e => Some(TokenDef {
            len: 0,
            rules: &["expr::=SPACE$({0})"],
        }),
        0x14f => Some(TokenDef {
            len: 0,
            rules: &["expr::=SQR({0})"],
        }),
        0x150 => Some(TokenDef {
            len: 0,
            rules: &["expr::=STICK({0})"],
        }),
        0x151 => Some(TokenDef {
            len: 0,
            rules: &["expr::=STR$({0})"],
        }),
        0x152 => Some(TokenDef {
            len: 0,
            rules: &["expr::=STRIG({0})"],
        }),
        0x153 => Some(TokenDef {
            len: 0,
            rules: &["expr::=STRING$({1}, {0})"],
        }),
        0x154 => Some(TokenDef {
            len: 0,
            rules: &["expr::=TAN({0})"],
        }),
        0x155 => Some(TokenDef {
            len: 0,
            rules: &["expr::=TIME$"],
        }),
        0x156 => Some(TokenDef {
            len: 0,
            rules: &["expr::=TIMER"],
        }),
        0x157 => Some(TokenDef {
            len: 0,
            rules: &["expr::=UBOUND({0})"],
        }),
        0x158 => Some(TokenDef {
            len: 0,
            rules: &["expr::=UBOUND({1}, {0})"],
        }),
        0x159 => Some(TokenDef {
            len: 0,
            rules: &["expr::=UCASE$({0})"],
        }),
        0x15a => Some(TokenDef {
            len: 0,
            rules: &["expr::=VAL({0})"],
        }),
        0x15b => Some(TokenDef {
            len: 0,
            rules: &["expr::=VARPTR({0})"],
        }),
        0x15c => Some(TokenDef {
            len: 2,
            rules: &["expr::=VARPTR$({0})"],
        }),
        0x15d => Some(TokenDef {
            len: 0,
            rules: &["expr::=VARSEG({0})"],
        }),
        0x15e => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} >= {0}"],
        }),
        0x15f => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} > {0}"],
        }),
        0x160 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} \\ {0}"],
        }),
        0x161 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} IMP {0}"],
        }),
        0x162 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} <= {0}"],
        }),
        0x163 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} < {0}"],
        }),
        0x164 => Some(TokenDef {
            len: 0,
            rules: &["expr::={#hprm}"],
        }),
        0x165 => Some(TokenDef {
            len: 2,
            rules: &["expr::={#int}"],
        }),
        0x166 => Some(TokenDef {
            len: 4,
            rules: &["expr::={#lng}"],
        }),
        0x167 => Some(TokenDef {
            len: 2,
            rules: &["expr::={#int&h}"],
        }),
        0x168 => Some(TokenDef {
            len: 4,
            rules: &["expr::={#lng&h}"],
        }),
        0x169 => Some(TokenDef {
            len: 2,
            rules: &["expr::={#int&o}"],
        }),
        0x16a => Some(TokenDef {
            len: 4,
            rules: &["expr::={#lng&o}"],
        }),
        0x16b => Some(TokenDef {
            len: 4,
            rules: &["expr::={#sng}"],
        }),
        0x16c => Some(TokenDef {
            len: 8,
            rules: &["expr::={#dbl}"],
        }),
        0x16d => Some(TokenDef {
            len: -1,
            rules: &["expr::={#qstr}"],
        }),
        0x16e => Some(TokenDef {
            len: 0,
            rules: &["expr::=({0})"],
        }),
        0x16f => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} MOD {0}"],
        }),
        0x170 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} * {0}"],
        }),
        0x171 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} <> {0}"],
        }),
        0x172 => Some(TokenDef {
            len: 0,
            rules: &["expr::={#nul}"],
        }),
        0x173 => Some(TokenDef {
            len: 0,
            rules: &["nularg::={#nul}"],
        }),
        0x174 => Some(TokenDef {
            len: 0,
            rules: &["expr::=NOT {0}"],
        }),
        0x175 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} OR {0}"],
        }),
        0x176 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} ^ {0}"],
        }),
        0x177 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} - {0}"],
        }),
        0x178 => Some(TokenDef {
            len: 0,
            rules: &["expr::=-{0}"],
        }),
        0x179 => Some(TokenDef {
            len: 0,
            rules: &["expr::={1} XOR {0}"],
        }),
        0x17a => Some(TokenDef {
            len: 0,
            rules: &["UEVENT"],
        }),
        0x17b => Some(TokenDef {
            len: 0,
            rules: &["SLEEP {0}"],
        }),
        0x17c => Some(TokenDef {
            len: 6,
            rules: &["astype::={#tabi:4}AS STRING * {#int:2}"],
        }),
        0x17d => Some(TokenDef {
            len: 2,
            rules: &["decl::=DIM {declmod:0}", "decl::=DIM"],
        }),
        0x181 => Some(TokenDef {
            len: 0,
            rules: &["expr::=CURDIR$"],
        }),
        0x183 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERROR$"],
        }),
        0x184 => Some(TokenDef {
            len: 0,
            rules: &["expr::=DIR$({0})"],
        }),
        0x186 => Some(TokenDef {
            len: 0,
            rules: &["expr::=BOF({0})"],
        }),
        0x188 => Some(TokenDef {
            len: 0,
            rules: &["expr::=GETINDEX$({0})"],
        }),
        0x194 => Some(TokenDef {
            len: 0,
            rules: &["expr::={0}"],
        }),
        0x192 => Some(TokenDef {
            len: 0,
            rules: &["DELETE {0}"],
        }),
        0x197 => Some(TokenDef {
            len: 0,
            rules: &["INSERT {1}, {0}"],
        }),
        0x198 => Some(TokenDef {
            len: 2,
            rules: &[
                "{#method:0}MOVEFIRST {0}",
                "{#method:4}MOVELAST {0}",
                "{#method:8}MOVENEXT {0}",
                "{#method:12}MOVEPREVIOUS {0}",
            ],
        }),
        0x199 => Some(TokenDef {
            len: 2,
            rules: &["ON LOCAL ERROR GOTO {#id}"],
        }),
        0x19b => Some(TokenDef {
            len: 2,
            rules: &["RETRIEVE {1}, {0}"],
        }),
        0x19f => Some(TokenDef {
            len: 0,
            rules: &["SEEKGT {1}, {0}"],
        }),
        0x1a1 => Some(TokenDef {
            len: 0,
            rules: &["SETINDEX {1}, {0}"],
        }),
        0x1a8 => Some(TokenDef {
            len: 0,
            rules: &["REDIM {0}"],
        }),
        0x1a9 => Some(TokenDef {
            len: 0,
            rules: &["implicit-control::="],
        }),
        0x1aa => Some(TokenDef {
            len: 2,
            rules: &["expr::=TYPEOF {0} IS {#id}"],
        }),
        0x1ab => Some(TokenDef {
            len: 0,
            rules: &["LOAD {0}"],
        }),
        0x1ac => Some(TokenDef {
            len: 0,
            rules: &["UNLOAD {0}"],
        }),
        0x1ad => Some(TokenDef {
            len: 0,
            rules: &["expr::=DOEVENTS"],
        }),
        0x1b0 => Some(TokenDef {
            len: 0,
            rules: &["expr::=ERROR$"],
        }),
        0x1b2 => Some(TokenDef {
            len: 0,
            rules: &["expr::=STR$({expr:0})"],
        }),
        0x1b3 => Some(TokenDef {
            len: 0,
            rules: &["expr::=FORMAT$({1}, {0})"],
        }),
        0x1ba => Some(TokenDef {
            len: 0,
            rules: &["expr::=DATE$"],
        }),
        0x1bc => Some(TokenDef {
            len: 0,
            rules: &["expr::=TIMEVALUE({0})"],
        }),
        0x1c0 => Some(TokenDef {
            len: 2,
            rules: &[""],
        }),
        0x1c1 => Some(TokenDef {
            len: 0,
            rules: &["expr::={0}"],
        }),
        0x1c2 => Some(TokenDef {
            len: -1,
            rules: &[""],
        }),
        0x1c3 => Some(TokenDef {
            len: 0,
            rules: &["expr::=INPUTBOX$({2}, {1}, {0})"],
        }),
        0x1c5 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x1c6 => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        0x1c7 | 0x1c8 => Some(TokenDef {
            len: 0,
            rules: &["nularg::="],
        }),
        0x1c9 => Some(TokenDef {
            len: 2,
            rules: &[
                "controlstmt::={#method:1}{0}.CLS",
                "controlstmt::={#method:2}{implicit-control:0}HIDE",
                "controlstmt::={#method:2}{0}.HIDE",
                "prnmod::={#method:4}{0}.PRINT",
                "controlstmt::={#method:6}{0}.REFRESH",
                "controlstmt::={#method:8}{0}.SETFOCUS",
                "controlstmt::={#method:9}{implicit-control:0}SHOW",
                "controlstmt::={#method:9}{0}.SHOW",
                "controlstmt::={#method:14}{0}.ENDDOC",
                "controlstmt::={#method:16}{0}.NEWPAGE",
            ],
        }),
        0x1ca => Some(TokenDef {
            len: 2,
            rules: &["{1}.ADDITEM {0}"],
        }),
        0x1cb => Some(TokenDef {
            len: 2,
            rules: &["{1}.REMOVEITEM {0}"],
        }),
        0x1cc => Some(TokenDef {
            len: 2,
            rules: &["{2}.MOVE {1}, {0}"],
        }),
        0x1ce => Some(TokenDef {
            len: 4,
            rules: &["{4}.MOVE {3}, {2}, {1}, {0}"],
        }),
        0x1d0 => Some(TokenDef {
            len: 2,
            rules: &["expr::={0}.GETTEXT"],
        }),
        0x3ff => Some(TokenDef {
            len: 0,
            rules: &[""],
        }),
        _ => None,
    }
}

#[derive(Clone)]
struct Item {
    kind: String,
    text: String,
}

struct Decoder<'a> {
    file: &'a str,
    sym: &'a [u8],
    code: &'a [u8],
    base: usize,
    at: usize,
    pcode: u16,
    hparam: u16,
    stack: Vec<Item>,
    pending: String,
    lines: Vec<String>,
    procedure_type: &'static str,
    modified_defaults: u32,
    return_name: Option<String>,
    return_suffix: &'static str,
}

impl<'a> Decoder<'a> {
    fn new(file: &'a str, sym: &'a [u8], code: &'a [u8], base: usize) -> Self {
        Self {
            file,
            sym,
            code,
            base,
            at: 0,
            pcode: 0,
            hparam: 0,
            stack: Vec::new(),
            pending: String::new(),
            lines: Vec::new(),
            procedure_type: "SUB",
            modified_defaults: 0,
            return_name: None,
            return_suffix: "",
        }
    }

    fn fail(&self, at: usize, name: impl Into<String>, message: impl Into<String>) -> FormError {
        error(self.file, self.base + at, name, message)
    }

    fn ident(&self, offset: u16) -> Result<String, FormError> {
        if matches!(offset, u16::MAX | 0xfffe) {
            return Ok("0".into());
        }
        let at = offset as usize;
        let header = self.sym.get(at..at + 4).ok_or_else(|| {
            self.fail(
                self.at,
                format!("Symbol 0x{offset:04x}"),
                "ungueltiger Symbolverweis",
            )
        })?;
        if header[2] & 2 != 0 {
            return read_u16(self.sym, at + 4)
                .map(|value| value.to_string())
                .ok_or_else(|| self.fail(self.at, "Symboltabelle", "abgeschnittene Zeilennummer"));
        }
        let len = header[3] as usize;
        self.sym
            .get(at + 4..at + 4 + len)
            .map(decode_cp437)
            .ok_or_else(|| self.fail(self.at, "Symboltabelle", "abgeschnittener Name"))
    }

    fn suffix(kind: u16) -> &'static str {
        match kind {
            1 => "%",
            2 => "&",
            3 => "!",
            4 => "#",
            5 => "@",
            7 => "$",
            _ => "",
        }
    }

    fn type_name(&self, kind: u16) -> Result<String, FormError> {
        Ok(match kind {
            0 => "ANY".into(),
            1 => "INTEGER".into(),
            2 => "LONG".into(),
            3 => "SINGLE".into(),
            4 => "DOUBLE".into(),
            5 => "CURRENCY".into(),
            7 => "STRING".into(),
            9 => "CONTROL".into(),
            // VBDOS verwendet fuer eingebaute Typen zusaetzlich die
            // internen Kennungen des Editors.
            0x013e => "STRING".into(),
            other => self.ident(other)?,
        })
    }

    fn peek(&self, offset: usize) -> Option<&Item> {
        self.stack.get(self.stack.len().checked_sub(offset + 1)?)
    }

    fn split_tag<'b>(&self, tag: &'b str) -> (&'b str, isize, bool) {
        if let Some(value) = tag.strip_prefix('$') {
            return ("", value.parse().unwrap_or(0), true);
        }
        if let Some((kind, value)) = tag.rsplit_once(':') {
            let relative = value.starts_with('$');
            let value = value.strip_prefix('$').unwrap_or(value);
            return (kind, value.parse().unwrap_or(0), relative);
        }
        if let Ok(offset) = tag.parse() {
            ("", offset, false)
        } else {
            (tag, 0, false)
        }
    }

    fn valid(&self, kind: &str, offset: isize, own: &str) -> bool {
        let Some(item) = usize::try_from(offset)
            .ok()
            .and_then(|offset| self.peek(offset))
        else {
            return kind.is_empty();
        };
        kind.is_empty()
            || kind.split('|').any(|expected| {
                expected == "*" || item.kind == if expected == "self" { own } else { expected }
            })
    }

    fn word(data: &[u8], at: usize) -> Option<u16> {
        read_u16(data, at)
    }

    fn call(&self, data: &[u8], parentheses: bool) -> Result<String, FormError> {
        let count = Self::word(data, 0)
            .ok_or_else(|| self.fail(self.at, "CALL", "Argumentzahl fehlt"))?
            as usize;
        let args = (0..count)
            .rev()
            .map(|index| format!("{{{index}}}"))
            .collect::<Vec<_>>()
            .join(", ");
        Ok(if args.is_empty() {
            String::new()
        } else if parentheses {
            format!("({args})")
        } else {
            format!(" {args}")
        })
    }

    fn proc_decl(
        &mut self,
        data: &[u8],
        offset: usize,
        parentheses: bool,
    ) -> Result<String, FormError> {
        let id =
            Self::word(data, offset).ok_or_else(|| self.fail(self.at, "Prozedur", "Name fehlt"))?;
        let flags = Self::word(data, offset + 2)
            .ok_or_else(|| self.fail(self.at, "Prozedur", "Flags fehlen"))?;
        let count = Self::word(data, offset + 4)
            .ok_or_else(|| self.fail(self.at, "Prozedur", "Argumentzahl fehlt"))?
            as usize;
        let mut name = self.ident(id)?;
        let kind = match (flags & 0x300) >> 8 {
            1 => "SUB",
            2 => "FUNCTION",
            3 => "DEF",
            _ => "",
        };
        if !kind.is_empty() {
            self.procedure_type = kind;
            name = format!("{kind} {name}");
        }
        if flags & 0x80 != 0 {
            let suffix = Self::suffix(flags & 7);
            if self.pcode == 0x058 {
                self.return_name = Some(name.strip_prefix("FUNCTION ").unwrap_or(&name).into());
                self.return_suffix = suffix;
            }
            name.push_str(suffix);
        }
        let mut args = Vec::with_capacity(count);
        for index in 1..=count {
            let at = offset + 6 + (index - 1) * 8;
            let mut arg = self.ident(
                Self::word(data, at)
                    .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Name fehlt"))?,
            )?;
            let arg_flags = Self::word(data, at + 2)
                .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Flags fehlen"))?;
            let arg_type = Self::word(data, at + 4)
                .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Typ fehlt"))?;
            if arg_flags & 0x200 != 0 {
                arg.push_str(Self::suffix(arg_type));
            }
            if arg_flags & 0x400 != 0 {
                arg.push_str("()");
            }
            if arg_flags & 0x800 != 0 {
                arg = format!("SEG {arg}");
            }
            if arg_flags & 0x1000 != 0 {
                arg = format!("BYVAL {arg}");
            }
            if arg_flags & 0x2000 != 0 {
                let ty = self.type_name(arg_type)?;
                arg.push_str(&format!(" AS {ty}"));
            }
            args.push(arg);
        }
        if flags & 0x8000 != 0 {
            name.push_str(" CDECL");
        }
        let alias_len = ((flags >> 10) & 0x1f) as usize;
        let tail = offset + 6 + count * 8;
        if alias_len != 0 {
            name.push_str(" ALIAS ");
            name.push_str(&decode_cp437(
                data.get(tail..tail + alias_len)
                    .ok_or_else(|| self.fail(self.at, "ALIAS", "Name abgeschnitten"))?,
            ));
        }
        if parentheses || !args.is_empty() {
            name.push_str(" (");
            name.push_str(&args.join(", "));
            name.push(')');
        }
        Ok(name)
    }

    fn array(&self, data: &[u8], declaration: bool) -> Result<String, FormError> {
        let declaration =
            declaration || read_u16(self.code, self.at).is_some_and(|word| word & 0x3ff == 0x1a8);
        let count = Self::word(data, 0)
            .ok_or_else(|| self.fail(self.at, "Feld", "Dimension fehlt"))?
            as usize;
        let mut name = self
            .ident(Self::word(data, 2).ok_or_else(|| self.fail(self.at, "Feld", "Name fehlt"))?)?;
        name.push_str(Self::suffix(self.hparam));
        let mut stack_offset =
            usize::from(declaration && self.peek(0).is_some_and(|item| item.kind == "astype"));
        if count != 0 {
            let mut dimensions = String::new();
            if declaration {
                let mut remaining = count;
                while remaining != 0 {
                    remaining -= 1;
                    dimensions.insert_str(0, &format!("{{{stack_offset}}}"));
                    stack_offset += 1;
                    if remaining & 1 != 0 {
                        if self.peek(stack_offset).is_none_or(|item| item.kind != "24") {
                            dimensions.insert_str(0, " TO ");
                        }
                    } else if remaining != 0 {
                        dimensions.insert_str(0, ", ");
                    }
                }
            } else {
                dimensions = (0..count)
                    .rev()
                    .map(|offset| format!("{{{offset}}}"))
                    .collect::<Vec<_>>()
                    .join(", ");
                stack_offset = count;
            }
            name.push('(');
            name.push_str(&dimensions);
            name.push(')');
        } else if !declaration {
            name.push_str("()");
        }
        if declaration && self.peek(0).is_some_and(|item| item.kind == "astype") {
            name.push_str("{0}");
        }
        if declaration {
            if self
                .peek(stack_offset)
                .is_some_and(|item| item.kind == "decls")
            {
                return Ok(format!("{{{stack_offset}}}, {name}"));
            }
            if self
                .peek(stack_offset)
                .is_some_and(|item| item.kind == "decl")
            {
                return Ok(format!("{{{stack_offset}}} {name}"));
            }
        }
        Ok(name)
    }

    fn varargs(&self, data: &[u8]) -> Result<String, FormError> {
        let count = Self::word(data, 0)
            .ok_or_else(|| self.fail(self.at, "Argumentliste", "Anzahl fehlt"))?
            as usize;
        let args = (0..count)
            .rev()
            .map(|index| format!("{{{index}}}"))
            .collect::<Vec<_>>()
            .join(", ");
        Ok(if args.is_empty() {
            args
        } else {
            format!(" {args}")
        })
    }

    fn data_tag(&mut self, tag: &str, offset: usize, data: &[u8]) -> Result<String, FormError> {
        let word = || {
            Self::word(data, offset)
                .ok_or_else(|| self.fail(self.at, tag, "Tokendaten abgeschnitten"))
        };
        Ok(match tag.to_ascii_lowercase().as_str() {
            "" | "nul" | "thaddr" | "optargs" => String::new(),
            "newline" => "\r\u{1}".into(),
            "newline-include" => "\r\u{2}".into(),
            "tabh" => format!(
                "\r\0{}{}",
                char::from(self.hparam as u8),
                char::from((self.hparam >> 8) as u8)
            ),
            "tabi" => {
                let value = word()?;
                format!(
                    "\r\0{}{}",
                    char::from(value as u8),
                    char::from((value >> 8) as u8)
                )
            }
            "indent" => " ".repeat(word()? as usize),
            "raw" => decode_pcode_bytes(data.get(offset..).unwrap_or_default()),
            "cstr" => decode_pcode_bytes(
                data.get(offset..)
                    .unwrap_or_default()
                    .split(|byte| *byte == 0)
                    .next()
                    .unwrap_or_default(),
            ),
            "qstr" => format!(
                "\"{}\"",
                decode_pcode_bytes(data.get(offset..).unwrap_or_default())
            ),
            "id" => self.ident(word()?)?,
            "id+" => {
                let name = self.ident(word()?)?;
                let suffix = Self::suffix(self.hparam);
                format!(
                    "{name}{}",
                    if suffix.is_empty() && self.return_name.as_deref() == Some(&name) {
                        self.return_suffix
                    } else {
                        suffix
                    }
                )
            }
            "id-list" => data
                .get(offset..)
                .unwrap_or_default()
                .chunks_exact(2)
                .map(|bytes| self.ident(u16::from_le_bytes([bytes[0], bytes[1]])))
                .collect::<Result<Vec<_>, _>>()?
                .join(", "),
            "type" => self.type_name(word()?)?,
            "type-abbr" => ["", "INT", "LNG", "SNG", "DBL", "STR"]
                .get(self.hparam as usize)
                .copied()
                .unwrap_or("")
                .into(),
            "hprm" => self.hparam.to_string(),
            "int" => (word()? as i16).to_string(),
            "int&h" => format!("&H{:X}", word()?),
            "int&o" => format!("&O{:o}", word()?),
            "lng" => i32::from_le_bytes(
                data.get(offset..offset + 4)
                    .ok_or_else(|| self.fail(self.at, tag, "LONG abgeschnitten"))?
                    .try_into()
                    .unwrap(),
            )
            .to_string(),
            "lng&h" => format!(
                "&H{:X}",
                u32::from_le_bytes(
                    data.get(offset..offset + 4)
                        .ok_or_else(|| self.fail(self.at, tag, "LONG abgeschnitten"))?
                        .try_into()
                        .unwrap()
                )
            ),
            "sng" => f32::from_le_bytes(
                data.get(offset..offset + 4)
                    .ok_or_else(|| self.fail(self.at, tag, "SINGLE abgeschnitten"))?
                    .try_into()
                    .unwrap(),
            )
            .to_string(),
            "dbl" => f64::from_le_bytes(
                data.get(offset..offset + 8)
                    .ok_or_else(|| self.fail(self.at, tag, "DOUBLE abgeschnitten"))?
                    .try_into()
                    .unwrap(),
            )
            .to_string(),
            "label" => {
                let name = self.ident(word()?)?;
                if name.bytes().all(|byte| byte.is_ascii_digit()) {
                    format!("{name} ")
                } else {
                    format!("{name}:")
                }
            }
            "call" => self.call(data, false)?,
            "call()" => self.call(data, true)?,
            "procdecl" => self.proc_decl(data, offset, false)?,
            "procdecl()" => self.proc_decl(data, offset, true)?,
            "id(expr)" => self.array(data, false)?,
            "id(decl)" => self.array(data, true)?,
            "varargs" => self.varargs(data)?,
            "proctype" => self.procedure_type.into(),
            "blockname" => {
                let id = word()?;
                if id == u16::MAX {
                    String::new()
                } else {
                    format!(" /{}/", self.ident(id)?)
                }
            }
            "action-verb" => {
                ["OR", "AND", "PRESET", "PSET", "XOR"][usize::from(word()?.min(4))].into()
            }
            "open-args" => {
                let flags = word()?;
                [
                    match flags & 0x3f {
                        1 => "FOR INPUT",
                        2 => "FOR OUTPUT",
                        4 => "FOR RANDOM",
                        8 => "FOR APPEND",
                        32 => "FOR BINARY",
                        _ => "",
                    },
                    match (flags >> 8) & 3 {
                        1 => "ACCESS READ",
                        2 => "ACCESS WRITE",
                        3 => "ACCESS READ WRITE",
                        _ => "",
                    },
                    match (flags >> 12) & 7 {
                        1 => "LOCK READ WRITE",
                        2 => "LOCK WRITE",
                        3 => "LOCK READ",
                        4 => "SHARED",
                        _ => "",
                    },
                ]
                .into_iter()
                .filter(|part| !part.is_empty())
                .collect::<Vec<_>>()
                .join(" ")
            }
            "keymode" => match word()? {
                1 => "ON",
                2 => "LIST",
                _ => "OFF",
            }
            .into(),
            // Die im Corpus verwendeten Varianten haben genau diese Form;
            // die Stack-Platzhalter werden anschliessend normal aufgeloest.
            "line-args" | "lock-args" => "{0}".into(),
            "circle-args" => "{1}, {0}".into(),
            "input-args" => String::new(),
            "defxxx" => {
                let raw = data
                    .get(offset + 2..offset + 6)
                    .ok_or_else(|| self.fail(self.at, "DEF", "Tokendaten abgeschnitten"))?;
                let mask = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
                let kind = match mask & 7 {
                    1 => "INT",
                    2 => "LNG",
                    3 => "SNG",
                    4 => "DBL",
                    5 => "STR",
                    _ => "",
                };
                let letters = mask & 0xffff_ffc0;
                if mask & 7 == 3 {
                    self.modified_defaults &= !letters;
                } else {
                    self.modified_defaults |= letters;
                }
                format!("DEF{kind} {}", letter_ranges(letters))
            }
            other => {
                return Err(self.fail(
                    self.at,
                    format!("Token-Tag {other}"),
                    "unbekannte P-Code-Struktur",
                ))
            }
        })
    }

    fn execute(&mut self, mut rule: String, data: &[u8]) -> Result<bool, FormError> {
        let mut kind = self.pcode.to_string();
        if let Some((left, right)) = rule.split_once("::=") {
            kind = left.into();
            rule = right.into();
        }
        if rule.starts_with('.') {
            self.pending
                .extend(self.stack.drain(..).map(|item| item.text));
            rule.remove(0);
        }
        let initial = self.stack.len();
        let mut deepest = None::<usize>;
        let mut relative = Vec::<(String, String, isize)>::new();
        let mut out = String::new();
        let mut cursor = 0;
        while let Some(open) = rule[cursor..].find('{').map(|value| cursor + value) {
            let Some(close) = rule[open + 1..].find('}').map(|value| open + 1 + value) else {
                return Err(self.fail(self.at, "Tokenregel", "offene Ersetzung"));
            };
            out.push_str(&rule[cursor..open]);
            let tag = &rule[open + 1..close];
            if let Some(tag) = tag.strip_prefix("##") {
                let (tag, offset, _) = self.split_tag(tag);
                let replacement = self.data_tag(tag, offset.max(0) as usize, data)?;
                rule.replace_range(open..=close, &replacement);
                cursor = open;
                continue;
            }
            if let Some(tag) = tag.strip_prefix('#') {
                let (tag, offset, _) = self.split_tag(tag);
                if tag == "method" {
                    if Self::word(data, 0) != Some(offset as u16) {
                        return Ok(false);
                    }
                    cursor = close + 1;
                    continue;
                }
                out.push_str(&self.data_tag(tag, offset.max(0) as usize, data)?);
            } else {
                let (expected, offset, is_relative) = self.split_tag(tag);
                if is_relative {
                    let marker = format!("\u{b}{}\u{c}", relative.len());
                    out.push_str(&marker);
                    relative.push((marker, expected.into(), offset));
                } else {
                    if !self.valid(expected, offset, &kind) {
                        return Ok(false);
                    }
                    if let Some(item) = usize::try_from(offset)
                        .ok()
                        .and_then(|offset| self.peek(offset))
                    {
                        out.push_str(&item.text);
                    }
                    deepest = deepest.max(usize::try_from(offset).ok());
                }
            }
            cursor = close + 1;
        }
        out.push_str(&rule[cursor..]);
        let mut consume = deepest.map_or(0, |offset| offset + 1);
        for (marker, expected, offset) in relative {
            let actual = consume as isize + offset;
            if !self.valid(&expected, actual, &kind) {
                return Ok(false);
            }
            let text = self
                .peek(actual as usize)
                .map(|item| item.text.as_str())
                .unwrap_or("");
            out = out.replace(&marker, text);
            consume = consume.max(actual as usize + 1);
        }
        self.stack.truncate(initial.saturating_sub(consume));
        self.stack.push(Item { kind, text: out });
        Ok(true)
    }

    fn postprocess(&mut self, final_block: bool) {
        while let Some(marker) = self.pending.find('\r') {
            let kind = self.pending.as_bytes().get(marker + 1).copied();
            if kind == Some(0) && marker + 3 < self.pending.len() {
                let bytes = self.pending.as_bytes();
                let column = u16::from_le_bytes([bytes[marker + 2], bytes[marker + 3]]) as usize;
                let spaces = column.saturating_sub(marker).max(1);
                self.pending
                    .replace_range(marker..marker + 4, &" ".repeat(spaces));
                continue;
            }
            if matches!(kind, Some(1 | 2)) {
                // Binär-P-Code enthält den Include-Inhalt bereits. Die
                // Herkunft bleibt Kommentar, keine erneut aktive Direktive.
                let line = self.pending[..marker].trim_end();
                let directive = line.trim_start().trim_start_matches('\'').trim_start();
                let line = if directive.to_ascii_uppercase().starts_with("$INCLUDE:") {
                    format!("' Expanded INCLUDE:{}", &directive[9..])
                } else {
                    line.to_owned()
                };
                self.lines.push(line);
                self.pending.drain(..marker + 2);
                continue;
            }
            if let (Some(count), Some(ch)) = (
                self.pending.as_bytes().get(marker + 1).copied(),
                self.pending.as_bytes().get(marker + 2).copied(),
            ) {
                self.pending.replace_range(
                    marker..marker + 3,
                    &char::from(ch).to_string().repeat(count as usize),
                );
                continue;
            }
            break;
        }
        if final_block && !self.pending.trim().is_empty() {
            self.lines.push(self.pending.trim_end().into());
            self.pending.clear();
        }
    }

    fn run(mut self) -> Result<(String, u32), FormError> {
        while self.at < self.code.len() {
            let token_at = self.at;
            let word = read_u16(self.code, self.at)
                .ok_or_else(|| self.fail(self.at, "P-Code", "Token abgeschnitten"))?;
            self.at += 2;
            self.pcode = word & 0x3ff;
            self.hparam = word >> 10;
            if self.pcode == 8 {
                break;
            }
            let def = token_def(self.pcode).ok_or_else(|| {
                self.fail(
                    token_at,
                    format!("Token 0x{:03x}", self.pcode),
                    "unbekannte P-Code-Struktur",
                )
            })?;
            let len = if def.len < 0 {
                let len = read_u16(self.code, self.at)
                    .ok_or_else(|| self.fail(self.at, "P-Code", "variable Laenge fehlt"))?
                    as usize;
                self.at += 2;
                len
            } else {
                def.len as usize
            };
            let data = self
                .code
                .get(self.at..self.at + len)
                .ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))?
                .to_owned();
            self.at += (len + 1) & !1;
            if matches!(self.pcode, 0x065 | 0x066) {
                let operands = usize::from(self.pcode == 0x066);
                let split = self.stack.len().saturating_sub(operands);
                if split > 0
                    && self.stack[split - 1].kind != "newline"
                    && !self.stack[split - 1].text.ends_with(" THEN ")
                    && !self.stack[split - 1].text.ends_with(" ELSE ")
                {
                    self.stack.insert(
                        split,
                        Item {
                            kind: "newline".into(),
                            text: "\r\u{1}".into(),
                        },
                    );
                }
            }
            if matches!(self.pcode, 0x1c5 | 0x1c6) {
                let first = self
                    .stack
                    .len()
                    .checked_sub(3)
                    .ok_or_else(|| self.fail(token_at, "MSGBOX", "Argumente fehlen"))?;
                if self.stack[first..]
                    .iter()
                    .any(|item| !matches!(item.kind.as_str(), "expr" | "nularg"))
                {
                    return Err(self.fail(token_at, "MSGBOX", "ungueltige Argumentstruktur"));
                }
                let mut args = self.stack.drain(first..).collect::<Vec<_>>();
                while args.last().is_some_and(|item| item.kind == "nularg") {
                    args.pop();
                }
                let call = format!(
                    "MSGBOX{}{}",
                    if self.pcode == 0x1c6 { "(" } else { " " },
                    args.iter()
                        .map(|item| item.text.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                ) + if self.pcode == 0x1c6 { ")" } else { "" };
                self.stack.push(Item {
                    kind: if self.pcode == 0x1c6 {
                        "expr"
                    } else {
                        "statement"
                    }
                    .into(),
                    text: call,
                });
                self.postprocess(false);
                continue;
            }
            let mut accepted = false;
            for rule in def.rules {
                if self.execute((*rule).into(), &data)? {
                    accepted = true;
                    break;
                }
            }
            if !accepted {
                let stack = self
                    .stack
                    .iter()
                    .map(|item| item.kind.as_str())
                    .collect::<Vec<_>>()
                    .join(", ");
                return Err(self.fail(
                    token_at,
                    format!("Token 0x{:03x}", self.pcode),
                    format!("keine passende P-Code-Regel; Stapel: {stack}"),
                ));
            }
            self.postprocess(false);
        }
        self.pending
            .extend(self.stack.drain(..).map(|item| item.text));
        self.postprocess(true);
        let output = if self.lines.is_empty() {
            String::new()
        } else {
            self.lines.join("\n") + "\n"
        };
        Ok((output, self.modified_defaults))
    }
}

type ModuleLayout = (usize, usize, Vec<(usize, usize)>);

fn candidate_layout(bytes: &[u8], base: usize) -> Option<ModuleLayout> {
    let symbol_len = usize::from(read_u16(bytes, base)?);
    let symbol_start = base + 2;
    let main_header = symbol_start.checked_add(symbol_len)?;
    let main_len = usize::from(read_u16(bytes, main_header)?);
    if main_len & 1 != 0 {
        return None;
    }
    let main_start = main_header + 2;
    let main_end = main_start.checked_add(main_len)?;
    if main_end + 16 > bytes.len()
        || main_len != 0 && read_u16(bytes, main_end - 2).map(|word| word & 0x3ff) != Some(8)
    {
        return None;
    }
    let mut blocks = vec![(main_start, main_end)];
    let mut at = main_end + 16;
    while at < bytes.len() {
        let name_len = usize::from(read_u16(bytes, at + 1)?);
        let name = bytes.get(at + 3..at + 3 + name_len)?;
        if bytes.get(at) != Some(&0)
            || !(1..=40).contains(&name_len)
            || !name.first().is_some_and(u8::is_ascii_alphabetic)
            || !name
                .iter()
                .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
        {
            return None;
        }
        let code_len = usize::from(read_u16(bytes, at + 6 + name_len)?);
        let start = at + 8 + name_len;
        let end = start.checked_add(code_len)?;
        if code_len & 1 != 0
            || end + 16 > bytes.len()
            || code_len != 0 && read_u16(bytes, end - 2).map(|word| word & 0x3ff) != Some(8)
        {
            return None;
        }
        blocks.push((start, end));
        at = end + 16;
    }
    (at == bytes.len()).then_some((symbol_start, main_header, blocks))
}

fn module_layout(bytes: &[u8], after_catalog: usize) -> Option<ModuleLayout> {
    for base in after_catalog..after_catalog.saturating_add(24).min(bytes.len()) {
        if let Some(layout) = candidate_layout(bytes, base) {
            return Some(layout);
        }
    }
    None
}

pub(super) fn decode(file: &str, bytes: &[u8], after_catalog: usize) -> Result<String, FormError> {
    let (symbol_start, symbol_end, blocks) = module_layout(bytes, after_catalog)
        .ok_or_else(|| error(file, after_catalog, "Codeteil", "unerkannte Struktur"))?;
    let sym = &bytes[symbol_start..symbol_end];
    let mut output = String::new();
    let mut pending_reset = 0;
    for (start, end) in blocks {
        let (block, block_reset) = Decoder::new(file, sym, &bytes[start..end], start).run()?;
        if block.is_empty() {
            pending_reset |= block_reset;
            continue;
        }
        if !output.is_empty() && !block.is_empty() {
            output.push('\n');
            if pending_reset != 0 {
                output.push_str("DEFSNG ");
                output.push_str(&letter_ranges(pending_reset));
                output.push('\n');
            }
        }
        output.push_str(&block);
        pending_reset = block_reset;
    }
    Ok(output)
}

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

    fn symbols(names: &[&str]) -> (Vec<u8>, Vec<u16>) {
        let mut table = Vec::new();
        let mut offsets = Vec::new();
        for name in names {
            offsets.push(table.len() as u16);
            table.extend_from_slice(&[0, 0, 0, name.len() as u8]);
            table.extend_from_slice(name.as_bytes());
        }
        (table, offsets)
    }

    #[test]
    fn embedded_include_is_a_comment_but_literals_remain_unchanged() {
        let mut decoder = Decoder::new("test.frm", &[], &[], 0);
        decoder.pending="  '$INCLUDE: 'embedded.bi'\r\u{1}DECLARE SUB Test()\r\u{2}PRINT \"$INCLUDE: 'literal.bi'\"\r\u{1}".into();
        decoder.postprocess(true);
        assert_eq!(
            decoder.lines,
            vec![
                "' Expanded INCLUDE: 'embedded.bi'",
                "DECLARE SUB Test()",
                "PRINT \"$INCLUDE: 'literal.bi'\""
            ]
        );
    }

    #[test]
    fn decodes_vbdos_declarations_without_losing_type_or_bounds() {
        let (sym, ids) = symbols(&["PrepVal", "StringToPrep", "Oldcontents"]);
        let mut decoder = Decoder::new("test.frm", &sym, &[], 0);
        let mut proc = Vec::new();
        for word in [ids[0], 0x02c7, 1, ids[1], 0x2000, 9, 0] {
            proc.extend_from_slice(&word.to_le_bytes());
        }
        decoder.pcode = 0x058;
        assert_eq!(
            decoder.proc_decl(&proc, 0, true).unwrap(),
            "FUNCTION PrepVal$ (StringToPrep AS CONTROL)"
        );
        decoder.hparam = 0;
        assert_eq!(
            decoder.data_tag("id+", 0, &ids[0].to_le_bytes()).unwrap(),
            "PrepVal$"
        );

        let mut decoder = Decoder::new("test.frm", &sym, &[], 0);
        decoder.stack = vec![
            Item {
                kind: "expr".into(),
                text: "0".into(),
            },
            Item {
                kind: "expr".into(),
                text: "6".into(),
            },
        ];
        let mut array = Vec::new();
        array.extend_from_slice(&2u16.to_le_bytes());
        array.extend_from_slice(&ids[2].to_le_bytes());
        assert_eq!(
            decoder.array(&array, true).unwrap(),
            "Oldcontents({1} TO {0})"
        );

        assert_eq!(
            decoder.array(&array, false).unwrap(),
            "Oldcontents({1}, {0})"
        );

        array[..2].copy_from_slice(&0u16.to_le_bytes());
        assert_eq!(decoder.array(&array, false).unwrap(), "Oldcontents()");
    }

    #[test]
    fn next_trennt_eine_vorangehende_anweisung_auch_ohne_newline_token() {
        let (sym, ids) = symbols(&["i"]);
        let mut code = Vec::new();
        code.extend_from_slice(&0u16.to_le_bytes());
        code.extend_from_slice(&0x00au16.to_le_bytes());
        code.extend_from_slice(&9u16.to_le_bytes());
        code.extend_from_slice(&[0, 0]);
        code.extend_from_slice(b"PRINT 1");
        code.push(0);
        code.extend_from_slice(&(0x0400u16 | 0x00b).to_le_bytes());
        code.extend_from_slice(&ids[0].to_le_bytes());
        code.extend_from_slice(&0x066u16.to_le_bytes());
        code.extend_from_slice(&[0xff, 0xff, 1, 0]);
        code.extend_from_slice(&0u16.to_le_bytes());
        code.extend_from_slice(&8u16.to_le_bytes());
        let (decoded, _) = Decoder::new("test.frm", &sym, &code, 0).run().unwrap();
        assert!(decoded.contains("PRINT 1\nNEXT i%\n"), "{decoded:?}");
    }

    #[test]
    fn decodes_defint_and_vbdos_isam_tokens() {
        let mut decoder = Decoder::new("test.frm", &[], &[], 0);
        assert_eq!(
            decoder
                .data_tag("defxxx", 0, &[0xff, 0xff, 0xc1, 0xff, 0xff, 0xff])
                .unwrap(),
            "DEFINT A-Z"
        );
        assert!(token_def(0x188)
            .unwrap()
            .rules
            .contains(&"expr::=GETINDEX$({0})"));
        assert!(token_def(0x181).unwrap().rules.contains(&"expr::=CURDIR$"));
        assert!(token_def(0x184)
            .unwrap()
            .rules
            .contains(&"expr::=DIR$({0})"));
        assert!(token_def(0x1c3)
            .unwrap()
            .rules
            .contains(&"expr::=INPUTBOX$({2}, {1}, {0})"));
        assert!(token_def(0x1a8).unwrap().rules.contains(&"REDIM {0}"));
    }

    #[test]
    fn resets_block_local_default_types_before_the_next_block() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&0u16.to_le_bytes());

        let first = [
            0x1b, 0x00, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0x00, 0x00, 0x08, 0x00,
        ];
        bytes.extend_from_slice(&(first.len() as u16).to_le_bytes());
        bytes.extend_from_slice(&first);
        bytes.extend_from_slice(&[0; 16]);

        let second = [
            0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, b'X', 0x00, 0x00, 0x00, 0x08, 0x00,
        ];
        bytes.extend_from_slice(&[0, 1, 0, b'P', 0, 0, 0]);
        bytes.extend_from_slice(&(second.len() as u16).to_le_bytes());
        bytes.extend_from_slice(&second);
        bytes.extend_from_slice(&[0; 16]);

        assert_eq!(
            decode("test.frm", &bytes, 0).unwrap(),
            "DEFINT A-Z\n\nDEFSNG A-Z\nX\n"
        );
    }
}
