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

///|
/// Scaling context and scaler scaffolding.
///
/// Port intent: align with upstream `swash::scale::ScaleContext/ScalerBuilder/Scaler`.
/// This is currently a functional skeleton: it provides the public API shape,
/// but does not yet implement actual outline extraction, hinting, or rasterization.

///|
struct ScaleContext {
  fonts : Ref[@swash.FontCache[ScalerProxy]]
  state : Ref[State]
  hinting_cache : Ref[HintingCache]
}

///|

///|
priv struct State {
  mut _scratch0 : Array[Byte]
  mut _scratch1 : Array[Byte]
  outline : Outline
  rcx : Ref[@zeno.Scratch]
}

///|

///|
fn State::State() -> State {
  State::{
    _scratch0: [],
    _scratch1: [],
    outline: Outline::Outline(),
    rcx: Ref(@zeno.Scratch::Scratch()),
  }
}

///|
pub fn ScaleContext::ScaleContext() -> ScaleContext {
  ScaleContext::with_max_entries(8)
}

///|
/// Creates a new scaling context with the specified maximum number of cache entries.
///
/// This mirrors the upstream API. The value is clamped to 1..=64.
pub fn ScaleContext::with_max_entries(max_entries : Int) -> ScaleContext {
  let max_entries = if max_entries < 1 {
    1
  } else if max_entries > 64 {
    64
  } else {
    max_entries
  }
  ScaleContext::{
    fonts: Ref(@swash.FontCache::FontCache(max_entries)),
    state: Ref(State::State()),
    hinting_cache: Ref(HintingCache::HintingCache()),
  }
}

///|
pub fn ScaleContext::builder(
  self : ScaleContext,
  font : @swash.FontRef,
) -> ScalerBuilder {
  let (id, proxy) = self.fonts.val.get(font, None, ScalerProxy::from_font)
  ScalerBuilder::{
    state: self.state,
    hinting_cache: self.hinting_cache,
    font,
    proxy,
    id0: id.0,
    id1: id.1,
    coords: [],
    size: 0.0,
    hint: false,
  }
}

///|
/// Creates a builder for constructing a scaler with this context, specified font and a
/// custom unique identifier.
pub fn ScaleContext::builder_with_id(
  self : ScaleContext,
  font : @swash.FontRef,
  id : (UInt64, UInt64),
) -> ScalerBuilder {
  let (id2, proxy) = self.fonts.val.get(font, Some(id), ScalerProxy::from_font)
  ScalerBuilder::{
    state: self.state,
    hinting_cache: self.hinting_cache,
    font,
    proxy,
    id0: id2.0,
    id1: id2.1,
    coords: [],
    size: 0.0,
    hint: false,
  }
}

///|
/// Builder for configuring a scaler.
struct ScalerBuilder {
  state : Ref[State]
  hinting_cache : Ref[HintingCache]
  font : @swash.FontRef
  proxy : Ref[ScalerProxy]
  id0 : UInt64
  id1 : UInt64
  coords : Array[Int]
  size : Double
  hint : Bool
}

///|
pub fn ScalerBuilder::size(
  self : ScalerBuilder,
  ppem : Double,
) -> ScalerBuilder {
  ScalerBuilder::{
    state: self.state,
    hinting_cache: self.hinting_cache,
    font: self.font,
    proxy: self.proxy,
    id0: self.id0,
    id1: self.id1,
    coords: self.coords,
    size: if ppem < 0.0 {
      0.0
    } else {
      ppem
    },
    hint: self.hint,
  }
}

///|
pub fn ScalerBuilder::hint(self : ScalerBuilder, yes : Bool) -> ScalerBuilder {
  ScalerBuilder::{
    state: self.state,
    hinting_cache: self.hinting_cache,
    font: self.font,
    proxy: self.proxy,
    id0: self.id0,
    id1: self.id1,
    coords: self.coords,
    size: self.size,
    hint: yes,
  }
}

///|
pub fn ScalerBuilder::variations(
  self : ScalerBuilder,
  settings : ArrayView[@swash.VariationSetting],
) -> ScalerBuilder {
  if self.proxy.val.coord_count == 0 {
    return self
  }
  let vars = self.font.variations().iter().to_array()
  if vars.is_empty() {
    return self
  }
  let n = vars.length()
  if self.coords.length() < n {
    for _ in self.coords.length().. n {
    self.coords.truncate(n)
  }
  for s in settings.iter() {
    for v in vars.iter() {
      if v.tag() == s.tag {
        let value = v.normalize(s.value)
        let ix = v.index()
        if ix >= 0 && ix < self.coords.length() {
          self.coords.set(ix, value)
        }
      }
    }
  }
  self
}

///|
pub fn ScalerBuilder::normalized_coords(
  self : ScalerBuilder,
  coords : ArrayView[Int],
) -> ScalerBuilder {
  self.coords.clear()
  for c in coords.iter() {
    self.coords.push(c)
  }
  self
}

///|
pub fn ScalerBuilder::build(self : ScalerBuilder) -> Scaler {
  let upem = self.proxy.val.metrics.units_per_em().to_int()
  let skrifa_size = if self.size != 0.0 && upem != 0 {
    @skrifa.Size::Size(self.size)
  } else {
    @skrifa.Size::unscaled()
  }
  let loc = if self.coords.is_empty() {
    @skrifa.LocationRef::default()
  } else {
    @skrifa.LocationRef::LocationRef(self.coords.op_as_view())
  }
  let skrifa_font = to_skrifa_font(self.font)
  let outlines = match skrifa_font {
    None => None
    Some(f) => Some(@skrifa_outline.OutlineGlyphCollection::from_font(f))
  }
  let outlines0 = outlines
  let hinting_instance = if self.hint && outlines is Some(oc) {
    self.hinting_cache.val.get(self.id0, self.id1, oc, skrifa_size, loc)
  } else {
    None
  }
  Scaler::{
    state: self.state,
    _hinting_cache: self.hinting_cache,
    font: self.font,
    outlines: outlines0,
    hinting_instance,
    proxy: self.proxy,
    coords: self.coords,
    size: self.size,
    skrifa_size,
    hint: self.hint,
  }
}

///|
struct Scaler {
  state : Ref[State]
  _hinting_cache : Ref[HintingCache]
  font : @swash.FontRef
  outlines : @skrifa_outline.OutlineGlyphCollection?
  hinting_instance : @skrifa_outline.HintingInstance?
  proxy : Ref[ScalerProxy]
  coords : Array[Int]
  size : Double
  skrifa_size : @skrifa.Size
  hint : Bool
}

///|
pub fn Scaler::size(self : Scaler) -> Double {
  self.size
}

///|
pub fn Scaler::hint(self : Scaler) -> Bool {
  self.hint
}

///|
test "ScaleContext caches ScalerProxy per font id (LRU eviction)" {
  let data = b"\x00\x01\x00\x00"
  let f0 = @swash.FontRef::from_offset(data, 0).unwrap()
  let f1 = @swash.FontRef::from_offset(data, 0).unwrap()
  let ctx = ScaleContext::with_max_entries(1)
  let s0 = ctx.builder(f0).build()
  let uid0 = s0.proxy.val.uid
  let s0_again = ctx.builder(f0).build()
  inspect(s0_again.proxy.val.uid == uid0, content="true")

  // Insert a different font id (different CacheKey), which should evict f0
  // because max_entries=1.
  let s1 = ctx.builder(f1).build()
  inspect(s1.proxy.val.uid != uid0, content="true")

  // f0 was evicted, so a new proxy should be constructed.
  let s0_new = ctx.builder(f0).build()
  inspect(s0_new.proxy.val.uid != uid0, content="true")
}

///|
pub fn Scaler::scale_outline_into(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  outline : Outline,
) -> Bool {
  outline.clear()
  self.scale_outline_layer_into(glyph_id, None, outline)
}

///|
pub fn Scaler::scale_color_outline_into(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  outline : Outline,
) -> Bool {
  outline.clear()
  if !self.has_color_outlines() {
    return false
  }
  let layers_opt = self.proxy.val.color.layers(self.font.data(), glyph_id)
  match layers_opt {
    None => false
    Some(layers) => {
      let len = layers.len().to_int()
      for i in 0.. return false
          Some(v) => v
        }
        if !self.scale_outline_layer_into(
            layer.glyph_id(),
            layer.color_index(),
            outline,
          ) {
          return false
        }
      }
      outline.set_color(true)
      true
    }
  }
}

///|
fn find_font_index_by_offset(data : Bytes, offset : Int) -> UInt? {
  match @swash.FontDataRef::from_data(data) {
    None => None
    Some(fdr) => {
      let mut i = 0
      for f in fdr.fonts() {
        if f.offset() == offset {
          return Some(i.reinterpret_as_uint())
        }
        i = i + 1
      }
      None
    }
  }
}

///|
fn to_skrifa_font(font : @swash.FontRef) -> @skrifa.FontRef? {
  if font.offset() == 0 {
    @skrifa.FontRef::from_bytes(font.data())
  } else {
    match find_font_index_by_offset(font.data(), font.offset()) {
      None => None
      Some(index) => @skrifa.FontRef::from_index(font.data(), index)
    }
  }
}

///|
struct OutlineWriter {
  outline : Outline
}

///|

///|
fn OutlineWriter::OutlineWriter(outline : Outline) -> OutlineWriter {
  OutlineWriter::{ outline, }
}

///|
pub impl @skrifa_outline.OutlinePen for OutlineWriter with fn move_to(
  self,
  x,
  y,
) {
  self.outline.move_to(Vector::Vector(x, y))
}

///|
pub impl @skrifa_outline.OutlinePen for OutlineWriter with fn line_to(
  self,
  x,
  y,
) {
  self.outline.line_to(Vector::Vector(x, y))
}

///|
pub impl @skrifa_outline.OutlinePen for OutlineWriter with fn quad_to(
  self,
  cx0,
  cy0,
  x,
  y,
) {
  self.outline.quad_to(Vector::Vector(cx0, cy0), Vector::Vector(x, y))
}

///|
pub impl @skrifa_outline.OutlinePen for OutlineWriter with fn curve_to(
  self,
  cx0,
  cy0,
  cx1,
  cy1,
  x,
  y,
) {
  self.outline.curve_to(
    Vector::Vector(cx0, cy0),
    Vector::Vector(cx1, cy1),
    Vector::Vector(x, y),
  )
}

///|
pub impl @skrifa_outline.OutlinePen for OutlineWriter with fn close(self) {
  self.outline.close()
}

///|

///|
pub fn Scaler::scale_outline(
  _self : Scaler,
  _glyph_id : @swash.GlyphId,
) -> Outline? {
  let self = _self
  let glyph_id = _glyph_id
  let outline = Outline::Outline()
  if self.scale_outline_into(glyph_id, outline) {
    Some(outline)
  } else {
    None
  }
}

///|
pub fn Scaler::scale_color_outline(
  self : Scaler,
  glyph_id : @swash.GlyphId,
) -> Outline? {
  let outline = Outline::Outline()
  if self.scale_color_outline_into(glyph_id, outline) {
    Some(outline)
  } else {
    None
  }
}

///|
pub fn Scaler::has_color_outlines(self : Scaler) -> Bool {
  self.proxy.val.color.colr != 0 && self.proxy.val.color.cpal != 0
}

///|
pub fn Scaler::has_outlines(self : Scaler) -> Bool {
  match self.outlines {
    None => {
      let loca = @internal.table_offset(self.font, @internal.LOCA)
      let glyf = @internal.table_offset(self.font, @internal.GLYF)
      loca != 0 && glyf != 0
    }
    Some(outlines) => outlines.format() is Some(_)
  }
}

///|
pub fn Scaler::has_bitmaps(self : Scaler) -> Bool {
  self.proxy.val.bitmaps.has_alpha()
}

///|
pub fn Scaler::has_color_bitmaps(self : Scaler) -> Bool {
  self.proxy.val.bitmaps.has_color()
}

///|
pub fn Scaler::scale_bitmap_into(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  strike : StrikeWith,
  image : Image,
) -> Bool {
  match self.scale_bitmap_impl(glyph_id, false, strike, image) {
    None => false
    Some(v) => v
  }
}

///|
pub fn Scaler::scale_bitmap(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  strike : StrikeWith,
) -> Image? {
  let image = Image::Image()
  if self.scale_bitmap_into(glyph_id, strike, image) {
    Some(image)
  } else {
    None
  }
}

///|
pub fn Scaler::scale_color_bitmap_into(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  strike : StrikeWith,
  image : Image,
) -> Bool {
  match self.scale_bitmap_impl(glyph_id, true, strike, image) {
    None => false
    Some(v) => v
  }
}

///|
pub fn Scaler::scale_color_bitmap(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  strike : StrikeWith,
) -> Image? {
  let image = Image::Image()
  if self.scale_color_bitmap_into(glyph_id, strike, image) {
    Some(image)
  } else {
    None
  }
}

///|
fn Scaler::scale_bitmap_impl(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  color : Bool,
  strike : StrikeWith,
  image : Image,
) -> Bool? {
  image.clear()
  let size = self.size
  let skrifa_font = match to_skrifa_font(self.font) {
    None => return None
    Some(f) => f
  }
  let strikes = if color {
    match
      @skrifa.BitmapStrikes::with_format(
        skrifa_font,
        @skrifa.BitmapFormat::Sbix,
      ) {
      None =>
        @skrifa.BitmapStrikes::with_format(
          skrifa_font,
          @skrifa.BitmapFormat::Cbdt,
        )
      Some(s) => Some(s)
    }
  } else {
    @skrifa.BitmapStrikes::with_format(skrifa_font, @skrifa.BitmapFormat::Ebdt)
  }
  let strikes = match strikes {
    None => return None
    Some(s) => s
  }
  let ppem = size.to_int()
  let gid = @skrifa.GlyphId::GlyphId(glyph_id)

  // Select a strike and glyph, mirroring upstream swash strike selection.
  let mut glyph : @skrifa.BitmapGlyph? = None
  let mut strike_ppem = 0
  match strike {
    StrikeWith::ExactSize => {
      if size == 0.0 {
        return None
      }
      for i in 0.. ()
          Some(s) =>
            if s.ppem_y() == ppem {
              match s.get(gid) {
                None => ()
                Some(g) => {
                  glyph = Some(g)
                  strike_ppem = s.ppem_y()
                  break
                }
              }
            }
        }
      }
    }
    StrikeWith::BestFit => {
      if size == 0.0 {
        return None
      }
      let mut best : @skrifa.BitmapGlyph? = None
      let mut best_size = 0
      for i in 0.. ()
          Some(s) =>
            match s.get(gid) {
              None => ()
              Some(g) => {
                best = Some(g)
                let s_ppem = s.ppem_y()
                if s_ppem > best_size {
                  best = Some(g)
                  best_size = s_ppem
                }
                if s_ppem >= ppem {
                  glyph = Some(g)
                  strike_ppem = s_ppem
                  break
                }
              }
            }
        }
      }
      if glyph is None {
        glyph = best
        strike_ppem = best_size
      }
    }
    StrikeWith::LargestSize => {
      let mut best : @skrifa.BitmapGlyph? = None
      let mut best_size = 0
      for i in 0.. ()
          Some(s) =>
            match s.get(gid) {
              None => ()
              Some(g) => {
                let s_ppem = s.ppem_y()
                if best is None || s_ppem > best_size {
                  best = Some(g)
                  best_size = s_ppem
                }
              }
            }
        }
      }
      glyph = best
      strike_ppem = best_size
    }
    StrikeWith::Index(index) => {
      let i = index.reinterpret_as_int()
      if i < 0 || i >= strikes.len() {
        return None
      }
      match strikes.get(i) {
        None => return None
        Some(s) =>
          match s.get(gid) {
            None => return None
            Some(g) => {
              glyph = Some(g)
              strike_ppem = s.ppem_y()
            }
          }
      }
    }
  }
  let glyph = match glyph {
    None => return None
    Some(g) => g
  }
  if strike_ppem == 0 {
    return None
  }

  // Decode into `image.data`, optionally scaling if requested.
  let w = match glyph.width() {
    None => return None
    Some(w) => w
  }
  let h = match glyph.height() {
    None => return None
    Some(h) => h
  }
  if w <= 0 || h <= 0 {
    image.placement = Placement::{ left: 0, top: 0, width: 0U, height: 0U }
    image.source = if color {
      Source::ColorBitmap(strike)
    } else {
      Source::Bitmap(strike)
    }
    image.content = if color { Content::Color } else { Content::Mask }
    return Some(true)
  }
  let scale = if size == 0.0 { 1.0 } else { size / strike_ppem.to_double() }
  let scaled_w = if size != 0.0 && scale != 1.0 {
    (w.to_double() * scale).to_int().reinterpret_as_uint()
  } else {
    w.reinterpret_as_uint()
  }
  let scaled_h = if size != 0.0 && scale != 1.0 {
    (h.to_double() * scale).to_int().reinterpret_as_uint()
  } else {
    h.reinterpret_as_uint()
  }
  self.state.val._scratch0.clear()
  self.state.val._scratch1.clear()
  let origin_x = glyph.origin_x()
  let origin_y = glyph.origin_y()
  match (color, glyph.format()) {
    (true, @skrifa.BitmapImageFormat::Png) => {
      let raw : Array[Byte] = []
      for b in glyph.data() {
        raw.push(b)
      }
      let raw_bytes = Bytes::from_array(raw.op_as_view())
      if size != 0.0 && scale != 1.0 {
        // Decode into scratch then resize into image buffer.
        let src_len = (w.reinterpret_as_uint() * h.reinterpret_as_uint() * 4U).reinterpret_as_int()
        self.state.val._scratch0 = Array::makei(src_len, _ => b'\x00')
        match
          decode_png(
            raw_bytes,
            self.state.val._scratch1,
            self.state.val._scratch0,
          ) {
          None => return None
          Some(_) => ()
        }
        let dst_len = (scaled_w * scaled_h * 4U).reinterpret_as_int()
        image.data = Array::makei(dst_len, _ => b'\x00')
        if !resize_mitchell_rgba(
            self.state.val._scratch0,
            w.reinterpret_as_uint(),
            h.reinterpret_as_uint(),
            image.data,
            scaled_w,
            scaled_h,
            self.state.val._scratch1,
          ) {
          return None
        }
      } else {
        // Decode directly into the destination buffer (do not alias state scratch).
        let dst_len = (w.reinterpret_as_uint() * h.reinterpret_as_uint() * 4U).reinterpret_as_int()
        image.data = Array::makei(dst_len, _ => b'\x00')
        match decode_png(raw_bytes, self.state.val._scratch1, image.data) {
          None => return None
          Some(_) => ()
        }
      }
      image.placement = Placement::{
        left: if size != 0.0 && scale != 1.0 {
          (origin_x.to_double() * scale).to_int()
        } else {
          origin_x
        },
        top: if size != 0.0 && scale != 1.0 {
          (origin_y.to_double() * scale).to_int()
        } else {
          origin_y
        },
        width: scaled_w,
        height: scaled_h,
      }
      image.source = Source::ColorBitmap(strike)
      image.content = Content::Color
      Some(true)
    }
    (false, _) => {
      // Decode EBDT bitmap glyph data as a 1-bit mask.
      if size != 0.0 && scale != 1.0 {
        let src_len = (w.reinterpret_as_uint() * h.reinterpret_as_uint()).reinterpret_as_int()
        self.state.val._scratch0 = Array::makei(src_len, _ => b'\x00')
        if !decode_ebdt_1bpp(
            glyph.data(),
            w.reinterpret_as_uint(),
            h.reinterpret_as_uint(),
            self.state.val._scratch0,
          ) {
          return None
        }
        let dst_len = (scaled_w * scaled_h).reinterpret_as_int()
        image.data = Array::makei(dst_len, _ => b'\x00')
        if !resize_mitchell_alpha(
            self.state.val._scratch0,
            w.reinterpret_as_uint(),
            h.reinterpret_as_uint(),
            image.data,
            scaled_w,
            scaled_h,
            self.state.val._scratch1,
          ) {
          return None
        }
      } else {
        let dst_len = (w.reinterpret_as_uint() * h.reinterpret_as_uint()).reinterpret_as_int()
        image.data = Array::makei(dst_len, _ => b'\x00')
        if !decode_ebdt_1bpp(
            glyph.data(),
            w.reinterpret_as_uint(),
            h.reinterpret_as_uint(),
            image.data,
          ) {
          return None
        }
      }
      image.placement = Placement::{
        left: if size != 0.0 && scale != 1.0 {
          (origin_x.to_double() * scale).to_int()
        } else {
          origin_x
        },
        top: if size != 0.0 && scale != 1.0 {
          (origin_y.to_double() * scale).to_int()
        } else {
          origin_y
        },
        width: scaled_w,
        height: scaled_h,
      }
      image.source = Source::Bitmap(strike)
      image.content = Content::Mask
      Some(true)
    }
    _ => None
  }
}

///|
fn Scaler::scale_outline_layer_into(
  self : Scaler,
  glyph_id : @swash.GlyphId,
  color_index : UInt16?,
  outline : Outline,
) -> Bool {
  match self.outlines {
    None => false
    Some(outlines) => {
      let gid = @skrifa.GlyphId::GlyphId(glyph_id)
      match outlines.get(gid) {
        None => false
        Some(glyph) => {
          outline.begin_layer(color_index)
          let loc = @skrifa.LocationRef::LocationRef(self.coords.op_as_view())
          let settings = match self.hinting_instance {
            None =>
              @skrifa_outline.DrawSettings::unhinted(self.skrifa_size, loc)
            Some(h) => @skrifa_outline.DrawSettings::hinted(h, false)
          }
          let pen = OutlineWriter::OutlineWriter(outline)
          match glyph.draw(settings, pen) {
            Ok(_) => {
              outline.maybe_close()
              outline.finish()
              true
            }
            Err(_) => false
          }
        }
      }
    }
  }
}

///|
test "ScaleContext builder scaffolding" {
  // Minimal TTF header tag 0x00010000 is enough for FontRef::from_offset.
  let data = Bytes::from_array([0, 1, 0, 0])
  let font = @swash.FontRef::from_offset(data, 0).unwrap()
  let cx = ScaleContext::ScaleContext()
  let scaler = cx.builder(font).size(14.0).hint(true).build()
  inspect(scaler.size().to_int(), content="14")
  inspect(scaler.hint(), content="true")
}