pub mod breakpoints;
pub mod memory;
pub mod static_analysis;
pub mod video;

use std::cell::Cell;
use std::fs;
use std::mem;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, Instant};

use hex_literal::hex;
use i8051::breakpoint::Breakpoints;
use i8051::peripheral::{P3_INT1, Serial, Timer};
use i8051::{Cpu, CpuContext, CpuView, DefaultPortMapper, PortMapper};
use ssu::session::SessionConfig;
use ssu::session::SessionUnsend;
use tracing::debug;
use tracing::{info, trace, warn};

use crate::host::comm::CommSession;
use crate::host::comm::connect_duart;
use crate::host::comm::connect_session;
use crate::machine::TerminalSystem;
use crate::machine::generic::duart::DUART;
use crate::machine::generic::rom::ROM;
use lk201::LK201;

use self::memory::{DiagnosticMonitor, Ports, RAM};

#[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
use crate::pc_trace::PcTrace;

pub(crate) struct System {
    pub rom: ROM,
    pub memory: RAM,
    pub instruction_count: usize,
    nvr_file: Option<PathBuf>,
    nvr_write: usize,

    video_row: Ports,
    serial: Serial,
    diagnostic_monitor: DiagnosticMonitor,
    timer: Timer,
    default: DefaultPortMapper,
    comm_a: CommSession,
    dtr_a: Rc<Cell<bool>>,
    comm_b: CommSession,
    dtr_b: Rc<Cell<bool>>,

    pub(crate) keyboard: LK201,
    pub(crate) breakpoints: Breakpoints,

    #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
    pub(crate) pc_trace: Option<PcTrace>,
}

impl TerminalSystem for System {
    #[inline]
    fn step(&mut self, cpu: &mut Cpu) {
        self.step(cpu);
    }

    #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
    fn flush_pc_trace_if_due(&mut self) {
        if let Some(trace) = &mut self.pc_trace {
            trace.flush_if_due();
        }
    }
}

impl System {
    pub(crate) fn new(
        rom: Vec<u8>,
        nvr: Option<&Path>,
        comm1: Option<SessionConfig>,
        comm2: Option<SessionConfig>,
        #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))] pc_trace: Option<&Path>,
    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        info!("Loading ROM into memory...");
        let rom_len = rom.len();
        let rom = ROM::new(rom);

        info!("Configuring video processor...");
        let video_row = Ports::new();
        info!("Configuring keyboard...");
        let (serial, in_kbd, out_kbd) = Serial::new(60);

        info!("Configuring UARTs...");
        let (duart, channel_a, channel_b) = DUART::new();

        let dtr_a = channel_a.dtr.clone();
        let dtr_b = channel_b.dtr.clone();

        #[cfg(feature = "demo")]
        let comm_a = if let Some(config) = comm1 {
            connect_duart(channel_a, config)?
        } else {
            connect_session(channel_a, crate::host::demo::DemoComm.boot()?)?
        };

        #[cfg(not(feature = "demo"))]
        let comm_a = connect_duart(channel_a, comm1.unwrap_or_default())?;

        let comm_b = connect_duart(channel_b, comm2.unwrap_or_default())?;

        let mut memory = RAM::new(rom.bank.clone(), video_row.sync.clone(), duart);
        let mut nvr_file = None;
        info!("Configuring NVR...");
        if let Some(nvr) = nvr {
            info!("Using NVR file: {:?}", nvr);
            nvr_file = Some(nvr.to_owned());
            if !nvr.exists() {
                warn!("NVR file does not exist, creating it");
                fs::write(nvr, vec![0xff; 128])?;
            }
            let mut nvr = fs::read(nvr)?;
            if nvr.len() < 128 {
                warn!("NVR file is too small, padding with zeros");
                nvr.resize(128, 0xff);
            } else if nvr.len() > 128 {
                warn!("NVR file is too large, truncating");
                nvr.truncate(128);
            }
            memory.nvr.mem.copy_from_slice(&nvr);
        } else {
            info!("No NVR file provided, using default");
            // Some checksums hand-modified (0x30, 0x50, 0x70) for tests to pass
            let initial_nvr = hex!(
                "65 44 88 1e 1e 85 54 88  85 54 00 00 04 50 00 00"
                "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00"
                "00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00"
                "03 00 c0 25 00 24 01 00  00 00 02 98 00 00 00 00"
                "01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01"
                "4a 00 c0 25 00 24 01 00  00 00 02 98 00 00 00 00"
                "01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01"
                "4a ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff"
            );

            memory.nvr.mem.fill(0xff);
            memory.nvr.mem[..initial_nvr.len()].copy_from_slice(&initial_nvr);
        }

        #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
        let pc_trace = if let Some(path) = pc_trace {
            Some(PcTrace::new(path.to_path_buf(), rom_len)?)
        } else {
            None
        };

        Ok(Self {
            instruction_count: 0,
            memory,
            rom,
            nvr_file,
            nvr_write: 0,
            video_row,
            serial,
            comm_a,
            dtr_a,
            comm_b,
            dtr_b,
            diagnostic_monitor: DiagnosticMonitor::default(),
            timer: Timer::default(),
            default: DefaultPortMapper::default(),
            keyboard: LK201::new(in_kbd.clone(), out_kbd),
            breakpoints: Breakpoints::new(),
            #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
            pc_trace,
        })
    }

    pub(crate) fn step(&mut self, cpu: &mut Cpu) {
        self.instruction_count += 1;
        #[cfg(not(target_arch = "wasm32"))]
        let start = Instant::now();
        let mut breakpoints = Breakpoints::default();
        mem::swap(&mut self.breakpoints, &mut breakpoints);
        breakpoints.run(true, cpu, self);
        mem::swap(&mut self.breakpoints, &mut breakpoints);

        let pc = cpu.pc_ext(self);
        // Trace VSYNC phase for timer interrupts
        // if pc == 0x928 {
        //     let flag = cpu.internal_ram[0x20] & (1 << 7) != 0;
        //     info!("PC = 0x928, phase = {:?}, flag = {flag}", self.video_row.sync.sync_gen.borrow().phase());
        // }

        #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
        let pc_trace_insn_len = self.pc_trace.as_ref().map(|_| cpu.decode(self, pc).len());

        let prev_0x1f = cpu.internal_ram[0x1f];
        cpu.step(self);
        let new_0x1f = cpu.internal_ram[0x1f];
        if prev_0x1f != new_0x1f {
            debug!(
                "0x1f changed from {prev_0x1f:02X} to {new_0x1f:02X} @ {:04X}",
                pc
            );
        }

        #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
        if let (Some(trace), Some(insn_len)) = (&mut self.pc_trace, pc_trace_insn_len) {
            trace.mark_range(pc, insn_len);
        }

        self.memory.tick();
        self.keyboard.tick();
        self.serial.tick(cpu);
        let prev_p3 = self.video_row.p3_read;
        self.video_row.p3_read &= !P3_INT1;
        if !self.memory.duart.interrupt {
            self.video_row.p3_read |= P3_INT1;
            if prev_p3 & P3_INT1 == 0 {
                trace!("DUART interrupt cleared");
            }
        } else if prev_p3 & P3_INT1 != 0 {
            trace!("DUART interrupt");
        }
        self.comm_a.tick();
        self.comm_b.tick();

        // Set DTR if either DTR1 or DTR2 is set (ideally this should gate on the 232/423 select pin)
        let dtr_a = !self.memory.duart.output_bits_inv & 0b1010 != 0b1010;
        let dtr_b = !self.memory.duart.output_bits_inv & (1 << 7) == 0;
        if self.dtr_a.replace(dtr_a) != dtr_a {
            info!("DUART pipe A DTR changed to {}", self.dtr_a.get());
        }
        if self.dtr_b.replace(dtr_b) != dtr_b {
            info!("DUART pipe B DTR changed to {}", self.dtr_b.get());
        }
        self.video_row.tick();
        let tick = self.timer.prepare_tick(cpu, self);
        self.timer.tick(cpu, tick);

        if self.memory.nvr.write_count > self.nvr_write {
            if let Some(nvr_file) = &self.nvr_file {
                fs::write(nvr_file, self.memory.nvr.mem).unwrap();
            }
            self.nvr_write = self.memory.nvr.write_count;
        }

        mem::swap(&mut self.breakpoints, &mut breakpoints);
        breakpoints.run(false, cpu, self);
        mem::swap(&mut self.breakpoints, &mut breakpoints);
        #[cfg(not(target_arch = "wasm32"))]
        if start.elapsed() > Duration::from_millis(100) {
            warn!("Step took too long: {:?}", start.elapsed());
        }
    }

    #[cfg(test)]
    pub(crate) fn dump_screen_text(&self) -> String {
        use crate::{host::screen::unicode, machine::vt420::video::decode_vram};
        use std::fmt::Write;

        let text = String::with_capacity(132 * 25);
        decode_vram(
            self.memory.vram.as_ref(),
            &self.memory.mapper,
            |text, _, _, _| {
                text.push('\n');
            },
            |text, _col, ch, _attrs| {
                if let Some(c) = unicode::map_char(ch) {
                    text.push(c)
                } else {
                    write!(text, "<{ch:03X}>").unwrap()
                }
            },
            text,
        )
    }
}

impl PortMapper for System {
    type WriteValue = <(
        Ports,
        (Serial, (DiagnosticMonitor, (Timer, DefaultPortMapper))),
    ) as PortMapper>::WriteValue;
    fn interest<C: CpuView>(&self, cpu: &C, addr: u8) -> bool {
        (
            &self.video_row,
            (
                &self.serial,
                (&self.diagnostic_monitor, (&self.timer, &self.default)),
            ),
        )
            .interest(cpu, addr)
    }
    fn read<C: CpuView>(&self, cpu: &C, addr: u8) -> u8 {
        (
            &self.video_row,
            (
                &self.serial,
                (&self.diagnostic_monitor, (&self.timer, &self.default)),
            ),
        )
            .read(cpu, addr)
    }
    fn prepare_write<C: CpuView>(&self, cpu: &C, addr: u8, value: u8) -> Self::WriteValue {
        (
            &self.video_row,
            (
                &self.serial,
                (&self.diagnostic_monitor, (&self.timer, &self.default)),
            ),
        )
            .prepare_write(cpu, addr, value)
    }
    fn write(&mut self, value: Self::WriteValue) {
        (
            &mut self.video_row,
            (
                &mut self.serial,
                (
                    &mut self.diagnostic_monitor,
                    (&mut self.timer, &mut self.default),
                ),
            ),
        )
            .write(value)
    }
    fn extend_short_read<C: CpuView>(&self, cpu: &C, addr: u8) -> u16 {
        (
            &self.video_row,
            (
                &self.serial,
                (&self.diagnostic_monitor, (&self.timer, &self.default)),
            ),
        )
            .extend_short_read(cpu, addr)
    }
    fn pc_extension<C: CpuView>(&self, _cpu: &C) -> u16 {
        self.rom.bank.get() as u16
    }
    fn read_latch<C: CpuView>(&self, cpu: &C, addr: u8) -> u8 {
        (
            &self.video_row,
            (
                &self.serial,
                (&self.diagnostic_monitor, (&self.timer, &self.default)),
            ),
        )
            .read_latch(cpu, addr)
    }
}

impl CpuContext for System {
    type Ports = System;
    type Xdata = RAM;
    type Code = ROM;
    fn ports(&self) -> &Self::Ports {
        self
    }
    fn ports_mut(&mut self) -> &mut Self::Ports {
        self
    }
    fn xdata(&self) -> &Self::Xdata {
        &self.memory
    }
    fn xdata_mut(&mut self) -> &mut Self::Xdata {
        &mut self.memory
    }
    fn code(&self) -> &Self::Code {
        &self.rom
    }
    fn code_mut(&mut self) -> &mut Self::Code {
        &mut self.rom
    }
}

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

    /// Run the ROM and simulation and ensure that we boot to the passed-test screen
    /// and setup comes up.
    ///
    /// We also check that the keyboard commands sent during diagnostics are fully parsed.
    #[test]
    fn test_boots() {
        let manifest_dir = env!("CARGO_MANIFEST_DIR");
        let rom = fs::read(format!("{manifest_dir}/roms/vt420/23-068E9-00.bin")).unwrap();
        let mut system = System::new(
            rom,
            None,
            Some(SessionConfig::default()),
            Some(SessionConfig::default()),
            #[cfg(all(feature = "pc-trace", not(target_arch = "wasm32")))]
            None,
        )
        .unwrap();

        system.keyboard.start_collecting_commands();

        let mut cpu = Cpu::new();
        for _ in 0..9850880 {
            system.step(&mut cpu);
        }

        let (keyboard_bytes, keyboard_commands) = system.keyboard.stop_collecting_commands();
        eprintln!("Keyboard bytes: {keyboard_bytes:02X?}");
        eprintln!("Keyboard commands:");
        for command in keyboard_commands {
            eprintln!("  {command:?}");
        }

        let screen = system.dump_screen_text();
        eprintln!("Screen text:\n{screen}\n");
        assert!(screen.contains("VT420 OK"), "{screen}");

        system.keyboard.sender().send_special_key(SpecialKey::F3);

        for _ in 0..1000000 {
            system.step(&mut cpu);
        }

        let screen = system.dump_screen_text();
        eprintln!("Screen text:\n{screen}\n");
        assert!(screen.contains("Set-Up=English"), "{screen}");
    }
}
