///|
/// Parse gvar table header, shared tuples, and glyph offsets
fn parse_gvar(
  reader : BinaryReader,
  offset : Int,
  num_glyphs : Int,
) -> GvarData? {
  let r = BinaryReader::at(reader.data, offset)
  let _version_major = r.read_uint16()
  let _version_minor = r.read_uint16()
  let axis_count = r.read_uint16()
  let shared_tuple_count = r.read_uint16()
  let shared_tuples_offset = r.read_uint32()
  let glyph_count = r.read_uint16()
  let flags = r.read_uint16()
  let glyph_var_data_array_offset = r.read_uint32()
  // Read glyph offsets
  let offsets_are_long = (flags & 1) != 0
  let glyph_offsets : Array[Int] = []
  for i = 0; i <= glyph_count; i = i + 1 {
    if offsets_are_long {
      glyph_offsets.push(r.read_uint32())
    } else {
      glyph_offsets.push(r.read_uint16() * 2)
    }
    ignore(i)
  }
  // Parse shared tuples
  let shared_tuples : Array[Array[Double]] = []
  let st_reader = BinaryReader::at(reader.data, offset + shared_tuples_offset)
  for i = 0; i < shared_tuple_count; i = i + 1 {
    let tuple : Array[Double] = []
    for j = 0; j < axis_count; j = j + 1 {
      tuple.push(st_reader.read_f2dot14())
      ignore(j)
    }
    shared_tuples.push(tuple)
    ignore(i)
  }
  ignore(num_glyphs)
  Some({
    axis_count,
    shared_tuples,
    gvar_offset: offset,
    glyph_var_data_offset: offset + glyph_var_data_array_offset,
    glyph_offsets,
  })
}

///|
/// Unpack packed point numbers from gvar data.
/// Returns empty array if all points are referenced.
fn unpack_points(reader : BinaryReader) -> Array[Int] {
  let count_byte = reader.read_uint8()
  if count_byte == 0 {
    return [] // all points
  }
  let count = if (count_byte & 0x80) != 0 {
    ((count_byte & 0x7F) << 8) | reader.read_uint8()
  } else {
    count_byte
  }
  let points : Array[Int] = Array::new(capacity=count)
  let mut i = 0
  while i < count {
    let run_header = reader.read_uint8()
    let run_count = (run_header & 0x7F) + 1
    let is_words = (run_header & 0x80) != 0
    let mut point = if points.length() > 0 {
      points[points.length() - 1]
    } else {
      0
    }
    for _j = 0; _j < run_count && i < count; _j = _j + 1 {
      if is_words {
        point = point + reader.read_uint16()
      } else {
        point = point + reader.read_uint8()
      }
      points.push(point)
      i = i + 1
    }
  }
  points
}

///|
/// Unpack RLE-compressed deltas from gvar data
fn unpack_deltas(reader : BinaryReader, count : Int) -> Array[Int] {
  let deltas : Array[Int] = Array::new(capacity=count)
  let mut i = 0
  while i < count {
    let run_header = reader.read_uint8()
    let run_count = (run_header & 0x3F) + 1
    let is_zeros = (run_header & 0x80) != 0
    let is_words = (run_header & 0x40) != 0
    for _j = 0; _j < run_count && i < count; _j = _j + 1 {
      if is_zeros {
        deltas.push(0)
      } else if is_words {
        deltas.push(reader.read_int16())
      } else {
        deltas.push(reader.read_int8())
      }
      i = i + 1
    }
  }
  deltas
}

///|
/// IUP (Interpolation of Untouched Points) for a single contour.
/// Given sparse delta assignments, interpolate deltas for unassigned points.
fn iup_contour(
  deltas : Array[Int],
  coords : Array[Int],
  end_point : Int,
  start_point : Int,
  touch_flags : Array[Bool],
) -> Unit {
  // Find touched points in this contour
  let touched : Array[Int] = []
  for i = start_point; i <= end_point; i = i + 1 {
    if touch_flags[i] {
      touched.push(i)
    }
  }
  if touched.is_empty() {
    return
  }
  if touched.length() == 1 {
    // All points get same delta
    let d = deltas[touched[0]]
    for i = start_point; i <= end_point; i = i + 1 {
      deltas[i] = d
    }
    return
  }
  // Interpolate between each pair of touched points
  let n_touched = touched.length()
  for ti = 0; ti < n_touched; ti = ti + 1 {
    let i0 = touched[ti]
    let i1 = touched[(ti + 1) % n_touched]
    let d0 = deltas[i0]
    let d1 = deltas[i1]
    let c0 = coords[i0]
    let c1 = coords[i1]
    // Points between i0 and i1 (wrapping around contour)
    let mut idx = i0 + 1
    if idx > end_point {
      idx = start_point
    }
    while idx != i1 {
      if !touch_flags[idx] {
        let c = coords[idx]
        deltas[idx] = iup_interpolate(c, c0, c1, d0, d1)
      }
      idx = idx + 1
      if idx > end_point {
        idx = start_point
      }
    }
  }
}

///|
/// IUP interpolation helper
fn iup_interpolate(coord : Int, c0 : Int, c1 : Int, d0 : Int, d1 : Int) -> Int {
  if c0 == c1 {
    if d0 == d1 {
      return d0
    }
    return (d0 + d1) / 2
  }
  // Sort so min_c <= max_c
  let (min_c, max_c, min_d, max_d) = if c0 < c1 {
    (c0, c1, d0, d1)
  } else {
    (c1, c0, d1, d0)
  }
  if coord <= min_c {
    return min_d
  }
  if coord >= max_c {
    return max_d
  }
  // Linear interpolation
  let num = (coord - min_c) * (max_d - min_d)
  let den = max_c - min_c
  min_d + (num + den / 2) / den
}

///|
/// Apply gvar deltas to a glyph's coordinates.
/// Returns (dx_array, dy_array) to be added to the original coordinates.
fn apply_gvar_deltas(
  data : Bytes,
  gvar : GvarData,
  glyph_id : Int,
  coords : Array[Double],
  x_coords : Array[Int],
  y_coords : Array[Int],
  end_points : Array[Int],
) -> (Array[Int], Array[Int]) {
  let num_points = x_coords.length()
  let dx_total : Array[Int] = Array::make(num_points, 0)
  let dy_total : Array[Int] = Array::make(num_points, 0)
  if glyph_id >= gvar.glyph_offsets.length() - 1 {
    return (dx_total, dy_total)
  }
  let var_offset = gvar.glyph_var_data_offset + gvar.glyph_offsets[glyph_id]
  let var_end = gvar.glyph_var_data_offset + gvar.glyph_offsets[glyph_id + 1]
  if var_offset >= var_end {
    return (dx_total, dy_total)
  }
  let reader = BinaryReader::at(data, var_offset)
  let tuple_count_word = reader.read_uint16()
  let tuple_count = tuple_count_word & 0x0FFF
  let has_shared_points = (tuple_count_word & 0x8000) != 0
  let data_offset = reader.read_uint16()
  // Shared points (if any)
  let serialized_data_start = var_offset + data_offset
  let ser_reader = BinaryReader::at(data, serialized_data_start)
  let shared_points : Array[Int] = if has_shared_points {
    unpack_points(ser_reader)
  } else {
    []
  }
  // Parse tuple variation headers
  for _t = 0; _t < tuple_count; _t = _t + 1 {
    let variation_data_size = reader.read_uint16()
    let tuple_index = reader.read_uint16()
    let has_embedded_peak = (tuple_index & 0x8000) != 0
    let has_intermediate = (tuple_index & 0x4000) != 0
    let has_private_points = (tuple_index & 0x2000) != 0
    let shared_idx = tuple_index & 0x0FFF
    // Get peak coordinates
    let peak : Array[Double] = []
    if has_embedded_peak {
      for _a = 0; _a < gvar.axis_count; _a = _a + 1 {
        peak.push(reader.read_f2dot14())
      }
    } else if shared_idx < gvar.shared_tuples.length() {
      let st = gvar.shared_tuples[shared_idx]
      for v in st {
        peak.push(v)
      }
    }
    // Get intermediate start/end
    let start : Array[Double] = []
    let end_vals : Array[Double] = []
    if has_intermediate {
      for _a = 0; _a < gvar.axis_count; _a = _a + 1 {
        start.push(reader.read_f2dot14())
      }
      for _a = 0; _a < gvar.axis_count; _a = _a + 1 {
        end_vals.push(reader.read_f2dot14())
      }
    }
    // Compute tuple scalar
    let scalar = compute_tuple_scalar(
      peak, start, end_vals, coords, has_intermediate,
    )
    if scalar == 0.0 {
      // Skip this tuple's serialized data
      let save_pos = ser_reader.position()
      ser_reader.seek(save_pos + variation_data_size)
      continue
    }
    // Read point indices
    let points = if has_private_points {
      unpack_points(ser_reader)
    } else {
      shared_points
    }
    let is_all_points = points.is_empty()
    let n_deltas = if is_all_points { num_points } else { points.length() }
    // Read deltas (x then y)
    let dx = unpack_deltas(ser_reader, n_deltas)
    let dy = unpack_deltas(ser_reader, n_deltas)
    if is_all_points {
      // Apply to all points directly
      for i = 0; i < num_points; i = i + 1 {
        dx_total[i] = dx_total[i] + (dx[i].to_double() * scalar + 0.5).to_int()
        dy_total[i] = dy_total[i] + (dy[i].to_double() * scalar + 0.5).to_int()
      }
    } else {
      // Sparse points: assign then IUP
      let dx_work : Array[Int] = Array::make(num_points, 0)
      let dy_work : Array[Int] = Array::make(num_points, 0)
      let touch_flags : Array[Bool] = Array::make(num_points, false)
      for i = 0; i < points.length(); i = i + 1 {
        let pi = points[i]
        if pi < num_points {
          dx_work[pi] = (dx[i].to_double() * scalar + 0.5).to_int()
          dy_work[pi] = (dy[i].to_double() * scalar + 0.5).to_int()
          touch_flags[pi] = true
        }
      }
      // IUP per contour
      let mut contour_start = 0
      for ep in end_points {
        iup_contour(dx_work, x_coords, ep, contour_start, touch_flags)
        iup_contour(dy_work, y_coords, ep, contour_start, touch_flags)
        contour_start = ep + 1
      }
      for i = 0; i < num_points; i = i + 1 {
        dx_total[i] = dx_total[i] + dx_work[i]
        dy_total[i] = dy_total[i] + dy_work[i]
      }
    }
  }
  (dx_total, dy_total)
}

///|
/// Compute the scalar for a gvar tuple.
/// Without intermediate regions, implicit region is:
///   peak > 0: start=0, end=1.0
///   peak < 0: start=-1.0, end=0
fn compute_tuple_scalar(
  peak : Array[Double],
  start : Array[Double],
  end_vals : Array[Double],
  coords : Array[Double],
  has_intermediate : Bool,
) -> Double {
  let mut scalar = 1.0
  for i = 0; i < peak.length(); i = i + 1 {
    let p = peak[i]
    let c = if i < coords.length() { coords[i] } else { 0.0 }
    if p == 0.0 {
      continue
    }
    if c == p {
      continue
    }
    let (s, e) = if has_intermediate {
      let sv = if i < start.length() { start[i] } else { 0.0 }
      let ev = if i < end_vals.length() { end_vals[i] } else { 0.0 }
      (sv, ev)
    } else if p > 0.0 {
      (0.0, 1.0)
    } else {
      (-1.0, 0.0)
    }
    if c <= s || c >= e {
      return 0.0
    }
    if c < p {
      scalar = scalar * (c - s) / (p - s)
    } else {
      scalar = scalar * (e - c) / (e - p)
    }
  }
  scalar
}