///|
/// A source breakpoint for the live tree-walking interpreter.
pub(all) struct DebugBreakpoint {
  source_name : String
  line : Int
}

///|
pub fn DebugBreakpoint::new(
  source_name : String,
  line : Int,
) -> DebugBreakpoint {
  { source_name, line, }
}

///|
/// One visible variable copied at a debugger stop.
pub(all) struct DebugVariable {
  name : String
  value : Value
}

///|
/// Live state captured immediately before a statement executes.
pub(all) struct DebugSnapshot {
  source_name : String
  span : SourceSpan
  call_depth : Int
  variables : Array[DebugVariable]
}

///|
/// The command returned synchronously by a live debugger hook.
pub(all) enum DebugCommand {
  DebugContinue
  DebugStepInto
  DebugStepOver
  DebugStepOut
  DebugTerminate
}

///|
priv struct RuntimeDebugger {
  breakpoints : Array[DebugBreakpoint]
  hook : (DebugSnapshot) -> DebugCommand
  mut mode : Int
  mut target_depth : Int
}

///|
fn debug_breakpoint_matches(
  breakpoints : Array[DebugBreakpoint],
  source_name : String,
  line : Int,
) -> Bool {
  for breakpoint in breakpoints {
    if breakpoint.source_name == source_name && breakpoint.line == line {
      return true
    }
  }
  false
}

///|
fn RuntimeContext::debug_before_statement(
  self : RuntimeContext,
  span : SourceSpan,
  environment : Environment,
) -> Diagnostic? {
  let debugger = match self.debugger {
    Some(debugger) => debugger
    None => return None
  }
  let should_pause = debug_breakpoint_matches(
      debugger.breakpoints,
      self.current_source,
      span.line(),
    ) ||
    debugger.mode == 1 ||
    (debugger.mode == 2 && self.call_depth <= debugger.target_depth) ||
    (debugger.mode == 3 && self.call_depth < debugger.target_depth)
  if !should_pause {
    return None
  }
  let snapshot : DebugSnapshot = {
    source_name: self.current_source,
    span,
    call_depth: self.call_depth,
    variables: environment.debug_bindings(),
  }
  match (debugger.hook)(snapshot) {
    DebugContinue => debugger.mode = 0
    DebugStepInto => debugger.mode = 1
    DebugStepOver => {
      debugger.mode = 2
      debugger.target_depth = self.call_depth
    }
    DebugStepOut => {
      debugger.mode = 3
      debugger.target_depth = self.call_depth
    }
    DebugTerminate =>
      return Some(
        Diagnostic::new(
          Runtime,
          DebugTerminated,
          "execution terminated by debugger",
          span,
        ).with_source(self.current_source),
      )
  }
  None
}

///|
/// Attach a synchronous live debugger to this VM. The hook executes before a
/// statement and may inspect current variables and select the next stop mode.
/// A hook used by a UI may block while waiting for user input, but must not
/// re-enter the same VM.
pub fn Vm::attach_debugger(
  self : Vm,
  breakpoints : Array[DebugBreakpoint],
  pause_on_entry : Bool,
  hook : (DebugSnapshot) -> DebugCommand,
) -> Bool {
  if self.closed {
    return false
  }
  let copied : Array[DebugBreakpoint] = []
  for breakpoint in breakpoints {
    if breakpoint.source_name.length() > 0 && breakpoint.line > 0 {
      copied.push(breakpoint)
    }
  }
  self.runtime().debugger = Some({
    breakpoints: copied,
    hook,
    mode: if pause_on_entry {
      1
    } else {
      0
    },
    target_depth: 0,
  })
  true
}

///|
pub fn Vm::detach_debugger(self : Vm) -> Bool {
  if self.closed {
    return false
  }
  self.runtime().debugger = None
  true
}