///|
/// Runtime error helpers.

///|
fn runtime_error_exc(
  kind : RuntimeErrorKind,
  message : String,
) -> (String, Array[String]) {
  match kind {
    RuntimeErrorKind::ZeroDivision =>
      ("ZeroDivisionError".to_string(), [message])
    RuntimeErrorKind::Name => ("NameError".to_string(), [message])
    RuntimeErrorKind::Type => ("TypeError".to_string(), [message])
    RuntimeErrorKind::Attribute => ("AttributeError".to_string(), [message])
    RuntimeErrorKind::Index => ("IndexError".to_string(), [message])
    RuntimeErrorKind::Key => ("KeyError".to_string(), [message])
    RuntimeErrorKind::NotImplemented =>
      ("NotImplementedError".to_string(), [message])
    RuntimeErrorKind::Runtime => {
      let prefixes : Array[String] = [
        "ValueError:", "OverflowError:", "ImportError:", "AssertionError:", "RuntimeError:",
        "EOFError:", "SyntaxError:", "IndentationError:", "StopIteration:", "UnboundLocalError:",
        "StopAsyncIteration:", "UnicodeError:", "UnicodeDecodeError:", "UnicodeEncodeError:",
        "UnicodeTranslateError:", "ModuleNotFoundError:", "OSError:", "FileNotFoundError:",
        "FileExistsError:", "PermissionError:", "IsADirectoryError:", "NotADirectoryError:",
        "SystemExit:", "SkipTest:", "struct.error:", "BufferError:", "RecursionError:",
      ]
      if message == "StopIteration" {
        return ("StopIteration".to_string(), [])
      }
      if message == "StopAsyncIteration" {
        return ("StopAsyncIteration".to_string(), [])
      }
      if message == "SystemExit" {
        return ("SystemExit".to_string(), [])
      }
      for prefix in prefixes {
        if message.has_prefix(prefix) {
          let mut type_name = prefix.trim(chars=":").to_owned()
          if type_name == "struct.error" {
            // `struct.error` is exposed as an exception class named `error` from
            // the `_struct` module. Matching relies on class names.
            type_name = "error".to_string()
          }
          let mut rest = match message.strip_prefix(prefix) {
            Some(view) => view.to_owned()
            None => "".to_string()
          }
          if rest.has_prefix(" ") {
            rest = match rest.strip_prefix(" ") {
              Some(view) => view.to_owned()
              None => rest
            }
          }
          let args = if rest.length() == 0 { [] } else { [rest] }
          return (type_name.to_string(), args)
        }
      }
      if message.has_prefix("line ") {
        return ("SyntaxError".to_string(), [message])
      }
      ("RuntimeError".to_string(), [message])
    }
  }
}

///|
let active_frame_stack : Ref[Array[TracebackFrame]] = { val: [], }

///|
let active_frame_env_stack : Ref[Array[TracebackEnv]] = { val: [], }

///|
let active_span_stack : Ref[Array[Span]] = { val: [], }

///|
fn snapshot_frame_env_stack() -> Array[TracebackEnv] {
  let stack = active_frame_env_stack.val
  if stack.length() == 0 {
    []
  } else {
    [stack[stack.length() - 1]]
  }
}

///|
fn set_traceback_frame_env(
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
) -> Unit {
  if active_frame_env_stack.val.length() == 0 {
    return
  }
  let idx = active_frame_env_stack.val.length() - 1
  active_frame_env_stack.val[idx] = TracebackEnv::{ locals, globals, }
}

///|
fn current_span() -> Span? {
  if active_span_stack.val.length() == 0 {
    None
  } else {
    Some(active_span_stack.val[active_span_stack.val.length() - 1])
  }
}

///|
fn snapshot_traceback() -> Array[TracebackFrame] {
  // Capture only the currently executing (innermost) frame at raise-time.
  // Outer frames are appended as the exception unwinds through callers, which
  // more closely matches CPython's traceback growth semantics.
  let stack = active_frame_stack.val
  if stack.length() == 0 {
    return []
  }
  let frames : Array[TracebackFrame] = []
  let f0 = stack[stack.length() - 1]
  frames.push(TracebackFrame::{
    name: f0.name,
    filename: f0.filename,
    line: f0.line,
    column: f0.column,
    end_column: 0,
  })
  // Best-effort: map the active span stack onto the active frame stack so
  // callers can display accurate "last instruction" locations for nested calls
  // (e.g. decorators, multi-line expressions, recursion).
  let spans = active_span_stack.val
  if spans.length() > frames.length() {
    // In the AST evaluator spans can nest within a single frame, making the
    // span stack longer than the frame stack. In that case, only trust the
    // current (innermost) span to avoid corrupting outer frame locations.
    match current_span() {
      Some(span) => {
        let mut last = frames[frames.length() - 1]
        let mut width = span.end - span.start
        if width <= 0 {
          width = 1
        }
        let end_column = if width > 512 { 0 } else { span.column + width }
        last = TracebackFrame::{
          name: last.name,
          filename: last.filename,
          line: span.line,
          column: span.column,
          end_column,
        }
        frames[frames.length() - 1] = last
      }
      None => ()
    }
    return frames
  }
  let mut i = 0
  let max = if frames.length() < spans.length() {
    frames.length()
  } else {
    spans.length()
  }
  while i < max {
    let frame_idx = frames.length() - 1 - i
    let span_idx = spans.length() - 1 - i
    let span = spans[span_idx]
    let last = frames[frame_idx]
    let mut width = span.end - span.start
    if width <= 0 {
      width = 1
    }
    let end_column = if width > 512 { 0 } else { span.column + width }
    frames[frame_idx] = TracebackFrame::{
      name: last.name,
      filename: last.filename,
      line: span.line,
      column: span.column,
      end_column,
    }
    i = i + 1
  }
  frames
}

///|
fn current_traceback_filename() -> String {
  if active_frame_stack.val.length() == 0 {
    "".to_string()
  } else {
    active_frame_stack.val[active_frame_stack.val.length() - 1].filename
  }
}

///|
fn ensure_recursion_available() -> Result[Unit, RuntimeError] {
  if active_frame_stack.val.length() >= current_config().max_recursion {
    Err(
      make_runtime_error(
        RuntimeErrorKind::Runtime,
        "RecursionError: maximum recursion depth exceeded".to_string(),
      ),
    )
  } else {
    Ok(())
  }
}

///|
fn push_traceback_frame(name : String, filename : String) -> Unit {
  active_frame_stack.val.push(TracebackFrame::{
    name,
    filename,
    line: 1,
    column: 1,
    end_column: 0,
  })
  active_frame_env_stack.val.push(TracebackEnv::{ locals: [], globals: [], })
}

///|
fn pop_traceback_frame() -> Unit {
  if active_frame_stack.val.length() == 0 {
    return
  }
  let _ = active_frame_stack.val.remove(active_frame_stack.val.length() - 1)
  if active_frame_env_stack.val.length() > 0 {
    let _ = active_frame_env_stack.val.remove(
      active_frame_env_stack.val.length() - 1,
    )
  }
}

///|
fn push_active_span(span : Span) -> Unit {
  active_span_stack.val.push(span)
}

///|
fn pop_active_span() -> Unit {
  if active_span_stack.val.length() == 0 {
    return
  }
  let span = active_span_stack.val.remove(active_span_stack.val.length() - 1)
  // Record the last completed source location for the currently active frame.
  // This intentionally happens on span pop (end of an instruction/step) so
  // callers paused in a nested call keep their previous `f_lineno` value.
  if active_frame_stack.val.length() == 0 {
    return
  }
  let idx = active_frame_stack.val.length() - 1
  let last = active_frame_stack.val[idx]
  let mut width = span.end - span.start
  if width <= 0 {
    width = 1
  }
  let end_column = if width > 512 { 0 } else { span.column + width }
  active_frame_stack.val[idx] = TracebackFrame::{
    name: last.name,
    filename: last.filename,
    line: span.line,
    column: span.column,
    end_column,
  }
}

///|
fn make_runtime_error(
  kind : RuntimeErrorKind,
  message : String,
) -> RuntimeError {
  let (exc_type, exc_args) = runtime_error_exc(kind, message)
  RuntimeError::{
    kind,
    message,
    span: current_span(),
    traceback: snapshot_traceback(),
    traceback_envs: snapshot_frame_env_stack(),
    exc_type,
    exc_args,
    exc_value: None,
    exc_cause: None,
    exc_context: None,
    exc_suppress_context: false,
  }
}

///|
fn make_parse_runtime_error(error : ParseError) -> RuntimeError {
  let message = format_parse_error(error)
  let (exc_type, exc_args) = match error.kind {
    ParseErrorKind::Indentation => ("IndentationError".to_string(), [message])
    _ => ("SyntaxError".to_string(), [message])
  }
  RuntimeError::{
    kind: RuntimeErrorKind::Runtime,
    message,
    span: Some(error.span),
    traceback: [],
    traceback_envs: [],
    exc_type,
    exc_args,
    exc_value: None,
    exc_cause: None,
    exc_context: None,
    exc_suppress_context: false,
  }
}

///|
pub fn format_parse_error(error : ParseError) -> String {
  if error.message.has_prefix("ValueError:") ||
    error.message.has_prefix("ImportError:") ||
    error.message.has_prefix("NameError:") ||
    error.message.has_prefix("AssertionError:") {
    return error.message
  }
  let msg = match error.kind {
    ParseErrorKind::Indentation => "expected indented block".to_string()
    _ =>
      if error.message == "unmatched ')'" ||
        error.message == "unmatched ']'" ||
        error.message == "unmatched '}'" {
        error.message
      } else {
        "invalid syntax".to_string()
      }
  }
  "line " + error.span.line.to_string() + ":1 " + msg
}

///|
pub fn format_runtime_error(error : RuntimeError) -> String {
  match error.kind {
    RuntimeErrorKind::ZeroDivision => "ZeroDivisionError: " + error.message
    RuntimeErrorKind::Name => "NameError: " + error.message
    RuntimeErrorKind::Type => "TypeError: " + error.message
    RuntimeErrorKind::Attribute => "AttributeError: " + error.message
    RuntimeErrorKind::Index => "IndexError: " + error.message
    RuntimeErrorKind::Key => "KeyError: " + error.message
    RuntimeErrorKind::NotImplemented => "NotImplementedError: " + error.message
    RuntimeErrorKind::Runtime => error.message
  }
}

///|
pub fn format_runtime_error_with_traceback(
  error : RuntimeError,
  limit : Int32,
) -> String {
  if limit == 0 || error.traceback.length() == 0 {
    return format_runtime_error(error)
  }
  let buf = StringBuilder()
  buf.write_string("Traceback (most recent call last):\n")
  let start = if error.traceback.length() > limit {
    error.traceback.length() - limit
  } else {
    0
  }
  for i = start; i < error.traceback.length(); i = i + 1 {
    let frame = error.traceback[i]
    buf.write_string("  File \"")
    buf.write_string(frame.filename)
    buf.write_string("\", line ")
    buf.write_string(frame.line.to_string())
    buf.write_string(", column ")
    buf.write_string(frame.column.to_string())
    buf.write_string(", in ")
    buf.write_string(frame.name)
    buf.write_char('\n')
  }
  buf.write_string(format_runtime_error(error))
  buf.to_string()
}