///|
/// The Metal frame's wire format: every number the host reads back out of a
/// frame buffer, and the code values the MSL source interprets.
///
/// One 76-byte vertex (19 float32), 9 int32 per item, 36 floats per shape
/// record, 11 words per layer record, all little-endian and all written here —
/// `metal_renderer.mm` reads the same layout, so the two files are one contract
/// and a constant that changes in only one of them is a mis-draw, not a
/// compile error. That is why the mode, mask, filter-kind, shader-effect and
/// blend codes are named constants instead of literals at each call site.
///
/// The brush payload lives here too. It is the one place where a colour is
/// flattened for the shader: a solid brush becomes a degenerate linear gradient
/// whose endpoints share a colour, and a box's gradient endpoints move into the
/// space the box SDF measures itself from, because that space is not the space a
/// path mesh carries.

///|
let metal_vertex_bytes : Int32 = 76

///|
/// Fragment modes, matching `moui_wgpu_renderer/native_visual_shader.mbt` so the
/// two diagnostic renderers agree on what each shape means.
let metal_mode_fill : Double = 0.0

///|
let metal_mode_stroke : Double = 1.0

///|
let metal_mode_shadow : Double = 2.0

///|
let metal_mode_path : Double = 3.0

///|
let metal_mode_composite : Double = 4.0

///|
/// Brush kinds as the fragment shader reads them: a linear gradient runs along
/// `start`→`end`, a radial one reads `start` as its centre and `end.x` as a
/// radius rather than a second point.
let metal_brush_linear : Double = 0.0

///|
let metal_brush_radial : Double = 1.0

///|
/// Layer mask kinds as the composite shader reads them.
let metal_mask_none : Double = 0.0

///|
let metal_mask_rounded : Double = 1.0

///|
/// Filter kinds as the composite shader reads them; `metal_blend_code` and these
/// share the numbering of `moui_wgpu_renderer/native_visual_advanced.mbt`.
let metal_filter_none : Double = 0.0

///|
let metal_filter_blur : Double = 1.0

///|
let metal_filter_saturate : Double = 2.0

///|
let metal_filter_brightness : Double = 3.0

///|
let metal_filter_contrast : Double = 4.0

///|
let metal_filter_matrix : Double = 5.0

///|
/// This route's own separable blur stages. `moui_wgpu_renderer` numbers no stage
/// kinds -- it convolves in one pass -- so these extend the shared numbering above
/// rather than sharing it: the horizontal pass writes the intermediate target and
/// the vertical pass is what the enclosing composite reads.
let metal_filter_blur_x : Double = 6.0

///|
let metal_filter_blur_y : Double = 7.0

///|
/// Shader-effect kinds the visual fragment draws per fragment, sharing the
/// numbering of `moui_wgpu_renderer/native_shader_kind`. `metal_effect_none` is
/// "no effect": the item is painted with its brush as usual, which is what an
/// effect name this route has no formulation for falls back to.
let metal_effect_none : Double = 0.0

///|
let metal_effect_checker : Double = 1.0

///|
let metal_effect_solid : Double = 2.0

///|
let metal_effect_gradient_debug : Double = 3.0

///|
let metal_effect_vignette : Double = 4.0

///|
/// Blend modes this route can express as a fixed-function Metal blend state.
/// `@core.Overlay` is not: it needs a snapshot of the backdrop, which Metal
/// cannot sample while rendering into it, so it is counted, not approximated.
let metal_blend_source_over : Int32 = 0

///|
let metal_blend_multiply : Int32 = 1

///|
let metal_blend_screen : Int32 = 2

///|
let metal_blend_darken : Int32 = 4

///|
let metal_blend_lighten : Int32 = 5

///|
priv struct MetalBrushPayload {
  kind : Double
  start : @core.Point
  end : @core.Point
  color0 : @core.Color
  color1 : @core.Color
}

///|
/// The per-item parameter blob as built by the CPU side.
priv struct MetalShapePayload {
  width : Double
  height : Double
  radius : Double
  mode : Double
  stroke_width : Double
  blur_radius : Double
  effect_kind : Double
  effect_amount : Double
  layer_opacity : Double
  mask_kind : Double
  mask_radius : Double
  filter_kind : Double
  filter_amount : Double
  texel_x : Double
  texel_y : Double
  matrix : Array[Double]
}

///|
/// One offscreen pass. `device_*` is the padded device-pixel target the layer
/// clip resolves to; it is also the origin the pass transform and the composite
/// uv are derived from, so the layer content and the quad that draws it land on
/// the same texels.
priv struct MetalLayerRecord {
  device_x : Int32
  device_y : Int32
  device_width : Int32
  device_height : Int32
  child_first : Int32
  child_count : Int32
  blend_mode : Int32
  pass_scale_x : Double
  pass_scale_y : Double
  pass_offset_x : Double
  pass_offset_y : Double
}

///|
fn metal_box_shape(
  width : Double,
  height : Double,
  radius : Double,
  mode : Double,
  stroke_width~ : Double = 0.0,
  blur_radius~ : Double = 0.0,
  effect_kind~ : Double = metal_effect_none,
  effect_amount~ : Double = 0.0,
) -> MetalShapePayload {
  MetalShapePayload::{
    width,
    height,
    radius,
    mode,
    stroke_width,
    blur_radius,
    effect_kind,
    effect_amount,
    layer_opacity: 1.0,
    mask_kind: metal_mask_none,
    mask_radius: 0.0,
    filter_kind: metal_filter_none,
    filter_amount: 0.0,
    texel_x: 0.0,
    texel_y: 0.0,
    matrix: metal_identity_color_matrix(),
  }
}

///|
fn metal_identity_color_matrix() -> Array[Double] {
  // Row-major with each row's translation last, because that is the order
  // `metal_color_matrix` copies an app's CSS values into and the order
  // `moui_matrix_filter` reads: `m[0..4]` is the red row, `m[15..19]` the alpha
  // row. The array `moui_wgpu_renderer` carries has the same numbers but means
  // something else there -- its shader indexes a `mat4x5f` by column -- so
  // copying it here made the "identity" map every channel onto red.
  [
    1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
  ]
}

///|
fn metal_color_matrix(values : Array[Double]) -> Array[Double] {
  let matrix = metal_identity_color_matrix()
  let count = values.length().min(20)
  for index in 0.. MetalFilterPayload {
  match filter {
    @core.FilterEffect::Blur(radius) =>
      MetalFilterPayload::{
        kind: metal_filter_blur,
        amount: radius.max(0.0),
        matrix: metal_identity_color_matrix(),
      }
    @core.FilterEffect::Saturate(amount) =>
      MetalFilterPayload::{
        kind: metal_filter_saturate,
        amount,
        matrix: metal_identity_color_matrix(),
      }
    Brightness(amount) =>
      MetalFilterPayload::{
        kind: metal_filter_brightness,
        amount,
        matrix: metal_identity_color_matrix(),
      }
    Contrast(amount) =>
      MetalFilterPayload::{
        kind: metal_filter_contrast,
        amount,
        matrix: metal_identity_color_matrix(),
      }
    ColorMatrix(values) =>
      MetalFilterPayload::{
        kind: metal_filter_matrix,
        amount: 1.0,
        matrix: metal_color_matrix(values),
      }
  }
}

///|
fn metal_unknown_filter_payload() -> MetalFilterPayload {
  MetalFilterPayload::{
    kind: metal_filter_none,
    amount: 0.0,
    matrix: metal_identity_color_matrix(),
  }
}

///|
/// The four names `builtin_shader_names()` declares, mapped to the codes the
/// visual fragment reads. A name outside that list is `metal_effect_none`: the
/// item then paints the brush the spec carries as its fallback, which is the
/// degradation the spec was written to allow.
fn metal_shader_effect_kind(name : String) -> Double {
  match name {
    "checker" => metal_effect_checker
    "solid" => metal_effect_solid
    "linear-gradient-debug" => metal_effect_gradient_debug
    "vignette" => metal_effect_vignette
    _ => metal_effect_none
  }
}

///|
/// The one uniform the builtins read, defaulted per effect the way the mainline
/// routes default it — 8 cells for a checker, 0.8 of the frame radius for a
/// vignette. The per-effect floors (`max(amount, 1.0)`, `max(amount, 0.8)`) are
/// applied in the fragment, as `SkiaRasterRenderer::draw_checker_effect` and
/// `draw_vignette_effect` apply them.
fn metal_shader_effect_amount(
  kind : Double,
  uniforms : Array[Double],
) -> Double {
  match uniforms.get(0) {
    Some(value) => value
    None => if kind == metal_effect_vignette { 0.8 } else { 8.0 }
  }
}

///|
fn metal_blend_code(mode : @core.BlendMode) -> Int32 {
  match mode {
    @core.BlendMode::SourceOver => metal_blend_source_over
    Multiply => metal_blend_multiply
    Screen => metal_blend_screen
    Darken => metal_blend_darken
    Lighten => metal_blend_lighten
    Overlay => -1
  }
}

///|
/// A brush flattened onto the vertex payload. A solid brush becomes a
/// degenerate linear gradient with both endpoints the same colour, which the
/// shader's `mix` resolves to that colour for every fragment.
fn metal_brush_payload(
  brush : @core.Brush,
  opacity : Double,
) -> MetalBrushPayload {
  match brush {
    @core.Solid(color) =>
      MetalBrushPayload::{
        kind: metal_brush_linear,
        start: @core.Point::new(x=0.0, y=0.0),
        end: @core.Point::new(x=1.0, y=0.0),
        color0: color.multiply_alpha(opacity),
        color1: color.multiply_alpha(opacity),
      }
    @core.LinearGradient(gradient) =>
      MetalBrushPayload::{
        kind: metal_brush_linear,
        start: gradient.start,
        end: gradient.end,
        color0: gradient.start_color.multiply_alpha(opacity),
        color1: gradient.end_color.multiply_alpha(opacity),
      }
    @core.RadialGradient(gradient) =>
      MetalBrushPayload::{
        kind: metal_brush_radial,
        start: gradient.center,
        end: @core.Point::new(x=gradient.radius.max(1.0), y=0.0),
        color0: gradient.center_color.multiply_alpha(opacity),
        color1: gradient.edge_color.multiply_alpha(opacity),
      }
  }
}

///|
/// Move a payload into the space a box fragment measures itself from. The box
/// SDF reads `local` relative to the rect origin, while a path mesh carries
/// absolute positions, so a gradient on a rect that does not start at the
/// surface origin has to be shifted by that origin or its axis lands offset.
/// A radial `end.x` is a radius, not a point, so it stays put.
fn metal_box_brush(
  payload : MetalBrushPayload,
  origin : @core.Point,
) -> MetalBrushPayload {
  let end = if payload.kind == metal_brush_radial {
    payload.end
  } else {
    @core.Point::new(x=payload.end.x - origin.x, y=payload.end.y - origin.y)
  }
  MetalBrushPayload::{
    ..payload,
    start: @core.Point::new(
      x=payload.start.x - origin.x,
      y=payload.start.y - origin.y,
    ),
    end,
  }
}

///|
let metal_pipeline_visual : Int32 = 0

///|
let metal_pipeline_text : Int32 = 1

///|
let metal_pipeline_image : Int32 = 2

///|
/// A layer composite: `texture_key` names the offscreen pass slot to sample and
/// the shape blob carries blend, mask and filter parameters.
let metal_pipeline_composite : Int32 = 3

///|
/// Push one draw: an item plus its parameter blob. They travel together because
/// the host indexes `shapes[item]` in lockstep with `items`; an item skipped for
/// an empty scissor must not leave a shape behind, or every later item would read
/// the wrong radius.
fn push_metal_draw(
  geometry : MetalFrameGeometry,
  start : Int32,
  clip : @core.Rect,
  pipeline : Int32,
  shape : MetalShapePayload,
  texture_key~ : Int32 = 0,
) -> Unit {
  let count = geometry.vertex_count - start
  let (x, y, width, height) = metal_scissor(
    clip,
    geometry.size,
    geometry.scale_factor,
  )
  if width <= 0 || height <= 0 || count <= 0 {
    geometry.clipped_away_ops = geometry.clipped_away_ops + 1
    truncate_metal_vertices(geometry, start)
    return
  }
  push_metal_i32le(geometry.items, start)
  push_metal_i32le(geometry.items, count)
  push_metal_i32le(geometry.items, x)
  push_metal_i32le(geometry.items, y)
  push_metal_i32le(geometry.items, width)
  push_metal_i32le(geometry.items, height)
  push_metal_i32le(geometry.items, pipeline)
  push_metal_i32le(geometry.items, texture_key)
  push_metal_i32le(geometry.items, geometry.owner)
  push_metal_shape(geometry.shapes, shape)
  geometry.item_count = geometry.item_count + 1
  geometry.shape_count = geometry.shape_count + 1
}

///|
/// `MOUIMetalShape`: four floats per row, the visual fields first so the L1
/// shaders keep reading only the two rows they know.
fn push_metal_shape(out : Array[Byte], shape : MetalShapePayload) -> Unit {
  push_metal_f32le(out, shape.width)
  push_metal_f32le(out, shape.height)
  push_metal_f32le(out, shape.radius)
  push_metal_f32le(out, shape.mode)
  push_metal_f32le(out, shape.stroke_width)
  push_metal_f32le(out, shape.blur_radius)
  push_metal_f32le(out, shape.effect_kind)
  push_metal_f32le(out, shape.effect_amount)
  push_metal_f32le(out, shape.layer_opacity)
  push_metal_f32le(out, shape.mask_kind)
  push_metal_f32le(out, shape.mask_radius)
  push_metal_f32le(out, shape.filter_kind)
  push_metal_f32le(out, shape.filter_amount)
  push_metal_f32le(out, shape.texel_x)
  push_metal_f32le(out, shape.texel_y)
  push_metal_f32le(out, 0.0)
  for value in shape.matrix {
    push_metal_f32le(out, value)
  }
  for _ in shape.matrix.length()..<20 {
    push_metal_f32le(out, 0.0)
  }
}

///|
fn truncate_metal_vertices(geometry : MetalFrameGeometry, keep : Int32) -> Unit {
  let keep_bytes = keep * metal_vertex_bytes
  while geometry.vertices.length() > keep_bytes {
    ignore(geometry.vertices.pop())
  }
  geometry.vertex_count = keep
}

///|
fn push_metal_uv_vertex(
  geometry : MetalFrameGeometry,
  x : Double,
  y : Double,
  local_x : Double,
  local_y : Double,
  u : Double,
  v : Double,
  colour : @core.Color,
) -> Unit {
  push_metal_vertex(
    geometry,
    x,
    y,
    local_x,
    local_y,
    u,
    v,
    0.0,
    0.0,
    1.0,
    0.0,
    colour.r,
    colour.g,
    colour.b,
    colour.a,
    colour.r,
    colour.g,
    colour.b,
    colour.a,
    0.0,
  )
}

///|
fn push_metal_brush_vertex(
  geometry : MetalFrameGeometry,
  x : Double,
  y : Double,
  local_x : Double,
  local_y : Double,
  brush : MetalBrushPayload,
) -> Unit {
  push_metal_brush_vertex_uv(geometry, x, y, local_x, local_y, 0.0, 0.0, brush)
}

///|
fn push_metal_brush_vertex_uv(
  geometry : MetalFrameGeometry,
  x : Double,
  y : Double,
  local_x : Double,
  local_y : Double,
  u : Double,
  v : Double,
  brush : MetalBrushPayload,
) -> Unit {
  push_metal_vertex(
    geometry,
    x,
    y,
    local_x,
    local_y,
    u,
    v,
    brush.start.x,
    brush.start.y,
    brush.end.x,
    brush.end.y,
    brush.color0.r,
    brush.color0.g,
    brush.color0.b,
    brush.color0.a,
    brush.color1.r,
    brush.color1.g,
    brush.color1.b,
    brush.color1.a,
    brush.kind,
  )
}

///|
/// One 76-byte vertex: clip-space position, shape-local point, atlas uv,
/// gradient endpoints, the two brush colours and the brush kind. Colours ride
/// per vertex rather than per item because the shared path mesh mixes brushes.
fn push_metal_vertex(
  geometry : MetalFrameGeometry,
  x : Double,
  y : Double,
  local_x : Double,
  local_y : Double,
  u : Double,
  v : Double,
  start_x : Double,
  start_y : Double,
  end_x : Double,
  end_y : Double,
  red0 : Double,
  green0 : Double,
  blue0 : Double,
  alpha0 : Double,
  red1 : Double,
  green1 : Double,
  blue1 : Double,
  alpha1 : Double,
  kind : Double,
) -> Unit {
  push_metal_f32le(geometry.vertices, pixel_x_to_ndc(x, geometry.size.width))
  push_metal_f32le(geometry.vertices, pixel_y_to_ndc(y, geometry.size.height))
  push_metal_f32le(geometry.vertices, local_x)
  push_metal_f32le(geometry.vertices, local_y)
  push_metal_f32le(geometry.vertices, u)
  push_metal_f32le(geometry.vertices, v)
  push_metal_f32le(geometry.vertices, start_x)
  push_metal_f32le(geometry.vertices, start_y)
  push_metal_f32le(geometry.vertices, end_x)
  push_metal_f32le(geometry.vertices, end_y)
  push_metal_f32le(geometry.vertices, red0)
  push_metal_f32le(geometry.vertices, green0)
  push_metal_f32le(geometry.vertices, blue0)
  push_metal_f32le(geometry.vertices, alpha0)
  push_metal_f32le(geometry.vertices, red1)
  push_metal_f32le(geometry.vertices, green1)
  push_metal_f32le(geometry.vertices, blue1)
  push_metal_f32le(geometry.vertices, alpha1)
  push_metal_f32le(geometry.vertices, kind)
  geometry.vertex_count = geometry.vertex_count + 1
}

///|
fn metal_scissor(
  rect : @core.Rect,
  size : @core.Size,
  scale_factor : Double,
) -> (Int32, Int32, Int32, Int32) {
  let scale = if scale_factor > 0.0 { scale_factor } else { 1.0 }
  let limit_x = size.width.max(0.0)
  let limit_y = size.height.max(0.0)
  let x0 = rect.origin.x.max(0.0).min(limit_x)
  let y0 = rect.origin.y.max(0.0).min(limit_y)
  let x1 = (rect.origin.x + rect.size.width).max(0.0).min(limit_x)
  let y1 = (rect.origin.y + rect.size.height).max(0.0).min(limit_y)
  (
    (x0 * scale).floor().to_int(),
    (y0 * scale).floor().to_int(),
    ((x1 - x0) * scale).ceil().max(0.0).to_int(),
    ((y1 - y0) * scale).ceil().max(0.0).to_int(),
  )
}

///|
fn transform_metal_point(
  transform : @core.Transform2D,
  x : Double,
  y : Double,
) -> @core.Point {
  @core.Point::new(
    x=transform.a * x + transform.c * y + transform.tx,
    y=transform.b * x + transform.d * y + transform.ty,
  )
}

///|
/// The axis-aligned box a transform puts a rect into. Culling has to read this
/// rather than the source rect: an op whose transform carries it into a layer's
/// masked frame is inside that frame, and a route that tests the untransformed
/// rect drops it before a single vertex exists.
fn metal_transformed_bounds(
  rect : @core.Rect,
  transform : @core.Transform2D,
) -> @core.Rect {
  let corners = [
    transform_metal_point(transform, rect.origin.x, rect.origin.y),
    transform_metal_point(
      transform,
      rect.origin.x + rect.size.width,
      rect.origin.y,
    ),
    transform_metal_point(
      transform,
      rect.origin.x,
      rect.origin.y + rect.size.height,
    ),
    transform_metal_point(
      transform,
      rect.origin.x + rect.size.width,
      rect.origin.y + rect.size.height,
    ),
  ]
  let mut min_x = corners[0].x
  let mut min_y = corners[0].y
  let mut max_x = corners[0].x
  let mut max_y = corners[0].y
  for point in corners[1:] {
    min_x = min_x.min(point.x)
    min_y = min_y.min(point.y)
    max_x = max_x.max(point.x)
    max_y = max_y.max(point.y)
  }
  @core.Rect::new(x=min_x, y=min_y, width=max_x - min_x, height=max_y - min_y)
}

///|
fn multiply_metal_transform(
  outer : @core.Transform2D,
  inner : @core.Transform2D,
) -> @core.Transform2D {
  @core.Transform2D::{
    a: outer.a * inner.a + outer.c * inner.b,
    b: outer.b * inner.a + outer.d * inner.b,
    c: outer.a * inner.c + outer.c * inner.d,
    d: outer.b * inner.c + outer.d * inner.d,
    tx: outer.a * inner.tx + outer.c * inner.ty + outer.tx,
    ty: outer.b * inner.tx + outer.d * inner.ty + outer.ty,
  }
}

///|
fn clamp_metal_unit(value : Double) -> Double {
  value.max(0.0).min(1.0)
}

///|
fn empty_metal_rect() -> @core.Rect {
  @core.Rect::new(x=0.0, y=0.0, width=0.0, height=0.0)
}

///|
fn pixel_x_to_ndc(x : Double, width : Double) -> Double {
  if width <= 0.0 {
    -1.0
  } else {
    x / width * 2.0 - 1.0
  }
}

///|
fn pixel_y_to_ndc(y : Double, height : Double) -> Double {
  if height <= 0.0 {
    1.0
  } else {
    1.0 - y / height * 2.0
  }
}

///|
fn push_metal_f32le(out : Array[Byte], value : Double) -> Unit {
  let bits : UInt32 = Float::from_double(value)
    .reinterpret_as_int()
    .reinterpret_as_uint()
  out.push((bits & 0xFF).to_byte())
  out.push(((bits >> 8) & 0xFF).to_byte())
  out.push(((bits >> 16) & 0xFF).to_byte())
  out.push(((bits >> 24) & 0xFF).to_byte())
}

///|
fn push_metal_i32le(out : Array[Byte], value : Int32) -> Unit {
  out.push((value & 0xFF).to_byte())
  out.push(((value >> 8) & 0xFF).to_byte())
  out.push(((value >> 16) & 0xFF).to_byte())
  out.push(((value >> 24) & 0xFF).to_byte())
}