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

///|
/// Shaper context and builder scaffolding.
///
/// Port intent: align with upstream `upstream swash/src/shape/mod.rs` for
/// `Direction`, `ShapeContext`, `ShaperBuilder`, and `Shaper`.
///
/// Note: the actual shaping engines (OT/AT/AAT) are implemented in later
/// tasks; for now, `Shaper::add_cluster` only pushes mapped clusters into the
/// underlying `Buffer`.

///|
/// Text direction.
pub(all) enum Direction {
  LeftToRight
  RightToLeft
}

///|
pub impl Eq for Direction with fn equal(self, other) {
  match self {
    LeftToRight => other is LeftToRight
    RightToLeft => other is RightToLeft
  }
}

///|
const DEFAULT_SIZE : Int32 = 16

///|
priv struct State {
  buffer : Buffer
  order : Array[Int32]
  glyphs : Array[GlyphData]
  store_builder : FeatureStoreBuilder
  mut disable_kern : Bool
  features : Array[@swash.FeatureSetting]
  selectors : Array[(UInt16, UInt16)]
}

///|

///|
fn State::State() -> State {
  {
    buffer: Buffer(),
    order: [],
    glyphs: [],
    store_builder: FeatureStoreBuilder::default(),
    disable_kern: false,
    features: [],
    selectors: [],
  }
}

///|
fn State::reset(self : State) -> Unit {
  self.buffer.clear()
  self.disable_kern = false
  self.features.clear()
  self.selectors.clear()
}

///|
struct ContextState {
  font_cache : @swash.FontCache[FontEntry]
  feature_cache : FeatureCache
  coords : Array[Int32]
  state : State
}

///|

///|
fn ContextState::ContextState(max_entries : Int32) -> ContextState {
  {
    font_cache: FontCache(max_entries),
    feature_cache: FeatureCache(max_entries),
    coords: [],
    state: State(),
  }
}

///|
/// Context that manages transient buffers for shaping.
///
/// This is intentionally minimal for ro7.1: caches and engines are introduced
/// in later shaping tasks.
pub struct ShapeContext {
  inner : Ref[ContextState]
}

///|

///|
/// Creates a new shaping context.
pub fn ShapeContext::ShapeContext() -> ShapeContext {
  ShapeContext::with_max_entries(DEFAULT_SIZE)
}

///|
/// Creates a new shaping context with the specified maximum number of cache entries.
pub fn ShapeContext::with_max_entries(max_entries : Int32) -> ShapeContext {
  let mut max_entries0 = max_entries
  if max_entries0 < 1 {
    max_entries0 = 1
  } else if max_entries0 > 64 {
    max_entries0 = 64
  }
  { inner: Ref(ContextState(max_entries0)), }
}

///|
/// Creates a new builder for constructing a shaper with this context.
pub fn ShapeContext::builder(
  self : ShapeContext,
  font : @swash.FontRef,
) -> ShaperBuilder {
  let id0 = font.key().value()
  let id1 = (-1).to_int64().reinterpret_as_uint64()
  ShapeContext::builder_with_id(self, font, (id0, id1))
}

///|
/// Creates a new builder for constructing a shaper with this context.
pub fn ShapeContext::builder_with_id(
  self : ShapeContext,
  font : @swash.FontRef,
  id : (UInt64, UInt64),
) -> ShaperBuilder {
  let (font_id, font_entry) = self.inner.val.font_cache.get(font, Some(id), font => {
    FontEntry(font)
  })
  self.inner.val.state.reset()
  self.inner.val.coords.clear()
  {
    cx: self.inner,
    font,
    id0: font_id.0,
    id1: font_id.1,
    size: 0.0,
    script: Latin,
    language: None,
    direction: LeftToRight,
    charmap: font_entry.val.charmap.materialize(font),
    dotted_circle: None,
    retain_ignorables: false,
    font_entry,
  }
}

///|
/// Builder for configuring a shaper.
pub struct ShaperBuilder {
  cx : Ref[ContextState]
  font : @swash.FontRef
  id0 : UInt64
  id1 : UInt64
  size : Double
  script : @swash.Script
  language : @swash.Language?
  direction : Direction
  font_entry : Ref[FontEntry]
  charmap : @swash.Charmap
  dotted_circle : GlyphId?
  retain_ignorables : Bool
}

///|
/// Specifies the script. The default value is `Script::Latin`.
pub fn ShaperBuilder::script(
  self : ShaperBuilder,
  script : @swash.Script,
) -> ShaperBuilder {
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: self.size,
    script,
    language: self.language,
    direction: self.direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle: self.dotted_circle,
    retain_ignorables: self.retain_ignorables,
  }
}

///|
/// Specifies the language. The default value is `None`.
pub fn ShaperBuilder::language(
  self : ShaperBuilder,
  language : @swash.Language?,
) -> ShaperBuilder {
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: self.size,
    script: self.script,
    language,
    direction: self.direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle: self.dotted_circle,
    retain_ignorables: self.retain_ignorables,
  }
}

///|
/// Specifies the text direction. The default value is `Direction::LeftToRight`.
pub fn ShaperBuilder::direction(
  self : ShaperBuilder,
  direction : Direction,
) -> ShaperBuilder {
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: self.size,
    script: self.script,
    language: self.language,
    direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle: self.dotted_circle,
    retain_ignorables: self.retain_ignorables,
  }
}

///|
/// Specifies the font size in pixels per em. The default value is `0` which
/// will produce glyphs with offsets and advances in font units.
pub fn ShaperBuilder::size(
  self : ShaperBuilder,
  ppem : Double,
) -> ShaperBuilder {
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: if ppem < 0.0 {
      0.0
    } else {
      ppem
    },
    script: self.script,
    language: self.language,
    direction: self.direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle: self.dotted_circle,
    retain_ignorables: self.retain_ignorables,
  }
}

///|
/// Adds feature settings to the shaper.
pub fn ShaperBuilder::features(
  self : ShaperBuilder,
  settings : Iter[@swash.FeatureSetting],
) -> ShaperBuilder {
  for s in settings {
    if s.tag == KERN {
      self.cx.val.state.disable_kern = s.value == 0
    }
    self.cx.val.state.features.push(s)
  }
  self
}

///|
/// Adds variation settings to the shaper.
pub fn ShaperBuilder::variations(
  self : ShaperBuilder,
  settings : Iter[@swash.VariationSetting],
) -> ShaperBuilder {
  if self.font_entry.val.coord_count != 0 {
    let vars = self.font.variations().iter().to_array()
    self.cx.val.coords.resize(vars.length(), 0)
    for setting in settings {
      for axis in vars.iter() {
        if axis.tag() == setting.tag {
          let value = axis.normalize(setting.value)
          let ix = axis.index()
          if ix >= 0 && ix < self.cx.val.coords.length() {
            self.cx.val.coords[ix] = value
          }
        }
      }
    }
  }
  self
}

///|
/// Specifies whether to insert dotted circles for broken clusters. The default
/// value is `false`.
pub fn ShaperBuilder::insert_dotted_circles(
  self : ShaperBuilder,
  yes : Bool,
) -> ShaperBuilder {
  let dotted_circle = if yes {
    let gid = self.charmap.map(0x25CC)
    if gid != 0 {
      Some(gid)
    } else {
      None
    }
  } else {
    None
  }
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: self.size,
    script: self.script,
    language: self.language,
    direction: self.direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle,
    retain_ignorables: self.retain_ignorables,
  }
}

///|
/// Specifies whether characters defined as default ignorable should be retained
/// by the shaper. The default is `false`.
pub fn ShaperBuilder::retain_ignorables(
  self : ShaperBuilder,
  yes : Bool,
) -> ShaperBuilder {
  {
    cx: self.cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    size: self.size,
    script: self.script,
    language: self.language,
    direction: self.direction,
    font_entry: self.font_entry,
    charmap: self.charmap,
    dotted_circle: self.dotted_circle,
    retain_ignorables: yes,
  }
}

///|
/// Builds a shaper for the current configuration.
pub fn ShaperBuilder::build(self : ShaperBuilder) -> Shaper {
  let cx = self.cx
  cx.val.state.buffer.dotted_circle = self.dotted_circle
  cx.val.state.buffer.is_rtl = self.direction == RightToLeft
  let engine = Engine::Engine(
    self.font_entry.val.metadata,
    self.font.data(),
    cx.val.coords[:],
    self.script,
    self.language,
  )
  let (store, sub_mask, pos_mask) = if engine.use_ot {
    let entry = cx.val.feature_cache.entry(
      (self.id0, self.id1),
      cx.val.coords[:],
      engine.has_feature_vars(),
      engine.tags(),
    )
    let store_ref = match entry {
      Present(s) => s
      New(s) => {
        engine.collect_features(cx.val.state.store_builder, s)
        s
      }
    }
    let buf = cx.val.state.buffer
    let (sub, pos) = store_ref.val.custom_masks(
      cx.val.state.features[:],
      buf.sub_args,
      buf.pos_args,
      self.direction,
    )
    (Some(store_ref), sub, pos)
  } else {
    (None, FeatureMask::default(), FeatureMask::default())
  }
  let joined = engine.use_ot && self.script.is_joined()
  {
    cx,
    font: self.font,
    id0: self.id0,
    id1: self.id1,
    font_entry: self.font_entry,
    charmap: self.charmap,
    size: self.size,
    script: self.script,
    language: self.language,
    direction: self.direction,
    retain_ignorables: self.retain_ignorables,
    joined,
    engine,
    store,
    sub_mask,
    pos_mask,
  }
}

///|
/// Maps character clusters to (eventually) positioned glyph clusters.
///
/// In ro7.1 this shaper only collects clusters into the underlying `Buffer`.
pub struct Shaper {
  cx : Ref[ContextState]
  font : @swash.FontRef
  id0 : UInt64
  id1 : UInt64
  font_entry : Ref[FontEntry]
  charmap : @swash.Charmap
  size : Double
  script : @swash.Script
  language : @swash.Language?
  direction : Direction
  retain_ignorables : Bool
  joined : Bool
  engine : Engine
  store : Ref[FeatureStore]?
  sub_mask : FeatureMask
  pos_mask : FeatureMask
}

///|
/// Adds a character cluster to the shaper.
pub fn Shaper::add_cluster(self : Shaper, cluster : CharCluster) -> Unit {
  let buf = self.cx.val.state.buffer
  match self.engine.mode {
    Simple => buf.push(cluster) |> ignore
    Myanmar => {
      let e = self.engine
      let sref = match self.store {
        None => return
        Some(v) => v
      }
      let s = sref.val
      let chars = cluster.mapped_chars()
      reorder_myanmar(chars, self.cx.val.state.order)
      let range = buf.push_order(cluster, self.cx.val.state.order[:])
      e.set_classes(buf, Some(range))
      let start = range.start
      e.gsub(s, s.groups.default, buf, Some(range)) |> ignore
      let end = buf.len()
      e.gsub(
        s,
        feature_bit_to_mask(s.groups.reph),
        buf,
        Some(IndexRange(start, end)),
      )
      |> ignore
      let end = buf.len()
      e.gsub(
        s,
        feature_bit_to_mask(s.groups.pref),
        buf,
        Some(IndexRange(start, end)),
      )
      |> ignore
      let end = buf.len()
      e.gsub(s, s.groups.stage1, buf, Some(IndexRange(start, end))) |> ignore
      let end = buf.len()
      e.gsub(s, s.groups.stage2, buf, Some(IndexRange(start, end))) |> ignore
    }
    Complex => {
      let e = self.engine
      let sref = match self.store {
        None => return
        Some(v) => v
      }
      let s = sref.val
      let range = buf.push(cluster)
      e.set_classes(buf, Some(range))
      let start = range.start
      // Default group.
      e.gsub(s, s.groups.default, buf, Some(range)) |> ignore
      for i in range.start.. ignore
      for i in start.. ignore
      for i in start.. ignore
      // Reordering.
      let total = buf.len() - start
      let len = if total < 64 { total } else { 64 }
      let end = start + len
      let slice : Array[GlyphData] = []
      for i in start.. Unit {
  let tokens : Array[@swash.Token] = []
  let mut offset = 0U
  for ch in s {
    // Keep source ranges in UTF-8 byte offsets to match upstream swash.
    let ch_len = @utf8.encode(ch.to_string())[:].length().reinterpret_as_uint()
    tokens.push(Token(ch, offset, ch_len, @swash.CharInfo::from_char(ch), 0U))
    offset = offset + ch_len
  }
  let parser = @swash.Parser::Parser(self.script, tokens.iter())
  let cluster = @swash.CharCluster::CharCluster()
  while parser.next(cluster) {
    cluster.map(fn(ch) { self.charmap.map(ch.to_int().reinterpret_as_uint()) })
    |> ignore
    self.add_cluster(cluster)
  }
}

///|
/// Returns the current normalized variation coordinates in use by the shaper.
pub fn Shaper::normalized_coords(self : Shaper) -> ArrayView[Int32] {
  self.engine.coords
}

///|
/// Returns the current font metrics in use by the shaper.
pub fn Shaper::metrics(self : Shaper) -> @swash.Metrics {
  let scale = if self.size != 0.0 { self.size } else { 1.0 }
  self.font_entry.val.metrics
  .materialize_metrics(self.font, self.engine.coords)
  .scale(scale)
}

///|
/// Shapes the text and invokes the specified closure with each resulting glyph cluster.
pub fn Shaper::shape_with(self : Shaper, f : (GlyphCluster) -> Unit) -> Unit {
  self.finish()
  let buf = self.cx.val.state.buffer
  buf.shaped_glyphs.clear()
  let empty_glyphs : Array[Glyph] = []
  let empty_ranges : Array[SourceRange] = []
  let empty_glyphs_view = empty_glyphs[:]
  let empty_ranges_view = empty_ranges[:]
  let sentinel_g = GlyphData::default()
  let sentinel_p = PositionData::default()
  sentinel_g.cluster = buf.ranges.length().reinterpret_as_uint()
  let mut last_cluster = 0U
  let len = buf.glyphs.length()
  for i in 0..<(len + 1) {
    let g = if i < len { buf.glyphs[i] } else { sentinel_g }
    let p = if i < len { buf.positions[i] } else { sentinel_p }
    if g.cluster != last_cluster {
      let last_ix = last_cluster.reinterpret_as_int()
      let cur_ix = g.cluster.reinterpret_as_int()
      // Simple and common case: no ligatures and no empty clusters.
      if last_ix > cur_ix || cur_ix - last_ix == 1 {
        let index = last_cluster.reinterpret_as_int()
        let info = buf.infos[index]
        let cluster = GlyphCluster::{
          source: buf.ranges[index],
          info: info.0,
          glyphs: buf.shaped_glyphs[:],
          components: empty_ranges_view,
          data: info.2,
        }
        f(cluster)
        buf.shaped_glyphs.clear()
      } else {
        // Collect the range for the non-empty cluster.
        let end = g.cluster.reinterpret_as_int()
        let start = last_cluster.reinterpret_as_int()
        let mut group_end = start + 1
        while group_end < end && buf.infos[group_end].1 {
          group_end = group_end + 1
        }
        if buf.shaped_glyphs.length() != 0 {
          // We have some glyphs. Emit the cluster.
          let source0 = buf.ranges[start]
          let source = @swash.SourceRange::SourceRange(
            source0.start(),
            buf.ranges[group_end - 1].end(),
          )
          let components = empty_ranges_view
          let comps : Array[SourceRange] = []
          let components = if group_end > start + 1 {
            for j in start.. group_end {
          // We have a trailing sequence of empty clusters. Emit them one by one.
          for j in group_end.. Unit {
  if self.cx.val.state.buffer.glyphs.length() == 0 {
    return
  }
  let e = self.engine
  let buf = self.cx.val.state.buffer
  match e.mode {
    Simple =>
      match e.sub_mode {
        Gsub => {
          let sref = match self.store {
            None => return
            Some(v) => v
          }
          let s = sref.val
          e.set_classes(buf, None)
          if self.joined {
            buf.set_join_masks()
          }
          e.gsub(s, self.sub_mask, buf, None) |> ignore
        }
        Morx => {
          e.collect_selectors(
            self.cx.val.state.features[:],
            self.cx.val.state.selectors,
          )
          e.morx(buf, self.cx.val.state.selectors[:])
        }
        _ => ()
      }
    Myanmar => {
      let sref = match self.store {
        None => return
        Some(v) => v
      }
      let s = sref.val
      e.gsub(s, self.sub_mask.or(s.groups.stage2), buf, None) |> ignore
    }
    Complex => {
      let sref = match self.store {
        None => return
        Some(v) => v
      }
      let s = sref.val
      if self.joined {
        buf.set_join_masks()
        e.gsub(s, s.groups.stage2.or(self.sub_mask), buf, None) |> ignore
      } else {
        e.gsub(s, self.sub_mask, buf, None) |> ignore
      }
    }
  }
  buf.setup_positions(e.sub_mode == Morx)
  match e.pos_mode {
    Gpos => {
      let sref = match self.store {
        None => return
        Some(v) => v
      }
      let s = sref.val
      e.gpos(s, self.pos_mask, buf, None) |> ignore
    }
    Kerx => e.kerx(buf, self.cx.val.state.disable_kern)
    Kern => if !self.cx.val.state.disable_kern { e.kern(buf) } else { () }
    _ => ()
  }
  let glyph_metrics = self.font_entry.val.metrics.materialize_glyph_metrics(
    self.font,
    self.engine.coords,
  )
  let len = buf.glyphs.length()
  for i in 0..= 0 {
        let g = buf.glyphs[i]
        if (g.flags & CURSIVE_ATTACH) != 0 {
          let base_offset = buf.positions[i].base.to_int()
          if base_offset != 0 && i + base_offset < len {
            let base = buf.positions[i + base_offset]
            buf.positions[i].x = buf.positions[i].x + base.x
            buf.positions[i].y = buf.positions[i].y + base.y
          }
        }
        if i == 0 {
          break
        }
        i = i - 1
      }
    } else {
      for i in 0..= 0 {
          let base = buf.positions[i - base_offset]
          let x = base.x - base.advance
          let y = base.y
          buf.positions[i].x = buf.positions[i].x + x
          buf.positions[i].y = buf.positions[i].y + y
        }
      }
    }
  }
  let upem = glyph_metrics.units_per_em()
  if self.size != 0.0 && upem != 0 {
    let s = self.size / upem.to_int().to_double()
    for i in 0..