///|
/// Metrics describing the rendered geometry of a string, mirroring the
/// subset of `TextMetrics` we currently expose.
pub struct TextMetrics {
  width : Double
  ascent : Double
  descent : Double
}

///|
pub impl Show for TextMetrics with fn output(self, logger) {
  logger.write_string("TextMetrics { width: ")
  logger.write_object(self.width)
  logger.write_string(", ascent: ")
  logger.write_object(self.ascent)
  logger.write_string(", descent: ")
  logger.write_object(self.descent)
  logger.write_string(" }")
}

///|
/// Measure a string using the currently-installed font. If no font has been
/// set on the context, returns all-zero metrics (matching the degenerate
/// default of HTML canvas before a font is chosen).
pub fn Context::measure_text(self : Context, text : String) -> TextMetrics {
  match self.state.font {
    None => { width: 0.0, ascent: 0.0, descent: 0.0 }
    Some(font) => {
      let size = self.state.font_size
      let width = font.measure_text(text, size)
      let upem = font.units_per_em.to_double()
      let ascent = font.ascent.to_double() / upem * size
      // `font.descent` is typically negative in TTF metrics; invert to get a
      // positive "pixels below the baseline" value, as TextMetrics expects.
      let descent = -font.descent.to_double() / upem * size
      { width, ascent, descent }
    }
  }
}

///|
/// Pixel advance (horizontal) for a single codepoint under the given font
/// and size. Falls back to `0.0` when the glyph id is out of range of the
/// `advance_widths` table (e.g. for unmapped codepoints).
fn advance_for(font : @font.TTFont, cp : Int, size_px : Double) -> Double {
  let gid = font.glyph_index(cp)
  if gid >= font.advance_widths.length() {
    0.0
  } else {
    font.advance_widths[gid].to_double() /
    font.units_per_em.to_double() *
    size_px
  }
}

///|
/// Rasterize `text` onto the canvas, using the current fill_style as the
/// glyph tint and the current transform to position the pen.
///
/// Text is drawn with an alphabetic baseline at `(x, y)`; each glyph bitmap
/// is produced by `@font.rasterize_glyph` and then blitted using source-over
/// blending with the glyph's alpha channel as coverage. If no font is set,
/// this is a no-op.
pub fn Context::fill_text(
  self : Context,
  text : String,
  x : Double,
  y : Double,
) -> Unit {
  let font = match self.state.font {
    None => return
    Some(f) => f
  }
  let size = self.state.font_size
  // Text rendering uses a single tint color. For solid fills we use the
  // color directly; for gradient fills we fall back to the first stop
  // (gradient-tinted text is a future enhancement).
  let color = match self.state.fill_style {
    FillStyle::Solid(c) => c
    FillStyle::Linear(g) => g.stops[0].color
    FillStyle::Radial(g) => g.stops[0].color
  }
  let global_alpha = self.state.global_alpha
  let pixels = self.canvas.pixels
  let cw = self.canvas.width
  let ch = self.canvas.height
  let mut pen_x = x
  let pen_y = y
  let size_bucket = (size * 4.0).to_int()
  for ch_char in text {
    let codepoint = ch_char.to_int()
    let gid = font.glyph_index(codepoint)
    let cache_key = gid * 10000 + size_bucket
    let bitmap = match self.glyph_cache.get(cache_key) {
      Some(cached) => cached
      None =>
        match @font.rasterize_glyph(font, codepoint, size) {
          None => {
            pen_x = pen_x + advance_for(font, codepoint, size)
            continue
          }
          Some(bm) => {
            self.glyph_cache[cache_key] = bm
            bm
          }
        }
    }
    let (dest_x, dest_y) = self.state.transform.transform_point(pen_x, pen_y)
    let dst_x0 = dest_x.floor().to_int()
    let dst_y0 = (dest_y - bitmap.height.to_double()).floor().to_int()
    blit_glyph(
      pixels,
      cw,
      ch,
      bitmap,
      dst_x0,
      dst_y0,
      color,
      global_alpha,
      self.state.clip,
    )
    pen_x = pen_x + advance_for(font, codepoint, size)
  }
  // `pen_y` is intentionally unused after the loop; keep it bound so the
  // reader can see the baseline stays fixed across glyphs.
  ignore(pen_y)
}

///|
/// Tints a grayscale/alpha glyph bitmap with `color` and blends via
/// source-over, optionally masked by a `ClipMask`.
///
/// `@font.rasterize_glyph` returns an RGBA interleaved buffer where the alpha
/// channel carries the coverage signal (the RGB channels are constant white).
/// We scale the 0..=255 alpha to the 0..=256 range that `blend_over` expects,
/// and when a `clip` mask is present we combine its per-pixel coverage with
/// the glyph coverage so text respects the active clip region.
fn blit_glyph(
  pixels : FixedArray[Byte],
  canvas_w : Int,
  canvas_h : Int,
  glyph : @font.GlyphBitmap,
  dst_x : Int,
  dst_y : Int,
  color : Color,
  global_alpha : Double,
  clip : ClipMask?,
) -> Unit {
  let gw = glyph.width
  let gh = glyph.height
  for gy in 0..= 0 && py < canvas_h {
      for gx in 0..= 0 && px < canvas_w {
          let src_offset = (gy * gw + gx) * 4
          let a_int = glyph.pixels[src_offset + 3]
          if a_int > 0 {
            let coverage = a_int * 256 / 255
            let clip_cov = match clip {
              None => 255
              Some(mask) => mask.data[py * canvas_w + px].to_int()
            }
            if clip_cov != 0 {
              let effective = if clip_cov == 255 {
                coverage
              } else {
                (coverage * clip_cov + 127) / 255
              }
              blend_over(
                pixels,
                (py * canvas_w + px) * 4,
                color,
                effective,
                global_alpha,
              )
            }
          }
        }
      }
    }
  }
}