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

///|
/// Edge detection for the autohint topology.
///
/// Ported from `fontations/skrifa/src/outline/autohint/topo/edges.rs`
/// (Apache-2.0 OR MIT).

///|
const AUTOHINT_FLAG_NORMAL : Int = 0

///|
const AUTOHINT_FLAG_ROUND : Int = 1

///|
const AUTOHINT_FLAG_SERIF : Int = 2

///|
const AUTOHINT_FLAG_DONE : Int = 4

///|
const AUTOHINT_FLAG_NEUTRAL : Int = 8

///|
const EDGE_DISTANCE_THRESHOLD_MAX : Int = 64 / 4

///|
priv struct AutoHintEdge {
  mut fpos : Int
  mut opos : Int
  mut pos : Int
  mut flags : Int
  mut dir : AutoHintDirection
  mut blue_edge : AutoHintScaledWidth?
  mut link_ix : Int?
  mut serif_ix : Int?
  mut scale : Int
  mut first_ix : Int
  mut last_ix : Int
}

///|
fn AutoHintEdge::default() -> AutoHintEdge {
  {
    fpos: 0,
    opos: 0,
    pos: 0,
    flags: AUTOHINT_FLAG_NORMAL,
    dir: None_,
    blue_edge: None,
    link_ix: None,
    serif_ix: None,
    scale: 0,
    first_ix: 0,
    last_ix: 0,
  }
}

///|
fn autohint_compute_blue_edges(
  axis : AutoHintAxis,
  scale : AutoHintScale,
  unscaled_blues : Array[AutoHintUnscaledBlue],
  blues : Array[AutoHintScaledBlue],
  group : AutoHintScriptGroup,
) -> Unit {
  if group is Default && axis.dim != AUTOHINT_DIM_VERTICAL {
    return
  }
  let axis_scale = if axis.dim == AUTOHINT_DIM_HORIZONTAL {
    scale.x_scale
  } else {
    scale.y_scale
  }
  let initial_best_dist = autohint_fixed_mul(
    (scale.units_per_em / 40).max(0),
    axis_scale,
  ).min(64 / 2)
  for edge_ix in 0.. dsho {
            (unscaled_blue.overshoot, blue.overshoot)
          } else {
            (unscaled_blue.position, blue.position)
          }
        }
        let dist = autohint_fixed_mul((edge.fpos - ref_pos).abs(), axis_scale)
        if dist < best_dist {
          best_dist = dist
          best_blue = Some(matching)
          best_is_neutral = is_neutral
        }
        if group is Default &&
          (edge.flags & AUTOHINT_FLAG_ROUND) != 0 &&
          dist != 0 &&
          !is_neutral {
          let is_under_ref = edge.fpos < unscaled_blue.position
          if is_top != is_under_ref {
            let dist2 = autohint_fixed_mul(
              (edge.fpos - unscaled_blue.overshoot).abs(),
              axis_scale,
            )
            if dist2 < best_dist {
              best_dist = dist2
              best_blue = Some(blue.overshoot)
              best_is_neutral = is_neutral
            }
          }
        }
      }
    }
    match best_blue {
      None => ()
      Some(b) => {
        edge.blue_edge = Some(b)
        if best_is_neutral {
          edge.flags = edge.flags | AUTOHINT_FLAG_NEUTRAL
        }
      }
    }
  }
}

///|
fn autohint_abs_i32(v : Int) -> Int {
  if v < 0 {
    -v
  } else {
    v
  }
}

///|
fn AutoHintSegment::link(
  self : AutoHintSegment,
  segments : Array[AutoHintSegment],
) -> AutoHintSegment? {
  match self.link_ix {
    None => None
    Some(ix) => segments.get(ix)
  }
}

///|
fn AutoHintAxis::insert_edge(
  self : AutoHintAxis,
  edge : AutoHintEdge,
  top_to_bottom_hinting : Bool,
) -> Unit {
  self.edges.push(edge)
  // If this is the first edge, we're done.
  if self.edges.length() == 1 {
    return
  }
  // Now move it into place.
  let mut ix = self.edges.length() - 1
  while ix > 0 {
    let prev_ix = ix - 1
    let prev_edge = self.edges[prev_ix]
    let prev_fpos = prev_edge.fpos
    if (top_to_bottom_hinting && prev_fpos > edge.fpos) ||
      (!top_to_bottom_hinting && prev_fpos < edge.fpos) {
      break
    }
    // Edges with the same position and minor direction should appear before
    // those with the major direction.
    if prev_fpos == edge.fpos && edge.dir == self.major_dir {
      break
    }
    self.edges[ix] = prev_edge
    ix = ix - 1
  }
  self.edges[ix] = edge
}

///|
fn AutoHintAxis::append_segment_to_edge(
  self : AutoHintAxis,
  segment_ix : Int,
  edge_ix : Int,
) -> Unit {
  let edge = self.edges.at(edge_ix)
  let first_ix = edge.first_ix
  let last_ix = edge.last_ix
  edge.last_ix = segment_ix
  let segment = self.segments.at(segment_ix)
  segment.edge_next_ix = Some(first_ix)
  self.segments.at(last_ix).edge_next_ix = Some(segment_ix)
}

///|
fn autohint_link_segments_to_edges(axis : AutoHintAxis) -> Unit {
  let segments = axis.segments
  for edge_ix in 0.. last_ix
        Some(next_ix) => next_ix
      }
    }
  }
}

///|
fn autohint_compute_edge_properties(axis : AutoHintAxis) -> Unit {
  let segments = axis.segments
  for edge_ix in 0.. false
        Some(serif_ix) => {
          let serif = segments.at(serif_ix)
          match serif.edge_ix {
            None => false
            Some(serif_edge_ix) => serif_edge_ix != edge_ix
          }
        }
      }
      // Check for links.
      let has_link = match segment.link_ix {
        None => false
        Some(link_ix) => segments.at(link_ix).edge_ix is Some(_)
      }
      if is_serif || has_link {
        let (edge2_ix0, segment2_ix0) = if is_serif {
          (edge.serif_ix, segment.serif_ix)
        } else {
          (edge.link_ix, segment.link_ix)
        }
        let edge2_ix = match (edge2_ix0, segment2_ix0) {
          (Some(edge2_ix), Some(segment2_ix)) => {
            let edge2 = axis.edges.at(edge2_ix)
            let edge_delta = autohint_abs_i32(edge.fpos - edge2.fpos)
            let segment2 = segments.at(segment2_ix)
            let segment_delta = autohint_abs_i32(segment.pos - segment2.pos)
            if segment_delta < edge_delta {
              segment2.edge_ix
            } else {
              Some(edge2_ix)
            }
          }
          (_, Some(segment2_ix)) => segments.at(segment2_ix).edge_ix
          (edge2_ix, _) => edge2_ix
        }
        if is_serif {
          axis.edges.at(edge_ix).serif_ix = edge2_ix
          if edge2_ix is Some(edge2_ix) {
            let edge2 = axis.edges.at(edge2_ix)
            edge2.flags = edge2.flags | AUTOHINT_FLAG_SERIF
          }
        } else {
          axis.edges.at(edge_ix).link_ix = edge2_ix
        }
      }
      if segment_ix == last_segment_ix {
        break
      }
      segment_ix = match next_segment_ix {
        None => last_segment_ix
        Some(ix) => ix
      }
    }
    let edge = axis.edges.at(edge_ix)
    edge.flags = AUTOHINT_FLAG_NORMAL
    if roundness > 0 && roundness >= straightness {
      edge.flags = edge.flags | AUTOHINT_FLAG_ROUND
    }
    // Drop serifs for linked edges.
    if edge.serif_ix is Some(_) && edge.link_ix is Some(_) {
      edge.serif_ix = None
    }
  }
}

///|
fn autohint_compute_edges(
  axis : AutoHintAxis,
  initial_edge_distance_threshold : Int,
  top_to_bottom_hinting : Bool,
  y_scale : Int,
  scale : Int,
  group : AutoHintScriptGroup,
) -> Unit {
  axis.edges.clear()
  // This is always passed as 0 in functions that take hinting direction in CJK.
  let top_to_bottom_hinting = if axis.dim == AUTOHINT_DIM_HORIZONTAL ||
    !(group is Default) {
    false
  } else {
    top_to_bottom_hinting
  }
  // Ignore horizontal segments less than 1 pixel in length.
  let segment_length_threshold = if axis.dim == AUTOHINT_DIM_HORIZONTAL {
    autohint_fixed_div(64, y_scale)
  } else {
    0
  }
  // Also ignore segments with a width delta larger than 0.5 pixels.
  let segment_width_threshold = autohint_fixed_div(32, scale)
  // Ensure that edge distance threshold is less than or equal to 0.25 pixels.
  let initial_threshold = initial_edge_distance_threshold
  let edge_distance_threshold = if group is Default {
    autohint_fixed_div(
      autohint_fixed_mul(initial_threshold, scale).min(
        EDGE_DISTANCE_THRESHOLD_MAX,
      ),
      scale,
    )
  } else {
    // CJK uses a slightly different computation here.
    let threshold = autohint_fixed_mul(initial_threshold, scale)
    if threshold > EDGE_DISTANCE_THRESHOLD_MAX {
      autohint_fixed_div(EDGE_DISTANCE_THRESHOLD_MAX, scale)
    } else {
      initial_threshold
    }
  }
  // Now build the sorted table of edges by looping over all segments
  // to find a matching edge, adding a new one if not found.
  for segment_ix in 0.. segment_width_threshold ||
        segment.dir == None_ {
        continue
      }
      // Ignore serif edges that are smaller than 1.5 pixels.
      if segment.serif_ix is Some(_) &&
        2 * segment.height < 3 * segment_length_threshold {
        continue
      }
    }
    // Look for a corresponding edge for this segment.
    let mut best_dist = AUTOHINT_SEG_MAX_SCORE
    let mut best_edge_ix : Int? = None
    for edge_ix in 0.. ()
          Some(link) => {
            // Check whether all linked segments of the candidate edge can make a single edge.
            let first_ix = edge.first_ix
            let mut seg1_ix = first_ix
            let mut dist2 = 0
            while true {
              let seg1 = axis.segments.at(seg1_ix)
              match seg1.link(axis.segments) {
                None => ()
                Some(link1) => {
                  dist2 = autohint_abs_i32(link.pos - link1.pos)
                  if dist2 >= edge_distance_threshold {
                    break
                  }
                }
              }
              if seg1.edge_next_ix == Some(first_ix) {
                break
              }
              match seg1.edge_next_ix {
                None => break
                Some(next_ix) => seg1_ix = next_ix
              }
            }
            if dist2 >= edge_distance_threshold {
              continue
            }
          }
        }
        best_dist = dist
        best_edge_ix = Some(edge_ix)
      }
    }
    match best_edge_ix {
      Some(edge_ix) => axis.append_segment_to_edge(segment_ix, edge_ix)
      None => {
        // We couldn't find an edge, so add a new one for this segment.
        let opos = autohint_fixed_mul(segment.pos, scale)
        let edge = AutoHintEdge::default()
        edge.fpos = segment.pos
        edge.opos = opos
        edge.pos = opos
        edge.dir = segment.dir
        edge.first_ix = segment_ix
        edge.last_ix = segment_ix
        axis.insert_edge(edge, top_to_bottom_hinting)
        axis.segments.at(segment_ix).edge_next_ix = Some(segment_ix)
      }
    }
  }
  if group is Default {
    // Loop again to find single point segments without a direction and
    // associate them with an existing edge if possible.
    for segment_ix in 0.. ()
        Some(edge_ix) => axis.append_segment_to_edge(segment_ix, edge_ix)
      }
    }
  }
  autohint_link_segments_to_edges(axis)
  autohint_compute_edge_properties(axis)
}

///|
test "autohint topo: edges simple" {
  AUTOHINT_FLAG_DONE |> ignore
  AUTOHINT_FLAG_NEUTRAL |> ignore
  let axis = AutoHintAxis::default()
  axis.reset(AUTOHINT_DIM_HORIZONTAL, None)
  let s0 = AutoHintSegment::default()
  s0.flags = AUTOHINT_FLAG_ROUND
  s0.dir = Up
  s0.pos = 100
  s0.delta = 0
  s0.height = 100
  s0.link_ix = Some(2)
  axis.segments.push(s0)
  let s1 = AutoHintSegment::default()
  s1.flags = AUTOHINT_FLAG_ROUND
  s1.dir = Up
  s1.pos = 105
  s1.delta = 0
  s1.height = 100
  axis.segments.push(s1)
  let s2 = AutoHintSegment::default()
  s2.dir = Down
  s2.pos = 200
  s2.delta = 0
  s2.height = 100
  s2.link_ix = Some(0)
  axis.segments.push(s2)
  let s3 = AutoHintSegment::default()
  s3.dir = None_
  s3.pos = 103
  s3.delta = 0
  s3.height = 100
  axis.segments.push(s3)
  autohint_compute_edges(axis, 10, false, 1 << 16, 1 << 16, Default)
  inspect(axis.edges.length().to_string(), content="2")
  inspect(axis.edges.at(0).fpos.to_string(), content="100")
  inspect(axis.edges.at(0).opos.to_string(), content="100")
  inspect(axis.edges.at(0).pos.to_string(), content="100")
  inspect(axis.edges.at(0).scale.to_string(), content="0")
  inspect(axis.edges.at(1).fpos.to_string(), content="200")
  // Edge 0 is round (1 round + 1 straight + 1 dirless segment ignored for roundness).
  inspect(
    ((axis.edges.at(0).flags & AUTOHINT_FLAG_ROUND) != 0).to_string(),
    content="true",
  )
  // Links are promoted from segment links.
  match axis.edges.at(0).link_ix {
    None => inspect("None", content="Some")
    Some(ix) => inspect(ix.to_string(), content="1")
  }
  match axis.edges.at(1).link_ix {
    None => inspect("None", content="Some")
    Some(ix) => inspect(ix.to_string(), content="0")
  }
}