///|
/// Error categories used by the JSON parser, binary parser, validator,
/// runtime builder, exporter, and CLI report model.
pub(all) enum PixelAnimErrorKind {
  ErrorNone
  ErrorJsonParse
  ErrorExpectedObject
  ErrorMissingField
  ErrorInvalidField
  ErrorInvalidFrame
  ErrorInvalidTag
  ErrorInvalidSlice
  ErrorInvalidLayer
  ErrorNotAseprite
  ErrorUnsupportedChunk
  ErrorChunkOutOfBounds
  ErrorTruncatedData
  ErrorUnsupportedCompression
  ErrorRuntimeState
  ErrorInvalidArgument
} derive(Eq, @debug.Debug)

///|
pub fn PixelAnimErrorKind::code(self : PixelAnimErrorKind) -> String {
  match self {
    ErrorNone => "none"
    ErrorJsonParse => "json-parse"
    ErrorExpectedObject => "expected-object"
    ErrorMissingField => "missing-field"
    ErrorInvalidField => "invalid-field"
    ErrorInvalidFrame => "invalid-frame"
    ErrorInvalidTag => "invalid-tag"
    ErrorInvalidSlice => "invalid-slice"
    ErrorInvalidLayer => "invalid-layer"
    ErrorNotAseprite => "not-aseprite"
    ErrorUnsupportedChunk => "unsupported-chunk"
    ErrorChunkOutOfBounds => "chunk-out-of-bounds"
    ErrorTruncatedData => "truncated-data"
    ErrorUnsupportedCompression => "unsupported-compression"
    ErrorRuntimeState => "runtime-state"
    ErrorInvalidArgument => "invalid-argument"
  }
}

///|
pub(all) struct PixelAnimError {
  kind : PixelAnimErrorKind
  path : String
  offset : Int
  message : String
} derive(Eq, @debug.Debug)

///|
pub fn PixelAnimError::none() -> PixelAnimError {
  { kind: ErrorNone, path: "$", offset: 0, message: "" }
}

///|
pub fn PixelAnimError::new(
  kind : PixelAnimErrorKind,
  message : String,
  path? : String = "$",
  offset? : Int = 0,
) -> PixelAnimError {
  { kind, path, offset, message }
}

///|
pub fn PixelAnimError::code(self : PixelAnimError) -> String {
  self.kind.code()
}

///|
pub fn PixelAnimError::is_error(self : PixelAnimError) -> Bool {
  self.kind != ErrorNone
}

///|
pub(all) struct Point {
  x : Int
  y : Int
} derive(Eq, @debug.Debug)

///|
pub fn Point::new(x : Int, y : Int) -> Point {
  { x, y }
}

///|
pub(all) struct Size {
  width : Int
  height : Int
} derive(Eq, @debug.Debug)

///|
pub fn Size::new(width : Int, height : Int) -> Size {
  { width, height }
}

///|
pub fn Size::empty() -> Size {
  { width: 0, height: 0 }
}

///|
pub fn Size::area(self : Size) -> Int {
  self.width * self.height
}

///|
pub fn Size::is_positive(self : Size) -> Bool {
  self.width > 0 && self.height > 0
}

///|
pub(all) struct Rect {
  x : Int
  y : Int
  width : Int
  height : Int
} derive(Eq, @debug.Debug)

///|
pub fn Rect::new(x : Int, y : Int, width : Int, height : Int) -> Rect {
  { x, y, width, height }
}

///|
pub fn Rect::empty() -> Rect {
  { x: 0, y: 0, width: 0, height: 0 }
}

///|
pub fn Rect::right(self : Rect) -> Int {
  self.x + self.width
}

///|
pub fn Rect::bottom(self : Rect) -> Int {
  self.y + self.height
}

///|
pub fn Rect::area(self : Rect) -> Int {
  self.width * self.height
}

///|
pub fn Rect::is_positive(self : Rect) -> Bool {
  self.width > 0 && self.height > 0
}

///|
pub fn Rect::contains_point(self : Rect, point : Point) -> Bool {
  point.x >= self.x &&
  point.y >= self.y &&
  point.x < self.right() &&
  point.y < self.bottom()
}

///|
pub fn Rect::contains_rect(self : Rect, other : Rect) -> Bool {
  other.x >= self.x &&
  other.y >= self.y &&
  other.right() <= self.right() &&
  other.bottom() <= self.bottom()
}

///|
pub fn Rect::intersects(self : Rect, other : Rect) -> Bool {
  self.x < other.right() &&
  self.right() > other.x &&
  self.y < other.bottom() &&
  self.bottom() > other.y
}

///|
pub(all) enum TagDirection {
  DirectionForward
  DirectionReverse
  DirectionPingPong
  DirectionPingPongReverse
  DirectionUnknown(String)
} derive(Eq, @debug.Debug)

///|
pub fn TagDirection::from_string(value : String) -> TagDirection {
  match value {
    "forward" | "Forward" => DirectionForward
    "reverse" | "Reverse" => DirectionReverse
    "pingpong" | "ping-pong" | "PingPong" => DirectionPingPong
    "pingpong_reverse" | "ping-pong-reverse" | "PingPongReverse" =>
      DirectionPingPongReverse
    _ => DirectionUnknown(value)
  }
}

///|
pub fn TagDirection::to_string(self : TagDirection) -> String {
  match self {
    DirectionForward => "forward"
    DirectionReverse => "reverse"
    DirectionPingPong => "pingpong"
    DirectionPingPongReverse => "pingpong_reverse"
    DirectionUnknown(value) => value
  }
}

///|
pub fn TagDirection::is_pingpong(self : TagDirection) -> Bool {
  match self {
    DirectionPingPong | DirectionPingPongReverse => true
    _ => false
  }
}

///|
pub(all) enum LoopMode {
  LoopOnce
  LoopRepeat
  LoopPingPong
} derive(Eq, @debug.Debug)

///|
pub fn LoopMode::to_string(self : LoopMode) -> String {
  match self {
    LoopOnce => "once"
    LoopRepeat => "repeat"
    LoopPingPong => "pingpong"
  }
}

///|
pub(all) struct FrameInfo {
  name : String
  index : Int
  rect : Rect
  rotated : Bool
  trimmed : Bool
  sprite_source_size : Rect
  source_size : Size
  duration_ms : Int
} derive(Eq, @debug.Debug)

///|
pub fn FrameInfo::new(
  name : String,
  index : Int,
  rect : Rect,
  duration_ms : Int,
  rotated? : Bool = false,
  trimmed? : Bool = false,
  sprite_source_size? : Rect = Rect::empty(),
  source_size? : Size = Size::empty(),
) -> FrameInfo {
  {
    name,
    index,
    rect,
    rotated,
    trimmed,
    sprite_source_size,
    source_size,
    duration_ms,
  }
}

///|
pub fn FrameInfo::is_valid(self : FrameInfo) -> Bool {
  self.index >= 0 && self.rect.is_positive() && self.duration_ms > 0
}

///|
pub fn FrameInfo::duration_seconds(self : FrameInfo) -> Double {
  self.duration_ms.to_double() / 1000.0
}

///|
pub(all) struct FrameTag {
  name : String
  from_index : Int
  to_index : Int
  direction : TagDirection
  color : String
  user_data : String
} derive(Eq, @debug.Debug)

///|
pub fn FrameTag::new(
  name : String,
  from_index : Int,
  to_index : Int,
  direction? : TagDirection = DirectionForward,
  color? : String = "",
  user_data? : String = "",
) -> FrameTag {
  { name, from_index, to_index, direction, color, user_data }
}

///|
pub fn FrameTag::frame_count(self : FrameTag) -> Int {
  if self.to_index >= self.from_index {
    self.to_index - self.from_index + 1
  } else {
    0
  }
}

///|
pub fn FrameTag::contains_index(self : FrameTag, index : Int) -> Bool {
  index >= self.from_index && index <= self.to_index
}

///|
pub(all) struct SliceKey {
  frame : Int
  bounds : Rect
  center : Rect
  pivot : Point
  has_center : Bool
  has_pivot : Bool
} derive(Eq, @debug.Debug)

///|
pub fn SliceKey::new(
  frame : Int,
  bounds : Rect,
  center? : Rect = Rect::empty(),
  pivot? : Point = Point::new(0, 0),
  has_center? : Bool = false,
  has_pivot? : Bool = false,
) -> SliceKey {
  { frame, bounds, center, pivot, has_center, has_pivot }
}

///|
pub fn SliceKey::is_valid(self : SliceKey) -> Bool {
  self.frame >= 0 && self.bounds.is_positive()
}

///|
pub(all) struct SliceInfo {
  name : String
  color : String
  keys : Array[SliceKey]
} derive(Eq, @debug.Debug)

///|
pub fn SliceInfo::new(
  name : String,
  keys : Array[SliceKey],
  color? : String = "",
) -> SliceInfo {
  { name, color, keys }
}

///|
pub fn SliceInfo::key_for_frame(
  self : SliceInfo,
  frame_index : Int,
) -> SliceKey? {
  let mut best : SliceKey? = None
  for key in self.keys {
    if key.frame <= frame_index {
      best = Some(key)
    }
  }
  best
}

///|
pub fn SliceInfo::has_keys(self : SliceInfo) -> Bool {
  self.keys.length() > 0
}

///|
pub(all) struct LayerInfo {
  name : String
  opacity : Int
  blend_mode : String
  visible : Bool
} derive(Eq, @debug.Debug)

///|
pub fn LayerInfo::new(
  name : String,
  opacity? : Int = 255,
  blend_mode? : String = "normal",
  visible? : Bool = true,
) -> LayerInfo {
  { name, opacity, blend_mode, visible }
}

///|
pub fn LayerInfo::is_visible(self : LayerInfo) -> Bool {
  self.visible && self.opacity > 0
}

///|
pub(all) struct SpriteSheetMeta {
  app : String
  version : String
  image : String
  format : String
  scale : String
  size : Size
  frame_tags : Array[FrameTag]
  layers : Array[LayerInfo]
  slices : Array[SliceInfo]
} derive(Eq, @debug.Debug)

///|
pub fn SpriteSheetMeta::empty() -> SpriteSheetMeta {
  {
    app: "",
    version: "",
    image: "",
    format: "",
    scale: "1",
    size: Size::empty(),
    frame_tags: [],
    layers: [],
    slices: [],
  }
}

///|
pub(all) struct SpriteSheet {
  frames : Array[FrameInfo]
  meta : SpriteSheetMeta
  source_length : Int
} derive(Eq, @debug.Debug)

///|
pub fn SpriteSheet::empty() -> SpriteSheet {
  { frames: [], meta: SpriteSheetMeta::empty(), source_length: 0 }
}

///|
pub fn SpriteSheet::frame_count(self : SpriteSheet) -> Int {
  self.frames.length()
}

///|
pub fn SpriteSheet::duration_ms(self : SpriteSheet) -> Int {
  for frame in self.frames; total = 0 {
    continue total + frame.duration_ms
  } nobreak {
    total
  }
}

///|
pub fn SpriteSheet::duration_seconds(self : SpriteSheet) -> Double {
  self.duration_ms().to_double() / 1000.0
}

///|
pub fn SpriteSheet::find_frame(self : SpriteSheet, name : String) -> FrameInfo? {
  for frame in self.frames {
    if frame.name == name {
      return Some(frame)
    }
  }
  None
}

///|
pub fn SpriteSheet::frame_at(self : SpriteSheet, index : Int) -> FrameInfo? {
  if index >= 0 && index < self.frames.length() {
    Some(self.frames[index])
  } else {
    None
  }
}

///|
pub fn SpriteSheet::find_tag(self : SpriteSheet, name : String) -> FrameTag? {
  for tag in self.meta.frame_tags {
    if tag.name == name {
      return Some(tag)
    }
  }
  None
}

///|
pub(all) struct PixelAnimParseResult {
  ok : Bool
  sheet : SpriteSheet
  error : PixelAnimError
} derive(Eq, @debug.Debug)

///|
pub fn PixelAnimParseResult::success(
  sheet : SpriteSheet,
) -> PixelAnimParseResult {
  { ok: true, sheet, error: PixelAnimError::none() }
}

///|
pub fn PixelAnimParseResult::failure(
  error : PixelAnimError,
) -> PixelAnimParseResult {
  { ok: false, sheet: SpriteSheet::empty(), error }
}

///|
pub(all) struct AseHeader {
  file_size : Int
  frame_count : Int
  width : Int
  height : Int
  color_depth : Int
  flags : Int
  speed_ms : Int
  transparent_index : Int
  color_count : Int
  pixel_width : Int
  pixel_height : Int
} derive(Eq, @debug.Debug)

///|
pub fn AseHeader::empty() -> AseHeader {
  {
    file_size: 0,
    frame_count: 0,
    width: 0,
    height: 0,
    color_depth: 0,
    flags: 0,
    speed_ms: 0,
    transparent_index: 0,
    color_count: 0,
    pixel_width: 1,
    pixel_height: 1,
  }
}

///|
pub fn AseHeader::canvas_size(self : AseHeader) -> Size {
  Size::new(self.width, self.height)
}

///|
pub(all) struct AseChunk {
  frame_index : Int
  offset : Int
  size : Int
  kind : Int
  kind_name : String
  name : String
  layer_index : Int
  cel_type : Int
  opacity : Int
  child_level : Int
  extra : String
} derive(Eq, @debug.Debug)

///|
pub fn AseChunk::new(
  frame_index : Int,
  offset : Int,
  size : Int,
  kind : Int,
  kind_name : String,
  name? : String = "",
  layer_index? : Int = -1,
  cel_type? : Int = -1,
  opacity? : Int = 255,
  child_level? : Int = 0,
  extra? : String = "",
) -> AseChunk {
  {
    frame_index,
    offset,
    size,
    kind,
    kind_name,
    name,
    layer_index,
    cel_type,
    opacity,
    child_level,
    extra,
  }
}

///|
pub fn AseChunk::end_offset(self : AseChunk) -> Int {
  self.offset + self.size
}

///|
pub(all) struct AseFrame {
  index : Int
  offset : Int
  size : Int
  duration_ms : Int
  chunk_count : Int
  chunks : Array[AseChunk]
} derive(Eq, @debug.Debug)

///|
pub fn AseFrame::new(
  index : Int,
  offset : Int,
  size : Int,
  duration_ms : Int,
  chunk_count : Int,
  chunks : Array[AseChunk],
) -> AseFrame {
  { index, offset, size, duration_ms, chunk_count, chunks }
}

///|
pub(all) struct AseFile {
  header : AseHeader
  frames : Array[AseFrame]
  source_length : Int
} derive(Eq, @debug.Debug)

///|
pub fn AseFile::empty() -> AseFile {
  { header: AseHeader::empty(), frames: [], source_length: 0 }
}

///|
pub fn AseFile::chunk_count(self : AseFile) -> Int {
  for frame in self.frames; total = 0 {
    continue total + frame.chunks.length()
  } nobreak {
    total
  }
}

///|
pub(all) struct AseParseResult {
  ok : Bool
  file : AseFile
  error : PixelAnimError
} derive(Eq, @debug.Debug)

///|
pub fn AseParseResult::success(file : AseFile) -> AseParseResult {
  { ok: true, file, error: PixelAnimError::none() }
}

///|
pub fn AseParseResult::failure(error : PixelAnimError) -> AseParseResult {
  { ok: false, file: AseFile::empty(), error }
}