//! Konsolen-Druckzustand: PRINT-Formatierung, 14-Zeichen-Druckzonen, TAB/SPC.
//! Wirkung ausschließlich auf dem [`TextScreen`].
//!
//! Die Druckspalte wird nicht mehr eigenständig mitgezählt, sondern vom
//! Bildschirm gelesen (`POS`). Damit stimmen Druckzonen und Cursor auch
//! nach `LOCATE`, `CLS` oder einem Umbruch am rechten Rand überein.

use crate::errors::RuntimeError;
use crate::fileio::Dateien;
use crate::format::format_print;
use crate::screen::TextScreen;
use crate::value::Value;

/// Breite einer Druckzone (Vorbild: 14 Zeichen).
pub const ZONE_WIDTH: usize = 14;

/// Wohin `PRINT` schreibt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Ziel {
    #[default]
    Bildschirm,
    /// `PRINT #n` — in eine offene Datei.
    Datei(i32),
    /// `LPRINT` — in den Druckerkanal.
    Drucker,
}

/// PRINT-Verhalten. Auf dem Bildschirm lebt die Spalte im Bildschirm; für
/// Datei und Drucker wird sie hier mitgezählt, damit Druckzonen, `TAB` und
/// `SPC` dort genauso wirken.
#[derive(Default)]
pub struct PrintState {
    pub ziel: Ziel,
    /// Spalte (0-basiert) für Datei- und Druckerausgabe.
    pub spalte: usize,
    /// Druckerpuffer (Grundlage von `LPOS`); wird beim Programmende
    /// ausgegeben.
    pub drucker: String,
}

impl PrintState {
    /// Aktuelle Druckspalte (0-basiert) des eingestellten Ziels.
    pub fn col(&self, screen: &TextScreen) -> usize {
        match self.ziel {
            Ziel::Bildschirm => screen.pos() - 1,
            _ => self.spalte,
        }
    }

    /// Text an das eingestellte Ziel ausgeben.
    pub fn write(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
        s: &str,
    ) -> Result<(), RuntimeError> {
        match self.ziel {
            Ziel::Bildschirm => screen.print(s),
            Ziel::Drucker => {
                self.drucker.push_str(s);
                self.spalte_nachfuehren(s);
            }
            Ziel::Datei(n) => {
                dateien.get(n)?.schreiben(s)?;
                self.spalte_nachfuehren(s);
            }
        }
        Ok(())
    }

    /// Spalte nach ausgegebenem Text nachführen (nur für Nicht-Bildschirm).
    fn spalte_nachfuehren(&mut self, s: &str) {
        match s.rfind('\n') {
            Some(i) => self.spalte = s[i + 1..].chars().count(),
            None => self.spalte += s.chars().count(),
        }
    }

    /// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
    /// Leerzeichen, Strings unverändert.
    pub fn print_value(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
        v: &Value,
    ) -> Result<(), RuntimeError> {
        let text = match v {
            Value::Str(s) => s.to_string(),
            _ => format_print(v),
        };
        self.write(screen, dateien, &text)
    }

    /// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
    pub fn print_comma(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
    ) -> Result<(), RuntimeError> {
        let col = self.col(screen);
        let next = (col / ZONE_WIDTH + 1) * ZONE_WIDTH;
        self.write(screen, dateien, &" ".repeat(next - col))
    }

    /// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
    /// dahinter, zuerst Zeilenumbruch (Vorbild).
    pub fn print_tab(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
        n: i32,
    ) -> Result<(), RuntimeError> {
        let target = (n.max(1) as usize) - 1;
        if self.col(screen) > target {
            self.write(screen, dateien, "\n")?;
        }
        let col = self.col(screen);
        if target > col {
            self.write(screen, dateien, &" ".repeat(target - col))?;
        }
        Ok(())
    }

    /// `SPC(n)` — n Leerzeichen.
    pub fn print_spc(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
        n: i32,
    ) -> Result<(), RuntimeError> {
        if n > 0 {
            self.write(screen, dateien, &" ".repeat(n as usize))?;
        }
        Ok(())
    }

    pub fn print_newline(
        &mut self,
        screen: &mut TextScreen,
        dateien: &mut Dateien,
    ) -> Result<(), RuntimeError> {
        self.write(screen, dateien, "\n")
    }
}

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

    /// Zeile `z` (1-basiert) als getrimmter Text.
    fn zeile(s: &TextScreen, z: usize) -> String {
        let t: String = (1..=s.cols()).map(|c| s.cell(z, c).ch).collect();
        t.trim_end().to_string()
    }

    fn ps() -> (PrintState, TextScreen, Dateien) {
        (PrintState::default(), TextScreen::new(), Dateien::default())
    }

    #[test]
    fn druckzonen() {
        let (mut ps, mut s, mut d) = ps();
        // "a" (1 Zeichen) , → Spalte 15 (1-basiert)
        ps.write(&mut s, &mut d, "a").unwrap();
        ps.print_comma(&mut s, &mut d).unwrap();
        ps.write(&mut s, &mut d, "b").unwrap();
        assert_eq!(zeile(&s, 1), "a             b");
        assert_eq!(s.pos(), 16);
    }

    #[test]
    fn volle_zone_springt_zur_uebernaechsten() {
        let (mut ps, mut s, mut d) = ps();
        ps.write(&mut s, &mut d, "12345678901234").unwrap(); // Zone voll
        ps.print_comma(&mut s, &mut d).unwrap();
        ps.write(&mut s, &mut d, "x").unwrap();
        // x beginnt in Spalte 29 (1-basiert)
        assert_eq!(s.cell(1, 29).ch, 'x');
        assert_eq!(s.pos(), 30);
    }

    #[test]
    fn zahlen_mit_vorzeichenspalte() {
        let (mut ps, mut s, mut d) = ps();
        ps.print_value(&mut s, &mut d, &Value::Int(1)).unwrap();
        ps.print_value(&mut s, &mut d, &Value::Int(-2)).unwrap();
        ps.print_newline(&mut s, &mut d).unwrap();
        assert_eq!(zeile(&s, 1), " 1 -2");
        assert_eq!(s.csrlin(), 2);
    }

    #[test]
    fn druckzone_folgt_dem_cursor_nach_locate() {
        let (mut ps, mut s, mut d) = ps();
        s.locate(3, 5).unwrap();
        ps.print_comma(&mut s, &mut d).unwrap(); // Spalte 4 → nächste Zone 14
        assert_eq!(s.pos(), 15);
    }
}
