///|
/// Layout metrics for text
pub struct LayoutMetrics {
  width : Double
  height : Double
  baseline : Double
} derive(Eq, Debug)

///|
pub extend LayoutMetrics with Debug::{to_repr}

///|
pub extend LayoutMetrics with Eq::{not_equal, equal}

///|
/// Create layout metrics
pub fn LayoutMetrics::new(
  width~ : Double,
  height~ : Double,
  baseline~ : Double,
) -> LayoutMetrics {
  { width, height, baseline, }
}

///|
/// Text alignment.
pub(all) enum TextAlign {
  Left
  Center
  Right
  Justify
} derive(Eq, Debug)

///|
pub extend TextAlign with Debug::{to_repr}

///|
pub extend TextAlign with Eq::{not_equal, equal}

///|
/// Text layout configuration
pub struct LayoutConfig {
  max_width : Double
  font_size : Double
  line_height : Double
  alignment : TextAlign
  letter_spacing : Double
  word_spacing : Double
} derive(Debug)

///|
pub extend LayoutConfig with Debug::{to_repr}

///|
/// Create a layout config with default values
pub fn LayoutConfig::new(
  max_width? : Double = 800.0,
  font_size? : Double = 16.0,
  line_height? : Double = 24.0,
  alignment? : TextAlign = Left,
  letter_spacing? : Double = 0.0,
  word_spacing? : Double = 0.0,
) -> LayoutConfig {
  {
    max_width,
    font_size,
    line_height,
    alignment,
    letter_spacing,
    word_spacing,
  }
}

///|
/// A positioned glyph for rendering
struct PositionedGlyph {
  glyph_id : Int
  origin_x : Double
  x : Double
  y : Double
  advance_width : Double
} derive(Debug)

///|
pub extend PositionedGlyph with Debug::{to_repr}

///|
/// Get this positioned glyph's glyph id.
pub fn PositionedGlyph::get_glyph_id(self : PositionedGlyph) -> Int {
  self.glyph_id
}

///|
/// Get this positioned glyph's x position in pixels.
pub fn PositionedGlyph::get_x(self : PositionedGlyph) -> Double {
  self.x
}

///|
/// Get this positioned glyph's y position in pixels.
pub fn PositionedGlyph::get_y(self : PositionedGlyph) -> Double {
  self.y
}

///|
/// Get this positioned glyph's advance width in pixels.
pub fn PositionedGlyph::get_advance_width(self : PositionedGlyph) -> Double {
  self.advance_width
}

///|
/// A line of positioned glyphs
struct LayoutLine {
  glyphs : Array[PositionedGlyph]
  width : Double
  y : Double
} derive(Debug)

///|
pub extend LayoutLine with Debug::{to_repr}

///|
/// Layout result containing positioned glyphs
struct LayoutResult {
  lines : Array[LayoutLine]
  total_width : Double
  total_height : Double
} derive(Debug)

///|
pub extend LayoutResult with Debug::{to_repr}

///|
priv enum LayoutToken {
  Text(String)
  LineBreak
}

///|
priv struct LayoutLineEntry {
  line : LayoutLine
  should_justify : Bool
}

///|
/// Layout text using the font's actual metrics
fn layout_text(
  font : Font,
  text : String,
  config : LayoutConfig,
) -> LayoutResult {
  let line_entries = []
  let units_per_em = font.get_units_per_em().to_double()
  let scale = config.font_size / units_per_em
  let (ascent, descent, _) = font.get_metrics()
  let line_height = config.line_height

  // Word wrap: break text into lines that fit within max_width.
  let tokens = split_layout_tokens(text)
  let mut current_line_glyphs = []
  let mut current_line_width = 0.0
  let mut current_y = ascent.to_double() * scale
  let mut has_visible_line = false

  for token in tokens {
    match token {
      LineBreak => {
        line_entries.push(
          make_layout_line_entry(
            current_line_glyphs,
            current_line_width,
            current_y,
            should_justify=false,
          ),
        )
        current_line_glyphs = []
        current_line_width = 0.0
        current_y = current_y + line_height
        has_visible_line = true
      }
      Text(word) => {
        // Calculate word width
        let word_width = calculate_word_width(font, word, scale, config)
        let is_space = is_space_token(word)

        // Check if word fits on current line
        let needs_break = current_line_glyphs.length() > 0 &&
          !is_space &&
          current_line_width + word_width > config.max_width

        if needs_break {
          line_entries.push(
            make_layout_line_entry(
              current_line_glyphs,
              current_line_width,
              current_y,
              should_justify=true,
            ),
          )
          current_line_glyphs = []
          current_line_width = 0.0
          current_y = current_y + line_height
          has_visible_line = true
        }

        // Add word glyphs to current line
        let word_glyphs = shape_word(
          font, word, scale, current_line_width, config,
        )
        for g in word_glyphs {
          current_line_glyphs.push(g)
        }
        current_line_width = current_line_width + word_width
        has_visible_line = true
      }
    }
  }

  // Add final line
  if current_line_glyphs.length() > 0 || has_visible_line {
    line_entries.push(
      make_layout_line_entry(
        current_line_glyphs,
        current_line_width,
        current_y,
        should_justify=false,
      ),
    )
  }

  let aligned_lines = align_layout_lines(font, line_entries, config)

  // Calculate total dimensions
  let mut total_width = 0.0
  for line in aligned_lines {
    if line.width > total_width {
      total_width = line.width
    }
  }
  let total_height = if aligned_lines.length() > 0 {
    current_y + descent.to_double() * scale
  } else {
    0.0
  }

  { lines: aligned_lines, total_width, total_height, }
}

///|
fn make_layout_line_entry(
  glyphs : Array[PositionedGlyph],
  width : Double,
  y : Double,
  should_justify~ : Bool,
) -> LayoutLineEntry {
  { line: { glyphs, width, y, }, should_justify, }
}

///|
fn align_layout_lines(
  font : Font,
  lines : Array[LayoutLineEntry],
  config : LayoutConfig,
) -> Array[LayoutLine] {
  let aligned = []
  for i in 0.. LayoutLine {
  match config.alignment {
    Left => line
    Center => shift_layout_line(line, alignment_extra(line, config) / 2.0)
    Right => shift_layout_line(line, alignment_extra(line, config))
    Justify =>
      if should_justify {
        justify_layout_line(font, line, config)
      } else {
        line
      }
  }
}

///|
fn alignment_extra(line : LayoutLine, config : LayoutConfig) -> Double {
  let extra = config.max_width - line.width
  if extra > 0.0 {
    extra
  } else {
    0.0
  }
}

///|
fn shift_layout_line(line : LayoutLine, offset : Double) -> LayoutLine {
  guard offset != 0.0 else { return line }
  let glyphs = []
  for glyph in line.glyphs {
    glyphs.push({
      glyph_id: glyph.glyph_id,
      origin_x: glyph.origin_x + offset,
      x: glyph.x + offset,
      y: glyph.y,
      advance_width: glyph.advance_width,
    })
  }
  { glyphs, width: line.width, y: line.y, }
}

///|
fn justify_layout_line(
  font : Font,
  line : LayoutLine,
  config : LayoutConfig,
) -> LayoutLine {
  let extra = alignment_extra(line, config)
  guard extra > 0.0 else { return line }
  let space_glyph_id = font.char_to_glyph(' '.to_int())
  let mut space_count = 0
  for i in 0.. 0 else { return line }

  let extra_per_space = extra / space_count.to_double()
  let glyphs = []
  let mut shift = 0.0
  for i in 0.. Bool {
  guard glyphs[index].glyph_id == space_glyph_id else { return false }
  has_non_space_before(glyphs, index, space_glyph_id) &&
  has_non_space_after(glyphs, index, space_glyph_id)
}

///|
fn has_non_space_before(
  glyphs : Array[PositionedGlyph],
  index : Int,
  space_glyph_id : Int,
) -> Bool {
  let mut i = index - 1
  while i >= 0 {
    if glyphs[i].glyph_id != space_glyph_id {
      return true
    }
    i = i - 1
  }
  false
}

///|
fn has_non_space_after(
  glyphs : Array[PositionedGlyph],
  index : Int,
  space_glyph_id : Int,
) -> Bool {
  let mut i = index + 1
  while i < glyphs.length() {
    if glyphs[i].glyph_id != space_glyph_id {
      return true
    }
    i = i + 1
  }
  false
}

///|
/// Split text into layout tokens, preserving spaces and explicit newlines.
fn split_layout_tokens(text : String) -> Array[LayoutToken] {
  let tokens = []
  let mut current_word = ""
  let mut current_word_class = None
  let mut i = 0
  let len = text.length()

  while i < len {
    let ch = text.get_char(i)
    match ch {
      Some(c) =>
        if c is '\n' || c is '\r' {
          // End current word
          if current_word.length() > 0 {
            tokens.push(LayoutToken::Text(current_word))
            current_word = ""
            current_word_class = None
          }
          tokens.push(LineBreak)
          if c is '\r' {
            match text.get_char(i + 1) {
              Some('\n') => i = i + 1
              _ => ()
            }
          }
        } else if c is ' ' || c is '\t' {
          // End current word
          if current_word.length() > 0 {
            tokens.push(Text(current_word))
            current_word = ""
            current_word_class = None
          }
          // Add space as separate word
          if c is ' ' {
            tokens.push(Text(" "))
          }
        } else {
          let curr_class = get_line_break_class(c.to_int())
          match current_word_class {
            Some(prev_class) =>
              if should_split_unspaced_layout_token(prev_class, curr_class) {
                tokens.push(Text(current_word))
                current_word = ""
                current_word_class = None
              }
            None => ()
          }
          current_word = current_word + c.to_string()
          current_word_class = Some(curr_class)
        }
      None => ignore(0)
    }
    i = i + 1
  }

  // Add final word
  if current_word.length() > 0 {
    tokens.push(Text(current_word))
  }

  tokens
}

///|
fn should_split_unspaced_layout_token(
  prev_class : LineBreakClass,
  curr_class : LineBreakClass,
) -> Bool {
  is_line_break_allowed(prev_class, curr_class) &&
  can_start_unspaced_layout_token(curr_class) &&
  (
    is_unspaced_layout_break_class(prev_class) ||
    is_unspaced_layout_break_class(curr_class)
  )
}

///|
fn can_start_unspaced_layout_token(class : LineBreakClass) -> Bool {
  match class {
    CL => false
    CP => false
    EX => false
    IS => false
    SY => false
    NS => false
    _ => true
  }
}

///|
fn is_unspaced_layout_break_class(class : LineBreakClass) -> Bool {
  match class {
    ID => true
    H2 => true
    H3 => true
    JL => true
    JV => true
    JT => true
    _ => false
  }
}

///|
/// Calculate the width of a word using font metrics with kerning
fn calculate_word_width(
  font : Font,
  word : String,
  scale : Double,
  config : LayoutConfig,
) -> Double {
  let mut width = 0.0
  let mut prev_glyph_id = -1
  let mut glyph_count = 0

  for ch in word {
    let char_code = ch.to_int()
    let glyph_id = font.char_to_glyph(char_code)
    let (advance, _) = font.get_h_metrics(glyph_id)

    // Apply kerning
    if prev_glyph_id >= 0 {
      let kern = font.get_kerning(prev_glyph_id, glyph_id)
      width = width + kern.to_double() * scale
    }

    width = width + advance.to_double() * scale
    prev_glyph_id = glyph_id
    glyph_count = glyph_count + 1
  }

  if glyph_count > 1 {
    width = width + config.letter_spacing * (glyph_count - 1).to_double()
  }
  if is_space_token(word) {
    width = width + config.word_spacing
  }

  width
}

///|
/// Shape a word into positioned glyphs with kerning
fn shape_word(
  font : Font,
  word : String,
  scale : Double,
  start_x : Double,
  config : LayoutConfig,
) -> Array[PositionedGlyph] {
  let glyphs = []
  let mut cursor_x = start_x
  let mut prev_glyph_id = -1
  let mut glyph_index = 0
  let glyph_count = count_glyphs(word)
  let is_space = is_space_token(word)

  for ch in word {
    let char_code = ch.to_int()
    let glyph_id = font.char_to_glyph(char_code)
    let (advance, lsb) = font.get_h_metrics(glyph_id)
    let mut advance_width = advance.to_double() * scale
    if is_space {
      advance_width = advance_width + config.word_spacing
    } else if glyph_index < glyph_count - 1 {
      advance_width = advance_width + config.letter_spacing
    }

    // Apply kerning
    if prev_glyph_id >= 0 {
      let kern = font.get_kerning(prev_glyph_id, glyph_id)
      cursor_x = cursor_x + kern.to_double() * scale
    }

    glyphs.push({
      glyph_id,
      origin_x: cursor_x,
      x: cursor_x + lsb.to_double() * scale,
      y: 0.0, // Will be set by line layout
      advance_width,
    })

    cursor_x = cursor_x + advance_width
    prev_glyph_id = glyph_id
    glyph_index = glyph_index + 1
  }

  glyphs
}

///|
fn is_space_token(word : String) -> Bool {
  word.length() is 1 && word.get_char(0) == Some(' ')
}

///|
fn count_glyphs(word : String) -> Int {
  let mut count = 0
  for _ in word {
    count = count + 1
  }
  count
}