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

///|
/// Minimal shaping port scaffold from `cosmic-text/src/shape.rs`.
///
/// This MVP produces 1 glyph per UTF-16 code unit and does not perform script runs,
/// harfbuzz shaping, BiDi, or font fallback.
pub(all) enum Shaping {
  Basic
  Advanced
}

///|
pub struct ShapeGlyph {
  start : Int
  end : Int
  x_advance : Float
  y_advance : Float
  x_offset : Float
  y_offset : Float
  font_id : Int
  font_weight : Int
  glyph_id : Int
  metadata : Int
  /// BiDi embedding level (LTR if divisible by 2).
  level : Int
  /// Optional Metrics override (font size / line height) for this glyph's span.
  metrics_opt : Metrics?
}

///|
pub fn ShapeGlyph::new(
  start : Int,
  end : Int,
  x_advance : Float,
  y_advance : Float,
  x_offset : Float,
  y_offset : Float,
  font_id : Int,
  glyph_id : Int,
  metadata : Int,
) -> ShapeGlyph {
  ShapeGlyph::{
    start,
    end,
    x_advance,
    y_advance,
    x_offset,
    y_offset,
    font_id,
    font_weight: 400,
    glyph_id,
    metadata,
    level: 0,
    metrics_opt: None,
  }
}

///|
pub struct ShapeLine {
  rtl : Bool
  glyphs : Array[ShapeGlyph]
}

///|
pub fn ShapeLine::new(rtl : Bool, glyphs : Array[ShapeGlyph]) -> ShapeLine {
  ShapeLine::{ rtl, glyphs }
}

///|
pub fn ShapeLine::empty() -> ShapeLine {
  ShapeLine::{ rtl: false, glyphs: [] }
}

///|
fn is_tab(code : Int) -> Bool {
  code == 9
}

///|
fn utf16_len_of_char(ch : Char) -> Int {
  // UTF-16: codepoints outside BMP take 2 code units.
  if ch.to_int() <= 0xFFFF {
    1
  } else {
    2
  }
}

///|
fn bidi_is_strong_rtl(b : @moon_swash.BidiClass) -> Bool {
  match b {
    R | AL | AN | RLE | RLO | RLI => true
    _ => false
  }
}

///|
fn bidi_is_strong_ltr(b : @moon_swash.BidiClass) -> Bool {
  match b {
    L | LRE | LRO | LRI => true
    _ => false
  }
}

///|
fn detect_base_rtl(text : String) -> Bool {
  // Heuristic: use the first strong directional character.
  for ch in text {
    let props = @moon_swash.CharInfo::from_char(ch).properties()
    let bidi = props.bidi_class()
    if bidi_is_strong_rtl(bidi) {
      return true
    }
    if bidi_is_strong_ltr(bidi) {
      return false
    }
  }
  false
}

///|
fn compute_bidi_levels(text : String, rtl : Bool) -> Array[Int] {
  let base = if rtl { 1 } else { 0 }
  let levels : Array[Int] = Array::makei(text.length(), _ => base)
  for p in text.iter2() {
    let idx = p.0
    let ch = p.1
    let props = @moon_swash.CharInfo::from_char(ch).properties()
    let bidi = props.bidi_class()
    let lev = if bidi_is_strong_rtl(bidi) {
      1
    } else if bidi_is_strong_ltr(bidi) {
      if rtl {
        2
      } else {
        0
      }
    } else {
      base
    }
    let end = idx + ch.utf16_len()
    let mut i = idx
    while i < end && i < levels.length() {
      levels.set(i, lev)
      i = i + 1
    }
  }
  levels
}

///|
fn script_eq(a : @moon_swash.Script, b : @moon_swash.Script) -> Bool {
  a.to_opentype() == b.to_opentype()
}

///|
fn shape_script_needs_fallback_stage(script : @moon_swash.Script) -> Bool {
  match script {
    @moon_swash.Script::Common
    | @moon_swash.Script::Inherited
    | @moon_swash.Script::Latin
    | @moon_swash.Script::Unknown => false
    _ => true
  }
}

///|
fn shape_script_in_array(
  scripts : Array[@moon_swash.Script],
  script : @moon_swash.Script,
) -> Bool {
  for s in scripts {
    if script_eq(s, script) {
      return true
    }
  }
  false
}

///|
fn collect_run_fallback_scripts(
  run_tokens : Array[@moon_swash.Token],
) -> Array[@moon_swash.Script] {
  let scripts : Array[@moon_swash.Script] = []
  for tok in run_tokens {
    let script = @moon_swash.CharInfo::from_char(tok.ch()).properties().script()
    if shape_script_needs_fallback_stage(script) &&
      !shape_script_in_array(scripts, script) {
      scripts.push(script)
    }
  }
  scripts
}

///|
fn shape_primary_script_seed(
  script : @moon_swash.Script,
) -> (@moon_swash.Script, Bool) {
  match script {
    Common | Inherited | Unknown => (@moon_swash.Script::Latin, true)
    _ => (script, false)
  }
}

///|
fn shape_levels_require_new_run(prev_level : Int, new_level : Int) -> Bool {
  prev_level != new_level
}

///|
fn entry_supports_any_run_script(
  entry : FontEntry,
  scripts : Array[@moon_swash.Script],
) -> Bool {
  if scripts.length() == 0 {
    return false
  }
  let systems = entry.font().writing_systems()
  for ws in systems.iter() {
    match ws.script() {
      None => ()
      Some(s) => if shape_script_in_array(scripts, s) { return true }
    }
  }
  false
}

///|
fn source_start_index(source : @moon_swash.SourceRange) -> Int {
  source.start().reinterpret_as_int()
}

///|
fn source_end_index(source : @moon_swash.SourceRange) -> Int {
  source.end().reinterpret_as_int()
}

///|
fn attrs_for_position(
  attrs_list : AttrsList,
  pos : Int,
  text_len : Int,
) -> Attrs {
  if text_len <= 0 {
    attrs_list.defaults()
  } else {
    let i = if pos < 0 {
      0
    } else if pos >= text_len {
      text_len - 1
    } else {
      pos
    }
    attrs_list.get_span(i)
  }
}

///|
fn clamp_range_to_source(
  start0 : Int,
  end0 : Int,
  source_start : Int,
  source_end : Int,
) -> (Int, Int) {
  let start1 = if start0 < source_start {
    source_start
  } else if start0 > source_end {
    source_end
  } else {
    start0
  }
  let end1 = if end0 < start1 {
    start1
  } else if end0 > source_end {
    source_end
  } else {
    end0
  }
  (start1, end1)
}

///|
fn build_cluster_starts(
  source : @moon_swash.SourceRange,
  components : ArrayView[@moon_swash.SourceRange],
  glyph_count : Int,
  rtl : Bool,
) -> Array[Int] {
  let starts : Array[Int] = []
  if glyph_count <= 0 {
    return starts
  }
  if components.length() == 0 {
    let source_start = source_start_index(source)
    for _ in 0.. Array[Int] {
  let n = starts.length()
  let ends : Array[Int] = []
  for _ in 0.. 0 {
      let next_start = starts[i]
      let next_end = ends[i]
      if starts[i - 1] == next_start {
        ends.set(i - 1, next_end)
      } else {
        ends.set(i - 1, next_start)
      }
      i = i - 1
    }
  }
  ends
}

///|
fn missing_contains(missing : Array[Int], start : Int) -> Bool {
  for m in missing {
    if m == start {
      return true
    }
  }
  false
}

///|
fn remove_missing_range(missing : Array[Int], start : Int, end : Int) -> Unit {
  let keep : Array[Int] = []
  for m in missing {
    if m < start || m >= end {
      keep.push(m)
    }
  }
  missing.clear()
  for m in keep {
    missing.push(m)
  }
}

///|
fn replace_cluster_glyphs(
  run_glyphs : Array[ShapeGlyph],
  start : Int,
  end : Int,
  replacement : Array[ShapeGlyph],
) -> Array[ShapeGlyph] {
  let out : Array[ShapeGlyph] = []
  let mut inserted = false
  for g in run_glyphs {
    let in_range = g.start >= start && g.end <= end
    if in_range && !inserted {
      for rg in replacement {
        out.push(rg)
      }
      inserted = true
    }
    if !in_range {
      out.push(g)
    }
  }
  if !inserted {
    for rg in replacement {
      out.push(rg)
    }
  }
  out
}

///|
fn merge_fallback_glyphs(
  run_glyphs : Array[ShapeGlyph],
  missing : Array[Int],
  fallback_glyphs : Array[ShapeGlyph],
  fallback_missing : Array[Int],
) -> Array[ShapeGlyph] {
  let mut merged = run_glyphs
  let mut fb_i = 0
  while fb_i < fallback_glyphs.length() {
    let start = fallback_glyphs[fb_i].start
    let end = fallback_glyphs[fb_i].end
    if !missing_contains(missing, start) ||
      missing_contains(fallback_missing, start) {
      fb_i = fb_i + 1
      continue
    }

    let replacement : Array[ShapeGlyph] = []
    while fb_i < fallback_glyphs.length() {
      let g = fallback_glyphs[fb_i]
      if g.start >= start && g.end <= end {
        replacement.push(g)
        fb_i = fb_i + 1
      } else {
        break
      }
    }

    remove_missing_range(missing, start, end)
    merged = replace_cluster_glyphs(merged, start, end, replacement)
  }
  merged
}

///|
fn better_monospace_fallback_candidate(
  cand : (Int, Bool, Int, Int, Int),
  best : (Int, Bool, Int, Int, Int),
) -> Bool {
  let cand_id = cand.0
  let cand_is_primary_default = cand.1
  let cand_weight_diff = cand.2
  let cand_non_matches = cand.3
  let cand_weight = cand.4
  let best_id = best.0
  let best_is_primary_default = best.1
  let best_weight_diff = best.2
  let best_non_matches = best.3
  let best_weight = best.4
  if cand_is_primary_default != best_is_primary_default {
    return cand_is_primary_default
  }
  if cand_weight_diff != best_weight_diff {
    return cand_weight_diff < best_weight_diff
  }
  if cand_non_matches != best_non_matches {
    return cand_non_matches < best_non_matches
  }
  if cand_weight != best_weight {
    return cand_weight < best_weight
  }
  cand_id < best_id
}

///|
fn reorder_monospace_fallback_candidates(
  font_system : FontSystem,
  run_attrs : Attrs,
  run_scripts : Array[@moon_swash.Script],
  run_tokens : Array[@moon_swash.Token],
  ordered : Array[Int],
) -> Array[Int] {
  if !(run_attrs.family_value() is Monospace) {
    return ordered
  }
  let codepoints : Array[UInt] = []
  for t in run_tokens {
    codepoints.push(t.ch().to_int().reinterpret_as_uint())
  }
  if codepoints.length() == 0 || ordered.length() <= 1 {
    return ordered
  }

  let req_weight = run_attrs.weight_value()
  let mut primary_default_mono_id : Int? = None
  for id in ordered {
    match font_system.get_font_entry(id) {
      None => ()
      Some(entry) =>
        if entry.is_monospace() && entry_weight_diff(entry, req_weight) == 0 {
          primary_default_mono_id = Some(id)
          break
        }
    }
  }

  let mut has_script_specific_monospace = false
  for id in ordered {
    match font_system.get_font_entry(id) {
      None => ()
      Some(entry) =>
        if entry.is_monospace() &&
          entry_supports_any_run_script(entry, run_scripts) {
          has_script_specific_monospace = true
          break
        }
    }
  }

  let mono_candidates : Array[(Int, Bool, Int, Int, Int)] = []
  let non_mono_candidates : Array[Int] = []
  for id in ordered {
    match font_system.get_font_entry(id) {
      None => non_mono_candidates.push(id)
      Some(entry) => {
        let keep_as_mono = if !entry.is_monospace() {
          false
        } else if has_script_specific_monospace {
          match primary_default_mono_id {
            Some(pid) =>
              pid == id || entry_supports_any_run_script(entry, run_scripts)
            None => entry_supports_any_run_script(entry, run_scripts)
          }
        } else {
          true
        }
        if keep_as_mono {
          let supported = font_system.count_supported_codepoints(id, codepoints)
          let info = (
            id,
            match primary_default_mono_id {
              Some(pid) => pid == id
              None => false
            },
            entry_weight_diff(entry, req_weight),
            codepoints.length() - supported,
            entry_weight_raw(entry),
          )
          mono_candidates.push(info)
        } else {
          non_mono_candidates.push(id)
        }
      }
    }
  }

  let n = mono_candidates.length()
  let used : Array[Bool] = Array::makei(n, _ => false)
  let reordered_mono : Array[Int] = []
  for _ in 0..= 0 {
      used.set(best, true)
      reordered_mono.push(mono_candidates[best].0)
    }
  }

  let reordered : Array[Int] = []
  for id in reordered_mono {
    reordered.push(id)
  }
  for id in non_mono_candidates {
    reordered.push(id)
  }
  reordered
}

///|
fn shape_run_with_font(
  font_system : FontSystem,
  attrs_list : AttrsList,
  levels : Array[Int],
  text_len : Int,
  line_rtl : Bool,
  run_rtl : Bool,
  cx : @shape.ShapeContext,
  script : @moon_swash.Script,
  run_tokens : Array[@moon_swash.Token],
  font_id : Int,
) -> (Array[ShapeGlyph], Array[Int]) {
  let font_opt = font_system.get_font(font_id)
  let font = match font_opt {
    None => {
      let missing : Array[Int] = []
      for t in run_tokens {
        missing.push(t.offset().reinterpret_as_int())
      }
      return ([], missing)
    }
    Some(f) => f
  }
  let cmap = @moon_swash.Charmap::from_font(font)
  let empty_features : Array[@moon_swash.Setting[UInt16]] = []
  let empty_variations : Array[@moon_swash.Setting[Double]] = []
  let shaper = cx
    .builder(font)
    .script(script)
    .direction(
      if run_rtl {
        @shape.Direction::RightToLeft
      } else {
        @shape.Direction::LeftToRight
      },
    )
    .language(None)
    .size(1.0)
    .features(empty_features.iter())
    .variations(empty_variations.iter())
    .build()

  let parser = @moon_swash.Parser::new(script, run_tokens.iter())
  let cluster = @moon_swash.CharCluster::new()
  while parser.next(cluster) {
    cluster.map(fn(ch) { cmap.map(ch.to_int().reinterpret_as_uint()) })
    |> ignore
    shaper.add_cluster(cluster)
  }

  let out : Array[ShapeGlyph] = []
  let missing : Array[Int] = []
  shaper.shape_with(fn(gc) {
    let source = gc.source()
    let source_start = source_start_index(source)
    let source_end = source_end_index(source)
    let shaped = gc.glyphs()
    let shaped_count = shaped.length()
    if shaped_count != 0 {
      let starts = build_cluster_starts(
        source,
        gc.components(),
        shaped_count,
        run_rtl,
      )
      let ends = build_cluster_ends(starts, source_end, run_rtl)
      for i in 0.. 1 glyph
/// - `glyph_id` is the code unit value
/// - `x_advance` is measured in "cells": 1.0 for normal chars, `tab_width` for tab.
pub fn ShapeLine::build(
  _self : ShapeLine,
  text : String,
  attrs_list : AttrsList,
  shaping : Shaping,
  tab_width : Int,
) -> ShapeLine {
  let rtl = shaping is Advanced && detect_base_rtl(text)
  let levels = compute_bidi_levels(text, rtl)
  let glyphs : Array[ShapeGlyph] = []
  let len = text.length()
  for i in 0.. 1 glyph),
/// but uses `moon_swash` charmap + glyph metrics so advances are font-derived.
pub fn ShapeLine::build_with_font_system(
  _self : ShapeLine,
  font_system : FontSystem,
  text : String,
  attrs_list : AttrsList,
  shaping : Shaping,
  tab_width : Int,
) -> ShapeLine {
  if shaping is Basic {
    return ShapeLine::build(
      ShapeLine::empty(),
      text,
      attrs_list,
      shaping,
      tab_width,
    )
  }
  let default_font_id_opt = font_system.resolve(attrs_list.defaults())
  let default_font_id = match default_font_id_opt {
    None =>
      return ShapeLine::build(
        ShapeLine::empty(),
        text,
        attrs_list,
        shaping,
        tab_width,
      )
    Some(id) => id
  }
  if font_system.get_font(default_font_id) is None {
    return ShapeLine::build(
      ShapeLine::empty(),
      text,
      attrs_list,
      shaping,
      tab_width,
    )
  }

  // Use swash's shaping engine for clusters/ligatures/marks. We intentionally use
  // size=1.0 so the returned advances/offsets are in "em" units (like upstream),
  // which are later multiplied by `font_size` during layout.
  let rtl = detect_base_rtl(text)
  let levels = compute_bidi_levels(text, rtl)
  let cx = @shape.ShapeContext::new()
  let runs : Array[
    (
      @moon_swash.Script,
      Array[@moon_swash.Script],
      Bool,
      Array[@moon_swash.Token],
    ),
  ] = []
  let mut off = 0U
  let mut cur_primary_script = @moon_swash.Script::Latin
  let mut cur_primary_pending = true
  let mut cur_level = if rtl { 1 } else { 0 }
  let mut cur_rtl = rtl
  let mut cur_scripts : Array[@moon_swash.Script] = []
  let mut cur_tokens : Array[@moon_swash.Token] = []
  for ch0 in text {
    let script0 = @moon_swash.CharInfo::from_char(ch0).properties().script()
    let len_u = utf16_len_of_char(ch0).reinterpret_as_uint()
    let i = off.reinterpret_as_int()
    let level_i = if levels.get(i) is Some(v) { v } else if rtl { 1 } else { 0 }
    let run_rtl = level_i % 2 != 0
    let attrs = attrs_list.get_span(i)
    // Tabs are shaped as spaces (upstream behavior).
    let ch = if ch0 == '\t' { ' ' } else { ch0 }
    let info = @moon_swash.CharInfo::from_char(ch)
    let tok = @moon_swash.Token::new(
      ch,
      off,
      len_u,
      info,
      attrs.metadata().reinterpret_as_uint(),
    )
    let primary = shape_primary_script_seed(script0)
    if cur_tokens.length() == 0 {
      cur_primary_script = primary.0
      cur_primary_pending = primary.1
      cur_level = level_i
      cur_rtl = run_rtl
      cur_scripts = []
    } else if shape_levels_require_new_run(cur_level, level_i) {
      runs.push((cur_primary_script, cur_scripts, cur_rtl, cur_tokens))
      cur_tokens = []
      cur_primary_script = primary.0
      cur_primary_pending = primary.1
      cur_level = level_i
      cur_rtl = run_rtl
      cur_scripts = []
    } else if cur_primary_pending &&
      !(script0 is Common || script0 is Inherited || script0 is Unknown) {
      cur_primary_script = script0
      cur_primary_pending = false
    }
    if shape_script_needs_fallback_stage(script0) &&
      !shape_script_in_array(cur_scripts, script0) {
      cur_scripts.push(script0)
    }
    cur_tokens.push(tok)
    off = off + len_u
  }
  let glyphs : Array[ShapeGlyph] = []
  if cur_tokens.length() != 0 {
    runs.push((cur_primary_script, cur_scripts, cur_rtl, cur_tokens))
  }
  for run in runs {
    let script = run.0
    let run_scripts = if run.1.length() == 0 {
      collect_run_fallback_scripts(run.3)
    } else {
      run.1
    }
    let run_rtl = run.2
    let run_tokens = run.3
    let run_start = if run_tokens.length() == 0 {
      0
    } else {
      run_tokens[0].offset().reinterpret_as_int()
    }
    let run_attrs = attrs_for_position(attrs_list, run_start, text.length())
    let run_font_id = match font_system.resolve(run_attrs) {
      None => default_font_id
      Some(id) => id
    }
    let (default_glyphs, default_missing) = shape_run_with_font(
      font_system,
      attrs_list,
      levels,
      text.length(),
      rtl,
      run_rtl,
      cx,
      script,
      run_tokens,
      run_font_id,
    )
    let mut run_glyphs = default_glyphs
    let missing = default_missing
    if missing.length() > 0 {
      let ordered0 = font_system.font_matches_for_scripts(
        run_attrs, run_scripts,
      )
      let ordered = reorder_monospace_fallback_candidates(
        font_system, run_attrs, run_scripts, run_tokens, ordered0,
      )
      for alt_font_id in ordered {
        if alt_font_id == run_font_id {
          continue
        }
        let (fb_glyphs, fb_missing) = shape_run_with_font(
          font_system,
          attrs_list,
          levels,
          text.length(),
          rtl,
          run_rtl,
          cx,
          script,
          run_tokens,
          alt_font_id,
        )
        run_glyphs = merge_fallback_glyphs(
          run_glyphs, missing, fb_glyphs, fb_missing,
        )
        if missing.length() == 0 {
          break
        }
      }
    }
    for g in run_glyphs {
      glyphs.push(g)
    }
  }

  // If shaping produces no glyphs, fall back to the simple shaper to keep layout stable.
  if glyphs.length() == 0 {
    return ShapeLine::build(
      ShapeLine::empty(),
      text,
      attrs_list,
      shaping,
      tab_width,
    )
  }

  // Apply tab stops using each tab glyph's own advance as the tab unit.
  let mut x = 0.0
  for i in 0..= 0 &&
      g0.start < text.length() &&
      is_tab(text.code_unit_at(g0.start).to_int())
    if is_tab_glyph {
      let step = g0.x_advance.to_double() * tab_width.to_double()
      if step != 0.0 {
        let next = ((x / step).floor() + 1.0) * step
        let adv = next - x
        let g1 = ShapeGlyph::{ ..g0, x_advance: Float::from_double(adv) }
        glyphs.set(i, g1)
        x = next
      } else {
        x = x + g0.x_advance.to_double()
      }
    } else {
      x = x + g0.x_advance.to_double()
    }
  }
  ShapeLine::{ rtl, glyphs }
}