///|
pub(all) enum LevelMode {
  OneLevel
  Mipmap
  Ripmap
} derive(Eq, Debug)

///|
pub(all) struct TileDescription {
  width : Int32
  height : Int32
  mode : LevelMode
  round_up : Bool
} derive(Eq, Debug)

///|
pub(all) struct LevelInfo {
  x : Int32
  y : Int32
  window : Window
  columns : Int32
  rows : Int32
} derive(Eq, Debug)

///|
fn level_size(size : Int32, level : Int32, round_up : Bool) -> Int32 {
  let unit = 1L << level
  let bias = if round_up { unit - 1L } else { 0L }
  imax(1, ((size.to_int64() + bias) / unit).to_int())
}

///|
fn level_count(size : Int32, round_up : Bool) -> Int32 {
  let mut level = 0
  while level_size(size, level, round_up) > 1 {
    level += 1
  }
  level + 1
}

///|
/// Levels in offset-table order. The base data window's origin applies to all levels.
pub fn TileDescription::levels(
  self : TileDescription,
  base : Window,
) -> Array[LevelInfo] raise ExrError {
  if self.width <= 0 || self.height <= 0 {
    raise Invalid("invalid tile dimensions", 0)
  }
  let nx = level_count(base.width(), self.round_up)
  let ny = level_count(base.height(), self.round_up)
  let (columns, rows) = match self.mode {
    OneLevel => (1, 1)
    Mipmap => (1, imax(nx, ny))
    Ripmap => (nx, ny)
  }
  let result = []
  for y = 0; y < rows; y = y + 1 {
    for i = 0; i < columns; i = i + 1 {
      let x = if self.mode == Mipmap { y } else { i }
      let w = level_size(base.width(), x, self.round_up)
      let h = level_size(base.height(), y, self.round_up)
      result.push({
        x,
        y,
        window: Window::new(
          base.x0,
          base.y0,
          base.x0 + (w - 1),
          base.y0 + (h - 1),
        ),
        columns: (w - 1) / self.width + 1,
        rows: (h - 1) / self.height + 1,
      })
    }
  }
  result
}

///|
fn read_tiles(bytes : Bytes) -> TileDescription raise ExrError {
  if bytes.length() != 9 {
    raise Invalid("tiledesc size", 0)
  }
  let r = reader(bytes)
  let width = index(r.uint(4))
  let height = index(r.uint(4))
  let flags = r.u8()
  let mode = match flags & 15 {
    0 => OneLevel
    1 => Mipmap
    2 => Ripmap
    _ => raise Invalid("tile level mode", 0)
  }
  if width == 0 || height == 0 || flags >= 32 {
    raise Invalid("tile dimensions or rounding mode", 0)
  }
  { width, height, mode, round_up: flags >= 16, }
}

///|
fn TileDescription::attribute(self : TileDescription) -> Attribute {
  let mode = match self.mode {
    OneLevel => 0
    Mipmap => 1
    Ripmap => 2
  }
  let out = sink()
  out.i32(self.width)
  out.i32(self.height)
  let flags = mode + (if self.round_up { 16 } else { 0 })
  out.uint(flags.to_uint64(), 1)
  { name: "tiles", kind: "tiledesc", value: out.finish(), }
}

///|
fn block_levels(h : Header) -> Array[LevelInfo] raise ExrError {
  let w = h.data_window
  match h.tiles {
    Some(t) => t.levels(w)
    None =>
      [
        {
          x: 0,
          y: 0,
          window: w,
          columns: 1,
          rows: (w.height() - 1) / h.compression.scanlines() + 1,
        },
      ]
  }
}

///|
fn block_size(h : Header) -> (Int32, Int32) {
  match h.tiles {
    Some(t) => (t.width, t.height)
    None => (h.data_window.width(), h.compression.scanlines())
  }
}

///|
fn block_window(
  level : LevelInfo,
  size : (Int32, Int32),
  x : Int32,
  y : Int32,
) -> Window {
  let (width, height) = size
  let w = level.window
  let x0 = x * width
  let y0 = y * height
  {
    x0: w.x0 + x0,
    y0: w.y0 + y0,
    x1: w.x0 + (x0 + imin(width, w.width() - x0) - 1),
    y1: w.y0 + (y0 + imin(height, w.height() - y0) - 1),
  }
}

///|
pub extend LevelMode with Eq::{equal, not_equal}

///|
pub extend LevelMode with @debug.Debug::{to_repr}

///|
pub extend TileDescription with Eq::{equal, not_equal}

///|
pub extend TileDescription with @debug.Debug::{to_repr}

///|
pub extend LevelInfo with Eq::{equal, not_equal}

///|
pub extend LevelInfo with @debug.Debug::{to_repr}