///|
pub(all) struct LodQualitySample {
  level : Int
  triangle_count : Int
  vertex_count : Int
  geometric_error : Double
  texture_error : Double
  silhouette_error : Double
  memory_bytes : Int
  screen_error : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LodQualityProfile {
  samples : Array[LodQualitySample]
  best_level : Int
  worst_level : Int
  total_memory_bytes : Int
  total_triangles : Int
  monotonic : Bool
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LodSelectionPolicy {
  error_tolerance : Double
  screen_height : Double
  hysteresis : Double
  minimum_level : Int
  maximum_level : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LodSelection {
  level : Int
  distance : Double
  screen_error : Double
  switched : Bool
  reason : String
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LodBudget {
  max_triangles : Int
  max_memory_bytes : Int
  max_error : Double
  require_monotonic : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LodBudgetResult {
  passed : Bool
  violations : Array[String]
  profile : LodQualityProfile
} derive(Debug, ToJson, Eq)

///|
pub fn lod_sample(
  level : Int,
  triangles : Int,
  vertices : Int,
  geometric_error : Double,
  texture_error : Double,
  silhouette_error : Double,
  memory_bytes : Int,
) -> LodQualitySample {
  let total_error = geometric_error + texture_error + silhouette_error
  {
    level,
    triangle_count: triangles,
    vertex_count: vertices,
    geometric_error,
    texture_error,
    silhouette_error,
    memory_bytes,
    screen_error: total_error,
  }
}

///|
pub fn lod_sample_screen_error(
  sample : LodQualitySample,
  distance : Double,
  screen_height : Double,
) -> Double {
  if distance <= 0.0 || screen_height <= 0.0 {
    sample.screen_error
  } else {
    sample.screen_error * screen_height / distance
  }
}

///|
pub fn lod_sample_is_better(
  left : LodQualitySample,
  right : LodQualitySample,
) -> Bool {
  left.screen_error < right.screen_error ||
  (
    left.screen_error == right.screen_error &&
    left.triangle_count < right.triangle_count
  )
}

///|
pub fn lod_sample_memory_per_triangle(sample : LodQualitySample) -> Double {
  if sample.triangle_count <= 0 {
    0.0
  } else {
    Double::from_int(sample.memory_bytes) /
    Double::from_int(sample.triangle_count)
  }
}

///|
pub fn lod_profile(samples : Array[LodQualitySample]) -> LodQualityProfile {
  let mut best = 0
  let mut worst = 0
  let mut memory = 0
  let mut triangles = 0
  let mut monotonic = true
  for index, sample in samples {
    memory = memory + sample.memory_bytes
    triangles = triangles + sample.triangle_count
    if index > 0 {
      match samples.get(index - 1) {
        Some(previous) => {
          if sample.triangle_count > previous.triangle_count {
            monotonic = false
          }
          if sample.screen_error < previous.screen_error {
            monotonic = false
          }
        }
        None => ()
      }
    }
    match samples.get(best) {
      Some(current) => if lod_sample_is_better(sample, current) { best = index }
      None => best = index
    }
    match samples.get(worst) {
      Some(current) =>
        if sample.screen_error > current.screen_error {
          worst = index
        }
      None => worst = index
    }
  }
  {
    samples,
    best_level: best,
    worst_level: worst,
    total_memory_bytes: memory,
    total_triangles: triangles,
    monotonic,
    valid: samples.length() > 0 && monotonic,
  }
}

///|
pub fn lod_profile_at(
  profile : LodQualityProfile,
  level : Int,
) -> LodQualitySample? {
  for sample in profile.samples {
    if sample.level == level {
      return Some(sample)
    }
  }
  None
}

///|
pub fn lod_profile_errors(profile : LodQualityProfile) -> Array[Double] {
  profile.samples.map(fn(sample) { sample.screen_error })
}

///|
pub fn lod_profile_memory(profile : LodQualityProfile) -> Array[Int] {
  profile.samples.map(fn(sample) { sample.memory_bytes })
}

///|
pub fn lod_profile_triangle_counts(profile : LodQualityProfile) -> Array[Int] {
  profile.samples.map(fn(sample) { sample.triangle_count })
}

///|
pub fn default_lod_selection_policy() -> LodSelectionPolicy {
  {
    error_tolerance: 1.0,
    screen_height: 1080.0,
    hysteresis: 0.1,
    minimum_level: 0,
    maximum_level: 32,
  }
}

///|
pub fn lod_select_level(
  profile : LodQualityProfile,
  distance : Double,
  policy : LodSelectionPolicy,
  previous_level : Int?,
) -> LodSelection {
  let mut selected = policy.maximum_level
  let mut selected_error = 1.0 / 0.0
  for sample in profile.samples {
    if sample.level < policy.minimum_level ||
      sample.level > policy.maximum_level {
      continue
    }
    let error = lod_sample_screen_error(sample, distance, policy.screen_height)
    if error <= policy.error_tolerance && error < selected_error {
      selected = sample.level
      selected_error = error
    }
  }
  if selected_error == 1.0 / 0.0 {
    match profile.samples.get(profile.samples.length() - 1) {
      Some(sample) => {
        selected = sample.level
        selected_error = lod_sample_screen_error(
          sample,
          distance,
          policy.screen_height,
        )
      }
      None => ()
    }
  }
  let switched = match previous_level {
    Some(previous) => previous != selected
    None => true
  }
  let reason = if !switched {
    "hysteresis"
  } else if selected == policy.minimum_level {
    "highest-detail"
  } else {
    "error-budget"
  }
  { level: selected, distance, screen_error: selected_error, switched, reason }
}

///|
pub fn lod_select_many(
  profile : LodQualityProfile,
  distances : Array[Double],
  policy : LodSelectionPolicy,
) -> Array[LodSelection] {
  distances.map(fn(distance) {
    lod_select_level(profile, distance, policy, None)
  })
}

///|
pub fn lod_transition_alpha(
  distance : Double,
  near_distance : Double,
  far_distance : Double,
) -> Double {
  if far_distance <= near_distance {
    if distance >= far_distance {
      1.0
    } else {
      0.0
    }
  } else {
    let value = (distance - near_distance) / (far_distance - near_distance)
    if value < 0.0 {
      0.0
    } else if value > 1.0 {
      1.0
    } else {
      value
    }
  }
}

///|
pub fn lod_budget_result(
  profile : LodQualityProfile,
  budget : LodBudget,
) -> LodBudgetResult {
  let violations : Array[String] = []
  if profile.total_triangles > budget.max_triangles {
    violations.push("total LOD triangles exceed budget")
  }
  if profile.total_memory_bytes > budget.max_memory_bytes {
    violations.push("total LOD memory exceeds budget")
  }
  for sample in profile.samples {
    if sample.screen_error > budget.max_error {
      violations.push("LOD error exceeds budget")
      break
    }
  }
  if budget.require_monotonic && !profile.monotonic {
    violations.push("LOD samples are not monotonic")
  }
  { passed: violations.length() == 0, violations, profile }
}

///|
pub fn lod_quality_score(profile : LodQualityProfile) -> Double {
  if profile.samples.length() == 0 {
    return 0.0
  }
  let mut score = 100.0
  if !profile.monotonic {
    score = score - 30.0
  }
  for sample in profile.samples {
    if sample.geometric_error > 1.0 {
      score = score - 5.0
    }
    if sample.texture_error > 1.0 {
      score = score - 5.0
    }
    if sample.silhouette_error > 1.0 {
      score = score - 5.0
    }
  }
  if score < 0.0 {
    0.0
  } else {
    score
  }
}

///|
pub fn lod_profile_lines(profile : LodQualityProfile) -> Array[String] {
  [
    "levels=\{profile.samples.length()}",
    "best_level=\{profile.best_level}",
    "worst_level=\{profile.worst_level}",
    "total_memory=\{profile.total_memory_bytes}",
    "total_triangles=\{profile.total_triangles}",
    "monotonic=\{profile.monotonic}",
    "quality_score=\{lod_quality_score(profile)}",
  ]
}