// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
pub struct Isometry2d {
  translation : @math.Vec2
  rotation : Float
}

///|
pub fn Isometry2d::identity() -> Isometry2d {
  Isometry2d::{ translation: @math.Vec2::new(0.0F, 0.0F), rotation: 0.0F }
}

///|
pub fn Isometry2d::new(
  translation : @math.Vec2,
  rotation : Float,
) -> Isometry2d {
  Isometry2d::{ translation, rotation }
}

///|
pub fn Isometry2d::transform_point(
  self : Isometry2d,
  point : @math.Vec2,
) -> @math.Vec2 {
  let cosv = @math.cos(self.rotation)
  let sinv = @math.sin(self.rotation)
  let rotated_x = point.x * cosv - point.y * sinv
  let rotated_y = point.x * sinv + point.y * cosv
  @math.Vec2::new(
    rotated_x + self.translation.x,
    rotated_y + self.translation.y,
  )
}

///|
pub(all) enum GizmoLineJoint {
  None
  Miter
  Round(Int)
  Bevel
}

///|
pub(all) enum GizmoLineStyle {
  Solid
  Dotted
  Dashed(gap_scale~ : Float, line_scale~ : Float)
}

///|
pub(all) struct GizmoLineConfig {
  width : Float
  perspective : Bool
  style : GizmoLineStyle
  joints : GizmoLineJoint
}

///|
const DEFAULT_GIZMO_LINE_WIDTH : Float = 2.0F

///|
pub fn GizmoLineConfig::default() -> GizmoLineConfig {
  GizmoLineConfig::{
    width: DEFAULT_GIZMO_LINE_WIDTH,
    perspective: false,
    style: GizmoLineStyle::Solid,
    joints: GizmoLineJoint::None,
  }
}

///|
pub(all) struct GizmoConfig {
  enabled : Bool
  line : GizmoLineConfig
  depth_bias : Float
  render_layers : RenderLayers
}

///|
pub fn GizmoConfig::default() -> GizmoConfig {
  GizmoConfig::{
    enabled: true,
    line: GizmoLineConfig::default(),
    depth_bias: 0.0F,
    render_layers: RenderLayers::default(),
  }
}

///|
pub struct Gizmos {
  layers : RenderLayers
}

///|
pub fn Gizmos::new() -> Gizmos {
  Gizmos::{ layers: gizmo_state.val.config.render_layers }
}

///|
pub fn Gizmos::with_layers(_self : Gizmos, layers : RenderLayers) -> Gizmos {
  Gizmos::{ layers, }
}

///|
pub impl @app.SystemParam for Gizmos with get() {
  Gizmos::new()
}

///|
pub fn gizmo_config() -> GizmoConfig {
  gizmo_state.val.config
}

///|
pub fn gizmo_set_config(config : GizmoConfig) -> Unit {
  gizmo_state.val.config = config
}

///|
const DEFAULT_CIRCLE_RESOLUTION : Int = 32

///|
const DEFAULT_ARC_RESOLUTION : Int = 8

///|
const DEFAULT_CORNER_RADIUS : Float = 0.1F

///|
const PI : Float = 3.141592653589793F

///|
const TAU : Float = 6.283185307179586F

///|
const HALF_PI : Float = 1.5707963267948966F

///|
fn float_from_int(value : Int) -> Float {
  Float::from_double(value.to_double())
}

///|
fn abs_float(value : Float) -> Float {
  if value < 0.0F {
    -value
  } else {
    value
  }
}

///|
fn max_int(value : Int, min : Int) -> Int {
  if value < min {
    min
  } else {
    value
  }
}

///|
fn grid_line_count(include_outer : Bool, count : Int) -> Int {
  if include_outer {
    count + 1
  } else {
    max_int(count - 1, 1)
  }
}

///|
fn max_float(value : Float, min : Float) -> Float {
  if value < min {
    min
  } else {
    value
  }
}

///|
fn min_float(a : Float, b : Float) -> Float {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn positive_or(value : Float, fallback : Float) -> Float {
  if value > 0.0F {
    value
  } else {
    fallback
  }
}

///|
fn render_layers_intersect(a : RenderLayers, b : RenderLayers) -> RenderLayers {
  RenderLayers::{ mask: a.mask & b.mask }
}

///|
fn gizmo_line_style_parts(style : GizmoLineStyle) -> (Int, Float, Float) {
  match style {
    Solid => (0, 1.0F, 1.0F)
    Dotted => (1, 1.0F, 1.0F)
    Dashed(gap_scale~, line_scale~) =>
      (2, positive_or(gap_scale, 1.0F), positive_or(line_scale, 1.0F))
  }
}

///|
fn gizmos_enabled() -> Bool {
  gizmo_state.val.config.enabled
}

///|
fn gizmo_effective_layers(layers : RenderLayers) -> RenderLayers {
  render_layers_intersect(layers, gizmo_state.val.config.render_layers)
}

///|
fn vec2_add(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x + b.x, a.y + b.y)
}

///|
fn vec2_sub(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x - b.x, a.y - b.y)
}

///|
fn vec2_scale(value : @math.Vec2, factor : Float) -> @math.Vec2 {
  @math.Vec2::new(value.x * factor, value.y * factor)
}

///|
fn vec2_length(value : @math.Vec2) -> Float {
  @math.pow(value.x * value.x + value.y * value.y, 0.5F)
}

///|
fn vec2_normalize_or_zero(value : @math.Vec2) -> @math.Vec2 {
  let length = vec2_length(value)
  if length <= 0.0F {
    @math.Vec2::new(0.0F, 0.0F)
  } else {
    vec2_scale(value, 1.0F / length)
  }
}

///|
fn vec2_distance(a : @math.Vec2, b : @math.Vec2) -> Float {
  vec2_length(vec2_sub(a, b))
}

///|
fn vec2_angle_to(a : @math.Vec2, b : @math.Vec2) -> Float {
  let cross = a.x * b.y - a.y * b.x
  let dot = a.x * b.x + a.y * b.y
  @math.atan2(cross, dot)
}

///|
fn tan(value : Float) -> Float {
  let cosv = @math.cos(value)
  if cosv == 0.0F {
    0.0F
  } else {
    @math.sin(value) / cosv
  }
}

///|
fn resolution_from_angle(angle : Float) -> Int {
  let ratio = abs_float(angle) / TAU
  let raw = ratio * float_from_int(DEFAULT_CIRCLE_RESOLUTION)
  let base = raw.to_int()
  let base_float = float_from_int(base)
  let mut result = if raw > base_float { base + 1 } else { base }
  result = max_int(result, 1)
  result
}

///|
fn arc_points(
  arc_angle : Float,
  radius : Float,
  resolution : Int,
) -> Array[@math.Vec2] {
  let steps = max_int(resolution, 1)
  let points : Array[@math.Vec2] = []
  let total = steps + 1
  let denom = float_from_int(steps)
  for i in 0.. Array[@math.Vec2] {
  let steps = max_int(resolution, 3)
  let points : Array[@math.Vec2] = []
  let total = steps + 1
  let denom = float_from_int(steps)
  for i in 0.. Unit {
  if !gizmos_enabled() {
    return
  }
  let config = gizmo_state.val.config
  let effective_layers = gizmo_effective_layers(layers)
  if effective_layers.mask == 0 {
    return
  }
  let line_config = config.line
  let width = positive_or(line_config.width, DEFAULT_GIZMO_LINE_WIDTH)
  gizmo_state.val.lines.push(GizmoLine::{
    start,
    end,
    start_color,
    end_color,
    layers: effective_layers,
    width,
    style: line_config.style,
  })
}

///|
fn gizmos_linestrip(
  positions : Array[@math.Vec2],
  color : Color,
  layers : RenderLayers,
) -> Unit {
  if !gizmos_enabled() {
    return
  }
  let count = positions.length()
  if count < 2 {
    return
  }
  for i in 0..<(count - 1) {
    gizmos_add_line(positions[i], positions[i + 1], color, color, layers)
  }
}

///|
fn gizmos_linestrip_gradient(
  positions : Array[(@math.Vec2, Color)],
  layers : RenderLayers,
) -> Unit {
  if !gizmos_enabled() {
    return
  }
  let count = positions.length()
  if count < 2 {
    return
  }
  for i in 0..<(count - 1) {
    let (start_pos, start_color) = positions[i]
    let (end_pos, end_color) = positions[i + 1]
    gizmos_add_line(start_pos, end_pos, start_color, end_color, layers)
  }
}

///|
fn gizmos_lineloop(
  positions : Array[@math.Vec2],
  color : Color,
  layers : RenderLayers,
) -> Unit {
  let count = positions.length()
  if count < 2 {
    return
  }
  gizmos_linestrip(positions, color, layers)
  gizmos_add_line(positions[count - 1], positions[0], color, color, layers)
}

///|
fn gizmos_arrow(
  start : @math.Vec2,
  end : @math.Vec2,
  color : Color,
  layers : RenderLayers,
  tip_length : Float,
  double_ended : Bool,
) -> Unit {
  gizmos_add_line(start, end, color, color, layers)
  let delta = vec2_sub(end, start)
  let length = vec2_length(delta)
  if length <= 0.0F {
    return
  }
  let direction = vec2_scale(delta, 1.0F / length)
  let tip_length = if tip_length <= 0.0F { length / 10.0F } else { tip_length }
  let tip_width = tip_length * 0.5F
  let base = vec2_sub(end, vec2_scale(direction, tip_length))
  let perp = @math.Vec2::new(-direction.y, direction.x)
  let tip_left = vec2_add(base, vec2_scale(perp, tip_width))
  let tip_right = vec2_sub(base, vec2_scale(perp, tip_width))
  gizmos_add_line(end, tip_left, color, color, layers)
  gizmos_add_line(end, tip_right, color, color, layers)
  if double_ended {
    let back_dir = vec2_scale(direction, -1.0F)
    let base_start = vec2_sub(start, vec2_scale(back_dir, tip_length))
    let perp_start = @math.Vec2::new(-back_dir.y, back_dir.x)
    let tip_left_start = vec2_add(base_start, vec2_scale(perp_start, tip_width))
    let tip_right_start = vec2_sub(
      base_start,
      vec2_scale(perp_start, tip_width),
    )
    gizmos_add_line(start, tip_left_start, color, color, layers)
    gizmos_add_line(start, tip_right_start, color, color, layers)
  }
}

///|
fn gizmos_draw_grid(
  isometry : Isometry2d,
  cell_count : @math.IVec2,
  spacing : @math.Vec2,
  skew : @math.Vec2,
  outer_edges_x : Bool,
  outer_edges_y : Bool,
  color : Color,
  layers : RenderLayers,
) -> Unit {
  let count_x = if cell_count.x < 0 { 0 } else { cell_count.x }
  let count_y = if cell_count.y < 0 { 0 } else { cell_count.y }
  if count_x == 0 || count_y == 0 {
    return
  }
  let skew_tan_x = tan(skew.x)
  let skew_tan_y = tan(skew.y)
  let dx = if count_x == 0 {
    @math.Vec2::new(0.0F, 0.0F)
  } else {
    @math.Vec2::new(spacing.x, spacing.x * skew_tan_y)
  }
  let dy = if count_y == 0 {
    @math.Vec2::new(0.0F, 0.0F)
  } else {
    @math.Vec2::new(spacing.y * skew_tan_x, spacing.y)
  }
  let half_x = float_from_int(count_x) * 0.5F
  let half_y = float_from_int(count_y) * 0.5F
  let grid_start = vec2_add(vec2_scale(dx, -half_x), vec2_scale(dy, -half_y))
  let x_line_count = grid_line_count(outer_edges_x, count_y)
  let y_line_count = grid_line_count(outer_edges_y, count_x)
  let x_start = if outer_edges_x {
    grid_start
  } else {
    vec2_add(grid_start, dy)
  }
  let y_start = if outer_edges_y {
    grid_start
  } else {
    vec2_add(grid_start, dx)
  }
  for i in 0.. Unit {
  if !gizmos_enabled() {
    gizmos_clear()
    return
  }
  if gizmo_state.val.pending_ellipses.length() > 0 {
    let entries : Array[PendingEllipse] = []
    for item in gizmo_state.val.pending_ellipses {
      entries.push(item)
    }
    gizmo_state.val.pending_ellipses.clear()
    for entry in entries {
      let points = ellipse_points(entry.half_size, entry.resolution)
      let transformed : Array[@math.Vec2] = []
      for point in points {
        transformed.push(entry.isometry.transform_point(point))
      }
      gizmos_linestrip(transformed, entry.color, entry.layers)
    }
  }
  if gizmo_state.val.pending_arcs.length() > 0 {
    let entries : Array[PendingArc] = []
    for item in gizmo_state.val.pending_arcs {
      entries.push(item)
    }
    gizmo_state.val.pending_arcs.clear()
    for entry in entries {
      let resolution = if entry.has_resolution {
        entry.resolution
      } else {
        resolution_from_angle(entry.arc_angle)
      }
      let points = arc_points(entry.arc_angle, entry.radius, resolution)
      let transformed : Array[@math.Vec2] = []
      for point in points {
        transformed.push(entry.isometry.transform_point(point))
      }
      gizmos_linestrip(transformed, entry.color, entry.layers)
    }
  }
  if gizmo_state.val.pending_rounded_rects.length() > 0 {
    let entries : Array[PendingRoundedRect] = []
    for item in gizmo_state.val.pending_rounded_rects {
      entries.push(item)
    }
    gizmo_state.val.pending_rounded_rects.clear()
    for entry in entries {
      let half_size = @math.Vec2::new(entry.size.x / 2.0F, entry.size.y / 2.0F)
      let max_radius = min_float(abs_float(half_size.x), abs_float(half_size.y))
      let radius = max_float(min_float(entry.corner_radius, max_radius), 0.0F)
      if radius <= 0.0F {
        let rect_points = [
          @math.Vec2::new(-half_size.x, half_size.y),
          @math.Vec2::new(half_size.x, half_size.y),
          @math.Vec2::new(half_size.x, -half_size.y),
          @math.Vec2::new(-half_size.x, -half_size.y),
        ]
        let transformed : Array[@math.Vec2] = []
        for point in rect_points {
          transformed.push(entry.isometry.transform_point(point))
        }
        gizmos_lineloop(transformed, entry.color, entry.layers)
      } else {
        let steps = max_int(entry.arc_resolution, 1)
        let points : Array[@math.Vec2] = []
        let tr_center = @math.Vec2::new(
          half_size.x - radius,
          half_size.y - radius,
        )
        let br_center = @math.Vec2::new(
          half_size.x - radius,
          -half_size.y + radius,
        )
        let bl_center = @math.Vec2::new(
          -half_size.x + radius,
          -half_size.y + radius,
        )
        let tl_center = @math.Vec2::new(
          -half_size.x + radius,
          half_size.y - radius,
        )
        let arc_defs = [
          (tr_center, HALF_PI, 0.0F),
          (br_center, 0.0F, -HALF_PI),
          (bl_center, -HALF_PI, -PI),
          (tl_center, -PI, -HALF_PI * 3.0F),
        ]
        for arc_index in 0.. 0 && i == 0 {
              continue
            }
            let t = float_from_int(i) / denom
            let angle = start_angle + (end_angle - start_angle) * t
            let x = @math.cos(angle) * radius + center.x
            let y = @math.sin(angle) * radius + center.y
            points.push(@math.Vec2::new(x, y))
          }
        }
        let transformed : Array[@math.Vec2] = []
        for point in points {
          transformed.push(entry.isometry.transform_point(point))
        }
        gizmos_lineloop(transformed, entry.color, entry.layers)
      }
    }
  }
  if gizmo_state.val.pending_arrows.length() > 0 {
    let entries : Array[PendingArrow] = []
    for item in gizmo_state.val.pending_arrows {
      entries.push(item)
    }
    gizmo_state.val.pending_arrows.clear()
    for entry in entries {
      gizmos_arrow(
        entry.start,
        entry.end,
        entry.color,
        entry.layers,
        entry.tip_length,
        entry.double_ended,
      )
    }
  }
  if gizmo_state.val.pending_grids.length() > 0 {
    let entries : Array[PendingGrid] = []
    for item in gizmo_state.val.pending_grids {
      entries.push(item)
    }
    gizmo_state.val.pending_grids.clear()
    for entry in entries {
      gizmos_draw_grid(
        entry.isometry,
        entry.cell_count,
        entry.spacing,
        entry.skew,
        entry.outer_edges_x,
        entry.outer_edges_y,
        entry.color,
        entry.layers,
      )
    }
  }
}

///|
fn gizmos_clear() -> Unit {
  gizmo_state.val.lines.clear()
  gizmo_state.val.pending_ellipses.clear()
  gizmo_state.val.pending_arcs.clear()
  gizmo_state.val.pending_rounded_rects.clear()
  gizmo_state.val.pending_arrows.clear()
  gizmo_state.val.pending_grids.clear()
}

///|
fn gizmos_reset() -> Unit {
  gizmos_clear()
  gizmo_state.val.config = GizmoConfig::default()
}

///|
fn gizmos_draw_lines(layers : RenderLayers) -> Unit {
  if !gizmos_enabled() {
    return
  }
  if !gizmo_state.val.config.render_layers.matches(layers) {
    return
  }
  for line in gizmo_state.val.lines {
    if line.layers.matches(layers) {
      let (style_kind, gap_scale, line_scale) = gizmo_line_style_parts(
        line.style,
      )
      @window.host_gpu_draw_gizmo_line(
        line.start.x,
        line.start.y,
        line.start_color.r,
        line.start_color.g,
        line.start_color.b,
        line.start_color.a,
        line.end.x,
        line.end.y,
        line.end_color.r,
        line.end_color.g,
        line.end_color.b,
        line.end_color.a,
        line.width,
        style_kind,
        gap_scale,
        line_scale,
      )
    }
  }
}

///|
pub fn Gizmos::line_2d(
  self : Gizmos,
  start : @math.Vec2,
  end : @math.Vec2,
  color : Color,
) -> Unit {
  gizmos_add_line(start, end, color, color, self.layers)
}

///|
pub fn Gizmos::line_gradient_2d(
  self : Gizmos,
  start : @math.Vec2,
  end : @math.Vec2,
  start_color : Color,
  end_color : Color,
) -> Unit {
  gizmos_add_line(start, end, start_color, end_color, self.layers)
}

///|
pub fn Gizmos::linestrip_2d(
  self : Gizmos,
  positions : Array[@math.Vec2],
  color : Color,
) -> Unit {
  gizmos_linestrip(positions, color, self.layers)
}

///|
pub fn Gizmos::lineloop_2d(
  self : Gizmos,
  positions : Array[@math.Vec2],
  color : Color,
) -> Unit {
  gizmos_lineloop(positions, color, self.layers)
}

///|
pub fn Gizmos::linestrip_gradient_2d(
  self : Gizmos,
  positions : Array[(@math.Vec2, Color)],
) -> Unit {
  gizmos_linestrip_gradient(positions, self.layers)
}

///|
pub fn Gizmos::ray_2d(
  self : Gizmos,
  start : @math.Vec2,
  vector : @math.Vec2,
  color : Color,
) -> Unit {
  let end = vec2_add(start, vector)
  gizmos_add_line(start, end, color, color, self.layers)
}

///|
pub fn Gizmos::ray_gradient_2d(
  self : Gizmos,
  start : @math.Vec2,
  vector : @math.Vec2,
  start_color : Color,
  end_color : Color,
) -> Unit {
  let end = vec2_add(start, vector)
  gizmos_add_line(start, end, start_color, end_color, self.layers)
}

///|
pub fn Gizmos::rect_2d(
  self : Gizmos,
  isometry : Isometry2d,
  size : @math.Vec2,
  color : Color,
) -> Unit {
  let half = @math.Vec2::new(size.x / 2.0F, size.y / 2.0F)
  let tl = isometry.transform_point(@math.Vec2::new(-half.x, half.y))
  let tr = isometry.transform_point(@math.Vec2::new(half.x, half.y))
  let br = isometry.transform_point(@math.Vec2::new(half.x, -half.y))
  let bl = isometry.transform_point(@math.Vec2::new(-half.x, -half.y))
  gizmos_lineloop([tl, tr, br, bl], color, self.layers)
}

///|
pub struct RoundedRectBuilder {
  id : Int
}

///|
pub fn RoundedRectBuilder::corner_radius(
  self : RoundedRectBuilder,
  radius : Float,
) -> RoundedRectBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_rounded_rects.length() {
    gizmo_state.val.pending_rounded_rects[self.id].corner_radius = radius
  }
  self
}

///|
pub fn RoundedRectBuilder::arc_resolution(
  self : RoundedRectBuilder,
  resolution : Int,
) -> RoundedRectBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_rounded_rects.length() {
    gizmo_state.val.pending_rounded_rects[self.id].arc_resolution = max_int(
      resolution, 1,
    )
  }
  self
}

///|
pub fn Gizmos::rounded_rect_2d(
  self : Gizmos,
  isometry : Isometry2d,
  size : @math.Vec2,
  color : Color,
) -> RoundedRectBuilder {
  let radius = min_float(abs_float(size.x), abs_float(size.y)) *
    DEFAULT_CORNER_RADIUS
  let entry = PendingRoundedRect::{
    isometry,
    size,
    color,
    corner_radius: radius,
    arc_resolution: DEFAULT_ARC_RESOLUTION,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_rounded_rects.length()
  gizmo_state.val.pending_rounded_rects.push(entry)
  RoundedRectBuilder::{ id, }
}

///|
pub struct Ellipse2dBuilder {
  id : Int
}

///|
pub fn Ellipse2dBuilder::resolution(
  self : Ellipse2dBuilder,
  segments : Int,
) -> Ellipse2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_ellipses.length() {
    gizmo_state.val.pending_ellipses[self.id].resolution = max_int(segments, 3)
  }
  self
}

///|
pub fn Gizmos::circle_2d(
  self : Gizmos,
  isometry : Isometry2d,
  radius : Float,
  color : Color,
) -> Ellipse2dBuilder {
  let entry = PendingEllipse::{
    isometry,
    half_size: @math.Vec2::new(radius, radius),
    color,
    resolution: DEFAULT_CIRCLE_RESOLUTION,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_ellipses.length()
  gizmo_state.val.pending_ellipses.push(entry)
  Ellipse2dBuilder::{ id, }
}

///|
pub fn Gizmos::ellipse_2d(
  self : Gizmos,
  isometry : Isometry2d,
  half_size : @math.Vec2,
  color : Color,
) -> Ellipse2dBuilder {
  let entry = PendingEllipse::{
    isometry,
    half_size,
    color,
    resolution: DEFAULT_CIRCLE_RESOLUTION,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_ellipses.length()
  gizmo_state.val.pending_ellipses.push(entry)
  Ellipse2dBuilder::{ id, }
}

///|
pub struct Arc2dBuilder {
  id : Int
}

///|
pub fn Arc2dBuilder::resolution(
  self : Arc2dBuilder,
  segments : Int,
) -> Arc2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_arcs.length() {
    gizmo_state.val.pending_arcs[self.id].resolution = max_int(segments, 1)
    gizmo_state.val.pending_arcs[self.id].has_resolution = true
  }
  self
}

///|
pub fn Gizmos::arc_2d(
  self : Gizmos,
  isometry : Isometry2d,
  arc_angle : Float,
  radius : Float,
  color : Color,
) -> Arc2dBuilder {
  let entry = PendingArc::{
    isometry,
    arc_angle,
    radius,
    color,
    resolution: DEFAULT_CIRCLE_RESOLUTION,
    has_resolution: false,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_arcs.length()
  gizmo_state.val.pending_arcs.push(entry)
  Arc2dBuilder::{ id, }
}

///|
pub fn Gizmos::short_arc_2d_between(
  self : Gizmos,
  center : @math.Vec2,
  start : @math.Vec2,
  end : @math.Vec2,
  color : Color,
) -> Arc2dBuilder {
  let from_axis = vec2_normalize_or_zero(vec2_sub(start, center))
  let to_axis = vec2_normalize_or_zero(vec2_sub(end, center))
  let rotation = vec2_angle_to(@math.Vec2::new(0.0F, 1.0F), from_axis)
  let arc_angle = vec2_angle_to(from_axis, to_axis)
  let radius = vec2_distance(center, start)
  let isometry = Isometry2d::new(center, rotation)
  self.arc_2d(isometry, arc_angle, radius, color)
}

///|
pub fn Gizmos::long_arc_2d_between(
  self : Gizmos,
  center : @math.Vec2,
  start : @math.Vec2,
  end : @math.Vec2,
  color : Color,
) -> Arc2dBuilder {
  let from_axis = vec2_normalize_or_zero(vec2_sub(start, center))
  let to_axis = vec2_normalize_or_zero(vec2_sub(end, center))
  let rotation = vec2_angle_to(@math.Vec2::new(0.0F, 1.0F), from_axis)
  let raw_angle = vec2_angle_to(from_axis, to_axis)
  let arc_angle = raw_angle - TAU
  let radius = vec2_distance(center, start)
  let isometry = Isometry2d::new(center, rotation)
  self.arc_2d(isometry, arc_angle, radius, color)
}

///|
pub struct ArrowBuilder {
  id : Int
}

///|
pub fn ArrowBuilder::with_tip_length(
  self : ArrowBuilder,
  length : Float,
) -> ArrowBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_arrows.length() {
    gizmo_state.val.pending_arrows[self.id].tip_length = length
  }
  self
}

///|
pub fn ArrowBuilder::with_double_end(self : ArrowBuilder) -> ArrowBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_arrows.length() {
    gizmo_state.val.pending_arrows[self.id].double_ended = true
  }
  self
}

///|
pub fn Gizmos::arrow_2d(
  self : Gizmos,
  start : @math.Vec2,
  end : @math.Vec2,
  color : Color,
) -> ArrowBuilder {
  let length = vec2_length(vec2_sub(end, start))
  let tip_length = if length <= 0.0F { 0.0F } else { length / 10.0F }
  let entry = PendingArrow::{
    start,
    end,
    color,
    tip_length,
    double_ended: false,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_arrows.length()
  gizmo_state.val.pending_arrows.push(entry)
  ArrowBuilder::{ id, }
}

///|
pub fn Gizmos::axes_2d(
  self : Gizmos,
  isometry : Isometry2d,
  length : Float,
) -> Unit {
  let origin = isometry.translation
  let x_end = isometry.transform_point(@math.Vec2::new(length, 0.0F))
  let y_end = isometry.transform_point(@math.Vec2::new(0.0F, length))
  let red = Color::new(1.0F, 0.0F, 0.0F, 1.0F)
  let green = Color::new(0.0F, 1.0F, 0.0F, 1.0F)
  self.arrow_2d(origin, x_end, red) |> ignore
  self.arrow_2d(origin, y_end, green) |> ignore
}

///|
pub fn Gizmos::cross_2d(
  self : Gizmos,
  isometry : Isometry2d,
  half_size : Float,
  color : Color,
) -> Unit {
  let axis_x = @math.Vec2::new(half_size, 0.0F)
  let axis_y = @math.Vec2::new(0.0F, half_size)
  let start_x = isometry.transform_point(axis_x)
  let end_x = isometry.transform_point(vec2_scale(axis_x, -1.0F))
  let start_y = isometry.transform_point(axis_y)
  let end_y = isometry.transform_point(vec2_scale(axis_y, -1.0F))
  gizmos_add_line(start_x, end_x, color, color, self.layers)
  gizmos_add_line(start_y, end_y, color, color, self.layers)
}

///|
pub struct Grid2dBuilder {
  id : Int
}

///|
pub fn Grid2dBuilder::skew_x(
  self : Grid2dBuilder,
  skew : Float,
) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    let current = gizmo_state.val.pending_grids[self.id].skew
    gizmo_state.val.pending_grids[self.id].skew = @math.Vec2::new(
      skew,
      current.y,
    )
  }
  self
}

///|
pub fn Grid2dBuilder::skew_y(
  self : Grid2dBuilder,
  skew : Float,
) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    let current = gizmo_state.val.pending_grids[self.id].skew
    gizmo_state.val.pending_grids[self.id].skew = @math.Vec2::new(
      current.x,
      skew,
    )
  }
  self
}

///|
pub fn Grid2dBuilder::skew(
  self : Grid2dBuilder,
  skew : @math.Vec2,
) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    gizmo_state.val.pending_grids[self.id].skew = skew
  }
  self
}

///|
pub fn Grid2dBuilder::outer_edges_x(self : Grid2dBuilder) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    gizmo_state.val.pending_grids[self.id].outer_edges_x = true
  }
  self
}

///|
pub fn Grid2dBuilder::outer_edges_y(self : Grid2dBuilder) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    gizmo_state.val.pending_grids[self.id].outer_edges_y = true
  }
  self
}

///|
pub fn Grid2dBuilder::outer_edges(self : Grid2dBuilder) -> Grid2dBuilder {
  if self.id >= 0 && self.id < gizmo_state.val.pending_grids.length() {
    gizmo_state.val.pending_grids[self.id].outer_edges_x = true
    gizmo_state.val.pending_grids[self.id].outer_edges_y = true
  }
  self
}

///|
pub fn Gizmos::grid_2d(
  self : Gizmos,
  isometry : Isometry2d,
  cell_count : @math.IVec2,
  spacing : @math.Vec2,
  color : Color,
) -> Grid2dBuilder {
  let entry = PendingGrid::{
    isometry,
    spacing,
    cell_count,
    skew: @math.Vec2::new(0.0F, 0.0F),
    outer_edges_x: false,
    outer_edges_y: false,
    color,
    layers: self.layers,
  }
  let id = gizmo_state.val.pending_grids.length()
  gizmo_state.val.pending_grids.push(entry)
  Grid2dBuilder::{ id, }
}

///|
pub fn Gizmos::curve_2d(
  self : Gizmos,
  points : Array[@math.Vec2],
  color : Color,
) -> Unit {
  gizmos_linestrip(points, color, self.layers)
}

///|
pub fn Gizmos::curve_gradient_2d(
  self : Gizmos,
  points : Array[(@math.Vec2, Color)],
) -> Unit {
  gizmos_linestrip_gradient(points, self.layers)
}