///|
/// Vector markers for block prefix lanes.
///
/// A task list item's marker is geometry, not text: the box, its border, and
/// the checkmark are drawn from `DrawCommand`s so the shape, weight, and color
/// are identical on every host and font. The lane still holds one placeholder
/// character, so caret, selection, and hit-test arithmetic stay
/// character-indexed; only the painted and measured lane width changes.

///|
/// Side of the marker square, in ems of the block font.
fn rich_text_marker_box_em() -> Double {
  1.0
}

///|
/// Painted width of a block's prefix lane. Marker blocks reserve the square
/// plus the label gap instead of the placeholder glyph's advance, so the label
/// column is the same on every font.
fn rich_text_block_marker_lane(
  block : RichTextBlock,
  font : @core.FontSpec,
  text_system : @core.TextSystem,
) -> Double {
  match block.marker {
    Some(_) => font.size * 1.55
    None => rich_text_width(block.prefix, font, text_system)
  }
}

///|
/// Draw the marker inside `lane`, a rect whose origin is the prefix lane start
/// and whose height is the first visual line box. Text paints its first line
/// centered in that box (`skia_text_baseline_y`), so the marker centers on the
/// same line to sit optically with the label.
fn append_rich_text_marker_commands(
  commands : Array[@core.DrawCommand],
  marker : RichTextMarker,
  lane : @core.Rect,
  font : @core.FontSpec,
) -> Unit {
  match marker {
    TaskCheckbox(checked_) => {
      let side = font.size * rich_text_marker_box_em()
      let box = @core.Rect::new(
        x=lane.origin.x,
        y=lane.origin.y + (lane.size.height - side) / 2.0,
        width=side,
        height=side,
      )
      match checked_ {
        true => {
          let rounded = @core.RoundedRect::new(
            rect=box,
            radius=rich_text_marker_corner_radius(side),
          )
          commands.push(
            FillRoundedRect(rounded, rich_text_marker_accent_color()),
          )
          commands.push(DrawPath(rich_text_marker_checkmark_path(box, side)))
        }
        false => {
          // Inset by half the stroke so the outline stays inside the square.
          let stroke = rich_text_marker_border_width(side)
          let rounded = @core.RoundedRect::new(
            rect=@core.Rect::new(
              x=box.origin.x + stroke / 2.0,
              y=box.origin.y + stroke / 2.0,
              width=box.size.width - stroke,
              height=box.size.height - stroke,
            ),
            radius=rich_text_marker_corner_radius(side),
          )
          commands.push(
            StrokeRoundedRect(rounded, rich_text_marker_outline_color(), stroke),
          )
        }
      }
    }
  }
}

///|
/// Marker fill for the checked state. GitHub's accent blue, so the box reads
/// as the same control the app's own checkbox uses.
fn rich_text_marker_accent_color() -> @core.Color {
  @core.Color::rgba(r=0.122, g=0.435, b=0.922)
}

///|
/// Marker border for the unchecked state: a neutral gray that reads as a solid
/// empty slot rather than as faint body text.
fn rich_text_marker_outline_color() -> @core.Color {
  @core.Color::rgba(r=0.573, g=0.600, b=0.631)
}

///|
/// Box corner radius. Proportional to the square so every editor font size and
/// appearance (16.5pt to 18pt) keeps the same pill-ish silhouette.
fn rich_text_marker_corner_radius(side : Double) -> Double {
  side * 0.22
}

///|
/// Unchecked outline weight.
fn rich_text_marker_border_width(side : Double) -> Double {
  side * 0.11
}

///|
/// Filled checkmark polygon.
///
/// The mark is a filled six-point outline rather than a stroked polyline: the
/// path contract has no cap/join fields, so a stroke would render with butt
/// caps and a miter spike on some backends and a round join on others. Filling
/// the outline of the two strokes (plus the outer miter vertex at the elbow)
/// draws the same chunky, slightly rounded mark everywhere.
fn rich_text_marker_checkmark_path(
  box : @core.Rect,
  side : Double,
) -> @core.PathSpec {
  // Mark geometry in box-relative units: down-stroke to the elbow, then a
  // longer up-stroke to the tip.
  let start = @core.Point::new(
    x=box.origin.x + side * 0.25,
    y=box.origin.y + side * 0.53,
  )
  let elbow = @core.Point::new(
    x=box.origin.x + side * 0.43,
    y=box.origin.y + side * 0.70,
  )
  let tip = @core.Point::new(
    x=box.origin.x + side * 0.76,
    y=box.origin.y + side * 0.32,
  )
  let half = side * 0.085
  let (down_x, down_y) = rich_text_marker_unit_normal(start, elbow)
  let (up_x, up_y) = rich_text_marker_unit_normal(elbow, tip)
  let outer_start = @core.Point::new(
    x=start.x + down_x * half,
    y=start.y + down_y * half,
  )
  let outer_elbow = @core.Point::new(
    x=elbow.x + down_x * half,
    y=elbow.y + down_y * half,
  )
  let inner_elbow = @core.Point::new(
    x=elbow.x - down_x * half,
    y=elbow.y - down_y * half,
  )
  let inner_start = @core.Point::new(
    x=start.x - down_x * half,
    y=start.y - down_y * half,
  )
  let inner_tip = @core.Point::new(x=tip.x - up_x * half, y=tip.y - up_y * half)
  let outer_tip = @core.Point::new(x=tip.x + up_x * half, y=tip.y + up_y * half)
  let outer_join = @core.Point::new(
    x=elbow.x + up_x * half,
    y=elbow.y + up_y * half,
  )
  // Outer corner of the elbow: the two offset edges meet here, so the outline
  // needs this vertex or the elbow loses its point.
  let miter = @core.Point::new(
    x=(outer_elbow.x + outer_join.x) / 2.0 + up_x * half * 0.6,
    y=(outer_elbow.y + outer_join.y) / 2.0 + up_y * half * 0.6,
  )
  @core.PathSpec::new(
    verbs=[
      MoveTo(outer_start),
      LineTo(miter),
      LineTo(outer_tip),
      LineTo(inner_tip),
      LineTo(inner_elbow),
      LineTo(inner_start),
      Close,
    ],
    fill=Some(@core.Brush::solid(@core.Color::white())),
  )
}

///|
/// Unit normal of the segment `from` -> `to`, pointing away from the segment's
/// travel direction (used on both sides of the stroke by offsetting +/-).
fn rich_text_marker_unit_normal(
  from : @core.Point,
  to : @core.Point,
) -> (Double, Double) {
  let dx = to.x - from.x
  let dy = to.y - from.y
  let length = (dx * dx + dy * dy).sqrt()
  if length <= 0.0 {
    (0.0, 0.0)
  } else {
    (-dy / length, dx / length)
  }
}