///|
pub fn fixture_aseprite_json() -> String {
  "{" +
  "\"frames\":[" +
  "{" +
  "\"filename\":\"hero_idle_0\"," +
  "\"frame\":{\"x\":0,\"y\":0,\"w\":16,\"h\":16}," +
  "\"rotated\":false,\"trimmed\":false," +
  "\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":16,\"h\":16}," +
  "\"sourceSize\":{\"w\":16,\"h\":16}," +
  "\"duration\":120" +
  "}," +
  "{" +
  "\"filename\":\"hero_idle_1\"," +
  "\"frame\":{\"x\":16,\"y\":0,\"w\":16,\"h\":16}," +
  "\"rotated\":false,\"trimmed\":false," +
  "\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":16,\"h\":16}," +
  "\"sourceSize\":{\"w\":16,\"h\":16}," +
  "\"duration\":120" +
  "}," +
  "{" +
  "\"filename\":\"hero_run_0\"," +
  "\"frame\":{\"x\":0,\"y\":16,\"w\":16,\"h\":16}," +
  "\"rotated\":false,\"trimmed\":true," +
  "\"spriteSourceSize\":{\"x\":1,\"y\":0,\"w\":14,\"h\":16}," +
  "\"sourceSize\":{\"w\":16,\"h\":16}," +
  "\"duration\":80" +
  "}," +
  "{" +
  "\"filename\":\"hero_run_1\"," +
  "\"frame\":{\"x\":16,\"y\":16,\"w\":16,\"h\":16}," +
  "\"rotated\":false,\"trimmed\":true," +
  "\"spriteSourceSize\":{\"x\":1,\"y\":0,\"w\":14,\"h\":16}," +
  "\"sourceSize\":{\"w\":16,\"h\":16}," +
  "\"duration\":80" +
  "}" +
  "]," +
  "\"meta\":{" +
  "\"app\":\"aseprite\",\"version\":\"1.3-fixture\",\"image\":\"hero.png\",\"format\":\"RGBA8888\"," +
  "\"size\":{\"w\":32,\"h\":32},\"scale\":\"1\"," +
  "\"frameTags\":[" +
  "{\"name\":\"idle\",\"from\":0,\"to\":1,\"direction\":\"forward\",\"color\":\"#7ec8e3\"}," +
  "{\"name\":\"run\",\"from\":2,\"to\":3,\"direction\":\"pingpong\",\"color\":\"#f9c74f\"}" +
  "]," +
  "\"layers\":[" +
  "{\"name\":\"body\",\"opacity\":255,\"blendMode\":\"normal\",\"visible\":true}," +
  "{\"name\":\"weapon\",\"opacity\":220,\"blendMode\":\"normal\",\"visible\":true}" +
  "]," +
  "\"slices\":[" +
  "{\"name\":\"hitbox_body\",\"color\":\"#ff0000\",\"keys\":[{\"frame\":0,\"bounds\":{\"x\":4,\"y\":2,\"w\":8,\"h\":12},\"pivot\":{\"x\":8,\"y\":14}}]}," +
  "{\"name\":\"hurtbox_body\",\"color\":\"#00ff00\",\"keys\":[{\"frame\":0,\"bounds\":{\"x\":3,\"y\":1,\"w\":10,\"h\":14}}]}," +
  "{\"name\":\"collision_feet\",\"color\":\"#0000ff\",\"keys\":[{\"frame\":0,\"bounds\":{\"x\":5,\"y\":13,\"w\":6,\"h\":3}}]}" +
  "]" +
  "}" +
  "}"
}

///|
pub fn fixture_object_frames_json() -> String {
  "{" +
  "\"frames\":{" +
  "\"coin_0\":{\"frame\":{\"x\":0,\"y\":0,\"w\":8,\"h\":8},\"rotated\":false,\"trimmed\":false,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":8,\"h\":8},\"sourceSize\":{\"w\":8,\"h\":8},\"duration\":90}," +
  "\"coin_1\":{\"frame\":{\"x\":8,\"y\":0,\"w\":8,\"h\":8},\"rotated\":false,\"trimmed\":false,\"spriteSourceSize\":{\"x\":0,\"y\":0,\"w\":8,\"h\":8},\"sourceSize\":{\"w\":8,\"h\":8},\"duration\":90}" +
  "}," +
  "\"meta\":{\"image\":\"coin.png\",\"format\":\"RGBA8888\",\"size\":{\"w\":16,\"h\":8},\"scale\":\"1\",\"frameTags\":[{\"name\":\"spin\",\"from\":0,\"to\":1,\"direction\":\"forward\"}],\"slices\":[]}" +
  "}"
}

///|
pub fn fixture_invalid_sprite_json() -> String {
  "{" +
  "\"frames\":[" +
  "{\"filename\":\"bad\",\"frame\":{\"x\":0,\"y\":0,\"w\":0,\"h\":16},\"duration\":0}," +
  "{\"filename\":\"bad\",\"frame\":{\"x\":40,\"y\":0,\"w\":16,\"h\":16},\"duration\":100}" +
  "]," +
  "\"meta\":{\"image\":\"bad.png\",\"size\":{\"w\":32,\"h\":16},\"frameTags\":[{\"name\":\"broken\",\"from\":0,\"to\":5,\"direction\":\"forward\"}],\"slices\":[{\"name\":\"hitbox\",\"keys\":[{\"frame\":99,\"bounds\":{\"x\":0,\"y\":0,\"w\":8,\"h\":8}}]}]}" +
  "}"
}

///|
pub fn fixture_malformed_json() -> String {
  "{\"frames\":"
}

///|
pub fn fixture_not_object_json() -> String {
  "[1,2,3]"
}

///|
fn push_byte(bytes : Array[Int], value : Int) -> Unit {
  bytes.push(value & 0xff)
}

///|
fn push_u16_le(bytes : Array[Int], value : Int) -> Unit {
  push_byte(bytes, value)
  push_byte(bytes, value >> 8)
}

///|
fn push_u32_le(bytes : Array[Int], value : Int) -> Unit {
  push_byte(bytes, value)
  push_byte(bytes, value >> 8)
  push_byte(bytes, value >> 16)
  push_byte(bytes, value >> 24)
}

///|
fn set_u32_le(bytes : Array[Int], offset : Int, value : Int) -> Unit {
  bytes[offset] = value & 0xff
  bytes[offset + 1] = (value >> 8) & 0xff
  bytes[offset + 2] = (value >> 16) & 0xff
  bytes[offset + 3] = (value >> 24) & 0xff
}

///|
fn push_ascii(bytes : Array[Int], text : String) -> Unit {
  for i in 0.. Unit {
  while bytes.length() < target {
    bytes.push(0)
  }
}

///|
fn push_ase_header(bytes : Array[Int], file_size : Int) -> Unit {
  push_u32_le(bytes, file_size)
  push_u16_le(bytes, 0xA5E0)
  push_u16_le(bytes, 1)
  push_u16_le(bytes, 32)
  push_u16_le(bytes, 32)
  push_u16_le(bytes, 32)
  push_u32_le(bytes, 1)
  push_u16_le(bytes, 100)
  push_u32_le(bytes, 0)
  push_u32_le(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_u16_le(bytes, 256)
  push_byte(bytes, 1)
  push_byte(bytes, 1)
  pad_to(bytes, 128)
}

///|
fn push_layer_chunk(bytes : Array[Int], name : String) -> Unit {
  let start = bytes.length()
  push_u32_le(bytes, 0)
  push_u16_le(bytes, 0x2004)
  push_u16_le(bytes, 1)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_byte(bytes, 255)
  push_byte(bytes, 0)
  push_u16_le(bytes, name.length())
  push_ascii(bytes, name)
  set_u32_le(bytes, start, bytes.length() - start)
}

///|
fn push_cel_chunk(bytes : Array[Int]) -> Unit {
  let start = bytes.length()
  push_u32_le(bytes, 0)
  push_u16_le(bytes, 0x2005)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_byte(bytes, 255)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  set_u32_le(bytes, start, bytes.length() - start)
}

///|
fn push_tag_chunk(bytes : Array[Int], name : String) -> Unit {
  let start = bytes.length()
  push_u32_le(bytes, 0)
  push_u16_le(bytes, 0x2016)
  push_u16_le(bytes, 1)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 0)
  push_u16_le(bytes, 0)
  push_u16_le(bytes, 0)
  push_byte(bytes, 0)
  push_byte(bytes, 255)
  push_byte(bytes, 255)
  push_byte(bytes, 255)
  push_byte(bytes, 0)
  push_u16_le(bytes, name.length())
  push_ascii(bytes, name)
  set_u32_le(bytes, start, bytes.length() - start)
}

///|
fn push_slice_chunk(bytes : Array[Int], name : String) -> Unit {
  let start = bytes.length()
  push_u32_le(bytes, 0)
  push_u16_le(bytes, 0x2020)
  push_u32_le(bytes, 1)
  push_u32_le(bytes, 1)
  push_u32_le(bytes, 0)
  push_u32_le(bytes, 0)
  push_u16_le(bytes, name.length())
  push_ascii(bytes, name)
  set_u32_le(bytes, start, bytes.length() - start)
}

///|
pub fn fixture_aseprite_binary() -> Array[Int] {
  let bytes : Array[Int] = []
  push_ase_header(bytes, 0)
  let frame_start = bytes.length()
  push_u32_le(bytes, 0)
  push_u16_le(bytes, 0xF1FA)
  push_u16_le(bytes, 4)
  push_u16_le(bytes, 100)
  push_u16_le(bytes, 0)
  push_u32_le(bytes, 4)
  push_layer_chunk(bytes, "Body")
  push_cel_chunk(bytes)
  push_tag_chunk(bytes, "idle")
  push_slice_chunk(bytes, "hitbox")
  let file_size = bytes.length()
  let frame_size = file_size - frame_start
  set_u32_le(bytes, 0, file_size)
  set_u32_le(bytes, frame_start, frame_size)
  bytes
}

///|
pub fn fixture_truncated_aseprite_binary() -> Array[Int] {
  [0x10, 0x00, 0x00, 0x00, 0xE0, 0xA5]
}

///|
pub fn fixture_not_aseprite_binary() -> Array[Int] {
  Array::makei(128, i => {
    if i == 4 {
      0x00
    } else if i == 5 {
      0x00
    } else {
      i & 0xff
    }
  })
}

///|
pub fn fixture_catalog_passed() -> Bool {
  let parsed = parse_sprite_sheet_json(fixture_aseprite_json())
  let object = parse_sprite_sheet_json(fixture_object_frames_json())
  let invalid = parse_sprite_sheet_json(fixture_invalid_sprite_json())
  let binary = parse_aseprite_binary(fixture_aseprite_binary())
  parsed.ok &&
  object.ok &&
  invalid.ok &&
  !validate_sprite_sheet(invalid.sheet).ok &&
  binary.ok &&
  validate_ase_file(binary.file).ok
}