// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// CFF/CFF2 (Type2) stem hinting for Y coordinates.
///
/// Ported from `fontations/skrifa/src/outline/cff/hint.rs`.

///|
const CFF_FIXED_ONE_BITS : Int = 0x0001_0000

///|
const CFF_FIXED_ZERO_BITS : Int = 0

///|
const CFF_FIXED_MAX_BITS : Int = 0x7FFF_FFFF

///|
const CFF_FIXED_INT_MASK : Int = 0xFFFF_0000

///|
priv struct CffFixed {
  bits : Int
}

///|
fn CffFixed::from_bits(bits : Int) -> CffFixed {
  { bits, }
}

///|
fn CffFixed::zero() -> CffFixed {
  CffFixed::from_bits(CFF_FIXED_ZERO_BITS)
}

///|
fn CffFixed::one() -> CffFixed {
  CffFixed::from_bits(CFF_FIXED_ONE_BITS)
}

///|
fn CffFixed::max() -> CffFixed {
  CffFixed::from_bits(CFF_FIXED_MAX_BITS)
}

///|
fn CffFixed::from_i32(v : Int) -> CffFixed {
  CffFixed::from_bits(v << 16)
}

///|
fn CffFixed::from_f64(v : Double) -> CffFixed {
  // Match typical Fixed::from_f64 rounding.
  CffFixed::from_bits((v * 65536.0).round().to_int())
}

///|
fn CffFixed::abs(self : CffFixed) -> CffFixed {
  if self.bits < 0 {
    CffFixed::from_bits(-self.bits)
  } else {
    self
  }
}

///|
fn CffFixed::max_with(self : CffFixed, other : CffFixed) -> CffFixed {
  if self.bits >= other.bits {
    self
  } else {
    other
  }
}

///|
fn CffFixed::min_with(self : CffFixed, other : CffFixed) -> CffFixed {
  if self.bits <= other.bits {
    self
  } else {
    other
  }
}

///|
fn CffFixed::wrapping_add(self : CffFixed, other : CffFixed) -> CffFixed {
  let a = self.bits.reinterpret_as_uint()
  let b = other.bits.reinterpret_as_uint()
  CffFixed::from_bits((a + b).reinterpret_as_int())
}

///|
fn CffFixed::wrapping_sub(self : CffFixed, other : CffFixed) -> CffFixed {
  let a = self.bits.reinterpret_as_uint()
  let b = other.bits.reinterpret_as_uint()
  CffFixed::from_bits((a - b).reinterpret_as_int())
}

///|
fn CffFixed::add(self : CffFixed, other : CffFixed) -> CffFixed {
  CffFixed::from_bits(self.bits + other.bits)
}

///|
fn CffFixed::sub(self : CffFixed, other : CffFixed) -> CffFixed {
  CffFixed::from_bits(self.bits - other.bits)
}

///|
fn CffFixed::neg(self : CffFixed) -> CffFixed {
  CffFixed::from_bits(-self.bits)
}

///|
fn cff_fixed_mul_bits(a_bits : Int, b_bits : Int) -> Int {
  // Match fontations `Fixed` multiplication:
  //   ((ab + 0x8000 - (ab < 0)) >> 16)
  //
  // Use Int64 intermediates to avoid overflow (MoonBit Int is 32-bit on wasm).
  let ab : Int64 = a_bits.to_int64() * b_bits.to_int64()
  let adj : Int64 = (0x8000).to_int64() -
    (if ab < 0 { 1 } else { 0 }).to_int64()
  let q : Int64 = (ab + adj) >> 16
  if q > (0x7FFF_FFFF).to_int64() {
    0x7FFF_FFFF
  } else if q < (-2147483648).to_int64() {
    -2147483648
  } else {
    q.to_int()
  }
}

///|
fn CffFixed::mul(self : CffFixed, other : CffFixed) -> CffFixed {
  CffFixed::from_bits(cff_fixed_mul_bits(self.bits, other.bits))
}

///|
fn CffFixed::div(self : CffFixed, other : CffFixed) -> CffFixed {
  // Match fontations `Fixed` division:
  //   ((((a as u64) << 16) + ((b as u64) >> 1)) / b) with sign handling.
  let mut sign = 1
  let mut a : Int64 = self.bits.to_int64()
  let mut b : Int64 = other.bits.to_int64()
  if a < 0 {
    a = -a
    sign = -1
  }
  if b < 0 {
    b = -b
    sign = -sign
  }
  let q : Int64 = if b == 0 {
    (0x7FFF_FFFF).to_int64()
  } else {
    let num : Int64 = (a << 16) + (b >> 1)
    num / b
  }
  let q = if q > (0x7FFF_FFFF).to_int64() {
    (0x7FFF_FFFF).to_int64()
  } else {
    q
  }
  let out : Int64 = if sign < 0 { -q } else { q }
  CffFixed::from_bits(out.to_int())
}

///|
fn CffFixed::mul_div(self : CffFixed, b : CffFixed, c : CffFixed) -> CffFixed {
  // Match fontations `Fixed::mul_div`:
  //   su*au + (bu>>1) / bu, with sign handling and bu==0 -> 0x7FFFFFFF.
  let mut sign = 1
  let mut su : Int64 = self.bits.to_int64()
  let mut au : Int64 = b.bits.to_int64()
  let mut bu : Int64 = c.bits.to_int64()
  if su < 0 {
    su = -su
    sign = -1
  }
  if au < 0 {
    au = -au
    sign = -sign
  }
  if bu < 0 {
    bu = -bu
    sign = -sign
  }
  let result : Int64 = if bu > 0 {
    // + (bu>>1) implements rounding to nearest.
    (su * au + (bu >> 1)) / bu
  } else {
    (0x7FFF_FFFF).to_int64()
  }
  let q = if result > (0x7FFF_FFFF).to_int64() {
    (0x7FFF_FFFF).to_int64()
  } else {
    result
  }
  let out : Int64 = if sign < 0 { -q } else { q }
  CffFixed::from_bits(out.to_int())
}

///|
fn CffFixed::round(self : CffFixed) -> CffFixed {
  // Equivalent to (x + 0x8000) & ~0xFFFF for two's complement.
  CffFixed::from_bits((self.bits + 0x8000) & CFF_FIXED_INT_MASK)
}

///|
fn CffFixed::fract(self : CffFixed) -> CffFixed {
  // Match fontations `Fixed::fract()`:
  //   fract = bits - floor(bits)
  // where floor() rounds toward -inf for negative values.
  let floor_bits = self.bits & CFF_FIXED_INT_MASK
  CffFixed::from_bits(self.bits - floor_bits)
}

///|
fn cff_hint_trunc(value : CffFixed) -> CffFixed {
  CffFixed::from_bits(value.bits & (0x3FF |> Int::lnot))
}

///|
fn cff_hint_half(value : CffFixed) -> CffFixed {
  CffFixed::from_bits(value.bits / 2)
}

///|
fn cff_hint_twice(value : CffFixed) -> CffFixed {
  let a = value.bits.reinterpret_as_uint()
  CffFixed::from_bits((a * 2).reinterpret_as_int())
}

///|
fn cff_hint_midpoint(a : CffFixed, b : CffFixed) -> CffFixed {
  a.add(cff_hint_half(b.sub(a)))
}

///|
const CFF_HINT_ICF_TOP_BITS : Int = 880 << 16

///|
const CFF_HINT_ICF_BOTTOM_BITS : Int = -120 << 16

///|
const CFF_HINT_MAX_BLUES : Int = 7

///|
const CFF_HINT_MAX_OTHER_BLUES : Int = 5

///|
const CFF_HINT_MAX_BLUE_ZONES : Int = CFF_HINT_MAX_BLUES +
  CFF_HINT_MAX_OTHER_BLUES

///|
const CFF_HINT_MAX_HINTS : Int = 96

///|
const CFF_HINT_MASK_SIZE : Int = (CFF_HINT_MAX_HINTS + 7) / 8

///|
const CFF_HINT_MIN_COUNTER_BITS : Int = 0x8000

///|
const CFF_HINT_EPSILON_BITS : Int = 1

///|
priv struct CffBlues {
  pairs : Array[(CffFixed, CffFixed)]
}

///|
fn CffBlues::default() -> CffBlues {
  { pairs: Array::new() }
}

///|
fn CffBlues::CffBlues(values : ArrayView[CffFixed]) -> CffBlues {
  let pairs : Array[(CffFixed, CffFixed)] = Array::new()
  let n = values.length()
  let mut i = 0
  while i + 1 < n {
    pairs.push((values.at(i), values.at(i + 1)))
    i = i + 2
  }
  { pairs, }
}

///|
fn CffBlues::values(self : CffBlues) -> ArrayView[(CffFixed, CffFixed)] {
  self.pairs.op_as_view()
}

///|
priv struct HintParams {
  mut blues : CffBlues
  mut family_blues : CffBlues
  mut other_blues : CffBlues
  mut family_other_blues : CffBlues
  mut blue_scale : CffFixed
  mut blue_shift : CffFixed
  mut blue_fuzz : CffFixed
  mut language_group : Int
}

///|
fn HintParams::default() -> HintParams {
  {
    blues: CffBlues::default(),
    other_blues: CffBlues::default(),
    family_blues: CffBlues::default(),
    family_other_blues: CffBlues::default(),
    blue_scale: CffFixed::from_f64(0.039625),
    blue_shift: CffFixed::from_i32(7),
    blue_fuzz: CffFixed::one(),
    language_group: 0,
  }
}

///|
priv struct BlueZone {
  mut is_bottom : Bool
  mut cs_bottom_edge : CffFixed
  mut cs_top_edge : CffFixed
  mut cs_flat_edge : CffFixed
  mut ds_flat_edge : CffFixed
}

///|
fn BlueZone::default() -> BlueZone {
  {
    is_bottom: false,
    cs_bottom_edge: CffFixed::zero(),
    cs_top_edge: CffFixed::zero(),
    cs_flat_edge: CffFixed::zero(),
    ds_flat_edge: CffFixed::zero(),
  }
}

///|
priv struct HintState {
  mut scale : CffFixed
  mut blue_scale : CffFixed
  mut blue_shift : CffFixed
  mut blue_fuzz : CffFixed
  mut language_group : Int
  mut suppress_overshoot : Bool
  mut do_em_box_hints : Bool
  mut boost : CffFixed
  mut darken_y : CffFixed
  mut zones : Array[BlueZone]
  mut zone_count : Int
}

///|
fn HintState::default() -> HintState {
  let zones : Array[BlueZone] = Array::new()
  for _ in 0.. HintState {
  let st = HintState::default()
  st.scale = scale
  st.blue_scale = params.blue_scale
  st.blue_shift = params.blue_shift
  st.blue_fuzz = params.blue_fuzz
  st.language_group = params.language_group
  st.suppress_overshoot = false
  st.do_em_box_hints = false
  st.boost = CffFixed::zero()
  st.darken_y = CffFixed::zero()
  st.zone_count = 0
  HintState::build_zones(st, params)
  st
}

///|
fn HintState::zones(self : HintState) -> ArrayView[BlueZone] {
  self.zones[0:self.zone_count].op_as_view()
}

///|
fn HintState::build_zones(self : HintState, params : HintParams) -> Unit {
  self.do_em_box_hints = false
  // Special language group behavior.
  let blues0 = params.blues.values()
  match (self.language_group, blues0.length()) {
    (1, 2) => {
      let (b0, t0) = blues0.at(0)
      let (b1, t1) = blues0.at(1)
      if b0.bits < CFF_HINT_ICF_BOTTOM_BITS &&
        t0.bits < CFF_HINT_ICF_BOTTOM_BITS &&
        b1.bits > CFF_HINT_ICF_TOP_BITS &&
        t1.bits > CFF_HINT_ICF_TOP_BITS {
        self.do_em_box_hints = true
        return
      }
    }
    (1, 0) => {
      self.do_em_box_hints = true
      return
    }
    _ => ()
  }
  let zones : Array[BlueZone] = Array::new()
  for _ in 0.. 0 {
        let (_, family_top) = fam_blues.at(0)
        let diff = flat0.sub(family_top).abs()
        if diff.bits < min_diff.bits && diff.bits < units_per_pixel.bits {
          zone.cs_flat_edge = family_top
        }
      }
    } else {
      let fam_blues = params.family_blues.values()
      for j in 1.. 0 {
    let max_scale = CffFixed::one().div(max_zone_height)
    if self.blue_scale.bits > max_scale.bits {
      self.blue_scale = max_scale
    }
  }
  if self.scale.bits < self.blue_scale.bits {
    self.suppress_overshoot = true
    let c06 = CffFixed::from_f64(0.6)
    self.boost = c06.sub(c06.mul_div(self.scale, self.blue_scale))
    self.boost = self.boost.min_with(CffFixed::from_bits(0x7FFF))
  }
  if self.darken_y.bits != 0 {
    self.boost = CffFixed::zero()
  }
  let scale = self.scale
  let boost0 = self.boost
  for z in 0.. StemHint {
  {
    is_used: false,
    min: CffFixed::zero(),
    max: CffFixed::zero(),
    ds_min: CffFixed::zero(),
    ds_max: CffFixed::zero(),
  }
}

///|
priv struct Hint {
  mut flags : Int
  mut index : Int
  mut cs_coord : CffFixed
  mut ds_coord : CffFixed
  mut scale : CffFixed
}

///|
fn Hint::default() -> Hint {
  {
    flags: 0,
    index: 0,
    cs_coord: CffFixed::zero(),
    ds_coord: CffFixed::zero(),
    scale: CffFixed::zero(),
  }
}

///|
fn Hint::is_valid(self : Hint) -> Bool {
  self.flags != 0
}

///|
fn Hint::is_bottom(self : Hint) -> Bool {
  (self.flags & (CFF_HINT_GHOST_BOTTOM | CFF_HINT_PAIR_BOTTOM)) != 0
}

///|
fn Hint::is_top(self : Hint) -> Bool {
  (self.flags & (CFF_HINT_GHOST_TOP | CFF_HINT_PAIR_TOP)) != 0
}

///|
fn Hint::is_pair(self : Hint) -> Bool {
  (self.flags & (CFF_HINT_PAIR_BOTTOM | CFF_HINT_PAIR_TOP)) != 0
}

///|
fn Hint::is_pair_top(self : Hint) -> Bool {
  (self.flags & CFF_HINT_PAIR_TOP) != 0
}

///|
fn Hint::is_locked(self : Hint) -> Bool {
  (self.flags & CFF_HINT_LOCKED) != 0
}

///|
fn Hint::is_synthetic(self : Hint) -> Bool {
  (self.flags & CFF_HINT_SYNTHETIC) != 0
}

///|
fn Hint::lock(self : Hint) -> Unit {
  self.flags = self.flags | CFF_HINT_LOCKED
}

///|
fn Hint::setup(
  self : Hint,
  stem : StemHint,
  index : Int,
  origin : CffFixed,
  scale : CffFixed,
  darken_y : CffFixed,
  is_bottom : Bool,
) -> Unit {
  // "Ghost hints" have special widths -21 (bottom) and -20 (top).
  let ghost_bottom_width = CffFixed::from_i32(-21)
  let ghost_top_width = CffFixed::from_i32(-20)
  let width = stem.max.sub(stem.min)
  if width.bits == ghost_bottom_width.bits {
    if is_bottom {
      self.cs_coord = stem.max
      self.flags = CFF_HINT_GHOST_BOTTOM
    } else {
      self.flags = 0
    }
  } else if width.bits == ghost_top_width.bits {
    if !is_bottom {
      self.cs_coord = stem.min
      self.flags = CFF_HINT_GHOST_TOP
    } else {
      self.flags = 0
    }
  } else if width.bits < 0 {
    if is_bottom {
      self.cs_coord = stem.max
      self.flags = CFF_HINT_PAIR_BOTTOM
    } else {
      self.cs_coord = stem.min
      self.flags = CFF_HINT_PAIR_TOP
    }
  } else if is_bottom {
    self.cs_coord = stem.min
    self.flags = CFF_HINT_PAIR_BOTTOM
  } else {
    self.cs_coord = stem.max
    self.flags = CFF_HINT_PAIR_TOP
  }
  if Hint::is_top(self) {
    self.cs_coord = self.cs_coord.add(cff_hint_twice(darken_y))
  }
  self.cs_coord = self.cs_coord.add(origin)
  self.scale = scale
  self.index = index
  if self.flags != 0 && stem.is_used {
    self.ds_coord = if Hint::is_top(self) { stem.ds_max } else { stem.ds_min }
    Hint::lock(self)
  } else {
    self.ds_coord = self.cs_coord.mul(scale)
  }
}

///|
fn HintState::capture(
  self : HintState,
  bottom_edge : Hint,
  top_edge : Hint,
) -> Bool {
  let fuzz = self.blue_fuzz
  let mut captured = false
  let mut adjustment = CffFixed::zero()
  for zone in HintState::zones(self).iter() {
    if zone.is_bottom &&
      Hint::is_bottom(bottom_edge) &&
      zone.cs_bottom_edge.wrapping_sub(fuzz).bits <= bottom_edge.cs_coord.bits &&
      bottom_edge.cs_coord.bits <= zone.cs_top_edge.wrapping_add(fuzz).bits {
      adjustment = if self.suppress_overshoot {
        zone.ds_flat_edge
      } else if zone.cs_top_edge.wrapping_sub(bottom_edge.cs_coord).bits >=
        self.blue_shift.bits {
        bottom_edge.ds_coord
        .round()
        .min_with(zone.ds_flat_edge.sub(CffFixed::one()))
      } else {
        bottom_edge.ds_coord.round()
      }
      adjustment = adjustment.sub(bottom_edge.ds_coord)
      captured = true
      break
    }
    if !zone.is_bottom &&
      Hint::is_top(top_edge) &&
      zone.cs_bottom_edge.wrapping_sub(fuzz).bits <= top_edge.cs_coord.bits &&
      top_edge.cs_coord.bits <= zone.cs_top_edge.wrapping_add(fuzz).bits {
      adjustment = if self.suppress_overshoot {
        zone.ds_flat_edge
      } else if top_edge.cs_coord.wrapping_sub(zone.cs_bottom_edge).bits >=
        self.blue_shift.bits {
        top_edge.ds_coord
        .round()
        .max_with(zone.ds_flat_edge.add(CffFixed::one()))
      } else {
        top_edge.ds_coord.round()
      }
      adjustment = adjustment.sub(top_edge.ds_coord)
      captured = true
      break
    }
  }
  if captured {
    if Hint::is_valid(bottom_edge) {
      bottom_edge.ds_coord = bottom_edge.ds_coord.add(adjustment)
      Hint::lock(bottom_edge)
    }
    if Hint::is_valid(top_edge) {
      top_edge.ds_coord = top_edge.ds_coord.add(adjustment)
      Hint::lock(top_edge)
    }
  }
  captured
}

///|
priv struct HintMap {
  edges : Array[Hint]
  mut len : Int
  mut is_valid : Bool
  scale : CffFixed
}

///|
fn HintMap::HintMap(scale : CffFixed) -> HintMap {
  let edges : Array[Hint] = Array::new()
  for _ in 0.. Unit {
  self.len = 0
  self.is_valid = false
}

///|
fn HintMap::transform(self : HintMap, coord : CffFixed) -> CffFixed {
  if self.len == 0 {
    return coord.mul(self.scale)
  }
  let limit = self.len - 1
  let mut i = 0
  while i < limit && coord.bits >= self.edges.at(i + 1).cs_coord.bits {
    i = i + 1
  }
  while i > 0 && coord.bits < self.edges.at(i).cs_coord.bits {
    i = i - 1
  }
  let first_edge = self.edges.at(0)
  if i == 0 && coord.bits < first_edge.cs_coord.bits {
    coord.sub(first_edge.cs_coord).mul(self.scale).add(first_edge.ds_coord)
  } else {
    let edge = self.edges.at(i)
    coord.sub(edge.cs_coord).mul(edge.scale).add(edge.ds_coord)
  }
}

///|
fn HintMap::insert(
  self : HintMap,
  bottom : Hint,
  top : Hint,
  initial : HintMap?,
) -> Unit {
  let mut is_pair = false
  let mut first_edge = Hint::default()
  if !Hint::is_valid(bottom) {
    is_pair = false
    first_edge = top
  } else if !Hint::is_valid(top) {
    is_pair = false
    first_edge = bottom
  } else {
    is_pair = true
    first_edge = bottom
  }
  let second_edge = top
  if is_pair && top.cs_coord.bits < bottom.cs_coord.bits {
    return
  }
  let edge_count = if is_pair { 2 } else { 1 }
  if self.len + edge_count > CFF_HINT_MAX_HINTS {
    return
  }
  let mut insert_ix = 0
  while insert_ix < self.len {
    if self.edges.at(insert_ix).cs_coord.bits >= first_edge.cs_coord.bits {
      break
    }
    insert_ix = insert_ix + 1
  }
  if insert_ix < self.len {
    let current = self.edges.at(insert_ix)
    if current.cs_coord.bits == first_edge.cs_coord.bits ||
      (is_pair && current.cs_coord.bits <= second_edge.cs_coord.bits) ||
      Hint::is_pair_top(current) {
      return
    }
  }
  if !Hint::is_locked(first_edge) && initial is Some(init) {
    if is_pair {
      let mid = HintMap::transform(
        init,
        cff_hint_midpoint(first_edge.cs_coord, second_edge.cs_coord),
      )
      let half_width = cff_hint_half(
        second_edge.cs_coord.sub(first_edge.cs_coord),
      ).mul(self.scale)
      first_edge.ds_coord = mid.sub(half_width)
      second_edge.ds_coord = mid.add(half_width)
    } else {
      first_edge.ds_coord = HintMap::transform(init, first_edge.cs_coord)
    }
  }
  if insert_ix > 0 &&
    first_edge.ds_coord.bits < self.edges.at(insert_ix - 1).ds_coord.bits {
    return
  }
  if insert_ix < self.len &&
    (
      (
        is_pair &&
        second_edge.ds_coord.bits > self.edges.at(insert_ix).ds_coord.bits
      ) ||
      first_edge.ds_coord.bits > self.edges.at(insert_ix).ds_coord.bits
    ) {
    return
  }
  if insert_ix != self.len {
    let mut src = self.len - 1
    let mut dst = self.len + edge_count - 1
    while src > insert_ix {
      self.edges.set(dst, self.edges.at(src))
      src = src - 1
      dst = dst - 1
    }
    self.edges.set(dst, self.edges.at(src))
  }
  self.edges.set(insert_ix, first_edge)
  if is_pair {
    self.edges.set(insert_ix + 1, second_edge)
  }
  self.len = self.len + edge_count
}

///|
fn HintMap::adjust(self : HintMap) -> Unit {
  let saved : Array[(Int, CffFixed)] = Array::new()
  for _ in 0..= self.len - 1 ||
        self.edges.at(j + 1).ds_coord.bits >=
        self.edges.at(j).ds_coord
        .add(move_up)
        .add(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS)).bits {
        if i == 0 ||
          self.edges.at(i - 1).ds_coord.bits <=
          self.edges.at(i).ds_coord
          .add(move_down)
          .sub(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS)).bits {
          if move_down.neg().bits < move_up.bits {
            move_down
          } else {
            move_up
          }
        } else {
          move_up
        }
      } else if i == 0 ||
        self.edges.at(i - 1).ds_coord.bits <=
        self.edges.at(i).ds_coord
        .add(move_down)
        .sub(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS)).bits {
        save_edge = move_up.bits < move_down.neg().bits
        move_down
      } else {
        save_edge = true
        CffFixed::zero()
      }
      if save_edge && j < self.len - 1 && !Hint::is_locked(self.edges.at(j + 1)) {
        saved.set(saved_count, (j, move_up.sub(adjustment)))
        saved_count = saved_count + 1
      }
      self.edges.at(i).ds_coord = self.edges.at(i).ds_coord.add(adjustment)
      if is_pair {
        self.edges.at(j).ds_coord = self.edges.at(j).ds_coord.add(adjustment)
      }
    }
    if i > 0 &&
      self.edges.at(i).cs_coord.bits != self.edges.at(i - 1).cs_coord.bits {
      let a = self.edges.at(i)
      let b = self.edges.at(i - 1)
      self.edges.at(i - 1).scale = a.ds_coord
        .sub(b.ds_coord)
        .div(a.cs_coord.sub(b.cs_coord))
    }
    if is_pair {
      if self.edges.at(j).cs_coord.bits != self.edges.at(j - 1).cs_coord.bits {
        let a = self.edges.at(j)
        let b = self.edges.at(j - 1)
        self.edges.at(j - 1).scale = a.ds_coord
          .sub(b.ds_coord)
          .div(a.cs_coord.sub(b.cs_coord))
      }
      i = i + 1
    }
    i = i + 1
  }
  let mut k = saved_count - 1
  while k >= 0 {
    let (j, adjustment) = saved.at(k)
    if self.edges.at(j + 1).ds_coord.bits >=
      self.edges.at(j).ds_coord
      .add(adjustment)
      .add(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS)).bits {
      self.edges.at(j).ds_coord = self.edges.at(j).ds_coord.add(adjustment)
      if Hint::is_pair(self.edges.at(j)) {
        self.edges.at(j - 1).ds_coord = self.edges.at(j - 1).ds_coord.add(
          adjustment,
        )
      }
    }
    if k == 0 {
      break
    }
    k = k - 1
  }
}

///|
priv struct HintMask {
  mask : Array[Byte]
  mut is_valid : Bool
}

///|
fn HintMask::default() -> HintMask {
  let mask : Array[Byte] = Array::new()
  for _ in 0.. HintMask {
  let mask : Array[Byte] = Array::new()
  for _ in 0.. Int {
  1 << (7 - (bit & 0x7))
}

///|
fn HintMask::from_bytes(bytes : ArrayView[Byte]) -> HintMask? {
  let len = bytes.length()
  if len > CFF_HINT_MASK_SIZE {
    return None
  }
  let m = HintMask::default()
  for i in 0.. HintMask {
  let m = HintMask::default()
  for i in 0.. Unit {
  let ix = bit >> 3
  let m = cff_hint_msb_mask(bit)
  self.mask.set(ix, (self.mask.at(ix).to_int() & (m |> Int::lnot)).to_byte())
}

///|
fn HintMask::get(self : HintMask, bit : Int) -> Bool {
  let ix = bit >> 3
  let m = cff_hint_msb_mask(bit)
  (self.mask.at(ix).to_int() & m) != 0
}

///|
fn HintMap::build(
  self : HintMap,
  state : HintState,
  mask0 : HintMask?,
  initial_map_opt : HintMap?,
  stems : Array[StemHint],
  stem_count : Int,
  origin : CffFixed,
  is_initial : Bool,
) -> Unit {
  let scale = state.scale
  let darken_y = CffFixed::zero()
  if !is_initial && initial_map_opt is Some(initial_map) {
    if !initial_map.is_valid {
      HintMap::build(
        initial_map,
        state,
        Some(HintMask::all()),
        None,
        stems,
        stem_count,
        origin,
        true,
      )
    }
  }
  let initial_map = initial_map_opt
  HintMap::clear(self)
  let mut mask = match mask0 {
    None => HintMask::all()
    Some(m) => m
  }
  if !mask.is_valid {
    mask = HintMask::all()
  }
  if state.do_em_box_hints {
    let bottom = Hint::default()
    bottom.cs_coord = CffFixed::from_bits(CFF_HINT_ICF_BOTTOM_BITS).sub(
      CffFixed::from_bits(CFF_HINT_EPSILON_BITS),
    )
    bottom.ds_coord = bottom.cs_coord
      .mul(scale)
      .round()
      .sub(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS))
    bottom.scale = scale
    bottom.flags = CFF_HINT_GHOST_BOTTOM | CFF_HINT_LOCKED | CFF_HINT_SYNTHETIC
    let top = Hint::default()
    top.cs_coord = CffFixed::from_bits(CFF_HINT_ICF_TOP_BITS)
      .add(CffFixed::from_bits(CFF_HINT_EPSILON_BITS))
      .add(cff_hint_twice(state.darken_y))
    top.ds_coord = top.cs_coord
      .mul(scale)
      .round()
      .add(CffFixed::from_bits(CFF_HINT_MIN_COUNTER_BITS))
    top.scale = scale
    top.flags = CFF_HINT_GHOST_TOP | CFF_HINT_LOCKED | CFF_HINT_SYNTHETIC
    let invalid = Hint::default()
    HintMap::insert(self, bottom, invalid, initial_map)
    HintMap::insert(self, invalid, top, initial_map)
  }
  // HintMask uses an Array internally; make a deep copy so that clearing bits
  // for the two-pass build doesn't mutate the caller's mask.
  let tmp_mask = HintMask::copy(mask)
  for i in 0.. 0 ||
      self.edges.at(self.len - 1).cs_coord.bits < 0 {
      let edge = Hint::default()
      edge.flags = CFF_HINT_GHOST_BOTTOM | CFF_HINT_LOCKED | CFF_HINT_SYNTHETIC
      edge.scale = scale
      let invalid = Hint::default()
      HintMap::insert(self, edge, invalid, None)
    }
  } else {
    for i in 0.. Double {
  v.bits.to_double() / 65536.0
}

///|
fn HintMask::equals(self : HintMask, other : HintMask) -> Bool {
  if self.is_valid != other.is_valid {
    return false
  }
  for i in 0.. CffHintingSink {
  let stem_hints : Array[StemHint] = Array::new()
  for _ in 0.. (CffFixed, CffFixed) {
  match self.matrix {
    None => (x, y)
    Some(m) => cff_matrix_transform_hinted(m, x, y)
  }
}

///|
fn CffHintingSink::finish(self : CffHintingSink) -> Unit {
  CffHintingSink::maybe_close_subpath(self)
}

///|
fn CffHintingSink::maybe_close_subpath(self : CffHintingSink) -> Unit {
  match (self.start_point, self.pending_line) {
    (Some((sx, sy)), Some((cs_x, cs_y, ds_x, ds_y))) => {
      if sx.bits != cs_x.bits || sy.bits != cs_y.bits {
        self.out.push(
          LineTo(cff_hint_fixed_to_double(ds_x), cff_hint_fixed_to_double(ds_y)),
        )
      }
      self.out.push(Close)
    }
    (Some(_), None) => self.out.push(Close)
    _ => ()
  }
  self.start_point = None
  self.pending_line = None
}

///|
fn CffHintingSink::flush_pending_line(self : CffHintingSink) -> Unit {
  match self.pending_line {
    None => ()
    Some((_cs_x, _cs_y, ds_x, ds_y)) => {
      self.pending_line = None
      self.out.push(
        LineTo(cff_hint_fixed_to_double(ds_x), cff_hint_fixed_to_double(ds_y)),
      )
    }
  }
}

///|
fn CffHintingSink::build_hint_map(
  self : CffHintingSink,
  mask : HintMask?,
  origin : CffFixed,
) -> Unit {
  HintMap::build(
    self.map,
    self.state,
    mask,
    Some(self.initial_map),
    self.stem_hints,
    self.stem_count,
    origin,
    false,
  )
}

///|
fn CffHintingSink::hint(self : CffHintingSink, coord : CffFixed) -> CffFixed {
  if !self.map.is_valid {
    CffHintingSink::build_hint_map(self, Some(self.mask), CffFixed::zero())
  }
  cff_hint_trunc(HintMap::transform(self.map, coord))
}

///|
fn CffHintingSink::scale(self : CffHintingSink, coord : CffFixed) -> CffFixed {
  cff_hint_trunc(coord.mul(self.state.scale))
}

///|
fn CffHintingSink::add_stem(
  self : CffHintingSink,
  min : CffFixed,
  max : CffFixed,
) -> Unit {
  let index = self.stem_count
  if index >= CFF_HINT_MAX_HINTS || self.map.is_valid {
    return
  }
  let stem = self.stem_hints.at(index)
  stem.min = min
  stem.max = max
  stem.is_used = false
  stem.ds_min = CffFixed::zero()
  stem.ds_max = CffFixed::zero()
  self.stem_count = index + 1
}

///|
fn CffHintingSink::hstem(
  self : CffHintingSink,
  min : CffFixed,
  max : CffFixed,
) -> Unit {
  CffHintingSink::add_stem(self, min, max)
}

///|
fn HintMask::from_bytes_view(bytes : BytesView) -> HintMask? {
  let a : Array[Byte] = Array::new()
  for b in bytes.iter() {
    a.push(b)
  }
  HintMask::from_bytes(a.op_as_view())
}

///|
fn CffHintingSink::hint_mask(
  self : CffHintingSink,
  mask_bytes : BytesView,
) -> Unit {
  let mask = match HintMask::from_bytes_view(mask_bytes) {
    None => HintMask::all()
    Some(m) => m
  }
  if !HintMask::equals(mask, self.mask) {
    self.mask = mask
    self.map.is_valid = false
  }
}

///|
fn CffHintingSink::counter_mask(
  self : CffHintingSink,
  mask_bytes : BytesView,
) -> Unit {
  let mask = match HintMask::from_bytes_view(mask_bytes) {
    None => HintMask::all()
    Some(m) => m
  }
  let map = HintMap(self.state.scale)
  HintMap::build(
    map,
    self.state,
    Some(mask),
    Some(self.initial_map),
    self.stem_hints,
    self.stem_count,
    CffFixed::zero(),
    false,
  )
}

///|
fn CffHintingSink::move_to(
  self : CffHintingSink,
  x : CffFixed,
  y : CffFixed,
) -> Unit {
  CffHintingSink::maybe_close_subpath(self)
  self.start_point = Some((x, y))
  let x1 = CffHintingSink::scale(self, x)
  let y1 = CffHintingSink::hint(self, y)
  let (x1, y1) = CffHintingSink::transform_hinted(self, x1, y1)
  self.out.push(
    MoveTo(cff_hint_fixed_to_double(x1), cff_hint_fixed_to_double(y1)),
  )
}

///|
fn CffHintingSink::line_to(
  self : CffHintingSink,
  x : CffFixed,
  y : CffFixed,
) -> Unit {
  CffHintingSink::flush_pending_line(self)
  let ds_x = CffHintingSink::scale(self, x)
  let ds_y = CffHintingSink::hint(self, y)
  let (ds_x, ds_y) = CffHintingSink::transform_hinted(self, ds_x, ds_y)
  self.pending_line = Some((x, y, ds_x, ds_y))
}

///|
fn CffHintingSink::curve_to(
  self : CffHintingSink,
  cx1 : CffFixed,
  cy1 : CffFixed,
  cx2 : CffFixed,
  cy2 : CffFixed,
  x : CffFixed,
  y : CffFixed,
) -> Unit {
  CffHintingSink::flush_pending_line(self)
  let cx1 = CffHintingSink::scale(self, cx1)
  let cy1 = CffHintingSink::hint(self, cy1)
  let cx2 = CffHintingSink::scale(self, cx2)
  let cy2 = CffHintingSink::hint(self, cy2)
  let x = CffHintingSink::scale(self, x)
  let y = CffHintingSink::hint(self, y)
  let (cx1, cy1) = CffHintingSink::transform_hinted(self, cx1, cy1)
  let (cx2, cy2) = CffHintingSink::transform_hinted(self, cx2, cy2)
  let (x, y) = CffHintingSink::transform_hinted(self, x, y)
  self.out.push(
    CurveTo(
      cff_hint_fixed_to_double(cx1),
      cff_hint_fixed_to_double(cy1),
      cff_hint_fixed_to_double(cx2),
      cff_hint_fixed_to_double(cy2),
      cff_hint_fixed_to_double(x),
      cff_hint_fixed_to_double(y),
    ),
  )
}

///|
fn CffHintingSink::close(_self : CffHintingSink) -> Unit {
  // Close emitted based on moves; see maybe_close_subpath.
}