//! Opt-in debugger control. All execution remains in the ordinary VM.
use super::*;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebugLocation {
    pub frame: u64,
    pub procedure: usize,
    pub module: u16,
    pub source: u32,
    pub line: u32,
    pub column: u32,
    pub pc: usize,
}
#[derive(Debug, Clone)]
pub struct DebugFrame {
    pub id: u64,
    pub procedure: String,
    pub file: String,
    pub location: DebugLocation,
}
#[derive(Debug, Clone)]
pub struct DebugError {
    pub code: u16,
    pub origin: Option<DebugLocation>,
    pub handler: Option<DebugLocation>,
    pub resume_pc: usize,
}
#[derive(Default)]
pub struct Debugger {
    pub enabled: bool,
    pub trace: bool,
    pub history_on: bool,
    pub history: VecDeque<DebugLocation>,
    history_pending: Option<DebugLocation>,
    pub break_errors: bool,
    pub error: Option<DebugError>,
    pub(super) error_pending: bool,
    pub immediate_error: Option<String>,
    pub compiler: Option<crate::project::DebugCompiler>,
    pub watches: Vec<DebugWatch>,
    pub(super) next_frame: u64,
    pub(super) breakpoints: HashSet<(u16, u32, u32)>,
    over: Option<(u64, usize, usize)>,
    cursor: Option<(u16, u32, u32)>,
    pub(super) immediate: Option<DebugEntry>,
}
#[derive(Debug, Clone)]
pub struct DebugWatch {
    pub expression: String,
    pub condition: bool,
    pub frame: Option<u64>,
    pub value: Result<Value, String>,
}
impl Debugger {
    pub fn cancel_motion(&mut self) {
        self.over = None;
        self.cursor = None;
    }
}
impl Vm {
    pub fn debug_location(&self) -> Option<DebugLocation> {
        let f = self.frames.last()?;
        Some(self.frame_location(f, f.last_stmt_pc))
    }
    fn frame_location(&self, f: &Frame, pc: usize) -> DebugLocation {
        let code = &self.module.procs[f.proc].code;
        let (source, column) = match pc.checked_sub(1).and_then(|p| code.get(p)) {
            Some(Instr::Source(s, c)) => (*s, *c),
            _ => (f.source, f.column),
        };
        let line = match code.get(pc) {
            Some(Instr::Stmt(l) | Instr::InitStmt(l)) => *l,
            _ => f.line,
        };
        DebugLocation {
            frame: f.id,
            procedure: f.proc,
            module: self.module.sources[source as usize].module,
            source,
            line,
            column,
            pc,
        }
    }
    pub fn execution_location(&self) -> Option<DebugLocation> {
        let frame = self.frames.last()?;
        let code = &self.module.procs[frame.proc].code;
        let pc = match code.get(frame.pc) {
            Some(Instr::Stmt(_)) => frame.pc,
            Some(Instr::Source(..)) if matches!(code.get(frame.pc + 1), Some(Instr::Stmt(_))) => {
                frame.pc + 1
            }
            _ => frame.last_stmt_pc,
        };
        Some(self.frame_location(frame, pc))
    }
    pub fn next_debug_location(&self) -> Option<DebugLocation> {
        let f = self.frames.last()?;
        let pc = self.module.procs[f.proc]
            .code
            .iter()
            .enumerate()
            .skip(f.pc)
            .find(|(_, i)| matches!(i, Instr::Stmt(l) if *l > 0))
            .map(|(i, _)| i)?;
        Some(self.frame_location(f, pc))
    }
    pub fn debug_frames(&self) -> Vec<DebugFrame> {
        self.frames
            .iter()
            .rev()
            .map(|f| {
                let location = self.frame_location(f, f.last_stmt_pc);
                DebugFrame {
                    id: f.id,
                    procedure: self.module.procs[f.proc].name.clone(),
                    file: self.module.sources[location.source as usize].path.clone(),
                    location,
                }
            })
            .collect()
    }
    pub fn module_name(&self, module: u16) -> &str {
        &self.module.modules[module as usize].0
    }
    pub fn source_files(&self) -> &[tb_frontend::source::SourceFile] {
        &self.module.sources
    }
    pub fn executable_line(&self, module: u16, source: u32, line: u32) -> bool {
        self.statement_locations()
            .iter()
            .any(|p| p.module == module && p.source == source && p.line == line)
    }
    pub fn statement_locations(&self) -> Vec<DebugLocation> {
        self.module
            .procs
            .iter()
            .enumerate()
            .flat_map(|(procedure, proc)| {
                proc.code
                    .iter()
                    .enumerate()
                    .filter_map(move |(pc, instruction)| {
                        let Instr::Stmt(line) = instruction else {
                            return None;
                        };
                        if *line == 0 {
                            return None;
                        }
                        let (source, column) =
                            match pc.checked_sub(1).and_then(|p| proc.code.get(p)) {
                                Some(Instr::Source(s, c)) => (*s, *c),
                                _ => (0, 1),
                            };
                        Some(DebugLocation {
                            frame: 0,
                            procedure,
                            module: self.module.sources[source as usize].module,
                            source,
                            line: *line,
                            column,
                            pc,
                        })
                    })
            })
            .collect()
    }
    pub fn add_source_breakpoint(&mut self, module: u16, source: u32, line: u32) -> bool {
        self.debug.enabled = true;
        if !self.executable_line(module, source, line) {
            return false;
        }
        self.debug.breakpoints.insert((module, source, line));
        true
    }
    pub fn clear_source_breakpoints(&mut self) {
        self.debug.breakpoints.clear();
    }
    pub fn step_over(&mut self) {
        self.debug.enabled = true;
        self.set_step(false);
        if let Some(f) = self.frames.last() {
            self.debug.over = Some((f.id, self.frames.len(), f.gosub.len()));
        }
    }
    pub fn run_to_cursor(&mut self, module: u16, source: u32, line: u32) -> Result<(), String> {
        if !self.executable_line(module, source, line) {
            return Err("Cursorzeile ist nicht ausführbar".into());
        }
        self.debug.enabled = true;
        self.set_step(false);
        self.debug.cursor = Some((module, source, line));
        Ok(())
    }
    pub(super) fn debug_boundary(&mut self, pc: usize, initializing: bool) -> Option<RunEvent> {
        let error_pending = std::mem::take(&mut self.debug.error_pending);
        if self
            .debug
            .immediate
            .as_ref()
            .is_some_and(|e| self.frames.last().is_some_and(|f| f.proc == e.proc))
        {
            return error_pending.then_some(RunEvent::Breakpoint {
                line: self.current_line(),
            });
        }
        if initializing || self.current_line() == 0 {
            return None;
        }
        let loc = self.debug_location()?;
        let f = self.frames.last_mut().unwrap();
        let line_entry = f.debug_line.is_none_or(|(source, line, prior)| {
            source != loc.source || line != loc.line || pc <= prior
        });
        f.debug_line = Some((loc.source, loc.line, pc));
        if self.debug.history_on {
            self.debug.history_pending = Some(loc.clone());
        }
        if error_pending {
            return Some(RunEvent::Breakpoint { line: loc.line });
        }
        let f = self.frames.last().unwrap();
        let key = (loc.module, loc.source, loc.line);
        let breakpoint = line_entry && self.debug.breakpoints.contains(&key);
        let cursor = self.debug.cursor == Some(key);
        let over = self.debug.over.is_some_and(|(id, depth, gosub)| {
            (f.id == id && f.gosub.len() <= gosub) || self.frames.len() < depth
        });
        let conditional = self.check_watchpoints();
        if breakpoint || conditional || cursor || over {
            self.debug.cancel_motion();
            return Some(if breakpoint || conditional || cursor {
                RunEvent::Breakpoint { line: loc.line }
            } else {
                RunEvent::Stepped { line: loc.line }
            });
        }
        None
    }
    pub(super) fn record_debug_execution(&mut self) {
        if let Some(location) = self.debug.history_pending.take() {
            if self.debug.history_on
                && self
                    .frames
                    .last()
                    .is_some_and(|f| f.id == location.frame && f.pc == location.pc + 1)
            {
                if self.debug.history.len() == 1024 {
                    self.debug.history.pop_front();
                }
                self.debug.history.push_back(location);
            }
        }
    }
    pub fn set_next_statement(&mut self, frame: u64, source: u32, line: u32) -> Result<(), String> {
        let f = self.frames.last().ok_or("Kein aktiver Frame")?;
        if f.id != frame {
            return Err("Nur der aktive Ausführungsrahmen darf verändert werden".into());
        }
        let target = self
            .statement_locations()
            .into_iter()
            .find(|p| p.procedure == f.proc && p.source == source && p.line == line)
            .ok_or("Ziel gehört nicht zum aktiven Prozedurrahmen")?;
        if f.pc != f.last_stmt_pc + 1 || self.immediate_active() {
            return Err(
                "Kein freier Anweisungsbeginn; zuerst bis zur nächsten Grenze fortsetzen".into(),
            );
        }
        if self.in_handler
            || !f.gosub.is_empty()
            || f.eingabe.is_some()
            || f.sleep.is_some()
            || f.waiting_form.is_some()
        {
            return Err("Aktiver Fehler-, GOSUB- oder Wartekontext erlaubt keinen Sprung".into());
        }
        let current = self.debug_location().ok_or("Kein Quellkontext")?;
        if current.module != target.module {
            return Err("Ziel liegt in einem anderen Modulkontext".into());
        }
        if let Some(compiler) = &self.debug.compiler {
            let position = |p: &DebugLocation| tb_frontend::SourcePos {
                source: p.source,
                line: p.line,
                column: p.column,
            };
            let proc = &self.module.procs[f.proc];
            if !compiler.same_control_context(
                current.module,
                if proc.kind == tb_frontend::hir::HProcKind::Main {
                    "<main>"
                } else {
                    &proc.name
                },
                position(&current),
                position(&target),
            ) {
                return Err("Ziel hat einen anderen strukturierten Kontrollkontext".into());
            }
        }
        let code = &self.module.procs[f.proc].code;
        let context = |at: usize| -> Vec<usize> {
            code.iter()
                .enumerate()
                .filter_map(|(pc, i)| {
                    let t = match i {
                        Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => {
                            *t as usize
                        }
                        _ => return None,
                    };
                    ((t <= at && at <= pc)
                        || (pc < at
                            && at < t
                            && (!matches!(i, Instr::Jump(_)) || self.debug.compiler.is_none())))
                    .then_some(pc)
                })
                .collect()
        };
        let special = code.iter().any(|i| match i {
            Instr::Gosub(t) | Instr::OnErrorLocal(t) | Instr::OnErrorGoto(t) => {
                let start = *t as usize;
                let end = code
                    .iter()
                    .enumerate()
                    .skip(start)
                    .find(|(_, i)| {
                        matches!(
                            i,
                            Instr::RetGosub
                                | Instr::Resume0
                                | Instr::ResumeNext
                                | Instr::ResumeLabel(_)
                        )
                    })
                    .map_or(code.len(), |(p, _)| p);
                start <= target.pc && target.pc <= end
            }
            _ => false,
        });
        if special || context(f.last_stmt_pc) != context(target.pc) {
            return Err("Ziel hat einen anderen Kontroll-, Schleifen- oder Handlerkontext".into());
        }
        let f = self.frames.last_mut().unwrap();
        f.pc = target.pc + 1;
        f.last_stmt_pc = target.pc;
        f.source = target.source;
        f.line = target.line;
        f.column = target.column;
        self.debug.history_pending = self
            .debug
            .history_on
            .then(|| self.debug_location())
            .flatten();
        self.debug.cancel_motion();
        self.frames.last_mut().unwrap().debug_line = Some((target.source, target.line, target.pc));
        Ok(())
    }
}

pub(super) struct DebugEntry {
    pub depth: usize,
    pub proc: usize,
    pub base: usize,
    pub locals_len: usize,
    pub strings_len: usize,
    pub sources_len: usize,
    pub stack: Vec<Value>,
    pub flags: u32,
    pub history_pending: Option<DebugLocation>,
    pub handler_return: Option<(usize, usize)>,
    pub error_state: (u16, u32, u32, bool, usize),
}
impl Vm {
    fn compile_debug(
        &self,
        frame: u64,
        text: &str,
        expression: bool,
    ) -> Result<crate::project::DebugCode, String> {
        let f = self
            .frames
            .iter()
            .find(|f| f.id == frame)
            .ok_or("Inspektionsrahmen nicht mehr erreichbar")?;
        let module = self.module.sources[f.source as usize].module;
        let proc = &self.module.procs[f.proc];
        self.debug
            .compiler
            .as_ref()
            .ok_or("Debug-Symbole fehlen")?
            .compile(
                module,
                if proc.kind == tb_frontend::hir::HProcKind::Main {
                    "<main>"
                } else {
                    &proc.name
                },
                text,
                expression,
            )
    }
    fn install_debug_code(&mut self, mut code: crate::project::DebugCode) -> Result<usize, String> {
        let offset = self.module.strings.len();
        if offset + code.strings.len() >= u16::MAX as usize {
            return Err("Zu viele Debug-Stringkonstanten".into());
        }
        for i in &mut code.procedure.code {
            match i {
                Instr::PushStr(id)
                | Instr::Unsupported(id)
                | Instr::LoadDynamicObjectProperty(id)
                | Instr::StoreDynamicObjectProperty(id) => *id += offset as u16,
                _ => {}
            }
        }
        self.module.strings.extend(code.strings);
        let id = self.module.procs.len();
        self.module.procs.push(code.procedure);
        Ok(id)
    }
    /// Side-effect-free bytecode only, executed by exec (no independent evaluator).
    pub fn evaluate_watch(&mut self, frame: u64, text: &str) -> Result<Value, String> {
        let code = self.compile_debug(frame, text, true)?;
        let result_slot = code.procedure.locals_init.len().saturating_sub(1) as u16;
        for i in &code.procedure.code {
            let mut bytes = Vec::new();
            i.encode(&mut bytes);
            let pure = match i {
                Instr::CallBuiltin(id, _) => *id <= ids::ATN || *id == ids::FORMAT_S,
                Instr::StoreLocal(slot) => *slot == result_slot,
                Instr::Source(_, _)
                | Instr::Stmt(_)
                | Instr::LoadGlobal(_)
                | Instr::LoadLocal(_)
                | Instr::LoadRef(_)
                | Instr::LoadArr(..)
                | Instr::LoadElem(_)
                | Instr::LoadField(_)
                | Instr::ArrBound(_)
                | Instr::FixStr(_)
                | Instr::LoadErr
                | Instr::LoadErl
                | Instr::RetFn => true,
                _ => matches!(bytes[0], 0x10..=0x17 | 0x40..=0x95),
            };
            if !pure {
                return Err(format!("Watch ist nicht nachweislich nebenwirkungsfrei: {i:?}; Aufrufe im Direktfenster verwenden"));
            }
        }
        let f = self
            .frames
            .iter()
            .find(|f| f.id == frame)
            .ok_or("Frame nicht erreichbar")?;
        let base = f.locals_base;
        let count = self.module.procs[f.proc].locals_init.len();
        let values = self.locals[base..base + count].to_vec();
        let strings_len = self.module.strings.len();
        let proc = self.install_debug_code(code)?;
        let stack = std::mem::take(&mut self.stack);
        self.push_frame(proc, 0);
        let new_base = self.frames.last().unwrap().locals_base;
        self.locals[new_base..new_base + count].clone_from_slice(&values);
        let mut host = tb_runtime::host::CaptureHost::default();
        let result = (|| {
            for (pc, i) in self.module.procs[proc].code.clone().into_iter().enumerate() {
                if matches!(i, Instr::Stmt(_) | Instr::Source(..)) {
                    continue;
                }
                if matches!(i, Instr::RetFn) {
                    return self.pop().map_err(|e| e.to_string());
                }
                if let Instr::LoadArr(global, slot, ..) = i {
                    let value = if global {
                        &self.globals[slot as usize]
                    } else {
                        &self.locals[new_base + slot as usize]
                    };
                    if matches!(value, Value::Empty) {
                        return Err(
                            "Array noch nicht initialisiert; Watch führt kein Auto-DIM aus".into(),
                        );
                    }
                }
                self.exec(i, pc, &mut host).map_err(|e| e.to_string())?;
            }
            Err("Watch ohne Ergebnis".into())
        })();
        self.pop_frame();
        self.stack = stack;
        self.module.procs.truncate(proc);
        self.module.strings.truncate(strings_len);
        result
    }
    pub fn evaluate_condition(&mut self, frame: u64, text: &str) -> Result<Value, String> {
        let value = self.evaluate_watch(frame, text)?;
        if !matches!(
            value,
            Value::Int(_) | Value::Lng(_) | Value::Sng(_) | Value::Dbl(_) | Value::Cur(_)
        ) {
            return Err("Watchpoint benötigt eine numerische BASIC-Bedingung".into());
        }
        Ok(value)
    }
    fn check_watchpoints(&mut self) -> bool {
        let Some(frame) = self.frames.last().map(|f| f.id) else {
            return false;
        };
        let mut watches = std::mem::take(&mut self.debug.watches);
        let mut stop = false;
        for watch in watches.iter_mut().filter(|w| w.condition) {
            watch.value = self.evaluate_condition(watch.frame.unwrap_or(frame), &watch.expression);
            stop |= watch.value.as_ref().is_ok_and(Self::truthy);
        }
        self.debug.watches = watches;
        stop
    }
    pub fn immediate_active(&self) -> bool {
        self.debug.immediate.is_some()
    }
    pub fn start_immediate(&mut self, frame: u64, text: &str) -> Result<(), String> {
        if self.immediate_active() {
            return Err("Direktkommando läuft bereits".into());
        }
        // Compile all input before changing a stack or a variable.
        let code = self.compile_debug(frame, text, false)?;
        let f = self
            .frames
            .iter()
            .find(|f| f.id == frame)
            .ok_or("Frame nicht erreichbar")?;
        let base = f.locals_base;
        let source = self.module.sources.len() as u32;
        let module = self.module.sources[f.source as usize].module;
        let entry = DebugEntry {
            depth: self.frames.len(),
            proc: self.module.procs.len(),
            base,
            locals_len: self.locals.len(),
            strings_len: self.module.strings.len(),
            sources_len: self.module.sources.len(),
            stack: self.stack.clone(),
            flags: self.flags,
            history_pending: self.debug.history_pending.take(),
            handler_return: None,
            error_state: (
                self.err,
                self.erl,
                self.zeile_nr,
                self.in_handler,
                self.resume_pc,
            ),
        };
        let proc = self.install_debug_code(code)?;
        self.module.sources.push(tb_frontend::source::SourceFile {
            module,
            path: "<Immediate>".into(),
        });
        for i in &mut self.module.procs[proc].code {
            if let Instr::Source(s, _) = i {
                *s = source;
            }
        }
        self.debug.immediate_error = None;
        self.stack.clear();
        self.push_frame(proc, 0);
        self.locals.truncate(entry.locals_len);
        let f = self.frames.last_mut().unwrap();
        f.locals_base = base;
        f.source = source;
        self.flags &= !F_STEP;
        self.debug.cancel_motion();
        self.debug.immediate = Some(entry);
        Ok(())
    }
    pub(super) fn restore_debug_handler(&mut self) {
        if let Some(entry) = &mut self.debug.immediate {
            if self.frames.len() == entry.depth + 1 {
                if let Some((stmt, _)) = entry.handler_return.take() {
                    let f = self.frames.last_mut().unwrap();
                    f.proc = entry.proc;
                    f.locals_base = entry.base;
                    f.last_stmt_pc = stmt;
                    self.resume_pc = stmt;
                }
            }
        }
    }
    pub(super) fn finish_immediate(&mut self) {
        let Some(entry) = self.debug.immediate.take() else {
            return;
        };
        self.frames.truncate(entry.depth);
        self.locals.truncate(entry.locals_len);
        self.stack = entry.stack;
        self.flags = entry.flags;
        (
            self.err,
            self.erl,
            self.zeile_nr,
            self.in_handler,
            self.resume_pc,
        ) = entry.error_state;
        self.module.procs.truncate(entry.proc);
        self.module.strings.truncate(entry.strings_len);
        self.module.sources.truncate(entry.sources_len);
        self.debug.error = None;
        self.debug.history_pending = entry.history_pending;
    }
}
