///|
/// Runtime instance transforms and draw-batch planning.
pub(all) struct InstanceTransform {
  translation : Vec3
  rotation : Quat
  scale : Vec3
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstanceRecord {
  id : Int
  mesh : Int
  material : Int
  transform : InstanceTransform
  bounds : SpatialAabb
  layer : Int
  visible : Bool
  lod_bias : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstanceBatchKey {
  mesh : Int
  material : Int
  lod : Int
  layer : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstanceBatch {
  key : InstanceBatchKey
  instance_ids : Array[Int]
  bounds : SpatialAabb
  visible_count : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct DrawCommand {
  mesh : Int
  material : Int
  lod : Int
  first_instance : Int
  instance_count : Int
  bounds : SpatialAabb
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstancePlan {
  instances : Array[InstanceRecord]
  batches : Array[InstanceBatch]
  commands : Array[DrawCommand]
  visible_instances : Int
  culled_instances : Int
  batch_count : Int
  draw_count : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstancePolicy {
  max_instances : Int
  max_batches : Int
  max_draws : Int
  max_lod : Int
  allow_negative_scale : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct InstanceBudgetResult {
  passed : Bool
  violations : Array[String]
  plan : InstancePlan
} derive(Debug, ToJson, Eq)

///|
pub fn instance_transform_identity() -> InstanceTransform {
  {
    translation: { x: 0.0, y: 0.0, z: 0.0 },
    rotation: { x: 0.0, y: 0.0, z: 0.0, w: 1.0 },
    scale: { x: 1.0, y: 1.0, z: 1.0 },
  }
}

///|
pub fn instance_transform(
  translation : Vec3,
  rotation : Quat,
  scale : Vec3,
) -> InstanceTransform {
  { translation, rotation: instance_quaternion_normalize(rotation), scale }
}

///|
pub fn instance_quaternion_normalize(value : Quat) -> Quat {
  let length = Double::sqrt(
    value.x * value.x +
    value.y * value.y +
    value.z * value.z +
    value.w * value.w,
  )
  if length <= 0.0 {
    { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }
  } else {
    {
      x: value.x / length,
      y: value.y / length,
      z: value.z / length,
      w: value.w / length,
    }
  }
}

///|
pub fn instance_quaternion_multiply(a : Quat, b : Quat) -> Quat {
  instance_quaternion_normalize({
    x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
    y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
    z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
    w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
  })
}

///|
pub fn instance_quaternion_rotate(rotation : Quat, vector : Vec3) -> Vec3 {
  let pure = Quat::{ x: vector.x, y: vector.y, z: vector.z, w: 0.0 }
  let inverse : Quat = {
    x: -rotation.x,
    y: -rotation.y,
    z: -rotation.z,
    w: rotation.w,
  }
  let result = instance_quaternion_multiply(
    instance_quaternion_multiply(rotation, pure),
    inverse,
  )
  { x: result.x, y: result.y, z: result.z }
}

///|
pub fn instance_transform_point(
  transform : InstanceTransform,
  point : Vec3,
) -> Vec3 {
  let scaled = {
    x: point.x * transform.scale.x,
    y: point.y * transform.scale.y,
    z: point.z * transform.scale.z,
  }
  topology_add(
    instance_quaternion_rotate(transform.rotation, scaled),
    transform.translation,
  )
}

///|
pub fn instance_transform_bounds(
  transform : InstanceTransform,
  bounds : SpatialAabb,
) -> SpatialAabb {
  let corners = [
    { x: bounds.minimum.x, y: bounds.minimum.y, z: bounds.minimum.z },
    { x: bounds.minimum.x, y: bounds.minimum.y, z: bounds.maximum.z },
    { x: bounds.minimum.x, y: bounds.maximum.y, z: bounds.minimum.z },
    { x: bounds.minimum.x, y: bounds.maximum.y, z: bounds.maximum.z },
    { x: bounds.maximum.x, y: bounds.minimum.y, z: bounds.minimum.z },
    { x: bounds.maximum.x, y: bounds.minimum.y, z: bounds.maximum.z },
    { x: bounds.maximum.x, y: bounds.maximum.y, z: bounds.minimum.z },
    { x: bounds.maximum.x, y: bounds.maximum.y, z: bounds.maximum.z },
  ]
  spatial_aabb_from_points(
    corners.map(fn(point) { instance_transform_point(transform, point) }),
  )
}

///|
pub fn instance_transform_combine(
  parent : InstanceTransform,
  child : InstanceTransform,
) -> InstanceTransform {
  let translation = instance_transform_point(parent, child.translation)
  let rotation = instance_quaternion_multiply(parent.rotation, child.rotation)
  let scale = {
    x: parent.scale.x * child.scale.x,
    y: parent.scale.y * child.scale.y,
    z: parent.scale.z * child.scale.z,
  }
  { translation, rotation, scale }
}

///|
pub fn instance_transform_lerp(
  left : InstanceTransform,
  right : InstanceTransform,
  factor : Double,
) -> InstanceTransform {
  let t = if factor < 0.0 { 0.0 } else if factor > 1.0 { 1.0 } else { factor }
  let rotation = instance_quaternion_normalize({
    x: left.rotation.x + (right.rotation.x - left.rotation.x) * t,
    y: left.rotation.y + (right.rotation.y - left.rotation.y) * t,
    z: left.rotation.z + (right.rotation.z - left.rotation.z) * t,
    w: left.rotation.w + (right.rotation.w - left.rotation.w) * t,
  })
  {
    translation: {
      x: left.translation.x + (right.translation.x - left.translation.x) * t,
      y: left.translation.y + (right.translation.y - left.translation.y) * t,
      z: left.translation.z + (right.translation.z - left.translation.z) * t,
    },
    rotation,
    scale: {
      x: left.scale.x + (right.scale.x - left.scale.x) * t,
      y: left.scale.y + (right.scale.y - left.scale.y) * t,
      z: left.scale.z + (right.scale.z - left.scale.z) * t,
    },
  }
}

///|
pub fn instance_record(
  id : Int,
  mesh : Int,
  material : Int,
  transform : InstanceTransform,
  local_bounds : SpatialAabb,
  layer : Int,
) -> InstanceRecord {
  {
    id,
    mesh,
    material,
    transform,
    bounds: instance_transform_bounds(transform, local_bounds),
    layer,
    visible: true,
    lod_bias: 0.0,
  }
}

///|
pub fn instance_set_visible(
  instance : InstanceRecord,
  visible : Bool,
) -> InstanceRecord {
  { ..instance, visible, }
}

///|
pub fn instance_set_lod_bias(
  instance : InstanceRecord,
  lod_bias : Double,
) -> InstanceRecord {
  { ..instance, lod_bias, }
}

///|
pub fn instance_key(instance : InstanceRecord, lod : Int) -> InstanceBatchKey {
  {
    mesh: instance.mesh,
    material: instance.material,
    lod,
    layer: instance.layer,
  }
}

///|
pub fn instance_key_equal(
  left : InstanceBatchKey,
  right : InstanceBatchKey,
) -> Bool {
  left.mesh == right.mesh &&
  left.material == right.material &&
  left.lod == right.lod &&
  left.layer == right.layer
}

///|
pub fn instance_lod_for_distance(
  distance : Double,
  thresholds : Array[Double],
  bias : Double,
) -> Int {
  let adjusted = distance + bias
  let mut lod = 0
  for threshold in thresholds {
    if adjusted >= threshold {
      lod = lod + 1
    }
  }
  lod
}

///|
pub fn instance_lod_for_screen_size(
  radius : Double,
  distance : Double,
  thresholds : Array[Double],
) -> Int {
  if distance <= 0.0 {
    return 0
  }
  instance_lod_for_distance(radius / distance, thresholds, 0.0)
}

///|
pub fn instance_batch_bounds(
  instances : Array[InstanceRecord],
  ids : Array[Int],
) -> SpatialAabb {
  match ids.get(0) {
    None => spatial_aabb_empty()
    Some(first_id) => {
      let mut result = spatial_aabb_empty()
      for instance in instances {
        if instance.id == first_id {
          result = instance.bounds
          break
        }
      }
      for id in ids {
        for instance in instances {
          if instance.id == id {
            result = spatial_aabb_union(result, instance.bounds)
            break
          }
        }
      }
      result
    }
  }
}

///|
pub fn instance_build_batches(
  instances : Array[InstanceRecord],
  lods : Array[Int],
) -> Array[InstanceBatch] {
  let batches : Array[InstanceBatch] = []
  for index, instance in instances {
    let lod = match lods.get(index) {
      Some(value) => value
      None => 0
    }
    if !instance.visible {
      continue
    }
    let key = instance_key(instance, lod)
    let mut found : Int? = None
    for batch_index, batch in batches {
      if instance_key_equal(batch.key, key) {
        found = Some(batch_index)
        break
      }
    }
    match found {
      None =>
        batches.push({
          key,
          instance_ids: [instance.id],
          bounds: instance.bounds,
          visible_count: 1,
        })
      Some(batch_index) =>
        match batches.get(batch_index) {
          None => ()
          Some(batch) => {
            let ids = batch.instance_ids.copy()
            ids.push(instance.id)
            batches[batch_index] = {
              ..batch,
              instance_ids: ids,
              bounds: spatial_aabb_union(batch.bounds, instance.bounds),
              visible_count: batch.visible_count + 1,
            }
          }
        }
    }
  }
  batches
}

///|
pub fn instance_build_commands(
  batches : Array[InstanceBatch],
) -> Array[DrawCommand] {
  let commands : Array[DrawCommand] = []
  let mut first_instance = 0
  for batch in batches {
    commands.push({
      mesh: batch.key.mesh,
      material: batch.key.material,
      lod: batch.key.lod,
      first_instance,
      instance_count: batch.instance_ids.length(),
      bounds: batch.bounds,
    })
    first_instance = first_instance + batch.instance_ids.length()
  }
  commands
}

///|
pub fn default_instance_policy() -> InstancePolicy {
  {
    max_instances: 1000000,
    max_batches: 100000,
    max_draws: 100000,
    max_lod: 8,
    allow_negative_scale: true,
  }
}

///|
pub fn instance_plan(
  instances : Array[InstanceRecord],
  lods : Array[Int],
) -> InstancePlan {
  let batches = instance_build_batches(instances, lods)
  let commands = instance_build_commands(batches)
  let visible = instances.filter(fn(instance) { instance.visible }).length()
  {
    instances,
    batches,
    commands,
    visible_instances: visible,
    culled_instances: instances.length() - visible,
    batch_count: batches.length(),
    draw_count: commands.length(),
  }
}

///|
pub fn instance_plan_cull(
  instances : Array[InstanceRecord],
  frustum : SpatialFrustum,
  layer_mask : Int,
  thresholds : Array[Double],
  camera : Vec3,
) -> InstancePlan {
  let prepared : Array[InstanceRecord] = []
  let lods : Array[Int] = []
  for instance in instances {
    let visible = instance.visible &&
      (instance.layer & layer_mask) != 0 &&
      spatial_frustum_intersects_bounds(frustum, instance.bounds)
    let distance = Double::sqrt(
      spatial_distance_squared(spatial_aabb_center(instance.bounds), camera),
    )
    let lod = instance_lod_for_distance(distance, thresholds, instance.lod_bias)
    prepared.push({ ..instance, visible, })
    lods.push(lod)
  }
  instance_plan(prepared, lods)
}

///|
pub fn instance_validate_plan(
  plan : InstancePlan,
  policy : InstancePolicy,
) -> InstanceBudgetResult {
  let violations : Array[String] = []
  if plan.instances.length() > policy.max_instances {
    violations.push("instance count exceeds policy")
  }
  if plan.batch_count > policy.max_batches {
    violations.push("batch count exceeds policy")
  }
  if plan.draw_count > policy.max_draws {
    violations.push("draw count exceeds policy")
  }
  for instance in plan.instances {
    if !policy.allow_negative_scale &&
      (
        instance.transform.scale.x < 0.0 ||
        instance.transform.scale.y < 0.0 ||
        instance.transform.scale.z < 0.0
      ) {
      violations.push("negative scale is not allowed")
      break
    }
  }
  { passed: violations.length() == 0, violations, plan }
}

///|
pub fn instance_batch_reduction(plan : InstancePlan) -> Double {
  if plan.visible_instances == 0 {
    return 0.0
  }
  1.0 -
  Double::from_int(plan.draw_count) / Double::from_int(plan.visible_instances)
}

///|
pub fn instance_plan_lines(plan : InstancePlan) -> Array[String] {
  [
    "instances=\{plan.instances.length()}",
    "visible_instances=\{plan.visible_instances}",
    "culled_instances=\{plan.culled_instances}",
    "batches=\{plan.batch_count}",
    "draws=\{plan.draw_count}",
    "batch_reduction=\{instance_batch_reduction(plan)}",
  ]
}