///|
fn ase_chunk_kind_name(kind : Int) -> String {
  match kind {
    0x0004 => "old-palette"
    0x0011 => "old-palette-2"
    0x2004 => "layer"
    0x2005 => "cel"
    0x2006 => "cel-extra"
    0x2007 => "color-profile"
    0x2016 => "tags"
    0x2017 => "path"
    0x2018 => "palette"
    0x2019 => "user-data"
    0x2020 => "slice"
    0x2022 => "tileset"
    _ => "unknown"
  }
}

///|
fn parse_ase_header(reader : ByteReader) -> AseParseResult {
  if !reader.slice_can_read(0, 128) {
    AseParseResult::failure(
      PixelAnimError::new(ErrorTruncatedData, "ASE data is shorter than header"),
    )
  } else if !reader.matches_u16(4, 0xA5E0) {
    AseParseResult::failure(
      PixelAnimError::new(
        ErrorNotAseprite,
        "expected Aseprite magic 0xA5E0",
        offset=4,
      ),
    )
  } else {
    let header = {
      file_size: reader.u32_le_at(0),
      frame_count: reader.u16_le_at(6),
      width: reader.u16_le_at(8),
      height: reader.u16_le_at(10),
      color_depth: reader.u16_le_at(12),
      flags: reader.u32_le_at(14),
      speed_ms: reader.u16_le_at(18),
      transparent_index: reader.byte_at(26),
      color_count: reader.u16_le_at(32),
      pixel_width: reader.byte_at(34),
      pixel_height: reader.byte_at(35),
    }
    AseParseResult::success({
      header,
      frames: [],
      source_length: reader.length(),
    })
  }
}

///|
pub fn parse_aseprite_file(bytes : Array[Int]) -> AseParseResult {
  let reader = ByteReader::new(bytes)
  let header_result = parse_ase_header(reader)
  if !header_result.ok {
    header_result
  } else {
    let file = header_result.file
    let frames : Array[AseFrame] = []
    let mut offset = 128
    for frame_index in 0.. 0 {
        new_chunk_count
      } else {
        old_chunk_count
      }
      let chunks = parse_frame_chunks(
        reader,
        frame_index,
        offset + 16,
        offset + frame_size,
        chunk_count,
      )
      frames.push(
        AseFrame::new(
          frame_index, offset, frame_size, duration, chunk_count, chunks,
        ),
      )
      offset = offset + frame_size
    }
    AseParseResult::success({
      header: file.header,
      frames,
      source_length: reader.length(),
    })
  }
}

///|
fn parse_frame_chunks(
  reader : ByteReader,
  frame_index : Int,
  start : Int,
  frame_end : Int,
  expected_count : Int,
) -> Array[AseChunk] {
  let chunks : Array[AseChunk] = []
  let mut offset = start
  let mut count = 0
  while offset + 6 <= frame_end && count < expected_count {
    let size = reader.u32_le_at(offset)
    let kind = reader.u16_le_at(offset + 4)
    if size < 6 || offset + size > frame_end {
      chunks.push(
        AseChunk::new(
          frame_index,
          offset,
          if size < 6 {
            6
          } else {
            size
          },
          kind,
          "invalid",
          extra="chunk bounds exceed frame",
        ),
      )
      break
    }
    chunks.push(parse_chunk_payload(reader, frame_index, offset, size, kind))
    offset = offset + size
    count = count + 1
  }
  chunks
}

///|
fn parse_chunk_payload(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  kind : Int,
) -> AseChunk {
  let data = offset + 6
  match kind {
    0x2004 => parse_layer_chunk(reader, frame_index, offset, size, data, kind)
    0x2005 => parse_cel_chunk(reader, frame_index, offset, size, data, kind)
    0x2016 => parse_tags_chunk(reader, frame_index, offset, size, data, kind)
    0x2018 => parse_palette_chunk(reader, frame_index, offset, size, data, kind)
    0x2020 => parse_slice_chunk(reader, frame_index, offset, size, data, kind)
    _ =>
      AseChunk::new(
        frame_index,
        offset,
        size,
        kind,
        ase_chunk_kind_name(kind),
        extra="metadata skipped",
      )
  }
}

///|
fn parse_layer_chunk(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  data : Int,
  kind : Int,
) -> AseChunk {
  let child_level = reader.u16_le_at(data + 2)
  let opacity = reader.byte_at(data + 16)
  let name_size = reader.u16_le_at(data + 18)
  let name = reader.ascii_fixed_at(data + 20, name_size)
  AseChunk::new(
    frame_index,
    offset,
    size,
    kind,
    ase_chunk_kind_name(kind),
    name~,
    opacity~,
    child_level~,
    extra="layer metadata",
  )
}

///|
fn parse_cel_chunk(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  data : Int,
  kind : Int,
) -> AseChunk {
  let layer_index = reader.u16_le_at(data)
  let opacity = reader.byte_at(data + 10)
  let cel_type = reader.u16_le_at(data + 11)
  let extra = match cel_type {
    0 => "raw-cel"
    1 => "linked-cel"
    2 => "compressed-cel"
    3 => "tilemap-cel"
    _ => "unknown-cel"
  }
  AseChunk::new(
    frame_index,
    offset,
    size,
    kind,
    ase_chunk_kind_name(kind),
    layer_index~,
    cel_type~,
    opacity~,
    extra~,
  )
}

///|
fn parse_tags_chunk(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  data : Int,
  kind : Int,
) -> AseChunk {
  let count = reader.u16_le_at(data)
  let mut name = ""
  if count > 0 {
    let first = data + 10
    let name_size = reader.u16_le_at(first + 9)
    name = reader.ascii_fixed_at(first + 11, name_size)
  }
  AseChunk::new(
    frame_index,
    offset,
    size,
    kind,
    ase_chunk_kind_name(kind),
    name~,
    extra="tag-count=\{count}",
  )
}

///|
fn parse_palette_chunk(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  data : Int,
  kind : Int,
) -> AseChunk {
  let entries = reader.u32_le_at(data)
  let first = reader.u32_le_at(data + 4)
  let last = reader.u32_le_at(data + 8)
  AseChunk::new(
    frame_index,
    offset,
    size,
    kind,
    ase_chunk_kind_name(kind),
    extra="entries=\{entries}, range=\{first}..\{last}",
  )
}

///|
fn parse_slice_chunk(
  reader : ByteReader,
  frame_index : Int,
  offset : Int,
  size : Int,
  data : Int,
  kind : Int,
) -> AseChunk {
  let keys = reader.u32_le_at(data)
  let flags = reader.u32_le_at(data + 4)
  let name_size = reader.u16_le_at(data + 16)
  let name = reader.ascii_fixed_at(data + 18, name_size)
  AseChunk::new(
    frame_index,
    offset,
    size,
    kind,
    ase_chunk_kind_name(kind),
    name~,
    extra="slice-keys=\{keys}, flags=\{flags}",
  )
}

///|
pub fn ase_file_supports_runtime_manifest(file : AseFile) -> Bool {
  file.header.width > 0 &&
  file.header.height > 0 &&
  file.frames.length() == file.header.frame_count
}

///|
pub fn collect_ase_chunk_names(file : AseFile) -> Array[String] {
  let names : Array[String] = []
  for frame in file.frames {
    for chunk in frame.chunks {
      names.push(chunk.kind_name)
    }
  }
  names
}

///|
pub fn ase_has_compressed_cels(file : AseFile) -> Bool {
  for frame in file.frames {
    for chunk in frame.chunks {
      if chunk.kind == 0x2005 && chunk.cel_type == 2 {
        return true
      }
    }
  }
  false
}