///|
fn parse_sprite_sheet_root(json : Json, source_length~ : Int) -> SpriteSheet {
  let frames = parse_frames(json_field(json, "frames"))
  { frames, meta: parse_meta(json_field(json, "meta")), source_length }
}

///|
fn parse_frames(value : Json?) -> Array[FrameInfo] {
  match value {
    Some(Array(items)) => parse_frame_array(items)
    Some(Object(_)) => parse_frame_object(value)
    _ => []
  }
}

///|
fn parse_frame_array(items : Array[Json]) -> Array[FrameInfo] {
  let frames : Array[FrameInfo] = []
  for i, item in items {
    match item {
      Object(_) =>
        frames.push(
          parse_frame_item(
            item,
            i,
            json_string_value(
              json_field(item, "filename"),
              default="frame_\{i}",
            ),
          ),
        )
      _ => ()
    }
  }
  frames
}

///|
fn parse_frame_object(value : Json?) -> Array[FrameInfo] {
  let frames : Array[FrameInfo] = []
  let pairs = json_object_pairs(value)
  for i, pair in pairs {
    let (name, item) = pair
    match item {
      Object(_) => frames.push(parse_frame_item(item, i, name))
      _ => ()
    }
  }
  frames
}

///|
fn parse_frame_item(json : Json, index : Int, name : String) -> FrameInfo {
  FrameInfo::new(
    name,
    index,
    json_rect_value(json_field(json, "frame")),
    json_int_value(json_field(json, "duration"), default=100),
    rotated=json_bool_value(json_field(json, "rotated")),
    trimmed=json_bool_value(json_field(json, "trimmed")),
    sprite_source_size=json_rect_value(json_field(json, "spriteSourceSize")),
    source_size=json_size_value(json_field(json, "sourceSize")),
  )
}

///|
fn parse_meta(value : Json?) -> SpriteSheetMeta {
  match value {
    Some(meta) =>
      match meta {
        Object(_) =>
          {
            app: json_string_value(json_field(meta, "app")),
            version: json_string_value(json_field(meta, "version")),
            image: json_string_value(json_field(meta, "image")),
            format: json_string_value(json_field(meta, "format")),
            scale: json_string_value(json_field(meta, "scale"), default="1"),
            size: json_size_value(json_field(meta, "size")),
            frame_tags: parse_frame_tags(json_array_field(meta, "frameTags")),
            layers: parse_layers(json_array_field(meta, "layers")),
            slices: parse_slices(json_array_field(meta, "slices")),
          }
        _ => SpriteSheetMeta::empty()
      }
    _ => SpriteSheetMeta::empty()
  }
}

///|
fn parse_frame_tags(items : Array[Json]) -> Array[FrameTag] {
  let out : Array[FrameTag] = []
  for item in items {
    match item {
      Object(_) =>
        out.push(
          FrameTag::new(
            json_string_value(json_field(item, "name")),
            json_int_value(json_field(item, "from")),
            json_int_value(json_field(item, "to")),
            direction=TagDirection::from_string(
              json_string_value(
                json_field(item, "direction"),
                default="forward",
              ),
            ),
            color=json_string_value(json_field(item, "color")),
            user_data=json_string_value(json_field(item, "data")),
          ),
        )
      _ => ()
    }
  }
  out
}

///|
fn parse_layers(items : Array[Json]) -> Array[LayerInfo] {
  let out : Array[LayerInfo] = []
  for item in items {
    match item {
      Object(_) =>
        out.push(
          LayerInfo::new(
            json_string_value(json_field(item, "name")),
            opacity=json_int_value(json_field(item, "opacity"), default=255),
            blend_mode=json_string_value(
              json_field(item, "blendMode"),
              default="normal",
            ),
            visible=json_bool_value(json_field(item, "visible"), default=true),
          ),
        )
      _ => ()
    }
  }
  out
}

///|
fn parse_slices(items : Array[Json]) -> Array[SliceInfo] {
  let out : Array[SliceInfo] = []
  for item in items {
    match item {
      Object(_) =>
        out.push(
          SliceInfo::new(
            json_string_value(json_field(item, "name")),
            parse_slice_keys(json_array_field(item, "keys")),
            color=json_string_value(json_field(item, "color")),
          ),
        )
      _ => ()
    }
  }
  out
}

///|
fn parse_slice_keys(items : Array[Json]) -> Array[SliceKey] {
  let out : Array[SliceKey] = []
  for item in items {
    match item {
      Object(_) => out.push(parse_slice_key(item))
      _ => ()
    }
  }
  out
}

///|
fn parse_slice_key(json : Json) -> SliceKey {
  let center = json_rect_value(json_field(json, "center"))
  let pivot_json = json_field(json, "pivot")
  let pivot = match pivot_json {
    Some(pivot_value) =>
      match pivot_value {
        Object(_) =>
          Point::new(
            json_int_value(json_field(pivot_value, "x")),
            json_int_value(json_field(pivot_value, "y")),
          )
        _ => Point::new(0, 0)
      }
    _ => Point::new(0, 0)
  }
  SliceKey::new(
    json_int_value(json_field(json, "frame")),
    json_rect_value(json_field(json, "bounds")),
    center~,
    pivot~,
    has_center=json_field(json, "center") != None,
    has_pivot=json_field(json, "pivot") != None,
  )
}

///|
pub fn infer_frame_rate(sheet : SpriteSheet) -> Int {
  if sheet.frames.length() == 0 {
    0
  } else {
    let total = sheet.duration_ms()
    if total <= 0 {
      0
    } else {
      sheet.frames.length() * 1000 / total
    }
  }
}

///|
pub fn sheet_uses_trimmed_frames(sheet : SpriteSheet) -> Bool {
  for frame in sheet.frames {
    if frame.trimmed {
      return true
    }
  }
  false
}

///|
pub fn sheet_has_rotated_frames(sheet : SpriteSheet) -> Bool {
  for frame in sheet.frames {
    if frame.rotated {
      return true
    }
  }
  false
}

///|
pub fn collect_frame_names(sheet : SpriteSheet) -> Array[String] {
  let names : Array[String] = []
  for frame in sheet.frames {
    names.push(frame.name)
  }
  names
}

///|
pub fn collect_tag_names(sheet : SpriteSheet) -> Array[String] {
  let names : Array[String] = []
  for tag in sheet.meta.frame_tags {
    names.push(tag.name)
  }
  names
}

///|
pub fn collect_slice_names(sheet : SpriteSheet) -> Array[String] {
  let names : Array[String] = []
  for slice in sheet.meta.slices {
    names.push(slice.name)
  }
  names
}