// nescope — a Nintendo Entertainment System emulator in pure MoonBit.
//
// This file holds the shared vocabulary: the error type, the CPU status flags,
// the button set and the small helpers every other module reads through.

///|
/// Every way a cartridge image or an emulator request can be rejected.
///
/// Loading never returns a half-filled cartridge: a malformed image surfaces as
/// one of these variants instead of an out-of-range read, and every variant
/// names the field or mapper that failed.
pub(all) suberror NesError {
  /// The buffer ended before the structure could be read completely.
  UnexpectedEnd(offset~ : Int64, needed~ : Int64, available~ : Int64)
  /// The 4-byte magic "NES\x1a" does not start the file.
  NotAnINes
  /// A header field that must take one of a small set of values holds
  /// something else.
  BadValue(field~ : String, value~ : Int64)
  /// The header declares a PRG or CHR size that the file cannot back.
  Truncated(kind~ : String, declared~ : Int64, present~ : Int64)
  /// A mapper this build does not implement.
  UnsupportedMapper(number~ : UInt32)
  /// A CPU opcode that has no handler, reported with the address that fetched
  /// it. On the 6502 this is a real class of input, not a bug: the official
  /// set has 151 of 256 opcodes, and the rest are unofficial.
  UnknownOpcode(opcode~ : UInt32, at~ : UInt32)
  /// A lockout chip (Tengen/Infrared) blocks the cartridge.
  LockoutChip
}

///|
/// Renders an error as a single line naming the field and value that failed.
pub impl Show for NesError with fn output(self, logger) {
  let text = match self {
    UnexpectedEnd(offset~, needed~, available~) =>
      "unexpected end at \{offset}: needed \{needed} bytes, \{available} available"
    NotAnINes => "not an iNES image: missing the NES\\x1a magic"
    BadValue(field~, value~) => "bad value for \{field}: \{value}"
    Truncated(kind~, declared~, present~) =>
      "\{kind} declared \{declared} bytes but the file has \{present}"
    UnsupportedMapper(number~) => "mapper \{number} is not implemented"
    UnknownOpcode(opcode~, at~) =>
      "unknown opcode $\{hex2(opcode)} at $\{hex4(at)}"
    LockoutChip => "the cartridge reports an unsupported lockout chip"
  }
  logger.write_string(text)
}

///|
/// Exposes the message as a plain string for tests and CLI output.
pub extend NesError with Show::{to_string, output}

///|
/// Formats a 32-bit value as `$` plus eight lowercase hex digits.
pub fn hex8(value : UInt32) -> String {
  "$" + hex_digits(value.to_int64(), 8)
}

///|
/// Formats a byte as `$` plus two lowercase hex digits, the width the 6502
/// disassembly column uses.
pub fn hex2(value : UInt32) -> String {
  "$" + hex_digits(value.to_int64() & 0xFFL, 2)
}

///|
/// Formats a 16-bit value as `$` plus four lowercase hex digits, the width of a
/// 6502 address.
pub fn hex4(value : UInt32) -> String {
  "$" + hex_digits(value.to_int64() & 0xFFFFL, 4)
}

///|
/// Renders a value in lowercase hex, zero-padded to `digits` characters.
///
/// Padding is done here rather than with a format specifier so that the same
/// text is produced on every backend, including wasm-gc.
fn hex_digits(value : Int64, digits : Int32) -> String {
  let text = value.to_string(radix=16)
  let pad = digits - text.length()
  if pad <= 0 {
    return text
  }
  let buf = StringBuilder(size_hint=digits)
  for _ in 0.. Int32 {
  match self {
    Carry => 0
    Zero => 1
    Interrupt => 2
    Decimal => 3
    Break => 4
    Unused => 5
    Overflow => 6
    Negative => 7
  }
}

///|
/// The mask that isolates a flag inside the packed status register.
pub fn Flag::mask(self : Flag) -> UInt32 {
  ((1UL << self.bit()) & 0xFFFF_FFFFUL).to_uint()
}

///|
/// The eight buttons a standard NES controller reports.
pub(all) enum Button {
  A
  B
  Select
  Start
  Up
  Down
  Left
  Right
}

///|
/// The position of a button in the controller's serial shift order.
///
/// The order is fixed by the hardware's shift register and is the order every
/// ROM's input code assumes: A first, then B, then the two selects, then the
/// d-pad from up to right.
pub fn Button::shift_index(self : Button) -> Int32 {
  match self {
    A => 0
    B => 1
    Select => 2
    Start => 3
    Up => 4
    Down => 5
    Left => 6
    Right => 7
  }
}

///|
/// The console region, which decides the CPU and PPU clock rates.
///
/// The rates differ enough that a PAL ROM run at NTSC speed drifts, so the rate
/// is carried in the type rather than assumed.
pub(all) enum Region {
  NTSC
  PAL
  Dendy
}

///|
/// CPU clock rate in hertz for the region.
pub fn Region::cpu_hz(self : Region) -> Int64 {
  match self {
    NTSC => 1_789_773L
    PAL => 1_662_607L
    Dendy => 1_773_448L
  }
}

///|
/// PPU dots per scanline for the region.
pub fn Region::dots_per_scanline(self : Region) -> Int32 {
  match self {
    NTSC => 341
    PAL => 341
    Dendy => 341
  }
}

///|
/// PPU scanlines per frame for the region, including the vertical blanking
/// lines the PPU does not render.
pub fn Region::scanlines_per_frame(self : Region) -> Int32 {
  match self {
    NTSC => 262
    PAL => 312
    Dendy => 312
  }
}