// 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.

///|
/// Support for applying embedded hinting instructions.
///
/// Ported from `fontations/skrifa/src/outline/hint.rs` (Apache-2.0 OR MIT).

///|
pub(all) suberror HintError {
  Unsupported
  UnexpectedEndOfBytecode
  /// Execution stopped on an opcode that this port does not yet implement.
  UnhandledOpcode(Int, Int)
  DefinitionInGlyphProgram
  NestedDefinition
  DefinitionTooLarge
  TooManyDefinitions
  InvalidDefinition(Int)
  ValueStackOverflow
  ValueStackUnderflow
  CallStackOverflow
  CallStackUnderflow
  InvalidStackValue(Int)
  InvalidPointIndex(Int)
  InvalidPointRange(Int, Int)
  InvalidContourIndex(Int)
  InvalidCvtIndex(Int)
  InvalidStorageIndex(Int)
  DivideByZero
  InvalidZoneIndex(Int)
  NegativeLoopCounter
  InvalidJump
  ExceededExecutionBudget
}

///|
pub struct HintingOptions {
  engine : Engine
  target : Target
}

///|
pub fn HintingOptions::default() -> HintingOptions {
  HintingOptions::{ engine: Engine::AutoFallback, target: Target::default() }
}

///|
/// Creates hinting options with explicit engine + target.
pub fn HintingOptions::new(engine : Engine, target : Target) -> HintingOptions {
  HintingOptions::{ engine, target }
}

///|
pub(all) enum Engine {
  Interpreter
  /// Automatic hinter that adjusts outlines without embedded instructions.
  ///
  /// Glyph styles can be precomputed per font and may be provided here as an
  /// optimization to avoid recomputing them for each instance.
  Auto(GlyphStyles?)
  AutoFallback
}

///|
/// Resolves `AutoFallback` based on outline collection preference.
pub fn Engine::resolve_auto_fallback(
  self : Engine,
  outlines : OutlineGlyphCollection,
) -> Engine {
  match self {
    Engine::AutoFallback =>
      if outlines.prefer_interpreter() {
        Engine::Interpreter
      } else {
        Engine::Auto(None)
      }
    _ => self
  }
}

///|
/// Converts an engine selection into `HintingOptions` with default target.
pub fn Engine::into(self : Engine) -> HintingOptions {
  HintingOptions::new(self, Target::default())
}

///|
pub(all) enum Target {
  Mono
  Smooth(SmoothMode, Bool, Bool)
}

///|
pub fn Target::default() -> Target {
  SmoothMode::Normal.into()
}

///|
pub(all) enum SmoothMode {
  Normal
  Light
  Lcd
  VerticalLcd
}

///|
pub fn SmoothMode::into(self : SmoothMode) -> Target {
  Target::Smooth(self, true, false)
}

///|
/// Modes that control hinting when using embedded instructions.
///
/// This mirrors the legacy FreeType hinting mode selector and is intentionally
/// undocumented upstream.
pub(all) enum HintingMode {
  Strong
  Smooth(LcdLayout?, Bool)
}

///|
/// Specifies direction of pixel layout for LCD-based subpixel hinting.
pub(all) enum LcdLayout {
  Horizontal
  Vertical
}

///|
pub fn HintingMode::into(self : HintingMode) -> HintingOptions {
  let target = match self {
    HintingMode::Strong => Target::Mono
    HintingMode::Smooth(lcd_subpixel, preserve_linear_metrics) => {
      let mode = match lcd_subpixel {
        None => SmoothMode::Normal
        Some(LcdLayout::Horizontal) => SmoothMode::Lcd
        Some(LcdLayout::Vertical) => SmoothMode::VerticalLcd
      }
      Target::Smooth(mode, true, preserve_linear_metrics)
    }
  }
  HintingOptions::{ engine: Engine::AutoFallback, target }
}

///|
pub fn Target::mode(self : Target) -> SmoothMode? {
  match self {
    Target::Smooth(mode, _, _) => Some(mode)
    _ => None
  }
}

///|
pub fn Target::symmetric_rendering(self : Target) -> Bool {
  match self {
    Target::Smooth(_, symmetric, _) => symmetric
    _ => false
  }
}

///|
pub fn Target::preserve_linear_metrics(self : Target) -> Bool {
  match self {
    Target::Smooth(_, _, preserve) => preserve
    _ => false
  }
}

///|
pub fn Target::is_smooth(self : Target) -> Bool {
  match self {
    Target::Smooth(_, _, _) => true
    _ => false
  }
}

///|
pub fn Target::is_grayscale_cleartype(self : Target) -> Bool {
  match self {
    Target::Smooth(SmoothMode::Normal, _, _) => true
    Target::Smooth(SmoothMode::Light, _, _) => true
    _ => false
  }
}

///|
pub fn Target::is_light(self : Target) -> Bool {
  match self {
    Target::Smooth(SmoothMode::Light, _, _) => true
    _ => false
  }
}

///|
pub fn Target::is_lcd(self : Target) -> Bool {
  match self {
    Target::Smooth(SmoothMode::Lcd, _, _) => true
    _ => false
  }
}

///|
pub fn Target::is_vertical_lcd(self : Target) -> Bool {
  match self {
    Target::Smooth(SmoothMode::VerticalLcd, _, _) => true
    _ => false
  }
}

///|
/// Hinting instance that uses information embedded in the font to perform
/// grid-fitting.
///
/// This follows the same high-level behavior as upstream:
/// - The backend engine is selected at configuration time (Interpreter/Auto).
/// - For TrueType outlines, `fpgm` + `prep` are executed during configuration.
pub struct HintingInstance {
  priv mut size : @moon_skrifa.Size
  priv coords : Array[@moon_skrifa.NormalizedCoord]
  priv mut target : Target
  priv mut kind : HinterKind
}

///|
priv enum HinterKind {
  None_
  Glyf(TtHintInstance)
  Auto(AutoHintInstance)
}

///|
/// Placeholder for the runtime autohinter instance.
///
/// The full implementation is tracked separately; this type is used to wire
/// `Engine::Auto` through `HintingInstance` and `DrawSettings`.
priv struct AutoHintInstance {
  priv _private : Unit
}

///|
fn AutoHintInstance::default() -> AutoHintInstance {
  AutoHintInstance::{ _private: () }
}

///|
fn hint_round_ppem(scale_bits : Int, upem : Int) -> Int {
  let v = scale_bits.to_int64() * upem.to_int64()
  let ppem_26_6 = (v + (0x8000).to_int64()) >> 16
  ((ppem_26_6 + (32).to_int64()) >> 6).to_int()
}

///|
pub fn HintingInstance::new(
  outlines : OutlineGlyphCollection,
  size : @moon_skrifa.Size,
  location : @moon_skrifa.LocationRef,
  options : HintingOptions,
) -> Result[HintingInstance, DrawError] {
  let h = HintingInstance::{
    size: @moon_skrifa.Size::unscaled(),
    coords: Array::new(),
    target: options.target,
    kind: HinterKind::None_,
  }
  match h.reconfigure(outlines, size, location, options) {
    Err(e) => Err(e)
    Ok(_) => Ok(h)
  }
}

///|
pub fn HintingInstance::size(self : HintingInstance) -> @moon_skrifa.Size {
  self.size
}

///|
pub fn HintingInstance::location(
  self : HintingInstance,
) -> @moon_skrifa.LocationRef {
  @moon_skrifa.LocationRef::new(self.coords.op_as_view())
}

///|
pub fn HintingInstance::target(self : HintingInstance) -> Target {
  self.target
}

///|
pub fn HintingInstance::is_enabled(self : HintingInstance) -> Bool {
  match self.kind {
    HinterKind::None_ => false
    HinterKind::Glyf(tt) => tt.is_enabled()
    HinterKind::Auto(_) => true
  }
}

///|
pub fn HintingInstance::reconfigure(
  self : HintingInstance,
  outlines : OutlineGlyphCollection,
  size : @moon_skrifa.Size,
  location : @moon_skrifa.LocationRef,
  options : HintingOptions,
) -> Result[Unit, DrawError] {
  self.size = size
  self.coords.clear()
  for c in location.effective_coords().iter() {
    self.coords.push(c)
  }
  self.target = options.target
  let engine = match options.engine {
    Engine::AutoFallback =>
      if outlines.prefer_interpreter() {
        Engine::Interpreter
      } else {
        Engine::Auto(None)
      }
    _ => options.engine
  }
  let prev = self.kind
  self.kind = HinterKind::None_
  match engine {
    Engine::Interpreter =>
      match outlines.format() {
        Some(OutlineGlyphFormat::Glyf) => {
          let tt = match prev {
            HinterKind::Glyf(inst) => inst
            _ => TtHintInstance::default()
          }
          let upem = outline_units_per_em(outlines.font).to_int()
          let size_for_scale = match size.ppem() {
            None => size
            Some(ppem) =>
              if outlines.fractional_size_hinting() {
                size
              } else {
                @moon_skrifa.Size::new(ppem.round())
              }
          }
          let (_, scale_bits) = glyf_compute_scale_bits(
            size_for_scale,
            upem.to_uint16(),
          )
          let rounded_ppem = hint_round_ppem(scale_bits, upem)
          match
            tt.reconfigure(
              outlines.font,
              scale_bits,
              rounded_ppem,
              self.target,
              self.coords.op_as_view(),
            ) {
            Err(e) => return Err(DrawError::HintingFailed(e))
            Ok(_) => ()
          }
          self.kind = HinterKind::Glyf(tt)
        }
        _ => ()
      }
    Engine::Auto(_styles) =>
      match outlines.format() {
        None => ()
        Some(_) => self.kind = HinterKind::Auto(AutoHintInstance::default())
      }
    _ => ()
  }
  Ok(())
}

///|
fn tt_bits_to_double(bits : Int) -> Double {
  bits.to_double() / 64.0
}

///|
fn tt_zone_emit_contour_freetype(
  out : Array[PathElement],
  zone : TtZone,
  start : Int,
  end : Int,
  x_shift : Int,
) -> Unit {
  let n = end - start + 1
  if n <= 0 {
    return
  }
  let first = zone.points.at(start)
  let last = zone.points.at(end)
  let first_on = (zone.flags.at(start) & TT_POINT_ON_CURVE) != 0
  let last_on = (zone.flags.at(end) & TT_POINT_ON_CURVE) != 0
  let (sx, sy, start_idx) = if first_on {
    (first.x - x_shift, first.y, start)
  } else if last_on {
    (last.x - x_shift, last.y, end)
  } else {
    (
      glyf_midpoint_i32(last.x - x_shift, first.x - x_shift),
      glyf_midpoint_i32(last.y, first.y),
      -1,
    )
  }
  out.push(MoveTo(tt_bits_to_double(sx), tt_bits_to_double(sy)))
  let mut control : (Int, Int)? = None
  if start_idx == -1 {
    for i in start..<(end + 1) {
      let p = zone.points.at(i)
      let on = (zone.flags.at(i) & TT_POINT_ON_CURVE) != 0
      let px = p.x - x_shift
      let py = p.y
      if on {
        match control {
          None => out.push(LineTo(tt_bits_to_double(px), tt_bits_to_double(py)))
          Some((cx, cy)) => {
            out.push(
              QuadTo(
                tt_bits_to_double(cx),
                tt_bits_to_double(cy),
                tt_bits_to_double(px),
                tt_bits_to_double(py),
              ),
            )
            control = None
          }
        }
      } else {
        match control {
          None => control = Some((px, py))
          Some((cx, cy)) => {
            let mx = glyf_midpoint_i32(cx, px)
            let my = glyf_midpoint_i32(cy, py)
            out.push(
              QuadTo(
                tt_bits_to_double(cx),
                tt_bits_to_double(cy),
                tt_bits_to_double(mx),
                tt_bits_to_double(my),
              ),
            )
            control = Some((px, py))
          }
        }
      }
    }
  } else {
    let base = start_idx - start
    for step in 1.. out.push(LineTo(tt_bits_to_double(px), tt_bits_to_double(py)))
          Some((cx, cy)) => {
            out.push(
              QuadTo(
                tt_bits_to_double(cx),
                tt_bits_to_double(cy),
                tt_bits_to_double(px),
                tt_bits_to_double(py),
              ),
            )
            control = None
          }
        }
      } else {
        match control {
          None => control = Some((px, py))
          Some((cx, cy)) => {
            let mx = glyf_midpoint_i32(cx, px)
            let my = glyf_midpoint_i32(cy, py)
            out.push(
              QuadTo(
                tt_bits_to_double(cx),
                tt_bits_to_double(cy),
                tt_bits_to_double(mx),
                tt_bits_to_double(my),
              ),
            )
            control = Some((px, py))
          }
        }
      }
    }
  }
  match control {
    Some((cx, cy)) =>
      out.push(
        QuadTo(
          tt_bits_to_double(cx),
          tt_bits_to_double(cy),
          tt_bits_to_double(sx),
          tt_bits_to_double(sy),
        ),
      )
    None => ()
  }
  out.push(Close)
}

///|
fn tt_zone_to_path_freetype(zone : TtZone) -> Array[PathElement] {
  if zone.contours.length() == 0 {
    return Array::new()
  }
  let last_end = zone.contours.at(zone.contours.length() - 1)
  let num_points = last_end + 1
  if num_points <= 0 || zone.points.length() < num_points + 4 {
    return Array::new()
  }
  let x_shift = zone.points.at(num_points).x
  let out : Array[PathElement] = Array::new()
  let mut start = 0
  for end_ix in zone.contours.iter() {
    if end_ix >= start {
      tt_zone_emit_contour_freetype(out, zone, start, end_ix, x_shift)
    }
    start = end_ix + 1
  }
  out
}

///|
const TAG_HDMX_TT_HINT : UInt = 0x68646D78 // "hdmx"

///|
const TAG_MAXP_TT_HINT : UInt = 0x6D617870 // "maxp"

///|
fn hint_read_u16_be(view : BytesView, offset : Int) -> Int? {
  if offset < 0 || offset + 2 > view.length() {
    None
  } else {
    let b0 = view.at(offset).to_int()
    let b1 = view.at(offset + 1).to_int()
    Some((b0 << 8) | b1)
  }
}

///|
fn hint_read_u32_be(view : BytesView, offset : Int) -> UInt? {
  if offset < 0 || offset + 4 > view.length() {
    None
  } else {
    let b0 = view.at(offset).to_uint()
    let b1 = view.at(offset + 1).to_uint()
    let b2 = view.at(offset + 2).to_uint()
    let b3 = view.at(offset + 3).to_uint()
    Some((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)
  }
}

///|
fn hint_hdmx_width(
  font : @moon_skrifa.FontRef,
  ppem : Int,
  gid : @moon_skrifa.GlyphId,
) -> Int? {
  let hdmx = match font.table(TAG_HDMX_TT_HINT) {
    None => return None
    Some(v) => v
  }
  if hdmx.length() < 8 {
    return None
  }
  let maxp = match font.table(TAG_MAXP_TT_HINT) {
    None => return None
    Some(v) => v
  }
  let num_glyphs = hint_read_u16_be(maxp, 4).unwrap_or(0)
  if num_glyphs <= 0 {
    return None
  }
  let idx = gid.to_uint64().to_int()
  if idx < 0 || idx >= num_glyphs {
    return None
  }
  let num_records = hint_read_u16_be(hdmx, 2).unwrap_or(0)
  let rec_size = hint_read_u32_be(hdmx, 4).unwrap_or(0).to_uint64().to_int()
  if rec_size <= 0 {
    return None
  }
  let base = 8
  for i in 0.. hdmx.length() {
      continue
    }
    // record: ppem (u8), max_width (u8), widths[num_glyphs]
    if off + 2 + num_glyphs > hdmx.length() {
      continue
    }
    let pix = hdmx.at(off).to_int()
    if pix == ppem {
      let widths_off = off + 2
      return Some(hdmx.at(widths_off + idx).to_int())
    }
  }
  None
}

///|
fn HintingInstance::hinted_path(
  self : HintingInstance,
  glyph : OutlineGlyph,
  is_pedantic : Bool,
  path_style : PathStyle,
) -> Result[(AdjustedMetrics, Array[PathElement]), DrawError] {
  match self.kind {
    HinterKind::None_ => Err(DrawError::NoSources)
    HinterKind::Auto(_) => {
      let settings = DrawSettings::unhinted(
        self.size,
        @moon_skrifa.LocationRef::new(self.coords.op_as_view()),
      ).with_path_style(path_style)
      let metrics = match glyph.draw(settings, NullPen::new()) {
        Err(e) => return Err(e)
        Ok(m) => m
      }
      // Match upstream: when hinting is requested, advance width is rounded to
      // integer pixels.
      let metrics = AdjustedMetrics::{
        has_overlaps: metrics.has_overlaps,
        lsb: metrics.lsb,
        advance_width: match metrics.advance_width {
          None => None
          Some(v) => Some(v.round())
        },
      }
      let path = match glyph.path(settings) {
        Err(e) => return Err(e)
        Ok(p) => p
      }
      Ok((metrics, path))
    }
    HinterKind::Glyf(tt) => {
      if path_style is PathStyle::HarfBuzz {
        return Err(DrawError::HarfBuzzHintingUnsupported)
      }
      if !tt.is_enabled() {
        let settings = DrawSettings::unhinted(
          self.size,
          @moon_skrifa.LocationRef::new(self.coords.op_as_view()),
        ).with_path_style(path_style)
        let metrics0 = match glyph.draw(settings, NullPen::new()) {
          Err(e) => return Err(e)
          Ok(m) => m
        }
        let metrics = AdjustedMetrics::{
          has_overlaps: metrics0.has_overlaps,
          lsb: metrics0.lsb,
          advance_width: match metrics0.advance_width {
            None => None
            Some(v) => Some(v.round())
          },
        }
        let path = match glyph.path(settings) {
          Err(e) => return Err(e)
          Ok(p) => p
        }
        return Ok((metrics, path))
      }
      self.hinted_glyf_path(glyph, is_pedantic)
    }
  }
}

///|
fn HintingInstance::hinted_glyf_path(
  self : HintingInstance,
  glyph : OutlineGlyph,
  is_pedantic : Bool,
) -> Result[(AdjustedMetrics, Array[PathElement]), DrawError] {
  let tt = match self.kind {
    HinterKind::Glyf(tt) => tt
    _ => return Err(DrawError::NoSources)
  }
  let font = glyph.font
  let gid = glyph.glyph_id
  let scale_bits = tt.retained.scale
  let fpgm = font.table(TAG_FPGM_TT).unwrap_or(tt_empty_bytes_view())
  let prep = font.table(TAG_PREP_TT).unwrap_or(tt_empty_bytes_view())
  let zone = match
    tt_load_glyph_zone_for_hinting(
      font,
      gid,
      self.coords.op_as_view(),
      scale_bits,
      tt,
      fpgm,
      prep,
      is_pedantic,
    ) {
    Err(e) => return Err(e)
    Ok(z) => z
  }
  if zone.contours.length() == 0 {
    return Ok((AdjustedMetrics::default(), Array::new()))
  }
  let last_end = zone.contours.at(zone.contours.length() - 1)
  let num_points = last_end + 1
  if num_points < 0 || zone.points.length() < num_points + 4 {
    return Err(DrawError::GlyphNotFound(gid))
  }
  let phantom0 = zone.points.at(num_points)
  let phantom1 = zone.points.at(num_points + 1)
  let mut hdmx : Int? = None
  if !tt.backward_compatibility() {
    match self.size.ppem() {
      None => ()
      Some(ppem) => {
        let ip = ppem.to_int()
        if ip.to_double() == ppem && ip >= 0 && ip <= 255 {
          hdmx = hint_hdmx_width(font, ip, gid)
        }
      }
    }
  }
  let advance0 = match hdmx {
    Some(w) => w.to_double()
    None => tt_bits_to_double(phantom1.x - phantom0.x)
  }
  let advance = advance0.round()
  let lsb = tt_bits_to_double(phantom0.x)
  let metrics = AdjustedMetrics::{
    has_overlaps: glyph.has_overlaps().unwrap_or(false),
    lsb: Some(lsb),
    advance_width: Some(advance),
  }
  Ok((metrics, tt_zone_to_path_freetype(zone)))
}