///|
fn webgpu_canvas_aspect_ratio() -> Double {
  let size = webgpu_canvas_size()
  if size.length() < 2 || size[1] <= 0.0 {
    1.0
  } else {
    size[0] / size[1]
  }
}

///|
fn pack_camera_view_projection3d(
  camera : @render3d_types.FrameCamera3D,
  aspect : Double,
) -> Array[Double] {
  let near = if camera.near > 0.0 { camera.near } else { 0.1 }
  let far = if camera.far > near { camera.far } else { near + 200.0 }
  let projection = if camera.projection == Orthographic {
    let width = camera.orthographic_size.map_or(20.0, fn(size) {
      if size[X].abs() > 0.0001 {
        size[X]
      } else {
        20.0
      }
    })
    let height = camera.orthographic_size.map_or(20.0, fn(size) {
      if size[Y].abs() > 0.0001 {
        size[Y]
      } else {
        20.0
      }
    })
    mat4_orthographic3d(width, height, near, far)
  } else {
    mat4_perspective3d(camera.fov_y, aspect, near, far)
  }
  mat4_multiply3d(
    projection,
    mat4_look_at3d(camera.position, camera.target, camera.up),
  )
}

///|
fn mat4_multiply3d(lhs : Array[Double], rhs : Array[Double]) -> Array[Double] {
  let out : Array[Double] = []
  for _ in 0..<16 {
    out.push(0.0)
  }
  for column in 0..<4 {
    for row in 0..<4 {
      out[column * 4 + row] = lhs[0 * 4 + row] * rhs[column * 4 + 0] +
        lhs[1 * 4 + row] * rhs[column * 4 + 1] +
        lhs[2 * 4 + row] * rhs[column * 4 + 2] +
        lhs[3 * 4 + row] * rhs[column * 4 + 3]
    }
  }
  out
}

///|
fn mat4_look_at3d(
  eye : @smath.Vec3,
  target : @smath.Vec3,
  up : @smath.Vec3,
) -> Array[Double] {
  let z_axis = normalize_or3d(eye - target, Vec3(0.0, 0.0, 1.0))
  let x_axis = normalize_or3d(up.cross(z_axis), Vec3(1.0, 0.0, 0.0))
  let y_axis = z_axis.cross(x_axis)
  [
    x_axis.x,
    y_axis.x,
    z_axis.x,
    0.0,
    x_axis.y,
    y_axis.y,
    z_axis.y,
    0.0,
    x_axis.z,
    y_axis.z,
    z_axis.z,
    0.0,
    -x_axis.dot(eye),
    -y_axis.dot(eye),
    -z_axis.dot(eye),
    1.0,
  ]
}

///|
fn mat4_perspective3d(
  fov_y_degrees : Double,
  aspect : Double,
  near : Double,
  far : Double,
) -> Array[Double] {
  let safe_aspect = if aspect.abs() > 0.0001 { aspect } else { 1.0 }
  let fov_y = fov_y_degrees * @math.PI / 180.0
  let f = 1.0 / @math.tan(fov_y / 2.0)
  [
    f / safe_aspect,
    0.0,
    0.0,
    0.0,
    0.0,
    f,
    0.0,
    0.0,
    0.0,
    0.0,
    far / (near - far),
    -1.0,
    0.0,
    0.0,
    near * far / (near - far),
    0.0,
  ]
}

///|
fn mat4_orthographic3d(
  width : Double,
  height : Double,
  near : Double,
  far : Double,
) -> Array[Double] {
  let half_width = width.abs().max(0.0001) / 2.0
  let half_height = height.abs().max(0.0001) / 2.0
  let left = -half_width
  let right = half_width
  let bottom = -half_height
  let top = half_height
  [
    2.0 / (right - left),
    0.0,
    0.0,
    0.0,
    0.0,
    2.0 / (top - bottom),
    0.0,
    0.0,
    0.0,
    0.0,
    1.0 / (near - far),
    0.0,
    -(right + left) / (right - left),
    -(top + bottom) / (top - bottom),
    near / (near - far),
    1.0,
  ]
}

///|
fn normalize_or3d(value : @smath.Vec3, fallback : @smath.Vec3) -> @smath.Vec3 {
  if value.length_squared() <= 0.0000001 {
    fallback
  } else {
    value.normalize()
  }
}