// High-level MoonBit wrappers for the TIC-80 WebAssembly API.

///|
/// A button on one of the four TIC-80 gamepads.
///
/// The explicit tags are the button IDs expected by the TIC-80 host ABI.
pub(all) enum Button {
  P1Up = 0
  P1Down = 1
  P1Left = 2
  P1Right = 3
  P1A = 4
  P1B = 5
  P1X = 6
  P1Y = 7
  P2Up = 8
  P2Down = 9
  P2Left = 10
  P2Right = 11
  P2A = 12
  P2B = 13
  P2X = 14
  P2Y = 15
  P3Up = 16
  P3Down = 17
  P3Left = 18
  P3Right = 19
  P3A = 20
  P3B = 21
  P3X = 22
  P3Y = 23
  P4Up = 24
  P4Down = 25
  P4Left = 26
  P4Right = 27
  P4A = 28
  P4B = 29
  P4X = 30
  P4Y = 31
} derive(Debug, Eq)

///|
fn Button::to_int(self : Button) -> Int = "%identity"

///|
/// Returns whether the selected gamepad button is currently held.
pub fn btn(button : Button) -> Bool {
  @raw.raw_btn(button.to_int()) != 0
}

///|
/// Returns whether any button on any gamepad is currently held.
pub fn any_btn() -> Bool {
  @raw.raw_btn(-1) != 0
}

///|
/// An immutable snapshot of all 32 TIC-80 gamepad buttons.
#valtype
pub struct ButtonState {
  priv bits : UInt
} derive(Debug, Eq)

///|
/// Captures all four gamepads in one host call.
pub fn button_state() -> ButtonState {
  { bits: @raw.raw_btn(-1).reinterpret_as_uint() }
}

///|
/// Returns whether the selected button was held in this snapshot.
pub fn ButtonState::held(self : ButtonState, button : Button) -> Bool {
  (self.bits & (1U << button.to_int())) != 0U
}

///|
/// Returns whether this snapshot contains any held button.
pub fn ButtonState::any(self : ButtonState) -> Bool {
  self.bits != 0U
}

///|
/// Returns whether a button was newly pressed.
pub fn btnp(button : Button, hold? : Int = -1, period? : Int = -1) -> Bool {
  @raw.raw_btnp(button.to_int(), hold, period) != 0
}

///|
/// Returns whether any button was newly pressed.
pub fn any_btnp() -> Bool {
  @raw.raw_btnp(-1, -1, -1) != 0
}

///|
/// Sets the screen clipping rectangle.
pub fn clip(x : Int, y : Int, w : Int, h : Int) -> Unit {
  @raw.raw_clip(x, y, w, h)
}

///|
/// Restores the clipping rectangle to the full screen.
pub fn clip_reset() -> Unit {
  @raw.raw_clip(-1, -1, -1, -1)
}

///|
/// Clears the screen with a color.
pub fn cls(color? : Int = 0) -> Unit {
  @raw.raw_cls(color)
}

///|
/// Draws a filled circle centered at `(x, y)`.
pub fn circ(x : Int, y : Int, radius : Int, color : Int) -> Unit {
  @raw.raw_circ(x, y, radius, color)
}

///|
/// Draws a one-pixel circle outline centered at `(x, y)`.
pub fn circb(x : Int, y : Int, radius : Int, color : Int) -> Unit {
  @raw.raw_circb(x, y, radius, color)
}

///|
/// Draws a filled ellipse centered at `(x, y)` with the given semi-axes.
pub fn elli(x : Int, y : Int, a : Int, b : Int, color : Int) -> Unit {
  @raw.raw_elli(x, y, a, b, color)
}

///|
/// Draws a one-pixel ellipse outline centered at `(x, y)` with the given
/// semi-axes.
pub fn ellib(x : Int, y : Int, a : Int, b : Int, color : Int) -> Unit {
  @raw.raw_ellib(x, y, a, b, color)
}

///|
/// Stops the current cartridge and returns to the TIC-80 console.
pub fn exit() -> Unit {
  @raw.raw_exit()
}

///|
/// Returns whether one of a sprite's eight user-defined flags is set.
///
/// `sprite_id` ranges from 0 through 511 and `flag` ranges from 0 through 7.
/// Flag meanings are defined by the cartridge. Panics when either argument is
/// outside its valid range.
pub fn fget(sprite_id : Int, flag : Int) -> Bool {
  check_sprite_flag_args(sprite_id, flag)
  @raw.raw_fget(sprite_id, flag) != 0
}

///|
/// Sets or clears one of a sprite's eight user-defined flags.
///
/// `sprite_id` ranges from 0 through 511 and `flag` ranges from 0 through 7.
/// The change affects runtime RAM; use `sync` with the `Flags` section and
/// `to_cart=true` to copy runtime flags back to a cartridge resource bank.
/// Panics when either argument is outside its valid range.
pub fn fset(sprite_id : Int, flag : Int, value : Bool) -> Unit {
  check_sprite_flag_args(sprite_id, flag)
  @raw.raw_fset(sprite_id, flag, value)
}

///|
fn check_sprite_flag_args(sprite_id : Int, flag : Int) -> Unit {
  if sprite_id < 0 || sprite_id >= 512 {
    abort("sprite id must be between 0 and 511")
  }
  if flag < 0 || flag >= 8 {
    abort("sprite flag must be between 0 and 7")
  }
}

///|
/// Draws ASCII text with a custom raster font. Panics if `text` contains a
/// non-ASCII code unit. Transparent colors are passed directly from
/// `transparent_colors`.
pub fn font(
  text : StringView,
  x : Int,
  y : Int,
  transparent_colors? : Bytes = b"",
  char_width? : Int = -1,
  char_height? : Int = -1,
  fixed? : Bool = false,
  scale? : Int = -1,
  alt? : Bool = false,
) -> Int {
  @raw.raw_font(
    encode_c_text(text),
    x,
    y,
    transparent_colors,
    transparent_colors.length(),
    char_width,
    char_height,
    fixed,
    scale,
    alt,
  )
}

///|
/// A TIC-80 keyboard key.
///
/// The explicit tags are the key codes expected by the TIC-80 host ABI.
pub(all) enum Key {
  A = 1
  B = 2
  C = 3
  D = 4
  E = 5
  F = 6
  G = 7
  H = 8
  I = 9
  J = 10
  K = 11
  L = 12
  M = 13
  N = 14
  O = 15
  P = 16
  Q = 17
  R = 18
  S = 19
  T = 20
  U = 21
  V = 22
  W = 23
  X = 24
  Y = 25
  Z = 26
  Digit0 = 27
  Digit1 = 28
  Digit2 = 29
  Digit3 = 30
  Digit4 = 31
  Digit5 = 32
  Digit6 = 33
  Digit7 = 34
  Digit8 = 35
  Digit9 = 36
  Minus = 37
  Equals = 38
  LeftBracket = 39
  RightBracket = 40
  Backslash = 41
  Semicolon = 42
  Apostrophe = 43
  Grave = 44
  Comma = 45
  Period = 46
  Slash = 47
  Space = 48
  Tab = 49
  Return = 50
  Backspace = 51
  Delete = 52
  Insert = 53
  PageUp = 54
  PageDown = 55
  Home = 56
  End = 57
  Up = 58
  Down = 59
  Left = 60
  Right = 61
  CapsLock = 62
  Ctrl = 63
  Shift = 64
  Alt = 65
  Escape = 66
  F1 = 67
  F2 = 68
  F3 = 69
  F4 = 70
  F5 = 71
  F6 = 72
  F7 = 73
  F8 = 74
  F9 = 75
  F10 = 76
  F11 = 77
  F12 = 78
  Numpad0 = 79
  Numpad1 = 80
  Numpad2 = 81
  Numpad3 = 82
  Numpad4 = 83
  Numpad5 = 84
  Numpad6 = 85
  Numpad7 = 86
  Numpad8 = 87
  Numpad9 = 88
  NumpadPlus = 89
  NumpadMinus = 90
  NumpadMultiply = 91
  NumpadDivide = 92
  NumpadEnter = 93
  NumpadPeriod = 94
} derive(Debug, Eq)

///|
fn Key::to_int(self : Key) -> Int = "%identity"

///|
/// Returns whether the selected keyboard key is currently held.
pub fn key(key : Key) -> Bool {
  @raw.raw_key(key.to_int()) != 0
}

///|
/// Returns whether any keyboard key is currently held.
pub fn any_key() -> Bool {
  @raw.raw_key(-1) != 0
}

///|
/// Returns whether a key was newly pressed or repeated.
pub fn keyp(key : Key, hold? : Int = -1, period? : Int = -1) -> Bool {
  @raw.raw_keyp(key.to_int(), hold, period) != 0
}

///|
/// Returns whether any keyboard key was newly pressed.
pub fn any_keyp() -> Bool {
  @raw.raw_keyp(-1, -1, -1) != 0
}

///|
/// Draws a straight line from `(x0, y0)` to `(x1, y1)`.
pub fn line(
  x0 : Float,
  y0 : Float,
  x1 : Float,
  y1 : Float,
  color : Int,
) -> Unit {
  @raw.raw_line(x0, y0, x1, y1, color)
}

///|
/// A sprite or map tile flip applied by TIC-80.
pub(all) enum Flip {
  NoFlip = 0
  Horizontal = 1
  Vertical = 2
  Both = 3
} derive(Debug, Eq)

///|
fn Flip::to_int(self : Flip) -> Int = "%identity"

///|
/// A clockwise sprite or map tile rotation applied by TIC-80.
pub(all) enum Rotation {
  NoRotation = 0
  Clockwise90 = 1
  Clockwise180 = 2
  Clockwise270 = 3
} derive(Debug, Eq)

///|
fn Rotation::to_int(self : Rotation) -> Int = "%identity"

///|
/// The current tile state passed to a map remap callback, and the replacement
/// state returned by it.
#valtype
pub(all) struct MapRemapResult {
  tile_id : Int
  flip : Flip
  rotate : Rotation
} derive(Debug, Eq)

///|
/// Draws a map region. When present, `remap` receives the current tile state
/// and its map coordinates, and returns the state to draw. The callback must be
/// capture-free because it is stored as a WebAssembly function reference.
pub fn map(
  x? : Int = -1,
  y? : Int = -1,
  w? : Int = -1,
  h? : Int = -1,
  sx? : Int = 0,
  sy? : Int = 0,
  transparent_colors? : Bytes = b"",
  scale? : Int = -1,
  remap? : FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult],
) -> Unit {
  let map_data : @raw.MapData? = match remap {
    None => None
    Some(remap) =>
      Some({
        trampoline: map_remap_trampoline_ref,
        user_data: map_remap_to_int(remap),
        result_buffer: FixedArray::make(12, b'\x00'),
      })
  }
  @raw.raw_map(
    x,
    y,
    w,
    h,
    sx,
    sy,
    transparent_colors,
    transparent_colors.length(),
    scale,
    map_data,
  )
}

///|
let map_remap_trampoline_ref : FuncRef[
  (Int, Int, Int, FixedArray[Byte]) -> Unit,
] = fn(remap, x, y, result_buffer) {
  let current : MapRemapResult = {
    tile_id: result_buffer[0].to_int(),
    flip: decode_flip(decode_i32_le(result_buffer, 4)),
    rotate: decode_rotation(decode_i32_le(result_buffer, 8)),
  }
  let replacement = map_remap_from_int(remap)(current, x, y)
  result_buffer[0] = replacement.tile_id.to_byte()
  encode_i32_le(result_buffer, 4, replacement.flip.to_int())
  encode_i32_le(result_buffer, 8, replacement.rotate.to_int())
}

///|
fn map_remap_to_int(
  remap : FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult],
) -> Int = "%identity"

///|
fn map_remap_from_int(
  remap : Int,
) -> FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult] = "%identity"

///|
fn decode_flip(value : Int) -> Flip {
  match value & 3 {
    0 => NoFlip
    1 => Horizontal
    2 => Vertical
    _ => Both
  }
}

///|
fn decode_rotation(value : Int) -> Rotation {
  match value & 3 {
    0 => NoRotation
    1 => Clockwise90
    2 => Clockwise180
    _ => Clockwise270
  }
}

///|
fn decode_i32_le(buffer : FixedArray[Byte], offset : Int) -> Int {
  buffer[offset].to_int() |
  (buffer[offset + 1].to_int() << 8) |
  (buffer[offset + 2].to_int() << 16) |
  (buffer[offset + 3].to_int() << 24)
}

///|
fn encode_i32_le(buffer : FixedArray[Byte], offset : Int, value : Int) -> Unit {
  buffer[offset] = value.to_byte()
  buffer[offset + 1] = (value >> 8).to_byte()
  buffer[offset + 2] = (value >> 16).to_byte()
  buffer[offset + 3] = (value >> 24).to_byte()
}

///|
/// Copies `length` bytes within TIC-80's 96 KiB RAM.
///
/// `dest` and `src` are byte addresses. The source and destination ranges may
/// overlap. A negative length or a range outside TIC-80 RAM is ignored. This
/// function does not copy MoonBit heap objects or `Bytes` values.
pub fn memcpy(dest : Int, src : Int, length : Int) -> Unit {
  @raw.raw_memcpy(dest, src, length)
}

///|
/// Fills `length` bytes of TIC-80 RAM with `value`.
///
/// `address` is a byte address. A negative length or a range outside TIC-80 RAM
/// is ignored. This function does not operate on MoonBit heap objects or
/// `Bytes` values.
pub fn memset(address : Int, value : Byte, length : Int) -> Unit {
  @raw.raw_memset(address, value, length)
}

///|
/// Returns the tile ID at map coordinates `(x, y)`.
pub fn mget(x : Int, y : Int) -> Int {
  @raw.raw_mget(x, y)
}

///|
/// Sets the tile ID at map coordinates `(x, y)`.
pub fn mset(x : Int, y : Int, tile_id : Int) -> Unit {
  @raw.raw_mset(x, y, tile_id)
}

///|
/// A snapshot of the mouse cursor, wheel, and button state.
///
/// `x` and `y` are screen coordinates. `scroll_x` and `scroll_y` are signed
/// wheel movements for the current frame.
pub(all) struct MouseState {
  x : Int
  y : Int
  scroll_x : Int
  scroll_y : Int
  left : Bool
  middle : Bool
  right : Bool
} derive(Debug, Eq)

///|
/// Returns an independent snapshot of the current mouse state.
pub fn mouse() -> MouseState {
  let buffer = FixedArray::make(9, b'\x00')
  @raw.raw_mouse(buffer)
  decode_mouse_state(buffer)
}

///|
fn decode_mouse_state(buffer : FixedArray[Byte]) -> MouseState {
  {
    x: decode_i16_le(buffer, 0),
    y: decode_i16_le(buffer, 2),
    scroll_x: decode_i8(buffer[4]),
    scroll_y: decode_i8(buffer[5]),
    left: buffer[6] != b'\x00',
    middle: buffer[7] != b'\x00',
    right: buffer[8] != b'\x00',
  }
}

///|
fn decode_i16_le(buffer : FixedArray[Byte], offset : Int) -> Int {
  let bits = buffer[offset].to_int() | (buffer[offset + 1].to_int() << 8)
  if (bits & 0x8000) != 0 {
    bits - 0x10000
  } else {
    bits
  }
}

///|
fn decode_i8(byte : Byte) -> Int {
  let bits = byte.to_int()
  if (bits & 0x80) != 0 {
    bits - 0x100
  } else {
    bits
  }
}

///|
/// Starts playing one of the eight tracks created in TIC-80's Music Editor.
///
/// `track` ranges from 0 through 7. By default playback starts at the beginning
/// of the track, loops after its final populated frame, does not sustain notes
/// across frame boundaries, and uses the tempo and speed stored in the track.
/// Use `frame` and `row` to start at a specific position, or `tempo` and `speed`
/// to override the track settings. Call `music_stop` to stop playback.
/// Panics if `track` is outside the range 0 through 7.
pub fn music(
  track : Int,
  frame? : Int = -1,
  row? : Int = -1,
  loop_? : Bool = true,
  sustain? : Bool = false,
  tempo? : Int = -1,
  speed? : Int = -1,
) -> Unit {
  check_music_track(track)
  @raw.raw_music(track, frame, row, loop_, sustain, tempo, speed)
}

///|
fn check_music_track(track : Int) -> Unit {
  if track < 0 || track >= 8 {
    abort("music: track must be between 0 and 7")
  }
}

///|
/// Stops the currently playing music and resets its channels.
///
/// This does not stop sound effects started independently with `sfx`.
pub fn music_stop() -> Unit {
  @raw.raw_music(-1, -1, -1, false, false, -1, -1)
}

///|
/// Returns the color of a pixel.
pub fn pix(x : Int, y : Int) -> Byte {
  @raw.raw_pix(x, y, -1)
}

///|
/// Draws a pixel.
pub fn pix_set(x : Int, y : Int, color : Int) -> Unit {
  ignore(@raw.raw_pix(x, y, color))
}

///|
/// Reads one byte from TIC-80 RAM at the byte address `address`.
///
/// Returns zero when `address` is outside TIC-80 RAM. Use `peek4`, `peek2`, or
/// `peek1` when the address is expressed in narrower units.
pub fn peek(address : Int) -> Byte {
  @raw.raw_peek(address, 8)
}

///|
/// Reads one four-bit nibble from TIC-80 RAM.
///
/// `address` is a nibble index rather than a byte address. Addresses `2 * n`
/// and `2 * n + 1` select the low and high nibbles of byte `n`, respectively.
/// Returns zero when the address is outside TIC-80 RAM.
pub fn peek4(address : Int) -> Byte {
  @raw.raw_peek4(address)
}

///|
/// Reads one two-bit value from TIC-80 RAM.
///
/// `address` is a two-bit-field index. Addresses `4 * n` through `4 * n + 3`
/// select the four fields of byte `n` from least to most significant. Returns
/// zero when the address is outside TIC-80 RAM.
pub fn peek2(address : Int) -> Byte {
  @raw.raw_peek2(address)
}

///|
/// Reads one bit from TIC-80 RAM.
///
/// `address` is a bit index. Addresses `8 * n` through `8 * n + 7` select the
/// bits of byte `n` from least to most significant. Returns zero when the
/// address is outside TIC-80 RAM.
pub fn peek1(address : Int) -> Byte {
  @raw.raw_peek1(address)
}

///|
/// Reads a value from a persistent-memory slot.
///
/// `index` ranges from 0 through 255. Panics when it is outside that range.
pub fn pmem(index : Int) -> UInt {
  check_pmem_index(index)
  @raw.raw_pmem(index, -1L)
}

///|
/// Writes a persistent-memory slot and returns its previous value.
///
/// `index` ranges from 0 through 255. Panics when it is outside that range.
pub fn pmem_set(index : Int, value : UInt) -> UInt {
  check_pmem_index(index)
  @raw.raw_pmem(index, value.to_int64())
}

///|
fn check_pmem_index(index : Int) -> Unit {
  if index < 0 || index >= 256 {
    abort("persistent-memory index must be between 0 and 255")
  }
}

///|
/// Writes one byte to TIC-80 RAM at the byte address `address`.
///
/// Writes outside TIC-80 RAM are ignored. Use `poke4`, `poke2`, or `poke1`
/// when the address is expressed in narrower units.
pub fn poke(address : Int, value : Byte) -> Unit {
  @raw.raw_poke(address, value.to_int(), 8)
}

///|
/// Writes one four-bit nibble to TIC-80 RAM.
///
/// `address` uses the nibble indexing described by `peek4`; only the low four
/// bits of `value` are stored. Writes outside TIC-80 RAM are ignored.
pub fn poke4(address : Int, value : Byte) -> Unit {
  @raw.raw_poke4(address, value.to_int())
}

///|
/// Writes one two-bit value to TIC-80 RAM.
///
/// `address` uses the two-bit-field indexing described by `peek2`; only the
/// low two bits of `value` are stored. Writes outside TIC-80 RAM are ignored.
pub fn poke2(address : Int, value : Byte) -> Unit {
  @raw.raw_poke2(address, value.to_int())
}

///|
/// Writes one bit to TIC-80 RAM.
///
/// `address` uses the bit indexing described by `peek1`; only the lowest bit
/// of `value` is stored. Writes outside TIC-80 RAM are ignored.
pub fn poke1(address : Int, value : Byte) -> Unit {
  @raw.raw_poke1(address, value.to_int())
}

///|
/// Draws ASCII text and returns its width. Panics if `text` contains a
/// non-ASCII code unit.
pub fn print(
  text : StringView,
  x? : Int = 0,
  y? : Int = 0,
  color? : Int = 15,
  fixed? : Bool = false,
  scale? : Int = 1,
  small_font? : Bool = false,
) -> Int {
  @raw.raw_print(encode_c_text(text), x, y, color, fixed, scale, small_font)
}

///|
/// Draws a filled rectangle at `(x, y)` with the given size.
pub fn rect(x : Int, y : Int, w : Int, h : Int, color : Int) -> Unit {
  @raw.raw_rect(x, y, w, h, color)
}

///|
/// Draws a one-pixel rectangle outline at `(x, y)` with the given size.
pub fn rectb(x : Int, y : Int, w : Int, h : Int, color : Int) -> Unit {
  @raw.raw_rectb(x, y, w, h, color)
}

///|
/// One of TIC-80's four sound-effect channels.
///
/// The explicit tags are the channel IDs expected by the TIC-80 host ABI.
pub(all) enum SfxChannel {
  Channel0 = 0
  Channel1 = 1
  Channel2 = 2
  Channel3 = 3
} derive(Debug, Eq)

///|
fn SfxChannel::to_int(self : SfxChannel) -> Int = "%identity"

///|
/// Plays one of the 64 effects created in TIC-80's SFX Editor.
///
/// `note` is a combined note number from 0 through 95, with twelve notes per
/// octave. When omitted, the note stored in the selected effect is used.
/// `duration` is measured in 60 Hz ticks; -1 plays continuously. The left and
/// right volumes range from 0 through 15. When `speed` is omitted, the speed
/// stored in the selected effect is used; explicit speeds range from -4 through
/// 3. Call `sfx_stop` to stop an effect on a channel.
///
/// Panics if `id` is outside the range 0 through 63.
pub fn sfx(
  id : Int,
  note? : Int,
  duration? : Int = -1,
  channel? : SfxChannel = Channel0,
  volume_left? : Int = 15,
  volume_right? : Int = 15,
  speed? : Int,
) -> Unit {
  if id < 0 || id >= 64 {
    abort("sfx: id must be between 0 and 63")
  }
  let combined_note = match note {
    None => read_sfx_default_note(id)
    Some(note) => note
  }
  let (raw_note, raw_octave) = split_sfx_note(combined_note)
  let raw_speed = match speed {
    None => 8
    Some(speed) => speed
  }
  @raw.raw_sfx(
    id,
    raw_note,
    raw_octave,
    duration,
    channel.to_int(),
    volume_left,
    volume_right,
    raw_speed,
  )
}

///|
/// Stops the sound effect playing on `channel`.
///
/// This does not stop music playback or effects on the other channels.
pub fn sfx_stop(channel? : SfxChannel = Channel0) -> Unit {
  @raw.raw_sfx(-1, 0, 0, -1, channel.to_int(), 15, 15, 8)
}

///|
fn read_sfx_default_note(id : Int) -> Int {
  // TIC-80 stores 64 66-byte effects at 0x100e4. The final two bytes hold
  // the effect's octave, speed, note, and stereo flags.
  let metadata_address = 0x100e4 + id * 66 + 64
  decode_sfx_default_note(peek(metadata_address), peek(metadata_address + 1))
}

///|
fn decode_sfx_default_note(metadata0 : Byte, metadata1 : Byte) -> Int {
  let octave = metadata0.to_int() & 0x07
  let note = metadata1.to_int() & 0x0f
  note + octave * 12
}

///|
fn split_sfx_note(note : Int) -> (Int, Int) {
  (note % 12, note / 12)
}

///|
/// Transparent colors are passed directly from `transparent_colors`.
pub fn spr(
  id : Int,
  x : Int,
  y : Int,
  transparent_colors? : Bytes = b"",
  scale? : Int = 1,
  flip? : Flip = NoFlip,
  rotate? : Rotation = NoRotation,
  w? : Int = 1,
  h? : Int = 1,
) -> Unit {
  @raw.raw_spr(
    id,
    x,
    y,
    transparent_colors,
    transparent_colors.length(),
    scale,
    flip.to_int(),
    rotate.to_int(),
    w,
    h,
  )
}

///|
/// A cartridge resource section that can be transferred by `sync`.
///
/// Each variant corresponds to one bit in TIC-80's resource synchronization
/// mask.
pub(all) enum SyncSection {
  Tiles
  Sprites
  Map
  Sfx
  Music
  Palette
  Flags
  Screen
} derive(Debug, Eq)

///|
fn SyncSection::to_int(self : SyncSection) -> Int = "%identity"

///|
/// Synchronizes cartridge resources with runtime memory. An empty `sections`
/// list selects every section. By default, data is copied from bank 0 into
/// runtime memory; set `to_cart` to copy runtime data back to the bank. Each
/// section can only be synchronized once per frame.
pub fn sync(
  sections? : ArrayView[SyncSection] = [],
  bank? : Int = 0,
  to_cart? : Bool = false,
) -> Unit {
  @raw.raw_sync(encode_sync_mask(sections), bank, to_cart)
}

///|
fn encode_sync_mask(sections : ArrayView[SyncSection]) -> Int {
  sections.fold(init=0, (mask, section) => mask | (1 << section.to_int()))
}

///|
/// Returns the elapsed time in milliseconds since the cartridge started.
pub fn time() -> Float {
  @raw.raw_time()
}

///|
/// Returns the current Unix timestamp in seconds.
pub fn tstamp() -> UInt {
  @raw.raw_tstamp()
}

///|
/// Writes ASCII text to the console. Panics if `text` contains a non-ASCII
/// code unit.
pub fn trace(text : StringView, color? : Int = -1) -> Unit {
  @raw.raw_trace(encode_c_text(text), color)
}

///|
fn encode_c_text(text : StringView) -> Bytes {
  @ascii.encode(text) + b"\x00"
}

///|
/// Draws a filled triangle with the given vertices.
pub fn tri(
  x1 : Float,
  y1 : Float,
  x2 : Float,
  y2 : Float,
  x3 : Float,
  y3 : Float,
  color : Int,
) -> Unit {
  @raw.raw_tri(x1, y1, x2, y2, x3, y3, color)
}

///|
/// Draws a one-pixel triangle outline with the given vertices.
pub fn trib(
  x1 : Float,
  y1 : Float,
  x2 : Float,
  y2 : Float,
  x3 : Float,
  y3 : Float,
  color : Int,
) -> Unit {
  @raw.raw_trib(x1, y1, x2, y2, x3, y3, color)
}

///|
/// The texture sampled by `ttri`.
///
/// `TileSheet` samples image RAM through the active tile or sprite-sheet
/// segment. `TileMap` samples map RAM and resolves its tile IDs through image
/// RAM. `OtherVideoBank` samples the framebuffer of the video bank that is not
/// currently selected by `vbank_set`.
pub(all) enum TextureSource {
  TileSheet = 0
  TileMap = 1
  OtherVideoBank = 2
} derive(Debug, Eq)

///|
fn TextureSource::to_int(self : TextureSource) -> Int = "%identity"

///|
/// Draws a triangle textured from `texture_source`.
///
/// The `u` and `v` coordinates are interpreted in the coordinate space of the
/// selected source. When `OtherVideoBank` is selected, `ttri` samples whichever
/// video bank is not currently selected: it normally samples `Bank1` while
/// drawing to `Bank0`, and samples `Bank0` while drawing to `Bank1`.
/// Transparent colors are passed directly from `transparent_colors`.
pub fn ttri(
  x1 : Float,
  y1 : Float,
  x2 : Float,
  y2 : Float,
  x3 : Float,
  y3 : Float,
  u1 : Float,
  v1 : Float,
  u2 : Float,
  v2 : Float,
  u3 : Float,
  v3 : Float,
  texture_source? : TextureSource = TileSheet,
  transparent_colors? : Bytes = b"",
  z1? : Float = 0.0,
  z2? : Float = 0.0,
  z3? : Float = 0.0,
  depth? : Bool = false,
) -> Unit {
  @raw.raw_ttri(
    x1,
    y1,
    x2,
    y2,
    x3,
    y3,
    u1,
    v1,
    u2,
    v2,
    u3,
    v3,
    texture_source.to_int(),
    transparent_colors,
    transparent_colors.length(),
    z1,
    z2,
    z3,
    depth,
  )
}

///|
/// One of TIC-80's two 16 KiB video-memory banks.
///
/// Drawing commands and direct VRAM access operate on the selected bank.
/// `Bank1` is composited over `Bank0`; pixels matching `Bank1`'s clear color
/// are transparent and reveal `Bank0`. Video banks are always available and
/// are unrelated to the cartridge resource banks selected by `sync`.
pub(all) enum VideoBank {
  Bank0 = 0
  Bank1 = 1
} derive(Debug, Eq)

///|
fn VideoBank::to_int(self : VideoBank) -> Int = "%identity"

///|
/// Returns the currently selected video bank without changing it.
///
/// Use `vbank_set` when subsequent drawing or direct VRAM operations should
/// target a different bank.
pub fn vbank() -> VideoBank {
  decode_video_bank(@raw.raw_vbank(-1))
}

///|
/// Selects the video bank used by subsequent drawing and direct VRAM
/// operations, and returns the previously selected bank.
///
/// The return value makes temporary switching straightforward: save it, draw
/// into another bank, then pass it back to `vbank_set` to restore the previous
/// target. This only switches VRAM; it does not select a cartridge resource
/// bank for `sync`.
pub fn vbank_set(bank : VideoBank) -> VideoBank {
  decode_video_bank(@raw.raw_vbank(bank.to_int()))
}

///|
fn decode_video_bank(value : Byte) -> VideoBank {
  if value == b'\x00' {
    Bank0
  } else {
    Bank1
  }
}