pub mod breakpoint;
pub mod trace;

use std::collections::HashMap;
use xenia_cpu::context::PpcContext;
use xenia_memory::MemoryAccess;

pub use breakpoint::Breakpoint;
pub use trace::TraceEntry;

/// The debugger. Hooks into every instruction step for observation.
pub struct Debugger {
    pub breakpoints: HashMap<u32, Breakpoint>,
    pub trace_log: Vec<TraceEntry>,
    pub trace_enabled: bool,
    pub max_trace_entries: usize,
    pub paused: bool,
    pub step_mode: StepMode,
    break_pending: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepMode {
    /// Run freely until breakpoint or pause
    Run,
    /// Execute one instruction then pause
    StepInto,
    /// Run but break after current function returns (when LR changes)
    StepOver { return_addr: u32 },
}

impl Debugger {
    pub fn new() -> Self {
        Self {
            breakpoints: HashMap::new(),
            trace_log: Vec::new(),
            trace_enabled: true,
            max_trace_entries: 100_000,
            paused: true, // Start paused for debugging
            step_mode: StepMode::StepInto,
            break_pending: false,
        }
    }

    /// Tier-3 perf: single branch that the hot interpreter loop checks
    /// before dispatching to [`pre_step`]/[`post_step`]. When the
    /// debugger is in "cold run" mode (not paused, no breakpoints,
    /// `StepMode::Run`, in-memory trace off), both hooks become dead
    /// code and we can skip the HashMap lookup + step-mode match + Vec
    /// maintenance entirely. The compiler reliably branch-predicts the
    /// stable branch direction across millions of instructions.
    #[inline]
    pub fn wants_hooks(&self) -> bool {
        self.trace_enabled
            || self.paused
            || self.break_pending
            || !matches!(self.step_mode, StepMode::Run)
            || !self.breakpoints.is_empty()
    }

    /// Called before each instruction executes.
    pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
        // Check breakpoints
        if let Some(bp) = self.breakpoints.get(&ctx.pc)
            && bp.enabled {
                self.break_pending = true;
                tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
            }
    }

    /// Called after each instruction executes.
    pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
        // Log to trace
        if self.trace_enabled {
            if self.trace_log.len() >= self.max_trace_entries {
                self.trace_log.remove(0);
            }
            self.trace_log.push(TraceEntry {
                pc: ctx.pc,
                cycle: ctx.cycle_count,
                gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]],
                lr: ctx.lr,
            });
        }

        // Handle step mode
        match self.step_mode {
            StepMode::StepInto => {
                self.break_pending = true;
            }
            StepMode::StepOver { return_addr } => {
                if ctx.pc == return_addr {
                    self.break_pending = true;
                }
            }
            StepMode::Run => {}
        }
    }

    /// Should we break execution?
    pub fn should_break(&self) -> bool {
        self.break_pending || self.paused
    }

    /// Add a breakpoint at the given address.
    pub fn add_breakpoint(&mut self, addr: u32) {
        self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None });
    }

    /// Remove a breakpoint.
    pub fn remove_breakpoint(&mut self, addr: u32) {
        self.breakpoints.remove(&addr);
    }

    /// Continue execution.
    pub fn continue_execution(&mut self) {
        self.paused = false;
        self.break_pending = false;
        self.step_mode = StepMode::Run;
    }

    /// Step one instruction.
    pub fn step_into(&mut self) {
        self.paused = false;
        self.break_pending = false;
        self.step_mode = StepMode::StepInto;
    }

    /// Clear break state after handling.
    pub fn acknowledge_break(&mut self) {
        self.break_pending = false;
        self.paused = true;
    }
}

impl Default for Debugger {
    fn default() -> Self {
        Self::new()
    }
}
