use std::borrow::Cow;
use std::collections::BTreeMap;

use crate::cpu::Flag;
use crate::{Cpu, CpuContext, Register};

use tracing::{Level, info};

pub enum Action {
    /// Log a message to the console.
    Log(Level, Cow<'static, str>),
    /// Set a register to a value.
    Set(Register, u16),
    /// Enable or disable tracing of instructions.
    SetTraceInstructions(bool),
    /// Enable or disable automatic tracing of registers.
    SetTraceRegisters(bool),
    /// Trace current instruction.
    TraceInstructions,
    /// Trace the current state of the CPU.
    TraceRegisters,
    /// Run an arbitrary function.
    Run(Box<dyn Fn(&mut Cpu)>),
}

impl Action {
    fn run(&self, cpu: &mut Cpu, breakpoints: &mut BreakpointState, ctx: &impl CpuContext) {
        match self {
            Self::Log(Level::DEBUG, message) => {
                tracing::debug!(target: "bp", "[BP] {}", message);
            }
            Self::Log(Level::INFO, message) => {
                tracing::info!(target: "bp", "[BP] {}", message);
            }
            Self::Log(Level::WARN, message) => {
                tracing::warn!(target: "bp", "[BP] {}", message);
            }
            Self::Log(Level::ERROR, message) => {
                tracing::error!(target: "bp", "[BP] {}", message);
            }
            Self::Log(Level::TRACE, message) => {
                tracing::trace!(target: "bp", "[BP] {}", message);
            }
            Self::Set(register, value) => cpu.register_set(*register, *value),
            Self::SetTraceInstructions(value) => breakpoints.trace_instructions = *value,
            Self::SetTraceRegisters(value) => breakpoints.trace_registers = *value,
            Self::TraceInstructions => {
                info!("{:#}", cpu.decode_pc(ctx));
            }
            Self::TraceRegisters => {
                if tracing::enabled!(Level::INFO) {
                    let regs = format!(
                        "  A={:02X?}  B={:02X?}  DPTR={:04X?}  C={} OV={} AC={} F0={}",
                        cpu.a(),
                        cpu.b(),
                        cpu.dptr(),
                        cpu.psw(Flag::C) as u8,
                        cpu.psw(Flag::OV) as u8,
                        cpu.psw(Flag::AC) as u8,
                        cpu.psw(Flag::F0) as u8
                    );

                    info!("{}", regs);

                    let mut regs = String::from("  ");
                    for i in 0..8 {
                        regs.push_str(&format!("R{}={:02X?} ", i, cpu.r(i)));
                    }
                    regs.push('\n');
                    info!("{}", regs);
                }
            }
            Self::Run(func) => func(cpu),
        }
    }
}

#[derive(Default)]
struct BreakpointState {
    trace_instructions: bool,
    trace_registers: bool,
}

#[derive(Default)]
pub struct Breakpoints {
    breakpoints_before: BTreeMap<u32, Vec<Action>>,
    breakpoints_after: BTreeMap<u32, Vec<Action>>,
    state: BreakpointState,
}

impl Breakpoints {
    pub fn new() -> Self {
        Self {
            breakpoints_before: BTreeMap::new(),
            breakpoints_after: BTreeMap::new(),
            state: Default::default(),
        }
    }

    pub fn add(&mut self, before: bool, addr: u32, action: Action) {
        if before {
            self.breakpoints_before
                .entry(addr)
                .or_default()
                .push(action);
        } else {
            self.breakpoints_after.entry(addr).or_default().push(action);
        }
    }

    pub fn remove(&mut self, addr: u32) {
        self.breakpoints_before.remove(&addr);
        self.breakpoints_after.remove(&addr);
    }

    pub fn clear(&mut self) {
        self.breakpoints_before.clear();
        self.breakpoints_after.clear();
    }

    pub fn run(&mut self, before: bool, cpu: &mut Cpu, ctx: &mut impl CpuContext) {
        let pc = cpu.pc_ext(ctx);

        let actions = if before {
            self.breakpoints_before
                .get(&pc)
                .map(|actions| actions.as_slice())
                .unwrap_or(&[])
        } else {
            self.breakpoints_after
                .get(&pc)
                .map(|actions| actions.as_slice())
                .unwrap_or(&[])
        };
        for action in actions {
            action.run(cpu, &mut self.state, ctx);
        }
        if self.state.trace_instructions && before {
            Action::TraceInstructions.run(cpu, &mut self.state, ctx);
        }
        if self.state.trace_registers && !before {
            Action::TraceRegisters.run(cpu, &mut self.state, ctx);
        }
    }
}
