///|
/// Shared table cell layout (ADR 0001 companion to the soft-wrap layer).
///
/// Table cells historically drew one unwrapped `DrawText` per cell, so text
/// longer than the column was simply clipped at the cell edge. This layout
/// wraps every cell's text at the cell's inner width, derives per-row heights
/// from the tallest wrapped cell, and exposes the per-line char ranges that
/// paint, hit-testing, caret rects, and selection all consume — keeping the
/// drawn table and its interaction geometry in sync.

///|
/// One visual line of a table cell. `start`/`end` are char offsets into
/// `RichTextTableCell.text`; `width` is the laid-out line width.
priv struct RichTextTableCellLine {
  start : Int
  end : Int
  width : Double
  text : String
}

///|
/// One styled segment of a cell line: a run's visible text placed on a
/// line at an x offset. `start`/`end` are offsets into the CELL's raw
/// `text` covering the segment's visible characters (markers excluded),
/// `source_start`/`source_end` the run's full construct range including
/// markers — caret/hit map through the former, reveal logic keys the
/// latter.
priv struct RichTextTableCellSegment {
  run : RichTextRun
  line : Int
  start : Int
  end : Int
  x : Double
  width : Double
  text : String
}

///|
priv struct RichTextTableCellLayout {
  lines : Array[RichTextTableCellLine]
  segments : Array[RichTextTableCellSegment]
  font : @core.FontSpec
}

///|
priv struct RichTextTableRowLayout {
  cells : Array[RichTextTableCellLayout]
  height : Double
}

///|
priv struct RichTextTableLayout {
  rows : Array[RichTextTableRowLayout]
  column_width : Double
  cell_inner_width : Double
  line_step : Double
}

///|
/// Height of every row above `row_index`, so a row's top y can be derived
/// without assuming the historical uniform `table.row_height`.
fn RichTextTableLayout::row_top(
  self : RichTextTableLayout,
  row_index : Int,
) -> Double {
  let mut y = 0.0
  for index, row in self.rows {
    if index >= row_index {
      break
    }
    y = y + row.height
  }
  y
}

///|
fn RichTextTableLayout::total_height(self : RichTextTableLayout) -> Double {
  let mut y = 0.0
  for row in self.rows {
    y = y + row.height
  }
  y
}

///|
fn rich_text_table_cell_font(
  table : RichTextTable,
  block : RichTextBlock,
  base_font : @core.FontSpec,
  row_index~ : Int,
  column_index~ : Int,
) -> @core.FontSpec {
  let header = match table.rows.get(row_index) {
    Some(row) =>
      match row.get(column_index) {
        Some(cell) => cell.header
        None => false
      }
    None => false
  }
  {
    ..rich_text_block_font(block, base_font),
    weight: if header {
      700
    } else {
      400
    },
  }
}

///|
fn rich_text_table_layout(
  table : RichTextTable,
  block : RichTextBlock,
  base_font : @core.FontSpec,
  frame_width~ : Double,
  text_system~ : @core.TextSystem,
) -> RichTextTableLayout {
  let column_count = rich_text_table_column_count(table)
  let column_width = if column_count > 0 {
    max_double(0.0, frame_width / column_count.to_double())
  } else {
    0.0
  }
  let cell_inner_width = max_double(
    1.0,
    column_width - table.cell_padding.left - table.cell_padding.right,
  )
  let line_step = max_double(
    1.0,
    table.row_height - table.cell_padding.top - table.cell_padding.bottom,
  )
  let rows : Array[RichTextTableRowLayout] = []
  for row_index, row in table.rows {
    let cells : Array[RichTextTableCellLayout] = []
    let mut line_count = 1
    for column_index, cell in row {
      let font = rich_text_table_cell_font(
        table,
        block,
        base_font,
        row_index~,
        column_index~,
      )
      // Styled cells lay out per-run segments (markers hidden); plain cells
      // keep the historical single-font paragraph layout.
      if cell.runs.length() > 0 {
        let styled = rich_text_table_cell_segments_layout(
          cell,
          font,
          cell_inner_width~,
          text_system~,
        )
        line_count = @core.max_int(line_count, styled.line_count)
        cells.push(styled.layout)
      } else {
        let chars = cell.text.to_array()
        let layout = text_system.layout_paragraph(
          @core.TextLayoutInput::new(
            text=cell.text,
            font~,
            max_width=cell_inner_width,
          ),
        )
        let lines : Array[RichTextTableCellLine] = []
        for line in layout.lines {
          let start = @core.clamp_int(line.text_range.start, 0, chars.length())
          let end = @core.clamp_int(line.text_range.end, start, chars.length())
          let text = String::from_array(chars[start:end])
          lines.push({
            start,
            end,
            width: rich_text_width(text, font, text_system),
            text,
          })
          line_count = @core.max_int(line_count, lines.length())
        }
        cells.push({ lines, segments: [], font, })
      }
    }
    rows.push({
      cells,
      height: table.row_height + (line_count - 1).to_double() * line_step,
    })
  }
  { rows, column_width, cell_inner_width, line_step, }
}

///|
priv struct RichTextTableCellSegmentsLayout {
  layout : RichTextTableCellLayout
  line_count : Int
}

///|
/// Layout a styled cell: walk the runs' visible text in order, wrapping at
/// the cell's inner width, and record each placed segment with its x offset
/// and the cell-relative char range of its VISIBLE characters (markers
/// occupy source chars but no width). Lines are derived for caret/hit's
/// y-band math: each line covers the source span of whatever it holds.
fn rich_text_table_cell_segments_layout(
  cell : RichTextTableCell,
  font : @core.FontSpec,
  cell_inner_width~ : Double,
  text_system~ : @core.TextSystem,
) -> RichTextTableCellSegmentsLayout {
  let segments : Array[RichTextTableCellSegment] = []
  let lines : Array[RichTextTableCellLine] = []
  let chars = cell.text.to_array()
  let mut x = 0.0
  let mut cursor = 0
  for run in cell.runs {
    let run_font = run.font.unwrap_or(font)
    let visible = rich_text_run_visual_text(run)
    let visible_chars = visible.to_array()
    // The run's source range within the cell (markers included); when absent
    // the visible text cannot be located and the run still paints at the
    // cursor (offsets only advance by its visible length).
    let range = match run.source_range {
      Some(range) => range
      None => { start: cursor, end: cursor, }
    }
    let mut index = 0
    while index < visible_chars.length() {
      let remaining_width = max_double(0.0, cell_inner_width - x)
      let piece = String::from_array(visible_chars[index:])
      let width = rich_text_width(piece, run_font, text_system)
      if width <= remaining_width || x <= 0.0 {
        // Fits (or the segment alone overflows the cell: place it anyway).
        let end = @core.min_int(
          index + visible_chars.length(),
          visible_chars.length(),
        )
        let segment_text = if index == 0 {
          visible
        } else {
          String::from_array(visible_chars[index:])
        }
        segments.push({
          run,
          line: lines.length(),
          start: range.start + index,
          end: range.start + end,
          x,
          width,
          text: segment_text,
        })
        x = x + width
        index = visible_chars.length()
        cursor = range.end
      } else {
        // Wrap: start a new line at the next cluster boundary that fits.
        // Cells are usually short; measuring cluster-wise keeps it simple.
        let mut taken = index
        let mut taken_width = 0.0
        while taken < visible_chars.length() {
          let probe_width = rich_text_width(
            String::from_array(visible_chars[index:taken + 1]),
            run_font,
            text_system,
          )
          if taken_width + probe_width > remaining_width && taken > index {
            break
          }
          taken_width = taken_width + probe_width
          taken = taken + 1
        }
        let line_text = String::from_array(visible_chars[index:taken])
        let line_width = rich_text_width(line_text, run_font, text_system)
        segments.push({
          run,
          line: lines.length(),
          start: range.start + index,
          end: range.start + taken,
          x,
          width: line_width,
          text: line_text,
        })
        lines.push({
          start: range.start + index,
          end: range.end,
          width: cell_inner_width,
          text: line_text,
        })
        x = 0.0
        index = taken
        cursor = range.end
      }
    }
  }
  // One trailing line record so empty cells and the last line have a band.
  if lines.is_empty() {
    lines.push({ start: 0, end: chars.length(), width: 0.0, text: "", })
  } else {
    let last = lines[lines.length() - 1]
    let next_start = last.end
    if next_start < chars.length() {
      lines.push({
        start: next_start,
        end: chars.length(),
        width: 0.0,
        text: "",
      })
    }
  }
  { layout: { lines, segments, font, }, line_count: lines.length(), }
}