///|
pub(all) struct RuntimeFrameSample {
  frame : Int
  cpu_millis : Double
  gpu_millis : Double
  upload_bytes : Int
  draw_calls : Int
  visible_instances : Int
  triangles : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct RuntimeStats {
  samples : Array[RuntimeFrameSample]
  frame_count : Int
  average_cpu_millis : Double
  average_gpu_millis : Double
  p95_cpu_millis : Double
  p95_gpu_millis : Double
  peak_upload_bytes : Int
  average_draw_calls : Double
  stable : Bool
} derive(Debug, ToJson, Eq)

///|
pub fn runtime_frame_sample(
  frame : Int,
  cpu_millis : Double,
  gpu_millis : Double,
  upload_bytes : Int,
  draw_calls : Int,
  visible_instances : Int,
  triangles : Int,
) -> RuntimeFrameSample {
  {
    frame,
    cpu_millis,
    gpu_millis,
    upload_bytes,
    draw_calls,
    visible_instances,
    triangles,
  }
}

///|
pub fn runtime_sample_average(
  samples : Array[RuntimeFrameSample],
) -> (Double, Double) {
  let mut cpu = 0.0
  let mut gpu = 0.0
  for sample in samples {
    cpu = cpu + sample.cpu_millis
    gpu = gpu + sample.gpu_millis
  }
  let divisor = if samples.length() == 0 {
    1.0
  } else {
    Double::from_int(samples.length())
  }
  (cpu / divisor, gpu / divisor)
}

///|
pub fn runtime_sort_values(values : Array[Double]) -> Unit {
  for pass in 0.. values[index + 1] {
        let old = values[index]
        values[index] = values[index + 1]
        values[index + 1] = old
      }
    }
  }
}

///|
pub fn runtime_percentile(
  values : Array[Double],
  percentile : Double,
) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let sorted = values.copy()
  runtime_sort_values(sorted)
  let p = if percentile < 0.0 {
    0.0
  } else if percentile > 1.0 {
    1.0
  } else {
    percentile
  }
  let index = (p * Double::from_int(sorted.length() - 1)).to_int()
  sorted[index]
}

///|
pub fn runtime_peak_upload(samples : Array[RuntimeFrameSample]) -> Int {
  let mut peak = 0
  for sample in samples {
    if sample.upload_bytes > peak {
      peak = sample.upload_bytes
    }
  }
  peak
}

///|
pub fn runtime_stats(samples : Array[RuntimeFrameSample]) -> RuntimeStats {
  let averages = runtime_sample_average(samples)
  let cpu : Array[Double] = samples.map(fn(sample) { sample.cpu_millis })
  let gpu : Array[Double] = samples.map(fn(sample) { sample.gpu_millis })
  let draw_calls = samples.map(fn(sample) {
    Double::from_int(sample.draw_calls)
  })
  let average_draw = if draw_calls.length() == 0 {
    0.0
  } else {
    draw_calls.fold(init=0.0, fn(total, value) { total + value }) /
    Double::from_int(draw_calls.length())
  }
  let p95_cpu = runtime_percentile(cpu, 0.95)
  let p95_gpu = runtime_percentile(gpu, 0.95)
  {
    samples,
    frame_count: samples.length(),
    average_cpu_millis: averages.0,
    average_gpu_millis: averages.1,
    p95_cpu_millis: p95_cpu,
    p95_gpu_millis: p95_gpu,
    peak_upload_bytes: runtime_peak_upload(samples),
    average_draw_calls: average_draw,
    stable: p95_cpu <= averages.0 * 2.0 && p95_gpu <= averages.1 * 2.0,
  }
}

///|
pub fn runtime_stats_merge(
  left : RuntimeStats,
  right : RuntimeStats,
) -> RuntimeStats {
  let samples = left.samples.copy()
  for sample in right.samples {
    samples.push(sample)
  }
  runtime_stats(samples)
}

///|
pub fn runtime_stats_frame_range(stats : RuntimeStats) -> (Int, Int) {
  match (stats.samples.get(0), stats.samples.get(stats.samples.length() - 1)) {
    (Some(first), Some(last)) => (first.frame, last.frame)
    _ => (0, 0)
  }
}

///|
pub fn runtime_stats_lines(stats : RuntimeStats) -> Array[String] {
  [
    "frames=\{stats.frame_count}",
    "average_cpu_ms=\{stats.average_cpu_millis}",
    "average_gpu_ms=\{stats.average_gpu_millis}",
    "p95_cpu_ms=\{stats.p95_cpu_millis}",
    "p95_gpu_ms=\{stats.p95_gpu_millis}",
    "peak_upload_bytes=\{stats.peak_upload_bytes}",
    "average_draw_calls=\{stats.average_draw_calls}",
    "stable=\{stats.stable}",
  ]
}

///|
pub fn runtime_stats_max_triangles(stats : RuntimeStats) -> Int {
  let mut maximum = 0
  for sample in stats.samples {
    if sample.triangles > maximum {
      maximum = sample.triangles
    }
  }
  maximum
}

///|
pub fn runtime_stats_max_instances(stats : RuntimeStats) -> Int {
  let mut maximum = 0
  for sample in stats.samples {
    if sample.visible_instances > maximum {
      maximum = sample.visible_instances
    }
  }
  maximum
}

///|
pub fn runtime_stats_average_triangles(stats : RuntimeStats) -> Double {
  if stats.samples.length() == 0 {
    return 0.0
  }
  let mut total = 0
  for sample in stats.samples {
    total = total + sample.triangles
  }
  Double::from_int(total) / Double::from_int(stats.samples.length())
}

///|
pub fn runtime_stats_average_instances(stats : RuntimeStats) -> Double {
  if stats.samples.length() == 0 {
    return 0.0
  }
  let mut total = 0
  for sample in stats.samples {
    total = total + sample.visible_instances
  }
  Double::from_int(total) / Double::from_int(stats.samples.length())
}

///|
pub fn runtime_stats_average_upload(stats : RuntimeStats) -> Double {
  if stats.samples.length() == 0 {
    return 0.0
  }
  let mut total = 0
  for sample in stats.samples {
    total = total + sample.upload_bytes
  }
  Double::from_int(total) / Double::from_int(stats.samples.length())
}

///|
pub fn runtime_stats_has_spike(
  stats : RuntimeStats,
  multiplier : Double,
) -> Bool {
  stats.p95_cpu_millis > stats.average_cpu_millis * multiplier ||
  stats.p95_gpu_millis > stats.average_gpu_millis * multiplier
}

///|
pub fn runtime_stats_frame_count(stats : RuntimeStats) -> Int {
  stats.samples.length()
}

///|
pub fn runtime_stats_total_upload(stats : RuntimeStats) -> Int {
  let mut total = 0
  for sample in stats.samples {
    total = total + sample.upload_bytes
  }
  total
}

///|
pub fn runtime_stats_total_draws(stats : RuntimeStats) -> Int {
  let mut total = 0
  for sample in stats.samples {
    total = total + sample.draw_calls
  }
  total
}