// Compact quality dashboards for automated consumers of the feature pipeline.

///|
pub(all) struct FeatureQualityReport {
  count : Int
  density : Double
  response_mean : Double
  response_variance : Double
  spatial_coverage : Double
  border_safe : Int
}

///|
pub(all) struct DescriptorQualityReport {
  count : Int
  bytes : Int
  unique : Int
  duplicate_fraction : Double
  bit_balance : Double
  mean_nearest_distance : Double
}

///|
pub(all) struct QualityDashboard {
  image1 : PhotometricReport
  image2 : PhotometricReport
  features1 : FeatureQualityReport
  features2 : FeatureQualityReport
  descriptors1 : DescriptorQualityReport
  descriptors2 : DescriptorQualityReport
  match_graph : MatchGraphStats
  gate : PipelineQualityGate
  fingerprint : Int
}

///|
pub fn feature_quality_report(
  keypoints : Array[Keypoint],
  width : Int,
  height : Int,
  border : Int,
) -> FeatureQualityReport {
  let summary = summarize_keypoints(keypoints)
  let border_size = if border < 0 { 0 } else { border }
  let safe_count = keypoints.length()
  let mut border_safe = 0
  let mut i = 0
  while i < keypoints.length() {
    if keypoints[i].x >= border_size &&
      keypoints[i].y >= border_size &&
      keypoints[i].x < width - border_size &&
      keypoints[i].y < height - border_size {
      border_safe = border_safe + 1
    }
    i = i + 1
  }
  {
    count: safe_count,
    density: feature_density(keypoints, width, height),
    response_mean: summary.mean_response,
    response_variance: keypoint_response_variance(keypoints),
    spatial_coverage: if width <= 0 || height <= 0 {
      0.0
    } else {
      Double::from_int(rect_area(summary.bounds)) /
      Double::from_int(width * height)
    },
    border_safe,
  }
}

///|
pub fn descriptor_quality_report(
  descriptors : Array[Descriptor],
) -> DescriptorQualityReport {
  let summary = summarize_descriptors(descriptors)
  let profile = descriptor_distance_profile(descriptors)
  let mut total_nearest = 0.0
  let mut i = 0
  while i < profile.length() {
    total_nearest = total_nearest + profile[i]
    i = i + 1
  }
  {
    count: summary.count,
    bytes: summary.bytes,
    unique: descriptor_unique_count(descriptors),
    duplicate_fraction: descriptor_duplicate_fraction(descriptors),
    bit_balance: summary.bit_balance,
    mean_nearest_distance: if profile.length() == 0 {
      0.0
    } else {
      total_nearest / Double::from_int(profile.length())
    },
  }
}

///|
fn quality_hash(value : Int, next : Int) -> Int {
  (value * 65599 + next + 17) & 0x7FFFFFFF
}

///|
pub fn pipeline_fingerprint(result : PipelineResult) -> Int {
  let mut fingerprint = 19
  fingerprint = quality_hash(fingerprint, result.keypoints1.length())
  fingerprint = quality_hash(fingerprint, result.keypoints2.length())
  fingerprint = quality_hash(fingerprint, result.descriptors1.length())
  fingerprint = quality_hash(fingerprint, result.descriptors2.length())
  fingerprint = quality_hash(fingerprint, result.matches.length())
  fingerprint = quality_hash(fingerprint, result.homography.inliers.length())
  let mut i = 0
  while i < result.matches.length() {
    fingerprint = quality_hash(fingerprint, result.matches[i].query_idx)
    fingerprint = quality_hash(fingerprint, result.matches[i].train_idx)
    fingerprint = quality_hash(fingerprint, result.matches[i].distance)
    i = i + 1
  }
  fingerprint
}

///|
pub fn build_quality_dashboard(
  image1 : GrayImage,
  image2 : GrayImage,
  result : PipelineResult,
) -> QualityDashboard {
  {
    image1: photometric_report(image1),
    image2: photometric_report(image2),
    features1: feature_quality_report(
      result.keypoints1,
      image1.width,
      image1.height,
      3,
    ),
    features2: feature_quality_report(
      result.keypoints2,
      image2.width,
      image2.height,
      3,
    ),
    descriptors1: descriptor_quality_report(result.descriptors1),
    descriptors2: descriptor_quality_report(result.descriptors2),
    match_graph: summarize_match_graph(
      result.matches,
      result.keypoints1.length(),
      result.keypoints2.length(),
    ),
    gate: pipeline_quality_from_result(result),
    fingerprint: pipeline_fingerprint(result),
  }
}

///|
pub fn format_quality_dashboard(dashboard : QualityDashboard) -> String {
  "fingerprint=\{dashboard.fingerprint} features=\{dashboard.features1.count}/\{dashboard.features2.count} unique_descriptors=\{dashboard.descriptors1.unique}/\{dashboard.descriptors2.unique} matches=\{dashboard.match_graph.match_count} inlier_ratio=\{dashboard.gate.inlier_ratio} passed=\{dashboard.gate.passed}"
}

///|
pub fn validate_descriptor_set(
  descriptors : Array[Descriptor],
  expected_bytes : Int,
) -> Bool {
  if expected_bytes < 0 {
    return false
  }
  let mut i = 0
  while i < descriptors.length() {
    if descriptors[i].data.length() != expected_bytes {
      return false
    }
    i = i + 1
  }
  true
}

///|
pub fn validate_match_indices(
  matches : Array[Match],
  query_count : Int,
  train_count : Int,
) -> Bool {
  let mut i = 0
  while i < matches.length() {
    if matches[i].query_idx < 0 ||
      matches[i].query_idx >= query_count ||
      matches[i].train_idx < 0 ||
      matches[i].train_idx >= train_count {
      return false
    }
    i = i + 1
  }
  true
}

///|
pub fn quality_gate_score(result : PipelineResult) -> Double {
  pipeline_quality_from_result(result).score
}

///|
pub fn quality_dashboard_is_reproducible(
  first : QualityDashboard,
  second : QualityDashboard,
) -> Bool {
  first.fingerprint == second.fingerprint &&
  first.match_graph.match_count == second.match_graph.match_count &&
  first.gate.passed == second.gate.passed
}

///|
pub(all) struct ThresholdTrial {
  threshold : Int
  keypoints : Int
  descriptors : Int
  mean_response : Double
  score : Double
}

///|
pub fn evaluate_fast_thresholds(
  img : GrayImage,
  thresholds : Array[Int],
) -> Array[ThresholdTrial] {
  let result : Array[ThresholdTrial] = []
  let mut i = 0
  while i < thresholds.length() {
    let config = FastConfig::new(thresholds[i], 9, true, 0, 3, 0.0)
    let keypoints = detect_fast_with_config(img, config)
    let summary = summarize_keypoints(keypoints)
    let score = if keypoints.length() == 0 {
      0.0
    } else {
      summary.mean_response / (1.0 + Double::from_int(keypoints.length()))
    }
    result.push({
      threshold: thresholds[i],
      keypoints: keypoints.length(),
      descriptors: keypoints.length(),
      mean_response: summary.mean_response,
      score,
    })
    i = i + 1
  }
  result
}

///|
pub fn choose_fast_threshold(
  trials : Array[ThresholdTrial],
  minimum_keypoints : Int,
) -> Int? {
  let mut found = false
  let mut best_threshold = 0
  let mut best_score = -1.0
  let minimum = if minimum_keypoints < 0 { 0 } else { minimum_keypoints }
  let mut i = 0
  while i < trials.length() {
    if trials[i].keypoints >= minimum &&
      (!found || trials[i].score > best_score) {
      found = true
      best_threshold = trials[i].threshold
      best_score = trials[i].score
    }
    i = i + 1
  }
  if found {
    Some(best_threshold)
  } else {
    None
  }
}

///|
pub fn feature_response_curve(
  img : GrayImage,
  thresholds : Array[Int],
) -> Array[Double] {
  let trials = evaluate_fast_thresholds(img, thresholds)
  let result = Array::make(trials.length(), 0.0)
  let mut i = 0
  while i < trials.length() {
    result[i] = trials[i].mean_response
    i = i + 1
  }
  result
}

///|
pub fn match_quality_curve(
  matches : Array[Match],
  thresholds : Array[Int],
) -> Array[Double] {
  let result = Array::make(thresholds.length(), 0.0)
  let mut i = 0
  while i < thresholds.length() {
    let mut count = 0
    let mut j = 0
    while j < matches.length() {
      if matches[j].distance <= thresholds[i] {
        count = count + 1
      }
      j = j + 1
    }
    result[i] = if matches.length() == 0 {
      0.0
    } else {
      Double::from_int(count) / Double::from_int(matches.length())
    }
    i = i + 1
  }
  result
}

///|
pub fn quality_weighted_match_score(
  matches : Array[Match],
  maximum_distance : Int,
) -> Double {
  if matches.length() == 0 || maximum_distance <= 0 {
    return 0.0
  }
  let mut total = 0.0
  let mut i = 0
  while i < matches.length() {
    let normalized = Double::from_int(matches[i].distance) /
      Double::from_int(maximum_distance)
    total = total + (if normalized > 1.0 { 0.0 } else { 1.0 - normalized })
    i = i + 1
  }
  total / Double::from_int(matches.length())
}

///|
pub fn clamp_quality(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
pub fn quality_score_from_counts(
  true_positive : Int,
  false_positive : Int,
  false_negative : Int,
) -> Double {
  let tp = if true_positive < 0 { 0 } else { true_positive }
  let fp = if false_positive < 0 { 0 } else { false_positive }
  let fn_count = if false_negative < 0 { 0 } else { false_negative }
  let precision_denominator = tp + fp
  let recall_denominator = tp + fn_count
  let precision = if precision_denominator == 0 {
    0.0
  } else {
    Double::from_int(tp) / Double::from_int(precision_denominator)
  }
  let recall = if recall_denominator == 0 {
    0.0
  } else {
    Double::from_int(tp) / Double::from_int(recall_denominator)
  }
  if precision + recall == 0.0 {
    0.0
  } else {
    2.0 * precision * recall / (precision + recall)
  }
}

///|
pub fn threshold_trial_area(trials : Array[ThresholdTrial]) -> Double {
  if trials.length() < 2 {
    return 0.0
  }
  let mut area = 0.0
  let mut i = 1
  while i < trials.length() {
    let width = Double::from_int(trials[i].threshold - trials[i - 1].threshold)
    area = area + width * (trials[i].score + trials[i - 1].score) / 2.0
    i = i + 1
  }
  if area < 0.0 {
    -area
  } else {
    area
  }
}

///|
pub fn best_threshold_trial(trials : Array[ThresholdTrial]) -> ThresholdTrial? {
  if trials.length() == 0 {
    return None
  }
  let mut best = trials[0]
  let mut i = 1
  while i < trials.length() {
    if trials[i].score > best.score ||
      (trials[i].score == best.score && trials[i].threshold < best.threshold) {
      best = trials[i]
    }
    i = i + 1
  }
  Some(best)
}

///|
pub fn dashboard_score(dashboard : QualityDashboard) -> Double {
  let feature_balance = if dashboard.features1.count == 0 &&
    dashboard.features2.count == 0 {
    1.0
  } else {
    let low = if dashboard.features1.count < dashboard.features2.count {
      dashboard.features1.count
    } else {
      dashboard.features2.count
    }
    let high = if dashboard.features1.count > dashboard.features2.count {
      dashboard.features1.count
    } else {
      dashboard.features2.count
    }
    if high == 0 {
      0.0
    } else {
      Double::from_int(low) / Double::from_int(high)
    }
  }
  clamp_quality(feature_balance * dashboard.gate.score)
}

///|
pub fn dashboard_match_density(dashboard : QualityDashboard) -> Double {
  if dashboard.match_graph.query_nodes <= 0 {
    0.0
  } else {
    Double::from_int(dashboard.match_graph.match_count) /
    Double::from_int(dashboard.match_graph.query_nodes)
  }
}

///|
pub fn dashboard_descriptor_balance(dashboard : QualityDashboard) -> Double {
  let first = dashboard.descriptors1.count
  let second = dashboard.descriptors2.count
  if first == 0 && second == 0 {
    1.0
  } else {
    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 dashboard_pass_rate(dashboards : Array[QualityDashboard]) -> Double {
  if dashboards.length() == 0 {
    return 0.0
  }
  let mut passed = 0
  let mut i = 0
  while i < dashboards.length() {
    if dashboards[i].gate.passed {
      passed = passed + 1
    }
    i = i + 1
  }
  Double::from_int(passed) / Double::from_int(dashboards.length())
}