///|
priv struct ImageVertices2D {
  vertices : Array[Double]
  sampler_code : Int
}

///|
fn pack_rect_fill_vertices2d(
  rect : @smath.Rect,
  transform : @smath.Transform,
  color : @render.Color,
) -> Array[Double] {
  let (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) = rect_points2d(rect, transform)
  let vertices : Array[Double] = []
  let (r, g, b, a) = color_rgba01(color)
  push_color_vertex2d(vertices, p0x, p0y, r, g, b, a)
  push_color_vertex2d(vertices, p1x, p1y, r, g, b, a)
  push_color_vertex2d(vertices, p2x, p2y, r, g, b, a)
  push_color_vertex2d(vertices, p0x, p0y, r, g, b, a)
  push_color_vertex2d(vertices, p2x, p2y, r, g, b, a)
  push_color_vertex2d(vertices, p3x, p3y, r, g, b, a)
  vertices
}

///|
fn pack_rect_stroke_vertices2d(
  rect : @smath.Rect,
  transform : @smath.Transform,
  color : @render.Color,
) -> Array[Double] {
  let (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) = rect_points2d(rect, transform)
  let vertices : Array[Double] = []
  let (r, g, b, a) = color_rgba01(color)
  push_color_vertex2d(vertices, p0x, p0y, r, g, b, a)
  push_color_vertex2d(vertices, p1x, p1y, r, g, b, a)
  push_color_vertex2d(vertices, p1x, p1y, r, g, b, a)
  push_color_vertex2d(vertices, p2x, p2y, r, g, b, a)
  push_color_vertex2d(vertices, p2x, p2y, r, g, b, a)
  push_color_vertex2d(vertices, p3x, p3y, r, g, b, a)
  push_color_vertex2d(vertices, p3x, p3y, r, g, b, a)
  push_color_vertex2d(vertices, p0x, p0y, r, g, b, a)
  vertices
}

///|
fn pack_gradient_rect_vertices2d(
  command : @render2d_types.GradientRectDrawCommand2D,
) -> Array[Double] {
  let vertices : Array[Double] = []
  for triangle in @render2d_types.tessellate_gradient_rect(command) {
    for point in [triangle.a, triangle.b, triangle.c] {
      let (r, g, b, a) = color_rgba01(point.color)
      push_color_vertex2d(
        vertices,
        point.position[X],
        point.position[Y],
        r,
        g,
        b,
        a,
      )
    }
  }
  vertices
}

///|
fn pack_rounded_chrome_vertices2d(
  command : @render2d_types.RoundedChromeDrawCommand2D,
) -> Array[Double] {
  let vertices : Array[Double] = []
  for triangle in @render2d_types.tessellate_rounded_chrome(command) {
    for point in [triangle.a, triangle.b, triangle.c] {
      let (r, g, b, a) = color_rgba01(point.color)
      push_color_vertex2d(
        vertices,
        point.position[X],
        point.position[Y],
        r,
        g,
        b,
        a,
      )
    }
  }
  vertices
}

///|
priv struct RoundedChromeVertexCacheEntry {
  cache_id : Int
  vertices : Array[Double]
  mut last_used : Int
}

///|
let rounded_chrome_vertex_cache : Map[
  @render2d_types.RoundedChromeDrawCommand2D,
  RoundedChromeVertexCacheEntry,
] = Map([])

///|
let rounded_chrome_vertex_cache_tick : Ref[Int] = { val: 0 }

///|
let rounded_chrome_vertex_cache_next_id : Ref[Int] = { val: 1 }

///|
fn cached_rounded_chrome_vertices2d(
  command : @render2d_types.RoundedChromeDrawCommand2D,
) -> RoundedChromeVertexCacheEntry {
  rounded_chrome_vertex_cache_tick.val += 1
  if rounded_chrome_vertex_cache.get(command) is Some(entry) {
    entry.last_used = rounded_chrome_vertex_cache_tick.val
    return entry
  }
  if rounded_chrome_vertex_cache.length() >= 4096 {
    let mut oldest_command : @render2d_types.RoundedChromeDrawCommand2D? = None
    let mut oldest_use = rounded_chrome_vertex_cache_tick.val
    for cached_command, entry in rounded_chrome_vertex_cache {
      if entry.last_used <= oldest_use {
        oldest_command = Some(cached_command)
        oldest_use = entry.last_used
      }
    }
    if oldest_command is Some(cached_command) {
      if rounded_chrome_vertex_cache.get(cached_command) is Some(entry) {
        webgpu_release_cached_color_vertices_2d(entry.cache_id)
      }
      rounded_chrome_vertex_cache.remove(cached_command)
    }
  }
  let entry = {
    cache_id: rounded_chrome_vertex_cache_next_id.val,
    vertices: pack_rounded_chrome_vertices2d(command),
    last_used: rounded_chrome_vertex_cache_tick.val,
  }
  rounded_chrome_vertex_cache_next_id.val += 1
  rounded_chrome_vertex_cache.set(command, entry)
  entry
}

///|
fn pack_colored_geometry_vertices2d(
  command : @render2d_types.ColoredGeometryDrawCommand2D,
) -> Array[Double] {
  let vertices : Array[Double] = []
  for triangle in command.triangles {
    for point in [triangle.a, triangle.b, triangle.c] {
      let (r, g, b, a) = color_rgba01(point.color)
      push_color_vertex2d(
        vertices,
        point.position[X],
        point.position[Y],
        r,
        g,
        b,
        a,
      )
    }
  }
  vertices
}

///|
fn pack_image_vertices2d(
  command : @render2d_types.ImageDrawCommand2D,
  image_width : Double,
  image_height : Double,
) -> ImageVertices2D {
  pack_image_vertices2d_raw(
    command.destination,
    command.transform,
    command.source,
    command.uv_transform,
    command.repeat,
    command.color,
    image_width,
    image_height,
  )
}

///|
fn pack_image_material_vertices2d(
  command : @render2d_types.ImageMaterialDrawCommand2D,
  image_width : Double,
  image_height : Double,
) -> Array[Double] {
  let image = command.image
  let destination = image.destination
  let shadow = command.drop_shadow
  let offset = shadow
    .map(fn(value) { value.offset })
    .unwrap_or(@smath.Vec2::zero())
  let blur = shadow.map(fn(value) { value.blur_radius }).unwrap_or(0.0)
  let left = blur * 2.0 + (-offset[X]).max(0.0)
  let top = blur * 2.0 + (-offset[Y]).max(0.0)
  let right = blur * 2.0 + offset[X].max(0.0)
  let bottom = blur * 2.0 + offset[Y].max(0.0)
  let expanded = @smath.Rect::{
    position: destination.position - Vec2(left, top),
    size: destination.size + Vec2(left + right, top + bottom),
  }
  let (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) = rect_points2d(
    expanded,
    image.transform,
  )
  let (u0, v0, u1, v1) = match image.source {
    Some(source) =>
      (
        source.position[X] / image_width,
        source.position[Y] / image_height,
        (source.position[X] + source.size[X]) / image_width,
        (source.position[Y] + source.size[Y]) / image_height,
      )
    None => (0.0, 0.0, 1.0, 1.0)
  }
  let uv_width = u1 - u0
  let uv_height = v1 - v0
  let dest_width = destination.size[X].max(0.000001)
  let dest_height = destination.size[Y].max(0.000001)
  let eu0 = u0 - left / dest_width * uv_width
  let ev0 = v0 - top / dest_height * uv_height
  let eu1 = u1 + right / dest_width * uv_width
  let ev1 = v1 + bottom / dest_height * uv_height
  let (bu0, bv0) = transform_uv2d(image.uv_transform, u0, v0)
  let (bu1, bv1) = transform_uv2d(image.uv_transform, u1, v1)
  let (tu0, tv0) = transform_uv2d(image.uv_transform, eu0, ev0)
  let (tu1, tv1) = transform_uv2d(image.uv_transform, eu1, ev1)
  let (r, g, b, a) = color_rgba01(image.color)
  let shadow_color = shadow
    .map(fn(value) { value.color })
    .unwrap_or(@render.Color::{ r: 0U, g: 0U, b: 0U, a: 0.0 })
  let (sr, sg, sb, sa) = color_rgba01(shadow_color)
  let effect = [
    command.opacity,
    command.brightness,
    command.saturation,
    if shadow is Some(_) {
      1.0
    } else {
      0.0
    },
  ]
  let shadow_data = [
    offset[X] / dest_width * uv_width,
    offset[Y] / dest_height * uv_height,
    blur / dest_width * uv_width,
    blur / dest_height * uv_height,
  ]
  let bounds = [bu0.min(bu1), bv0.min(bv1), bu0.max(bu1), bv0.max(bv1)]
  let vertices : Array[Double] = []
  for
    vertex in [
      [p0x, p0y, tu0, tv0],
      [p1x, p1y, tu1, tv0],
      [p2x, p2y, tu1, tv1],
      [p0x, p0y, tu0, tv0],
      [p2x, p2y, tu1, tv1],
      [p3x, p3y, tu0, tv1],
    ] {
    for value in vertex {
      vertices.push(value)
    }
    for value in [r, g, b, a] + effect + [sr, sg, sb, sa] + shadow_data + bounds {
      vertices.push(value)
    }
  }
  vertices
}

///|
fn pack_image_vertices2d_raw(
  destination : @smath.Rect,
  transform : @smath.Transform,
  source : @render2d_types.ImageRegion2D?,
  uv_transform : @smath.Transform,
  repeat : @smath.RepeatMode,
  color : @render.Color,
  image_width : Double,
  image_height : Double,
) -> ImageVertices2D {
  let (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) = rect_points2d(
    destination, transform,
  )
  let mut u0 = 0.0
  let mut v0 = 0.0
  let mut u1 = 1.0
  let mut v1 = 1.0
  let mut sampler_code = 0
  match source {
    Some(source) => {
      u0 = source.position[X] / image_width
      v0 = source.position[Y] / image_height
      u1 = (source.position[X] + source.size[X]) / image_width
      v1 = (source.position[Y] + source.size[Y]) / image_height
    }
    None => {
      let scale_u = destination.size[X] / image_width
      let scale_v = destination.size[Y] / image_height
      match repeat {
        Repeat => {
          u1 = scale_u
          v1 = scale_v
          sampler_code = 3
        }
        RepeatX => {
          u1 = scale_u
          sampler_code = 1
        }
        RepeatY => {
          v1 = scale_v
          sampler_code = 2
        }
        NoRepeat => ()
      }
    }
  }
  let (tu0, tv0) = transform_uv2d(uv_transform, u0, v0)
  let (tu1, tv0b) = transform_uv2d(uv_transform, u1, v0)
  let (tu1b, tv1) = transform_uv2d(uv_transform, u1, v1)
  let (tu0b, tv1b) = transform_uv2d(uv_transform, u0, v1)
  let (r, g, b, a) = color_rgba01(color)
  let vertices : Array[Double] = []
  push_tex_vertex2d(vertices, p0x, p0y, tu0, tv0, r, g, b, a)
  push_tex_vertex2d(vertices, p1x, p1y, tu1, tv0b, r, g, b, a)
  push_tex_vertex2d(vertices, p2x, p2y, tu1b, tv1, r, g, b, a)
  push_tex_vertex2d(vertices, p0x, p0y, tu0, tv0, r, g, b, a)
  push_tex_vertex2d(vertices, p2x, p2y, tu1b, tv1, r, g, b, a)
  push_tex_vertex2d(vertices, p3x, p3y, tu0b, tv1b, r, g, b, a)
  { vertices, sampler_code }
}

///|
fn pack_text_vertices2d(
  command : @render2d_types.TextDrawCommand2D,
  texture_width : Double,
  texture_height : Double,
) -> Array[Double] {
  let mut x = command.position[X]
  let mut y = command.position[Y]
  match command.style.align {
    Center => x = x - texture_width / 2.0
    Right => x = x - texture_width
    Left => ()
  }
  match command.style.baseline {
    Center => y = y - texture_height / 2.0
    Bottom => y = y - texture_height
    Top => ()
  }
  pack_text_vertices2d_raw(
    { position: Vec2(x, y), size: Vec2(texture_width, texture_height) },
    command.transform,
  )
}

///|
fn pack_text_vertices2d_raw(
  rect : @smath.Rect,
  transform : @smath.Transform,
) -> Array[Double] {
  let (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) = rect_points2d(rect, transform)
  let vertices : Array[Double] = []
  push_tex_vertex2d(vertices, p0x, p0y, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0)
  push_tex_vertex2d(vertices, p1x, p1y, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0)
  push_tex_vertex2d(vertices, p2x, p2y, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
  push_tex_vertex2d(vertices, p0x, p0y, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0)
  push_tex_vertex2d(vertices, p2x, p2y, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
  push_tex_vertex2d(vertices, p3x, p3y, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0)
  vertices
}

///|
fn pack_circle_fill_vertices2d(
  center : @smath.Vec2,
  radius : Double,
  transform : @smath.Transform,
  color : @render.Color,
) -> Array[Double] {
  let vertices : Array[Double] = []
  let (r, g, b, a) = color_rgba01(color)
  let center_x = center[X] + transform.tx
  let center_y = center[Y] + transform.ty
  for index in 0..<40 {
    let angle0 = index.to_double() / 40.0 * @math.PI * 2.0
    let angle1 = (index + 1).to_double() / 40.0 * @math.PI * 2.0
    let (x0, y0) = transformed_local_point2d(
      center,
      transform,
      @math.cos(angle0) * radius,
      @math.sin(angle0) * radius,
    )
    let (x1, y1) = transformed_local_point2d(
      center,
      transform,
      @math.cos(angle1) * radius,
      @math.sin(angle1) * radius,
    )
    push_color_vertex2d(vertices, center_x, center_y, r, g, b, a)
    push_color_vertex2d(vertices, x0, y0, r, g, b, a)
    push_color_vertex2d(vertices, x1, y1, r, g, b, a)
  }
  vertices
}

///|
fn pack_circle_stroke_vertices2d(
  center : @smath.Vec2,
  radius : Double,
  transform : @smath.Transform,
  color : @render.Color,
) -> Array[Double] {
  let vertices : Array[Double] = []
  let (r, g, b, a) = color_rgba01(color)
  for index in 0..<40 {
    let angle0 = index.to_double() / 40.0 * @math.PI * 2.0
    let angle1 = (index + 1).to_double() / 40.0 * @math.PI * 2.0
    let (x0, y0) = transformed_local_point2d(
      center,
      transform,
      @math.cos(angle0) * radius,
      @math.sin(angle0) * radius,
    )
    let (x1, y1) = transformed_local_point2d(
      center,
      transform,
      @math.cos(angle1) * radius,
      @math.sin(angle1) * radius,
    )
    push_color_vertex2d(vertices, x0, y0, r, g, b, a)
    push_color_vertex2d(vertices, x1, y1, r, g, b, a)
  }
  vertices
}

///|
fn pack_geometry_vertices2d(
  command : @render2d_types.GeometryDrawCommand2D,
) -> Array[Double] {
  let vertices : Array[Double] = []
  let (r, g, b, a) = color_rgba01(command.color)
  for point in command.vertices {
    let (x, y) = transformed_local_point2d(
      command.origin,
      command.transform,
      point[X],
      point[Y],
    )
    push_color_vertex2d(vertices, x, y, r, g, b, a)
  }
  vertices
}

///|
fn rect_points2d(
  rect : @smath.Rect,
  transform : @smath.Transform,
) -> (Double, Double, Double, Double, Double, Double, Double, Double) {
  let origin = rect.position
  let width = rect.size[X]
  let height = rect.size[Y]
  let (p0x, p0y) = transformed_local_point2d(origin, transform, 0.0, 0.0)
  let (p1x, p1y) = transformed_local_point2d(origin, transform, width, 0.0)
  let (p2x, p2y) = transformed_local_point2d(origin, transform, width, height)
  let (p3x, p3y) = transformed_local_point2d(origin, transform, 0.0, height)
  (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y)
}

///|
fn transformed_local_point2d(
  origin : @smath.Vec2,
  transform : @smath.Transform,
  local_x : Double,
  local_y : Double,
) -> (Double, Double) {
  (
    transform.a * local_x + transform.c * local_y + transform.tx + origin[X],
    transform.b * local_x + transform.d * local_y + transform.ty + origin[Y],
  )
}

///|
fn transform_uv2d(
  transform : @smath.Transform,
  u : Double,
  v : Double,
) -> (Double, Double) {
  (
    transform.a * u + transform.c * v + transform.tx,
    transform.b * u + transform.d * v + transform.ty,
  )
}

///|
fn color_rgba01(color : @render.Color) -> (Double, Double, Double, Double) {
  (
    color.r.reinterpret_as_int().to_double() / 255.0,
    color.g.reinterpret_as_int().to_double() / 255.0,
    color.b.reinterpret_as_int().to_double() / 255.0,
    color.a,
  )
}

///|
fn push_color_vertex2d(
  vertices : Array[Double],
  x : Double,
  y : Double,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> Unit {
  vertices.push(x)
  vertices.push(y)
  vertices.push(r)
  vertices.push(g)
  vertices.push(b)
  vertices.push(a)
}

///|
fn push_tex_vertex2d(
  vertices : Array[Double],
  x : Double,
  y : Double,
  u : Double,
  v : Double,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> Unit {
  vertices.push(x)
  vertices.push(y)
  vertices.push(u)
  vertices.push(v)
  vertices.push(r)
  vertices.push(g)
  vertices.push(b)
  vertices.push(a)
}