///|
pub(all) enum BvhQualityGrade {
  QualityExcellent
  QualityUsable
  QualityNeedsReview
  QualityBlocked
} derive(Eq, @debug.Debug)

///|
pub fn BvhQualityGrade::to_string(self : BvhQualityGrade) -> String {
  match self {
    QualityExcellent => "excellent"
    QualityUsable => "usable"
    QualityNeedsReview => "needs-review"
    QualityBlocked => "blocked"
  }
}

///|
pub fn BvhQualityGrade::recommended_action(self : BvhQualityGrade) -> String {
  match self {
    QualityExcellent => "accept"
    QualityUsable => "accept-with-notes"
    QualityNeedsReview => "inspect-before-import"
    QualityBlocked => "fix-before-import"
  }
}

///|
pub(all) struct BvhQualitySignal {
  severity : BvhIssueSeverity
  code : String
  score_delta : Int
  message : String
} derive(Eq, @debug.Debug)

///|
pub fn BvhQualitySignal::new(
  severity : BvhIssueSeverity,
  code : String,
  score_delta : Int,
  message : String,
) -> BvhQualitySignal {
  { severity, code, score_delta, message }
}

///|
fn quality_signal_from_issue(issue : BvhValidationIssue) -> BvhQualitySignal {
  let delta = match issue.severity {
    IssueError => -40
    IssueWarning => -10
    IssueInfo => 0
  }
  BvhQualitySignal::new(issue.severity, issue.code, delta, issue.message)
}

///|
pub(all) struct BvhQualityReport {
  grade : BvhQualityGrade
  score : Int
  signals : Array[BvhQualitySignal]
  recommended_action : String
} derive(Eq, @debug.Debug)

///|
pub fn BvhQualityReport::new(
  score : Int,
  signals : Array[BvhQualitySignal],
) -> BvhQualityReport {
  let grade = if score >= 90 {
    QualityExcellent
  } else if score >= 75 {
    QualityUsable
  } else if score >= 50 {
    QualityNeedsReview
  } else {
    QualityBlocked
  }
  { grade, score, signals, recommended_action: grade.recommended_action() }
}

///|
pub fn BvhQualityReport::ok_for_import(self : BvhQualityReport) -> Bool {
  self.grade == QualityExcellent || self.grade == QualityUsable
}

///|
fn clamp_quality_score(score : Int) -> Int {
  if score < 0 {
    0
  } else if score > 100 {
    100
  } else {
    score
  }
}

///|
fn push_motion_quality_signals(
  document : BvhDocument,
  signals : Array[BvhQualitySignal],
) -> Unit {
  if document.motion.frame_count < 2 {
    signals.push(
      BvhQualitySignal::new(
        IssueWarning,
        "very-short-motion",
        -8,
        "motion has fewer than two frames",
      ),
    )
  }
  if document.motion.frame_time > 0.1 {
    signals.push(
      BvhQualitySignal::new(
        IssueWarning,
        "low-frame-rate",
        -5,
        "frame time is above 0.1s; importer preview may look choppy",
      ),
    )
  }
  if detect_root_teleports(document, 100.0).length() > 0 {
    signals.push(
      BvhQualitySignal::new(
        IssueWarning,
        "root-teleport",
        -12,
        "root motion contains a large per-frame translation jump",
      ),
    )
  }
  if loop_closure_error(document) < 0.001 &&
    root_motion_distance(document) > 0.0 {
    signals.push(
      BvhQualitySignal::new(
        IssueInfo,
        "loop-closes",
        2,
        "first and last root positions are close",
      ),
    )
  }
}

///|
fn quality_score_from_signals(signals : Array[BvhQualitySignal]) -> Int {
  let raw = for signal in signals; score = 100 {
    continue score + signal.score_delta
  } nobreak {
    score
  }
  clamp_quality_score(raw)
}

///|
pub fn quality_report(document : BvhDocument) -> BvhQualityReport {
  let validation = validate_document(document)
  let signals : Array[BvhQualitySignal] = []
  for issue in validation.issues {
    signals.push(quality_signal_from_issue(issue))
  }
  push_motion_quality_signals(document, signals)
  BvhQualityReport::new(quality_score_from_signals(signals), signals)
}

///|
pub fn quality_report_from_bvh(input : String) -> BvhQualityReport {
  let parsed = parse_bvh(input)
  if !parsed.ok {
    BvhQualityReport::new(0, [
      BvhQualitySignal::new(
        IssueError,
        parsed.error.code(),
        -100,
        parsed.error.message,
      ),
    ])
  } else {
    quality_report(parsed.document)
  }
}

///|
pub fn quality_report_to_text(report : BvhQualityReport) -> String {
  let lines : Array[String] = [
    "quality=\{report.grade.to_string()}",
    "score=\{report.score}",
    "action=\{report.recommended_action}",
  ]
  for signal in report.signals {
    lines.push(
      "- \{signal.severity.to_string()} \{signal.code} \{signal.score_delta}: \{signal.message}",
    )
  }
  lines.join("\n")
}

///|
pub fn quality_signal_to_json(signal : BvhQualitySignal) -> String {
  let result =
    $|{"severity":\{json_string(signal.severity.to_string())},"code":\{json_string(signal.code)},"scoreDelta":\{signal.score_delta},"message":\{json_string(signal.message)}}
  result
}

///|
pub fn quality_report_to_json(report : BvhQualityReport) -> String {
  let signals : Array[String] = []
  for signal in report.signals {
    signals.push(quality_signal_to_json(signal))
  }
  let result =
    $|{"grade":\{json_string(report.grade.to_string())},"score":\{report.score},"okForImport":\{report.ok_for_import()},"recommendedAction":\{json_string(report.recommended_action)},"signals":[\{signals.join(",")}]}
  result
}

///|
pub fn quality_report_to_csv(report : BvhQualityReport) -> String {
  let rows : Array[String] = [
    "grade,score,ok_for_import,recommended_action,severity,code,score_delta,message",
  ]
  if report.signals.length() == 0 {
    rows.push(
      "\{report.grade.to_string()},\{report.score},\{report.ok_for_import()},\{report.recommended_action},,,,",
    )
  } else {
    for signal in report.signals {
      rows.push(
        "\{report.grade.to_string()},\{report.score},\{report.ok_for_import()},\{report.recommended_action},\{signal.severity.to_string()},\{signal.code},\{signal.score_delta},\{signal.message}",
      )
    }
  }
  rows.join("\n")
}

///|
pub fn quality_action_for_bvh(input : String) -> String {
  quality_report_from_bvh(input).recommended_action
}