// Pipeline-level reports, benchmark aggregation, and release-quality gates.

///|
pub(all) struct BenchmarkAggregate {
  count : Int
  total_iterations : Int
  total_features1 : Int
  total_features2 : Int
  total_matches : Int
  mean_features1 : Double
  mean_features2 : Double
  mean_matches : Double
  mean_inlier_ratio : Double
  minimum_ms : Double
  maximum_ms : Double
  mean_ms : Double
}

///|
pub fn aggregate_benchmarks(
  samples : Array[BenchmarkResult],
) -> BenchmarkAggregate {
  if samples.length() == 0 {
    return {
      count: 0,
      total_iterations: 0,
      total_features1: 0,
      total_features2: 0,
      total_matches: 0,
      mean_features1: 0.0,
      mean_features2: 0.0,
      mean_matches: 0.0,
      mean_inlier_ratio: 0.0,
      minimum_ms: 0.0,
      maximum_ms: 0.0,
      mean_ms: 0.0,
    }
  }
  let mut total_iterations = 0
  let mut total_features1 = 0
  let mut total_features2 = 0
  let mut total_matches = 0
  let mut total_ratio = 0.0
  let mut total_ms = 0.0
  let mut minimum_ms = samples[0].total_ms
  let mut maximum_ms = samples[0].total_ms
  let mut i = 0
  while i < samples.length() {
    let sample = samples[i]
    total_iterations = total_iterations + sample.iterations
    total_features1 = total_features1 + sample.features1
    total_features2 = total_features2 + sample.features2
    total_matches = total_matches + sample.matches
    total_ratio = total_ratio + sample.inlier_ratio
    total_ms = total_ms + sample.total_ms
    if sample.total_ms < minimum_ms {
      minimum_ms = sample.total_ms
    }
    if sample.total_ms > maximum_ms {
      maximum_ms = sample.total_ms
    }
    i = i + 1
  }
  let count = Double::from_int(samples.length())
  {
    count: samples.length(),
    total_iterations,
    total_features1,
    total_features2,
    total_matches,
    mean_features1: Double::from_int(total_features1) / count,
    mean_features2: Double::from_int(total_features2) / count,
    mean_matches: Double::from_int(total_matches) / count,
    mean_inlier_ratio: total_ratio / count,
    minimum_ms,
    maximum_ms,
    mean_ms: total_ms / count,
  }
}

///|
pub fn benchmark_throughput(aggregate : BenchmarkAggregate) -> Double {
  if aggregate.count == 0 || aggregate.mean_ms <= 0.0 {
    0.0
  } else {
    Double::from_int(aggregate.total_iterations) *
    1000.0 /
    (aggregate.mean_ms * Double::from_int(aggregate.count))
  }
}

///|
pub fn benchmark_average_latency(aggregate : BenchmarkAggregate) -> Double {
  if aggregate.total_iterations <= 0 {
    0.0
  } else {
    aggregate.mean_ms / Double::from_int(aggregate.total_iterations)
  }
}

///|
pub fn benchmark_speedup(
  baseline : BenchmarkAggregate,
  candidate : BenchmarkAggregate,
) -> Double {
  if candidate.mean_ms <= 0.0 {
    0.0
  } else {
    baseline.mean_ms / candidate.mean_ms
  }
}

///|
pub fn format_benchmark_aggregate(aggregate : BenchmarkAggregate) -> String {
  "samples=\{aggregate.count} iterations=\{aggregate.total_iterations} mean_features1=\{aggregate.mean_features1} mean_features2=\{aggregate.mean_features2} mean_matches=\{aggregate.mean_matches} mean_ms=\{aggregate.mean_ms} throughput=\{benchmark_throughput(aggregate)}"
}

///|
pub(all) struct PhotometricReport {
  width : Int
  height : Int
  mean : Double
  variance : Double
  minimum : Int
  maximum : Int
  nonzero : Int
  contrast : Double
}

///|
pub fn photometric_report(img : GrayImage) -> PhotometricReport {
  let moments = image_moments(img)
  let mut nonzero = 0
  let mut i = 0
  while i < img.data.length() {
    if img.data[i] > 0 {
      nonzero = nonzero + 1
    }
    i = i + 1
  }
  {
    width: img.width,
    height: img.height,
    mean: moments.0,
    variance: moments.1,
    minimum: moments.2,
    maximum: moments.3,
    nonzero,
    contrast: Double::from_int(moments.3 - moments.2),
  }
}

///|
pub(all) struct PhotometricComparison {
  overlap_width : Int
  overlap_height : Int
  mse : Double
  mean_difference : Double
  foreground_iou : Double
}

///|
pub fn compare_photometric(
  left : GrayImage,
  right : GrayImage,
) -> PhotometricComparison {
  let width = if left.width < right.width { left.width } else { right.width }
  let height = if left.height < right.height {
    left.height
  } else {
    right.height
  }
  let left_report = photometric_report(left)
  let right_report = photometric_report(right)
  {
    overlap_width: width,
    overlap_height: height,
    mse: image_mse(left, right),
    mean_difference: left_report.mean - right_report.mean,
    foreground_iou: binary_iou(left, right),
  }
}

///|
pub fn image_similarity(left : GrayImage, right : GrayImage) -> Double {
  let mse = image_mse(left, right)
  if mse <= 0.0 {
    1.0
  } else {
    1.0 / (1.0 + mse)
  }
}

///|
pub(all) struct PipelineQualityGate {
  passed : Bool
  inlier_ratio : Double
  inlier_count : Int
  match_count : Int
  maximum_error : Double
  score : Double
}

///|
pub fn pipeline_quality_gate(
  inlier_ratio : Double,
  inlier_count : Int,
  match_count : Int,
  maximum_error : Double,
) -> PipelineQualityGate {
  let ratio = if inlier_ratio < 0.0 {
    0.0
  } else if inlier_ratio > 1.0 {
    1.0
  } else {
    inlier_ratio
  }
  let safe_inliers = if inlier_count < 0 { 0 } else { inlier_count }
  let safe_matches = if match_count < 0 { 0 } else { match_count }
  let safe_error = if maximum_error < 0.0 { 0.0 } else { maximum_error }
  let ratio_score = ratio
  let count_score = if safe_inliers >= 20 {
    1.0
  } else {
    Double::from_int(safe_inliers) / 20.0
  }
  let error_score = 1.0 / (1.0 + safe_error)
  let score = ratio_score * count_score * error_score
  {
    passed: ratio >= 0.5 &&
    safe_inliers >= 3 &&
    safe_matches > 0 &&
    safe_error <= 3.0,
    inlier_ratio: ratio,
    inlier_count: safe_inliers,
    match_count: safe_matches,
    maximum_error: safe_error,
    score,
  }
}

///|
pub fn format_quality_gate(gate : PipelineQualityGate) -> String {
  let status = if gate.passed { "passed" } else { "failed" }
  "quality=\{status} ratio=\{gate.inlier_ratio} inliers=\{gate.inlier_count} matches=\{gate.match_count} max_error=\{gate.maximum_error} score=\{gate.score}"
}

///|
pub(all) struct PyramidSummary {
  levels : Int
  original_width : Int
  original_height : Int
  final_width : Int
  final_height : Int
  final_scale : Double
}

///|
pub fn summarize_pyramid(pyramid : ImagePyramid) -> PyramidSummary {
  let levels = pyramid.level_count()
  if levels == 0 {
    return {
      levels: 0,
      original_width: 0,
      original_height: 0,
      final_width: 0,
      final_height: 0,
      final_scale: 0.0,
    }
  }
  let first = pyramid.level(0)
  let last = pyramid.level(levels - 1)
  {
    levels,
    original_width: first.width,
    original_height: first.height,
    final_width: last.width,
    final_height: last.height,
    final_scale: pyramid.scale(levels - 1),
  }
}

///|
pub fn pyramid_pixel_capacity(pyramid : ImagePyramid) -> Int {
  let mut total = 0
  let mut i = 0
  while i < pyramid.level_count() {
    let level = pyramid.level(i)
    total = total + level.width * level.height
    i = i + 1
  }
  total
}

///|
pub fn feature_density(
  keypoints : Array[Keypoint],
  width : Int,
  height : Int,
) -> Double {
  if width <= 0 || height <= 0 {
    0.0
  } else {
    Double::from_int(keypoints.length()) / Double::from_int(width * height)
  }
}

///|
pub fn match_coverage(
  matches : Array[Match],
  query_count : Int,
  train_count : Int,
) -> Double {
  if query_count <= 0 || train_count <= 0 {
    return 0.0
  }
  let graph = summarize_match_graph(matches, query_count, train_count)
  let query_coverage = Double::from_int(graph.unique_queries) /
    Double::from_int(query_count)
  let train_coverage = Double::from_int(graph.unique_trains) /
    Double::from_int(train_count)
  if query_coverage < train_coverage {
    query_coverage
  } else {
    train_coverage
  }
}

///|
pub(all) struct PipelineReport {
  features1 : KeypointStats
  features2 : KeypointStats
  descriptors1 : DescriptorStats
  descriptors2 : DescriptorStats
  matches : MatchGraphStats
  affine_quality : MatchQuality
  homography_quality : MatchQuality
  gate : PipelineQualityGate
}

///|
pub fn analyze_pipeline(result : PipelineResult) -> PipelineReport {
  let feature1 = summarize_keypoints(result.keypoints1)
  let feature2 = summarize_keypoints(result.keypoints2)
  let descriptor1 = summarize_descriptors(result.descriptors1)
  let descriptor2 = summarize_descriptors(result.descriptors2)
  let graph = summarize_match_graph(
    result.matches,
    result.keypoints1.length(),
    result.keypoints2.length(),
  )
  let affine = evaluate_affine_matches(
    result.affine.transform,
    result.matches,
    result.keypoints1,
    result.keypoints2,
    3.0,
  )
  let homography = evaluate_homography_matches(
    result.homography.model,
    result.matches,
    result.keypoints1,
    result.keypoints2,
    3.0,
  )
  let gate = pipeline_quality_gate(
    result.homography.inlier_ratio,
    result.homography.inliers.length(),
    result.matches.length(),
    result.homography.stats.maximum,
  )
  {
    features1: feature1,
    features2: feature2,
    descriptors1: descriptor1,
    descriptors2: descriptor2,
    matches: graph,
    affine_quality: affine,
    homography_quality: homography,
    gate,
  }
}

///|
pub fn format_pipeline_report(report : PipelineReport) -> String {
  "features1=\{report.features1.count} features2=\{report.features2.count} descriptors1=\{report.descriptors1.bytes} descriptors2=\{report.descriptors2.bytes} matches=\{report.matches.match_count} coverage=\{report.matches.unique_queries}/\{report.matches.query_nodes} affine_ratio=\{report.affine_quality.ratio} homography_ratio=\{report.homography_quality.ratio} \{format_quality_gate(report.gate)}"
}

///|
pub fn pipeline_quality_from_result(
  result : PipelineResult,
) -> PipelineQualityGate {
  pipeline_quality_gate(
    result.homography.inlier_ratio,
    result.homography.inliers.length(),
    result.matches.length(),
    result.homography.stats.maximum,
  )
}

///|
pub fn pipeline_is_usable(result : PipelineResult) -> Bool {
  pipeline_quality_from_result(result).passed
}

///|
pub fn pipeline_feature_balance(result : PipelineResult) -> Double {
  let first = result.keypoints1.length()
  let second = result.keypoints2.length()
  if first == 0 && second == 0 {
    return 1.0
  }
  let low = if first < second { first } else { second }
  let high = if first > second { first } else { second }
  if high == 0 {
    0.0
  } else {
    Double::from_int(low) / Double::from_int(high)
  }
}

///|
pub fn pipeline_descriptor_balance(result : PipelineResult) -> Double {
  let first = result.descriptors1.length()
  let second = result.descriptors2.length()
  if first == 0 && second == 0 {
    return 1.0
  }
  let low = if first < second { first } else { second }
  let high = if first > second { first } else { second }
  if high == 0 {
    0.0
  } else {
    Double::from_int(low) / Double::from_int(high)
  }
}

///|
pub fn pipeline_inlier_distance(result : PipelineResult) -> Double {
  if result.homography.inliers.length() == 0 {
    0.0
  } else {
    result.homography.stats.mean
  }
}

///|
pub fn pipeline_summary_lines(result : PipelineResult) -> Array[String] {
  let report = analyze_pipeline(result)
  [
    "features=\{report.features1.count}/\{report.features2.count}",
    "descriptors=\{report.descriptors1.bytes}/\{report.descriptors2.bytes} bytes",
    "matches=\{report.matches.match_count} unique=\{report.matches.unique_queries}/\{report.matches.unique_trains}",
    "homography=\{report.homography_quality.inliers}/\{report.homography_quality.count} mean_error=\{report.homography_quality.mean_residual}",
    format_quality_gate(report.gate),
  ]
}

///|
pub fn benchmark_results_are_comparable(
  left : BenchmarkResult,
  right : BenchmarkResult,
) -> Bool {
  left.width == right.width &&
  left.height == right.height &&
  left.iterations > 0 &&
  right.iterations > 0
}

///|
pub fn benchmark_relative_latency(
  left : BenchmarkResult,
  right : BenchmarkResult,
) -> Double {
  if right.total_ms <= 0.0 {
    0.0
  } else {
    left.total_ms / right.total_ms
  }
}

///|
pub fn benchmark_feature_rate(result : BenchmarkResult) -> Double {
  if result.total_ms <= 0.0 {
    0.0
  } else {
    Double::from_int(result.features1 + result.features2) *
    1000.0 /
    result.total_ms
  }
}

///|
pub fn benchmark_match_rate(result : BenchmarkResult) -> Double {
  if result.total_ms <= 0.0 {
    0.0
  } else {
    Double::from_int(result.matches) * 1000.0 / result.total_ms
  }
}