//! Datei-E/A: `OPEN`/`CLOSE`, sequenzieller, wahlfreier und binärer Zugriff,
//! Recordpuffer mit Festtextfeldern, Positions- und Statusfunktionen sowie
//! die Dateisystem-Anweisungen.
//!
//! Feste Strings in Recordpuffern liegen als **UTF-32** (4 Bytes je Zeichen),
//! damit `STRING * n` unabhängig vom Inhalt genau `4 · n` Bytes belegt und
//! die Recordlänge konstant bleibt (Entscheidung 2026-09-02). Binärdateien
//! sind dadurch bewusst nicht mit denen des Vorbilds austauschbar.

use std::collections::BTreeMap;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};

use crate::errors::RuntimeError;

/// Zugriffsart einer offenen Datei.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Modus {
    Input,
    Output,
    Append,
    Random,
    Binary,
}

impl Modus {
    /// Aus der Kurzsyntax `OPEN "I", #1, …`.
    pub fn aus_text(s: &str) -> Option<Modus> {
        match s.trim().to_ascii_uppercase().chars().next()? {
            'I' => Some(Modus::Input),
            'O' => Some(Modus::Output),
            'A' => Some(Modus::Append),
            'R' => Some(Modus::Random),
            'B' => Some(Modus::Binary),
            _ => None,
        }
    }
}

/// Ein Feld des Recordpuffers (`FIELD`): die gebundene Variable und ihr
/// Bereich im Puffer.
#[derive(Debug, Clone)]
pub struct Feld {
    pub referenz: crate::value::VarRef,
    pub start: usize,
    pub laenge: usize,
}

/// Eine offene Datei.
#[derive(Debug)]
pub struct Datei {
    pub modus: Modus,
    pub pfad: PathBuf,
    datei: BufReader<File>,
    /// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
    pub reclen: usize,
    /// Recordpuffer für `FIELD`/`GET`/`PUT`.
    pub puffer: Vec<u8>,
    pub felder: Vec<Feld>,
    /// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
    /// jeweils 1-basiert.
    pub position: u64,
    /// Ein Komma kann auch vor EOF noch ein leeres Feld eröffnen.
    leeres_feld: bool,
}

impl Datei {
    fn oeffnen(pfad: &Path, modus: Modus, reclen: usize) -> Result<Datei, RuntimeError> {
        let mut opt = OpenOptions::new();
        match modus {
            Modus::Input => {
                opt.read(true);
            }
            Modus::Output => {
                opt.write(true).create(true).truncate(true);
            }
            Modus::Append => {
                opt.write(true).create(true).append(true);
            }
            Modus::Random | Modus::Binary => {
                opt.read(true).write(true).create(true);
            }
        }
        let datei = BufReader::new(opt.open(pfad).map_err(|e| fehler_am_pfad(e, pfad))?);
        let reclen = if modus == Modus::Random {
            reclen.max(1)
        } else {
            1
        };
        Ok(Datei {
            modus,
            pfad: pfad.to_path_buf(),
            datei,
            reclen,
            puffer: vec![b' '; reclen],
            felder: Vec::new(),
            position: 1,
            leeres_feld: false,
        })
    }

    /// Dateigröße in Bytes.
    pub fn laenge(&self) -> Result<u64, RuntimeError> {
        self.datei
            .get_ref()
            .metadata()
            .map(|m| m.len())
            .map_err(fehler_aus_io)
    }

    /// `EOF` — bei sequenziellem Lesen: nichts mehr im Puffer; sonst:
    /// Position hinter dem letzten Datensatz bzw. Byte.
    pub fn eof(&mut self) -> Result<bool, RuntimeError> {
        match self.modus {
            Modus::Input => {
                Ok(!self.leeres_feld && self.datei.fill_buf().map_err(fehler_aus_io)?.is_empty())
            }
            Modus::Random => Ok((self.position - 1) * self.reclen as u64 >= self.laenge()?),
            Modus::Binary => Ok(self.position > self.laenge()?),
            // Beim Schreiben ist das Ende immer erreicht.
            Modus::Output | Modus::Append => Ok(true),
        }
    }

    /// Text anhängen (`PRINT #`, `WRITE #`).
    pub fn schreiben(&mut self, text: &str) -> Result<(), RuntimeError> {
        if !matches!(self.modus, Modus::Output | Modus::Append) {
            return Err(RuntimeError(54)); // Bad file mode
        }
        self.datei
            .get_mut()
            .write_all(text.as_bytes())
            .map_err(fehler_aus_io)
    }

    /// `SEEK`: auch den Lesepuffer verwerfen; Position bleibt 1-basiert.
    pub fn position_setzen(&mut self, pos: u64) -> Result<(), RuntimeError> {
        if pos == 0 {
            return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
        }
        if self.modus == Modus::Input {
            self.datei
                .seek(SeekFrom::Start(pos - 1))
                .map_err(fehler_aus_io)?;
        }
        self.position = pos;
        self.leeres_feld = false;
        Ok(())
    }

    fn byte_lesen(&mut self) -> Result<Option<u8>, RuntimeError> {
        let mut b = [0];
        if self.datei.read(&mut b).map_err(fehler_aus_io)? == 0 {
            return Ok(None);
        }
        self.position += 1;
        self.leeres_feld = false;
        Ok(Some(b[0]))
    }

    fn crlf_beenden(&mut self, b: u8) -> Result<(), RuntimeError> {
        if b == b'\r' && self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'\n') {
            self.byte_lesen()?;
        }
        Ok(())
    }

    /// Eine Zeile ab dem gemeinsamen Cursor, ohne genau ein Zeilenende.
    pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
        if self.modus != Modus::Input {
            return Err(RuntimeError(54));
        }
        let mut roh = Vec::new();
        let mut gelesen = false;
        self.leeres_feld = false;
        while let Some(b) = self.byte_lesen()? {
            gelesen = true;
            if matches!(b, b'\r' | b'\n') {
                self.crlf_beenden(b)?;
                break;
            }
            roh.push(b);
        }
        if !gelesen {
            return Ok(None);
        }
        String::from_utf8(roh)
            .map(Some)
            .map_err(|_| RuntimeError(57))
    }

    /// Nächstes INPUT-#-Feld, ohne nachfolgende Felder oder Zeilen zu verbrauchen.
    pub fn feld_lesen(&mut self) -> Result<String, RuntimeError> {
        if self.modus != Modus::Input {
            return Err(RuntimeError(54));
        }
        let mut roh = Vec::new();
        let mut in_quote = false;
        let mut hatte_quote = false;
        let mut gelesen = std::mem::take(&mut self.leeres_feld);
        while let Some(b) = self.byte_lesen()? {
            gelesen = true;
            match b {
                b'"' if !hatte_quote && roh.iter().all(u8::is_ascii_whitespace) => {
                    roh.clear();
                    in_quote = true;
                    hatte_quote = true;
                }
                b'"' if in_quote => {
                    if self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'"') {
                        self.byte_lesen()?;
                        roh.push(b'"');
                    } else {
                        in_quote = false;
                    }
                }
                b',' if !in_quote => {
                    self.leeres_feld = true;
                    break;
                }
                b'\r' | b'\n' if !in_quote => {
                    self.crlf_beenden(b)?;
                    break;
                }
                b if hatte_quote && !in_quote && b.is_ascii_whitespace() => {}
                _ => roh.push(b),
            }
        }
        if !gelesen || in_quote {
            return Err(RuntimeError(62));
        }
        let text = String::from_utf8(roh).map_err(|_| RuntimeError(57))?;
        Ok(if hatte_quote {
            text
        } else {
            text.trim().to_owned()
        })
    }

    /// INPUT$: exakt n UTF-8-Codepoints; beschädigte Daten sind Fehler 57.
    pub fn zeichen_lesen(&mut self, n: usize) -> Result<String, RuntimeError> {
        if !matches!(self.modus, Modus::Input | Modus::Binary) {
            return Err(RuntimeError(54));
        }
        if self.modus == Modus::Binary {
            self.datei
                .seek(SeekFrom::Start(self.position - 1))
                .map_err(fehler_aus_io)?;
        }
        let mut text = String::new();
        for _ in 0..n {
            let mut bytes = [0; 4];
            bytes[0] = self.byte_lesen()?.ok_or(RuntimeError(62))?;
            let breite = match bytes[0] {
                0..=0x7f => 1,
                0xc2..=0xdf => 2,
                0xe0..=0xef => 3,
                0xf0..=0xf4 => 4,
                _ => return Err(RuntimeError(57)),
            };
            for b in &mut bytes[1..breite] {
                *b = self.byte_lesen()?.ok_or(RuntimeError(57))?;
            }
            text.push_str(std::str::from_utf8(&bytes[..breite]).map_err(|_| RuntimeError(57))?);
        }
        Ok(text)
    }

    /// Datensatz `nr` (1-basiert) in den Puffer lesen.
    pub fn record_lesen(&mut self, nr: u64) -> Result<(), RuntimeError> {
        let offset = (nr - 1) * self.reclen as u64;
        self.datei
            .seek(SeekFrom::Start(offset))
            .map_err(fehler_aus_io)?;
        self.puffer = vec![b' '; self.reclen];
        // Ein unvollständig gelesener Satz bleibt mit Leerzeichen gefüllt.
        let mut gelesen = 0;
        while gelesen < self.reclen {
            match self.datei.read(&mut self.puffer[gelesen..]) {
                Ok(0) => break,
                Ok(n) => gelesen += n,
                Err(e) => return Err(fehler_aus_io(e)),
            }
        }
        self.position = nr + 1;
        Ok(())
    }

    /// Puffer als Datensatz `nr` schreiben.
    pub fn record_schreiben(&mut self, nr: u64) -> Result<(), RuntimeError> {
        let offset = (nr - 1) * self.reclen as u64;
        self.datei
            .seek(SeekFrom::Start(offset))
            .map_err(fehler_aus_io)?;
        let puffer = std::mem::take(&mut self.puffer);
        self.datei
            .get_mut()
            .write_all(&puffer)
            .map_err(fehler_aus_io)?;
        self.puffer = puffer;
        self.position = nr + 1;
        Ok(())
    }

    /// `n` Bytes ab Position `pos` (1-basiert) lesen.
    pub fn bytes_lesen(&mut self, pos: u64, n: usize) -> Result<Vec<u8>, RuntimeError> {
        self.datei
            .seek(SeekFrom::Start(pos - 1))
            .map_err(fehler_aus_io)?;
        let mut buf = vec![0u8; n];
        let mut gelesen = 0;
        while gelesen < n {
            match self.datei.read(&mut buf[gelesen..]) {
                Ok(0) => break,
                Ok(k) => gelesen += k,
                Err(e) => return Err(fehler_aus_io(e)),
            }
        }
        if gelesen < n {
            return Err(RuntimeError(62)); // Input past end of file
        }
        self.position = pos + n as u64;
        Ok(buf)
    }

    /// Bytes ab Position `pos` (1-basiert) schreiben.
    pub fn bytes_schreiben(&mut self, pos: u64, daten: &[u8]) -> Result<(), RuntimeError> {
        self.datei
            .seek(SeekFrom::Start(pos - 1))
            .map_err(fehler_aus_io)?;
        self.datei
            .get_mut()
            .write_all(daten)
            .map_err(fehler_aus_io)?;
        self.position = pos + daten.len() as u64;
        Ok(())
    }

    fn schliessen(&mut self) -> Result<(), RuntimeError> {
        self.datei.get_mut().flush().map_err(fehler_aus_io)
    }
}

/// Tabelle der offenen Dateien, nach Dateinummer.
#[derive(Debug, Default)]
pub struct Dateien {
    offen: BTreeMap<i32, Datei>,
}

impl Dateien {
    /// `OPEN` — Datei an eine Nummer binden.
    pub fn oeffnen(
        &mut self,
        nummer: i32,
        pfad: &str,
        modus: Modus,
        reclen: usize,
    ) -> Result<(), RuntimeError> {
        if nummer < 1 {
            return Err(RuntimeError(52)); // Bad file name or number
        }
        if self.offen.contains_key(&nummer) {
            return Err(RuntimeError(55)); // File already open
        }
        let p = pfad_normieren(pfad);
        self.offen
            .insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
        Ok(())
    }

    pub fn get(&mut self, nummer: i32) -> Result<&mut Datei, RuntimeError> {
        self.offen.get_mut(&nummer).ok_or(RuntimeError(52))
    }

    pub fn ist_offen(&self, nummer: i32) -> bool {
        self.offen.contains_key(&nummer)
    }

    /// `CLOSE #n` — eine Datei schließen.
    pub fn schliessen(&mut self, nummer: i32) -> Result<(), RuntimeError> {
        match self.offen.remove(&nummer) {
            Some(mut d) => d.schliessen(),
            None => Ok(()), // CLOSE auf eine geschlossene Nummer ist folgenlos
        }
    }

    /// `CLOSE` / `RESET` — alle Dateien schließen.
    pub fn alle_schliessen(&mut self) -> Result<(), RuntimeError> {
        let nummern: Vec<i32> = self.offen.keys().copied().collect();
        for n in nummern {
            self.schliessen(n)?;
        }
        Ok(())
    }

    /// Zu welcher Datei und welchem Bereich gehört diese Variable?
    pub fn feld_finden(&self, r: &crate::value::VarRef) -> Option<(i32, usize, usize)> {
        for (n, d) in &self.offen {
            for f in &d.felder {
                if gleiche_referenz(&f.referenz, r) {
                    return Some((*n, f.start, f.laenge));
                }
            }
        }
        None
    }

    /// `FREEFILE` — kleinste freie Nummer.
    pub fn freie_nummer(&self) -> i32 {
        (1..).find(|n| !self.offen.contains_key(n)).unwrap_or(1)
    }
}

/// Zeigen zwei Referenzen auf denselben Speicherort?
fn gleiche_referenz(a: &crate::value::VarRef, b: &crate::value::VarRef) -> bool {
    use crate::value::VarRef::*;
    match (a, b) {
        (Global(x), Global(y)) => x == y,
        (Stack(x), Stack(y)) => x == y,
        (Elem(x, i), Elem(y, j)) => std::rc::Rc::ptr_eq(x, y) && i == j,
        (Field(x, p), Field(y, q)) => std::rc::Rc::ptr_eq(x, y) && p == q,
        _ => false,
    }
}

// ---- Recordpuffer: feste Strings als UTF-32 ---------------------------------

/// Bytes je Zeichen eines festen Strings im Recordpuffer.
pub const UTF32_BREITE: usize = 4;

/// Festen String in den Puffer schreiben: links- bzw. rechtsbündig,
/// überlange Werte werden abgeschnitten.
pub fn feld_setzen(puffer: &mut [u8], start: usize, laenge: usize, wert: &str, rechts: bool) {
    let zeichen: Vec<char> = wert.chars().take(laenge).collect();
    let fehlt = laenge - zeichen.len();
    let mut alle: Vec<char> = Vec::with_capacity(laenge);
    if rechts {
        alle.extend(std::iter::repeat_n(' ', fehlt));
        alle.extend(zeichen);
    } else {
        alle.extend(zeichen);
        alle.extend(std::iter::repeat_n(' ', fehlt));
    }
    for (i, c) in alle.iter().enumerate() {
        let off = start + i * UTF32_BREITE;
        if off + UTF32_BREITE <= puffer.len() {
            puffer[off..off + UTF32_BREITE].copy_from_slice(&(*c as u32).to_le_bytes());
        }
    }
}

/// Festen String aus dem Puffer lesen.
pub fn feld_lesen(puffer: &[u8], start: usize, laenge: usize) -> String {
    let mut s = String::with_capacity(laenge);
    for i in 0..laenge {
        let off = start + i * UTF32_BREITE;
        if off + UTF32_BREITE > puffer.len() {
            break;
        }
        let code = u32::from_le_bytes([
            puffer[off],
            puffer[off + 1],
            puffer[off + 2],
            puffer[off + 3],
        ]);
        s.push(char::from_u32(code).unwrap_or(' '));
    }
    s
}

// ---- Werte im Recordpuffer --------------------------------------------------

use crate::value::{RecordObj, TypeInit, UdtLayout, Value};

/// Bytebreite eines Feldtyps im Recordpuffer. Feste Strings zählen
/// `UTF32_BREITE` Bytes je Zeichen; Strings variabler Länge sind in Records
/// nicht zulässig (Vorbild: nur `STRING * n`).
pub fn breite(t: &TypeInit, layouts: &[UdtLayout]) -> Option<usize> {
    Some(match t {
        TypeInit::Int => 2,
        TypeInit::Lng | TypeInit::Sng => 4,
        TypeInit::Dbl | TypeInit::Cur => 8,
        TypeInit::FixedStr(n) => *n as usize * UTF32_BREITE,
        TypeInit::Udt(i) => {
            let felder = &layouts.get(*i as usize)?.fields;
            let mut summe = 0;
            for f in felder {
                summe += breite(f, layouts)?;
            }
            summe
        }
        TypeInit::Str | TypeInit::Empty => return None,
    })
}

/// Wert in den Puffer schreiben; liefert die belegte Bytezahl.
pub fn wert_schreiben(
    puffer: &mut [u8],
    offset: usize,
    wert: &Value,
    typ: &TypeInit,
    layouts: &[UdtLayout],
) -> Result<usize, RuntimeError> {
    let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
    if offset + n > puffer.len() {
        return Err(RuntimeError(59)); // Bad record length
    }
    match (typ, wert) {
        (TypeInit::Int, v) => puffer[offset..offset + 2]
            .copy_from_slice(&(crate::value::as_f64(v) as i16).to_le_bytes()),
        (TypeInit::Lng, v) => puffer[offset..offset + 4]
            .copy_from_slice(&(crate::value::as_f64(v) as i32).to_le_bytes()),
        (TypeInit::Sng, v) => puffer[offset..offset + 4]
            .copy_from_slice(&(crate::value::as_f64(v) as f32).to_le_bytes()),
        (TypeInit::Dbl, v) => {
            puffer[offset..offset + 8].copy_from_slice(&crate::value::as_f64(v).to_le_bytes())
        }
        (TypeInit::Cur, Value::Cur(c)) => {
            puffer[offset..offset + 8].copy_from_slice(&c.to_le_bytes())
        }
        (TypeInit::Cur, v) => puffer[offset..offset + 8]
            .copy_from_slice(&crate::value::f64_to_cur(crate::value::as_f64(v))?.to_le_bytes()),
        (TypeInit::FixedStr(len), Value::Str(s)) => {
            feld_setzen(puffer, offset, *len as usize, s, false)
        }
        (TypeInit::Udt(i), Value::Rec(r)) => {
            let felder = &layouts
                .get(*i as usize)
                .ok_or(RuntimeError::TYPE_MISMATCH)?
                .fields;
            let rec = r.borrow();
            let mut off = offset;
            for (ft, fv) in felder.iter().zip(rec.fields.iter()) {
                off += wert_schreiben(puffer, off, fv, ft, layouts)?;
            }
        }
        _ => return Err(RuntimeError::TYPE_MISMATCH),
    }
    Ok(n)
}

/// Wert aus dem Puffer lesen.
pub fn wert_lesen(
    puffer: &[u8],
    offset: usize,
    typ: &TypeInit,
    layouts: &[UdtLayout],
) -> Result<(Value, usize), RuntimeError> {
    let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
    if offset + n > puffer.len() {
        return Err(RuntimeError(59));
    }
    let b = &puffer[offset..offset + n];
    let v = match typ {
        TypeInit::Int => Value::Int(i16::from_le_bytes([b[0], b[1]])),
        TypeInit::Lng => Value::Lng(i32::from_le_bytes([b[0], b[1], b[2], b[3]])),
        TypeInit::Sng => Value::Sng(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
        TypeInit::Dbl => Value::Dbl(f64::from_le_bytes(b[..8].try_into().unwrap())),
        TypeInit::Cur => Value::Cur(i64::from_le_bytes(b[..8].try_into().unwrap())),
        TypeInit::FixedStr(len) => Value::Str(std::rc::Rc::from(
            feld_lesen(puffer, offset, *len as usize).as_str(),
        )),
        TypeInit::Udt(i) => {
            let felder = &layouts
                .get(*i as usize)
                .ok_or(RuntimeError::TYPE_MISMATCH)?
                .fields;
            let mut off = offset;
            let mut werte = Vec::with_capacity(felder.len());
            for ft in felder {
                let (v, k) = wert_lesen(puffer, off, ft, layouts)?;
                werte.push(v);
                off += k;
            }
            Value::Rec(std::rc::Rc::new(std::cell::RefCell::new(RecordObj {
                fields: werte,
            })))
        }
        TypeInit::Str | TypeInit::Empty => return Err(RuntimeError::TYPE_MISMATCH),
    };
    Ok((v, n))
}

// ---- Pfade und Dateisystem --------------------------------------------------

/// Pfadtrennzeichen des Vorbilds (`\`) auf die Plattform abbilden.
pub fn pfad_normieren(p: &str) -> PathBuf {
    if std::path::MAIN_SEPARATOR == '\\' {
        PathBuf::from(p)
    } else {
        PathBuf::from(p.replace('\\', "/"))
    }
}

fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
    use std::io::ErrorKind::*;
    match e.kind() {
        NotADirectory => RuntimeError(76),    // Path not found
        NotFound => RuntimeError(53),         // File not found
        PermissionDenied => RuntimeError(70), // Permission denied
        AlreadyExists => RuntimeError(58),    // File already exists
        _ => RuntimeError(57),                // Device I/O error
    }
}

/// NotFound am Dateieintrag (53) gegenüber fehlendem Elternpfad (76).
fn fehler_am_pfad(e: std::io::Error, pfad: &Path) -> RuntimeError {
    if e.kind() == std::io::ErrorKind::NotFound
        && pfad
            .parent()
            .is_some_and(|p| !p.as_os_str().is_empty() && !p.is_dir())
    {
        RuntimeError(76)
    } else {
        fehler_aus_io(e)
    }
}

/// `KILL` — Datei löschen.
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
    let p = pfad_normieren(pfad);
    std::fs::remove_file(&p).map_err(|e| fehler_am_pfad(e, &p))
}

/// `NAME alt AS neu`.
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
    let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
    std::fs::metadata(&a).map_err(|e| fehler_am_pfad(e, &a))?;
    if n.exists() {
        return Err(RuntimeError(58)); // File already exists
    }
    std::fs::rename(&a, &n).map_err(|e| fehler_am_pfad(e, &n))
}

/// `MKDIR` / `RMDIR` / `CHDIR`.
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
    let p = pfad_normieren(pfad);
    std::fs::create_dir(&p).map_err(|e| fehler_am_pfad(e, &p))
}

pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
    std::fs::remove_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => RuntimeError(76),
        _ => fehler_aus_io(e),
    })
}

pub fn verzeichnis_wechseln(pfad: &str) -> Result<(), RuntimeError> {
    std::env::set_current_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => RuntimeError(76),
        _ => fehler_aus_io(e),
    })
}

/// `CURDIR$`.
pub fn aktuelles_verzeichnis() -> String {
    std::env::current_dir()
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_default()
}

/// Verzeichnissuche für `DIR$` und `FILES`: alle Namen, die auf das Muster
/// passen (`*` und `?` wie beim Vorbild).
pub fn suchen(muster: &str) -> Result<Vec<String>, RuntimeError> {
    let p = pfad_normieren(muster);
    let (verzeichnis, maske) = match p.file_name() {
        Some(n) => (
            p.parent()
                .filter(|e| !e.as_os_str().is_empty())
                .map(|e| e.to_path_buf())
                .unwrap_or_else(|| PathBuf::from(".")),
            n.to_string_lossy().into_owned(),
        ),
        None => (p.clone(), "*".to_string()),
    };
    let eintraege = std::fs::read_dir(&verzeichnis).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => RuntimeError(76),
        _ => fehler_aus_io(e),
    })?;
    let mut namen: Vec<String> = eintraege
        .filter_map(|e| e.ok())
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .filter(|n| passt(n, &maske))
        .collect();
    namen.sort();
    Ok(namen)
}

/// Musterabgleich mit `*` (beliebig viele Zeichen) und `?` (genau eines).
pub fn passt(name: &str, muster: &str) -> bool {
    let n: Vec<char> = name.chars().collect();
    let m: Vec<char> = muster.chars().collect();
    fn rek(n: &[char], m: &[char]) -> bool {
        match m.first() {
            None => n.is_empty(),
            Some('*') => rek(n, &m[1..]) || (!n.is_empty() && rek(&n[1..], m)),
            Some('?') => !n.is_empty() && rek(&n[1..], &m[1..]),
            Some(c) => !n.is_empty() && n[0].eq_ignore_ascii_case(c) && rek(&n[1..], &m[1..]),
        }
    }
    rek(&n, &m)
}

/// Elemente einer `INPUT #`-Zeile trennen: Komma trennt, Anführungszeichen
/// klammern, Leerraum am Rand fällt weg.
pub fn felder_trennen(zeile: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut aktuell = String::new();
    let mut in_quote = false;
    let mut hatte_quote = false;
    for c in zeile.chars() {
        match c {
            '"' if !in_quote && aktuell.trim().is_empty() => {
                in_quote = true;
                hatte_quote = true;
                aktuell.clear();
            }
            '"' if in_quote => in_quote = false,
            ',' if !in_quote => {
                out.push(if hatte_quote {
                    std::mem::take(&mut aktuell)
                } else {
                    std::mem::take(&mut aktuell).trim().to_string()
                });
                hatte_quote = false;
            }
            _ => aktuell.push(c),
        }
    }
    out.push(if hatte_quote {
        aktuell
    } else {
        aktuell.trim().to_string()
    });
    out
}

/// Ein Element für `WRITE #` aufbereiten: Strings in Anführungszeichen.
pub fn write_element(text: &str, ist_string: bool) -> String {
    if ist_string {
        format!("\"{}\"", text.replace('"', "\"\""))
    } else {
        text.trim().to_string()
    }
}

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

    #[test]
    fn muster_mit_stern_und_fragezeichen() {
        assert!(passt("test.txt", "*.txt"));
        assert!(passt("test.txt", "t?st.txt"));
        assert!(!passt("test.txt", "*.bas"));
        assert!(passt("TEST.TXT", "*.txt")); // Vorbild: Groß/klein egal
        assert!(passt("a", "*"));
    }

    #[test]
    fn festes_feld_ist_inhaltsunabhaengig_gleich_lang() {
        let mut p = vec![0u8; 10 * UTF32_BREITE];
        feld_setzen(&mut p, 0, 10, "abc", false);
        assert_eq!(feld_lesen(&p, 0, 10), "abc       ");
        feld_setzen(&mut p, 0, 10, "äöüßÄÖÜ", false);
        assert_eq!(feld_lesen(&p, 0, 10), "äöüßÄÖÜ   ");
        // Der Puffer hat in beiden Fällen dieselbe Länge.
        assert_eq!(p.len(), 40);
    }

    #[test]
    fn rset_schneidet_ab_und_richtet_rechts_aus() {
        let mut p = vec![0u8; 5 * UTF32_BREITE];
        feld_setzen(&mut p, 0, 5, "ABCDEFG", true);
        assert_eq!(feld_lesen(&p, 0, 5), "ABCDE");
        feld_setzen(&mut p, 0, 5, "xy", true);
        assert_eq!(feld_lesen(&p, 0, 5), "   xy");
    }

    #[test]
    fn felder_einer_input_zeile() {
        assert_eq!(felder_trennen("a,b,c"), vec!["a", "b", "c"]);
        assert_eq!(felder_trennen(" 1 , 2 "), vec!["1", "2"]);
        assert_eq!(felder_trennen("\"a,b\",5"), vec!["a,b", "5"]);
    }

    #[test]
    fn modus_aus_kurzsyntax() {
        assert_eq!(Modus::aus_text("I"), Some(Modus::Input));
        assert_eq!(Modus::aus_text("output"), Some(Modus::Output));
        assert_eq!(Modus::aus_text("x"), None);
    }
}
