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

///|
/// Autohint topology: segment computation and linking.
///
/// Ported from `fontations/skrifa/src/outline/autohint/topo/segments.rs`
/// (Apache-2.0 OR MIT).

///|
priv enum AutoHintScriptGroup {
  Default
  Cjk
}

///|
fn autohint_derived_constant(units_per_em : Int, value : Int) -> Int {
  value * units_per_em / 2048
}

///|
fn autohint_fixed_div(a : Int, b : Int) -> Int {
  let mut sign = 1
  let mut aa = a.to_int64()
  let mut bb = b.to_int64()
  if aa < 0 {
    aa = -aa
    sign = -sign
  }
  if bb < 0 {
    bb = -bb
    sign = -sign
  }
  let q : Int64 = if bb == 0 {
    (0x7FFFFFFF).to_int64()
  } else {
    ((aa << 16) + (bb >> 1)) / bb
  }
  let out = q.to_int()
  if sign < 0 {
    -out
  } else {
    out
  }
}

///|
fn autohint_fixed_mul(a : Int, b : Int) -> Int {
  let prod = a.to_int64() * b.to_int64()
  let round : Int64 = (0x8000 - (if prod < 0 { 1 } else { 0 })).to_int64()
  ((prod + round) >> 16).to_int()
}

// Bounds for score, position and coordinate values.

///|
const AUTOHINT_SEG_MAX_SCORE : Int = 32000

///|
const AUTOHINT_SEG_MIN_SCORE : Int = -32000

///|
priv struct AutoHintAxis {
  mut dim : Int
  mut major_dir : AutoHintDirection
  segments : Array[AutoHintSegment]
  edges : Array[AutoHintEdge]
}

///|
fn AutoHintAxis::default() -> AutoHintAxis {
  { dim: 0, major_dir: None_, segments: Array::new(), edges: Array::new() }
}

///|
const AUTOHINT_DIM_HORIZONTAL : Int = 0

///|
const AUTOHINT_DIM_VERTICAL : Int = 1

///|
fn AutoHintAxis::reset(
  self : AutoHintAxis,
  dim : Int,
  orientation : AutoHintOrientation?,
) -> Unit {
  self.dim = dim
  self.major_dir = match (dim, orientation) {
    (AUTOHINT_DIM_HORIZONTAL, Some(Clockwise)) => Down
    (AUTOHINT_DIM_VERTICAL, Some(Clockwise)) => Right
    (AUTOHINT_DIM_HORIZONTAL, _) => Up
    (AUTOHINT_DIM_VERTICAL, _) => Left
    _ => None_
  }
  self.segments.clear()
  self.edges.clear()
}

///|
priv struct AutoHintSegment {
  mut flags : Int
  mut dir : AutoHintDirection
  mut pos : Int
  mut delta : Int
  mut min_coord : Int
  mut max_coord : Int
  mut height : Int
  mut score : Int
  mut len : Int
  mut link_ix : Int?
  mut serif_ix : Int?
  mut first_ix : Int
  mut last_ix : Int
  mut edge_ix : Int?
  mut edge_next_ix : Int?
}

///|
fn AutoHintSegment::default() -> AutoHintSegment {
  {
    flags: 0,
    dir: None_,
    pos: 0,
    delta: 0,
    min_coord: 0,
    max_coord: 0,
    height: 0,
    score: AUTOHINT_SEG_MAX_SCORE,
    len: 0,
    link_ix: None,
    serif_ix: None,
    first_ix: 0,
    last_ix: 0,
    edge_ix: None,
    edge_next_ix: None,
  }
}

///|
fn AutoHintSegment::first_point(
  self : AutoHintSegment,
  points : Array[AutoHintPoint],
) -> AutoHintPoint {
  points.at(self.first_ix)
}

///|
fn AutoHintSegment::last_point(
  self : AutoHintSegment,
  points : Array[AutoHintPoint],
) -> AutoHintPoint {
  points.at(self.last_ix)
}

///|
fn autohint_segment_make(
  dir : AutoHintDirection,
  first_ix : Int,
  last_ix : Int,
) -> AutoHintSegment {
  let seg = AutoHintSegment::default()
  seg.dir = dir
  seg.first_ix = first_ix
  seg.last_ix = last_ix
  seg.score = AUTOHINT_SEG_MAX_SCORE
  seg
}

///|
priv struct AutoHintSegmentState {
  mut min_pos : Int
  mut max_pos : Int
  mut min_coord : Int
  mut max_coord : Int
  mut min_flags : AutoHintPointFlags
  mut max_flags : AutoHintPointFlags
  mut min_on_coord : Int
  mut max_on_coord : Int
}

///|
fn AutoHintSegmentState::default() -> AutoHintSegmentState {
  {
    min_pos: AUTOHINT_SEG_MAX_SCORE,
    max_pos: AUTOHINT_SEG_MIN_SCORE,
    min_coord: AUTOHINT_SEG_MAX_SCORE,
    max_coord: AUTOHINT_SEG_MIN_SCORE,
    min_flags: AutoHintPointFlags::default(),
    max_flags: AutoHintPointFlags::default(),
    min_on_coord: AUTOHINT_SEG_MAX_SCORE,
    max_on_coord: AUTOHINT_SEG_MIN_SCORE,
  }
}

///|
fn autohint_segment_state_copy(
  dst : AutoHintSegmentState,
  src : AutoHintSegmentState,
) -> Unit {
  dst.min_pos = src.min_pos
  dst.max_pos = src.max_pos
  dst.min_coord = src.min_coord
  dst.max_coord = src.max_coord
  dst.min_flags = src.min_flags
  dst.max_flags = src.max_flags
  dst.min_on_coord = src.min_on_coord
  dst.max_on_coord = src.max_on_coord
}

///|
fn AutoHintSegmentState::apply_to_segment(
  self : AutoHintSegmentState,
  segment : AutoHintSegment,
  flat_threshold : Int,
) -> Unit {
  segment.pos = (self.min_pos + self.max_pos) >> 1
  segment.delta = (self.max_pos - self.min_pos) >> 1
  if (!self.min_flags.is_on_curve() || !self.max_flags.is_on_curve()) &&
    self.max_on_coord - self.min_on_coord < flat_threshold {
    segment.flags = segment.flags | 1
  }
  segment.min_coord = self.min_coord
  segment.max_coord = self.max_coord
  segment.height = self.max_coord - self.min_coord
}

///|
fn autohint_assign_point_uvs(outline : AutoHintOutline, dim : Int) -> Unit {
  if dim == AUTOHINT_DIM_HORIZONTAL {
    for i in 0.. Bool {
  let flat_threshold = outline.units_per_em / 14
  axis.segments.clear()
  let major_dir = axis.major_dir.normalize()
  let mut segment_dir = major_dir
  for contour in outline.contours.iter() {
    let is_single_point_contour = contour.length() == 1
    let mut point_ix = contour.first()
    let mut last_ix = contour.prev(point_ix)
    let state = AutoHintSegmentState::default()
    let prev_state = AutoHintSegmentState::default()
    let mut prev_segment_ix : Int? = None
    let mut segment_ix = 0
    // Check if we're starting on an edge and if so, find the starting point.
    if outline.points.at(point_ix).out_dir.is_same_axis(major_dir) &&
      outline.points.at(last_ix).out_dir.is_same_axis(major_dir) {
      last_ix = point_ix
      while true {
        point_ix = contour.prev(point_ix)
        if !outline.points.at(point_ix).out_dir.is_same_axis(major_dir) {
          point_ix = contour.next(point_ix)
          break
        }
        if point_ix == last_ix {
          break
        }
      }
    }
    last_ix = point_ix
    let mut on_edge = false
    let mut passed = false
    while true {
      if on_edge {
        let point = outline.points.at(point_ix)
        state.min_pos = state.min_pos.min(point.u)
        state.max_pos = state.max_pos.max(point.u)
        let v = point.v
        if v < state.min_coord {
          state.min_coord = v
          state.min_flags = point.flags
        }
        if v > state.max_coord {
          state.max_coord = v
          state.max_flags = point.flags
        }
        if point.is_on_curve() {
          state.min_on_coord = state.min_on_coord.min(point.v)
          state.max_on_coord = state.max_on_coord.max(point.v)
        }
        if point.out_dir != segment_dir || point_ix == last_ix {
          match prev_segment_ix {
            None => {
              let seg = axis.segments.at(segment_ix)
              seg.last_ix = point_ix
              AutoHintSegmentState::apply_to_segment(state, seg, flat_threshold)
              axis.segments.set(segment_ix, seg)
              prev_segment_ix = Some(segment_ix)
              autohint_segment_state_copy(prev_state, state)
            }
            Some(prev_ix) =>
              if axis.segments.at(segment_ix).first_ix !=
                axis.segments.at(prev_ix).last_ix {
                let seg = axis.segments.at(segment_ix)
                seg.last_ix = point_ix
                AutoHintSegmentState::apply_to_segment(
                  state, seg, flat_threshold,
                )
                axis.segments.set(segment_ix, seg)
                prev_segment_ix = Some(segment_ix)
                autohint_segment_state_copy(prev_state, state)
              } else {
                let prev_seg = axis.segments.at(prev_ix)
                if prev_seg.last_point(outline.points).in_dir == point.in_dir {
                  // identical directions: unify
                  state.min_pos = prev_state.min_pos.min(state.min_pos)
                  state.max_pos = prev_state.max_pos.max(state.max_pos)
                  if prev_state.min_coord < state.min_coord {
                    state.min_coord = prev_state.min_coord
                    state.min_flags = prev_state.min_flags
                  }
                  if prev_state.max_coord > state.max_coord {
                    state.max_coord = prev_state.max_coord
                    state.max_flags = prev_state.max_flags
                  }
                  state.min_on_coord = prev_state.min_on_coord.min(
                    state.min_on_coord,
                  )
                  state.max_on_coord = prev_state.max_on_coord.max(
                    state.max_on_coord,
                  )
                  prev_seg.last_ix = point_ix
                  AutoHintSegmentState::apply_to_segment(
                    state, prev_seg, flat_threshold,
                  )
                  axis.segments.set(prev_ix, prev_seg)
                  // pick longer segment properties
                } else if (prev_state.max_coord - prev_state.min_coord).abs() >
                  (state.max_coord - state.min_coord).abs() {
                  prev_state.min_pos = prev_state.min_pos.min(state.min_pos)
                  prev_state.max_pos = prev_state.max_pos.max(state.max_pos)
                  prev_seg.last_ix = point_ix
                  prev_seg.pos = (prev_state.min_pos + prev_state.max_pos) >> 1
                  prev_seg.delta = (prev_state.max_pos - prev_state.min_pos) >>
                    1
                  axis.segments.set(prev_ix, prev_seg)
                } else {
                  state.min_pos = state.min_pos.min(prev_state.min_pos)
                  state.max_pos = state.max_pos.max(prev_state.max_pos)
                  let seg = axis.segments.at(segment_ix)
                  seg.last_ix = point_ix
                  AutoHintSegmentState::apply_to_segment(
                    state, seg, flat_threshold,
                  )
                  axis.segments.set(prev_ix, seg)
                  autohint_segment_state_copy(prev_state, state)
                }
                axis.segments.pop() |> ignore
              }
          }
          on_edge = false
        }
      }
      if point_ix == last_ix {
        if passed {
          break
        }
        passed = true
      }
      let point = outline.points.at(point_ix)
      if !on_edge &&
        (point.out_dir.is_same_axis(major_dir) || is_single_point_contour) {
        if axis.segments.length() > 1000 {
          axis.segments.clear()
          return false
        }
        segment_ix = axis.segments.length()
        segment_dir = point.out_dir
        let segment = autohint_segment_make(segment_dir, point_ix, point_ix)
        state.min_pos = point.u
        state.max_pos = point.u
        state.min_coord = point.v
        state.max_coord = point.v
        state.min_flags = point.flags
        state.max_flags = point.flags
        if !point.is_on_curve() {
          state.min_on_coord = AUTOHINT_SEG_MAX_SCORE
          state.max_on_coord = AUTOHINT_SEG_MIN_SCORE
        } else {
          state.min_on_coord = point.v
          state.max_on_coord = point.v
        }
        on_edge = true
        if is_single_point_contour {
          segment.pos = state.min_pos
          if !point.is_on_curve() {
            segment.flags = segment.flags | 1
          }
          segment.min_coord = point.v
          segment.max_coord = point.v
          segment.height = 0
          on_edge = false
        }
        axis.segments.push(segment)
      }
      point_ix = contour.next(point_ix)
    }
  }
  true
}

///|
fn autohint_adjust_segment_heights(
  outline : AutoHintOutline,
  axis : AutoHintAxis,
) -> Unit {
  for i in 0..> 1)
      }
      if next.v > last.v {
        segment.height = segment.height + ((next.v - last.v) >> 1)
      }
    } else {
      if prev.v > first.v {
        segment.height = segment.height + ((prev.v - first.v) >> 1)
      }
      if next.v < last.v {
        segment.height = segment.height + ((last.v - next.v) >> 1)
      }
    }
    axis.segments.set(i, segment)
  }
}

///|
fn autohint_compute_segments(
  outline : AutoHintOutline,
  axis : AutoHintAxis,
  group : AutoHintScriptGroup,
) -> Bool {
  group |> ignore
  autohint_assign_point_uvs(outline, axis.dim)
  if !autohint_build_segments(outline, axis) {
    return false
  }
  autohint_adjust_segment_heights(outline, axis)
  true
}

///|
fn autohint_link_segments_default(
  outline : AutoHintOutline,
  axis : AutoHintAxis,
  max_width : Int,
) -> Unit {
  // Heuristic value to set up a minimum for overlapping.
  let len_threshold = autohint_derived_constant(outline.units_per_em, 8).max(1)
  // Heuristic value to weight lengths.
  let len_score = autohint_derived_constant(outline.units_per_em, 6000)
  let dist_score = 3000
  for ix1 in 0.. pos1 {
        let min0 = seg1v.min_coord.max(seg2.min_coord)
        let max0 = seg1v.max_coord.min(seg2.max_coord)
        let len = max0 - min0
        if len >= len_threshold {
          let dist = pos2 - pos1
          let dist_demerit = if max_width != 0 {
            let delta = (dist << 10) / max_width - (1 << 10)
            if delta > 10000 {
              AUTOHINT_SEG_MAX_SCORE
            } else if delta > 0 {
              delta * delta / dist_score
            } else {
              0
            }
          } else {
            dist
          }
          let score = dist_demerit + len_score / len
          if score < seg1v.score {
            let s1 = axis.segments.at(ix1)
            s1.score = score
            s1.link_ix = Some(ix2)
            axis.segments.set(ix1, s1)
          }
          if score < seg2.score {
            let s2 = axis.segments.at(ix2)
            s2.score = score
            s2.link_ix = Some(ix1)
            axis.segments.set(ix2, s2)
          }
        }
      }
    }
  }
  // Serif pass.
  for ix1 in 0.. ()
      Some(ix2) => {
        let seg2_link = axis.segments.at(ix2).link_ix
        if seg2_link != Some(ix1) {
          let s1 = axis.segments.at(ix1)
          s1.link_ix = None
          s1.serif_ix = seg2_link
          axis.segments.set(ix1, s1)
        }
      }
    }
  }
}

///|
fn autohint_link_segments_cjk(
  outline : AutoHintOutline,
  axis : AutoHintAxis,
  scale : Int,
) -> Unit {
  // Heuristic value to set up a minimum for overlapping.
  let len_threshold = autohint_derived_constant(outline.units_per_em, 8)
  let dist_threshold = autohint_fixed_div(64 * 3, scale)
  for ix1 in 0..= len_threshold {
        if autohint_check_seg_cjk(dist, len, seg1v) {
          let s = axis.segments.at(ix1)
          s.score = dist
          s.len = len
          s.link_ix = Some(ix2)
          axis.segments.set(ix1, s)
        }
        if autohint_check_seg_cjk(dist, len, seg2) {
          let s = axis.segments.at(ix2)
          s.score = dist
          s.len = len
          s.link_ix = Some(ix1)
          axis.segments.set(ix2, s)
        }
      }
    }
  }
  // Serif / cleanup pass (ported structure, simplified).
  for ix1 in 0..= dist_threshold {
      continue
    }
    let link1_ix = match seg1.link_ix {
      None => continue
      Some(v) => v
    }
    let link1 = axis.segments.at(link1_ix)
    if link1.link_ix != Some(ix1) || link1.pos <= seg1.pos {
      continue
    }
    for ix2 in 0.. seg1.pos || ix1 == ix2 {
        continue
      }
      let link2_ix = match seg2.link_ix {
        None => continue
        Some(v) => v
      }
      let link2 = axis.segments.at(link2_ix)
      if link2.link_ix != Some(ix2) || link2.pos < link1.pos {
        continue
      }
      if seg1.pos == seg2.pos && link1.pos == link2.pos {
        continue
      }
      if seg2.score <= seg1.score || seg1.score * 4 <= seg2.score {
        continue
      }
      if seg1.len >= seg2.len * 3 {
        for i in 0..
              if v == ix2 {
                seg.link_ix = None
                seg.serif_ix = Some(link1_ix)
              } else if v == link2_ix {
                seg.link_ix = None
                seg.serif_ix = Some(ix1)
              }
            None => ()
          }
          axis.segments.set(i, seg)
        }
      } else {
        let s1 = axis.segments.at(ix1)
        let l1 = axis.segments.at(link1_ix)
        s1.link_ix = None
        l1.link_ix = None
        axis.segments.set(ix1, s1)
        axis.segments.set(link1_ix, l1)
        break
      }
    }
  }
  for ix1 in 0.. continue
      Some(ix2) => axis.segments.at(ix2)
    }
    if seg2.link_ix != Some(ix1) {
      let s1 = axis.segments.at(ix1)
      s1.link_ix = None
      if seg2.score < dist_threshold || seg1.score < seg2.score * 4 {
        s1.serif_ix = seg2.link_ix
      }
      axis.segments.set(ix1, s1)
    }
  }
}

///|
fn autohint_link_segments(
  outline : AutoHintOutline,
  axis : AutoHintAxis,
  scale : Int,
  group : AutoHintScriptGroup,
  max_width : Int?,
) -> Unit {
  // Silence unused_constructor warnings until the full autohinter integration
  // is in place.
  AutoHintScriptGroup::Cjk |> ignore
  match group {
    Default =>
      autohint_link_segments_default(outline, axis, max_width.unwrap_or(0))
    Cjk => autohint_link_segments_cjk(outline, axis, scale)
  }
}

///|
fn autohint_check_seg_cjk(dist : Int, len : Int, seg : AutoHintSegment) -> Bool {
  dist * 8 < seg.score * 9 && (dist * 8 < seg.score * 7 || seg.len < len)
}