///|
pub(all) enum BvhImportProfileKind {
  ProfileUnity
  ProfileBlender
  ProfileStrictCI
  ProfilePreview
} derive(Eq, @debug.Debug)

///|
pub fn BvhImportProfileKind::to_string(self : BvhImportProfileKind) -> String {
  match self {
    ProfileUnity => "unity"
    ProfileBlender => "blender"
    ProfileStrictCI => "strict-ci"
    ProfilePreview => "preview"
  }
}

///|
pub(all) struct BvhImportProfile {
  kind : BvhImportProfileKind
  name : String
  max_joint_count : Int
  max_depth : Int
  max_duration_seconds : Double
  min_frame_rate : Double
  max_frame_rate : Double
  max_root_speed : Double
  max_root_teleport : Double
  min_quality_score : Int
  require_root_position : Bool
  allow_unknown_channels : Bool
  allow_child_position_channels : Bool
  require_loop_closure : Bool
} derive(Eq, @debug.Debug)

///|
pub fn BvhImportProfile::new(
  kind : BvhImportProfileKind,
  name : String,
  max_joint_count? : Int = 0,
  max_depth? : Int = 0,
  max_duration_seconds? : Double = 0.0,
  min_frame_rate? : Double = 0.0,
  max_frame_rate? : Double = 0.0,
  max_root_speed? : Double = 0.0,
  max_root_teleport? : Double = 0.0,
  min_quality_score? : Int = 75,
  require_root_position? : Bool = true,
  allow_unknown_channels? : Bool = false,
  allow_child_position_channels? : Bool = false,
  require_loop_closure? : Bool = false,
) -> BvhImportProfile {
  {
    kind,
    name,
    max_joint_count,
    max_depth,
    max_duration_seconds,
    min_frame_rate,
    max_frame_rate,
    max_root_speed,
    max_root_teleport,
    min_quality_score,
    require_root_position,
    allow_unknown_channels,
    allow_child_position_channels,
    require_loop_closure,
  }
}

///|
pub fn unity_import_profile() -> BvhImportProfile {
  BvhImportProfile::new(
    ProfileUnity,
    "Unity humanoid/import gate",
    max_joint_count=256,
    max_depth=64,
    max_duration_seconds=180.0,
    min_frame_rate=24.0,
    max_frame_rate=120.0,
    max_root_speed=250.0,
    max_root_teleport=100.0,
    min_quality_score=75,
    require_root_position=true,
    allow_unknown_channels=false,
    allow_child_position_channels=false,
    require_loop_closure=false,
  )
}

///|
pub fn blender_import_profile() -> BvhImportProfile {
  BvhImportProfile::new(
    ProfileBlender,
    "Blender import gate",
    max_joint_count=512,
    max_depth=96,
    max_duration_seconds=600.0,
    min_frame_rate=12.0,
    max_frame_rate=240.0,
    max_root_speed=500.0,
    max_root_teleport=200.0,
    min_quality_score=75,
    require_root_position=false,
    allow_unknown_channels=false,
    allow_child_position_channels=true,
    require_loop_closure=false,
  )
}

///|
pub fn strict_ci_import_profile() -> BvhImportProfile {
  BvhImportProfile::new(
    ProfileStrictCI,
    "Strict CI asset gate",
    max_joint_count=128,
    max_depth=48,
    max_duration_seconds=120.0,
    min_frame_rate=24.0,
    max_frame_rate=120.0,
    max_root_speed=120.0,
    max_root_teleport=50.0,
    min_quality_score=90,
    require_root_position=true,
    allow_unknown_channels=false,
    allow_child_position_channels=false,
    require_loop_closure=false,
  )
}

///|
pub fn preview_import_profile() -> BvhImportProfile {
  BvhImportProfile::new(
    ProfilePreview,
    "Loose preview gate",
    max_joint_count=1024,
    max_depth=128,
    max_duration_seconds=1200.0,
    min_frame_rate=1.0,
    max_frame_rate=480.0,
    max_root_speed=0.0,
    max_root_teleport=0.0,
    min_quality_score=50,
    require_root_position=false,
    allow_unknown_channels=true,
    allow_child_position_channels=true,
    require_loop_closure=false,
  )
}

///|
pub fn default_import_profiles() -> Array[BvhImportProfile] {
  [
    unity_import_profile(),
    blender_import_profile(),
    strict_ci_import_profile(),
    preview_import_profile(),
  ]
}

///|
pub fn import_profile_by_name(name : String) -> BvhImportProfile {
  match name {
    "unity" | "Unity" => unity_import_profile()
    "blender" | "Blender" => blender_import_profile()
    "strict" | "strict-ci" | "ci" => strict_ci_import_profile()
    "preview" | "loose" => preview_import_profile()
    _ => preview_import_profile()
  }
}

///|
pub fn document_frame_rate(document : BvhDocument) -> Double {
  if document.motion.frame_time > 0.0 {
    1.0 / document.motion.frame_time
  } else {
    0.0
  }
}

///|
fn profile_limit_enabled_int(value : Int) -> Bool {
  value > 0
}

///|
fn profile_limit_enabled_double(value : Double) -> Bool {
  value > 0.0
}

///|
fn profile_limit_severity(profile : BvhImportProfile) -> BvhIssueSeverity {
  match profile.kind {
    ProfileStrictCI => IssueError
    _ => IssueWarning
  }
}

///|
fn profile_issue(
  profile : BvhImportProfile,
  code : String,
  message : String,
  path? : String = "$",
) -> BvhValidationIssue {
  BvhValidationIssue::new(profile_limit_severity(profile), code, message, path~)
}

///|
fn blocking_profile_issue(
  code : String,
  message : String,
  path? : String = "$",
) -> BvhValidationIssue {
  BvhValidationIssue::error(code, message, path~)
}

///|
fn contains_child_position_channels(joint : BvhJoint) -> Bool {
  for child in joint.children {
    if child.position_channel_count() > 0 {
      break true
    }
    if contains_child_position_channels(child) {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn validation_issue_allowed_by_profile(
  issue : BvhValidationIssue,
  profile : BvhImportProfile,
) -> Bool {
  if issue.code == "unknown-channel" && profile.allow_unknown_channels {
    true
  } else if issue.code == "child-position-channels" &&
    profile.allow_child_position_channels {
    true
  } else {
    false
  }
}

///|
fn push_baseline_validation_issues(
  document : BvhDocument,
  profile : BvhImportProfile,
  issues : Array[BvhValidationIssue],
) -> Unit {
  let validation = validate_document(document)
  for issue in validation.issues {
    if issue.severity == IssueError &&
      !validation_issue_allowed_by_profile(issue, profile) {
      issues.push(issue)
    }
  }
}

///|
fn push_shape_profile_issues(
  document : BvhDocument,
  profile : BvhImportProfile,
  issues : Array[BvhValidationIssue],
) -> Unit {
  let stats = analyze_bvh(document)
  if profile.require_root_position &&
    document.root.position_channel_count() == 0 {
    issues.push(
      blocking_profile_issue(
        "profile-root-position-required",
        "profile requires root position channels",
        path=document.root.path,
      ),
    )
  }
  if !profile.allow_unknown_channels && contains_unknown_channels(document.root) {
    issues.push(
      blocking_profile_issue(
        "profile-unknown-channels", "profile does not allow unknown BVH channels",
      ),
    )
  }
  if !profile.allow_child_position_channels &&
    contains_child_position_channels(document.root) {
    issues.push(
      profile_issue(
        profile, "profile-child-position-channels", "profile prefers position channels only on the root joint",
      ),
    )
  }
  if profile_limit_enabled_int(profile.max_joint_count) &&
    stats.joint_count > profile.max_joint_count {
    issues.push(
      profile_issue(
        profile,
        "profile-too-many-joints",
        "joint count \{stats.joint_count} exceeds profile limit \{profile.max_joint_count}",
      ),
    )
  }
  if profile_limit_enabled_int(profile.max_depth) &&
    stats.max_depth > profile.max_depth {
    issues.push(
      profile_issue(
        profile,
        "profile-too-deep",
        "skeleton depth \{stats.max_depth} exceeds profile limit \{profile.max_depth}",
      ),
    )
  }
}

///|
fn push_motion_profile_issues(
  document : BvhDocument,
  profile : BvhImportProfile,
  issues : Array[BvhValidationIssue],
) -> Unit {
  let stats = analyze_bvh(document)
  let fps = document_frame_rate(document)
  if profile_limit_enabled_double(profile.max_duration_seconds) &&
    stats.duration_seconds > profile.max_duration_seconds {
    issues.push(
      profile_issue(
        profile,
        "profile-motion-too-long",
        "duration \{stats.duration_seconds}s exceeds profile limit \{profile.max_duration_seconds}s",
      ),
    )
  }
  if profile_limit_enabled_double(profile.min_frame_rate) &&
    fps < profile.min_frame_rate {
    issues.push(
      profile_issue(
        profile,
        "profile-frame-rate-low",
        "frame rate \{fps} is below profile minimum \{profile.min_frame_rate}",
      ),
    )
  }
  if profile_limit_enabled_double(profile.max_frame_rate) &&
    fps > profile.max_frame_rate {
    issues.push(
      profile_issue(
        profile,
        "profile-frame-rate-high",
        "frame rate \{fps} is above profile maximum \{profile.max_frame_rate}",
      ),
    )
  }
  if profile_limit_enabled_double(profile.max_root_speed) {
    let speed = max_root_speed(document)
    if speed > profile.max_root_speed {
      issues.push(
        profile_issue(
          profile,
          "profile-root-speed-high",
          "max root speed \{speed} exceeds profile limit \{profile.max_root_speed}",
        ),
      )
    }
  }
  if profile_limit_enabled_double(profile.max_root_teleport) {
    let teleports = detect_root_teleports(document, profile.max_root_teleport)
    if teleports.length() > 0 {
      issues.push(
        profile_issue(
          profile,
          "profile-root-teleport",
          "root motion has \{teleports.length()} large translation jump(s)",
        ),
      )
    }
  }
  if profile.require_loop_closure {
    let closure = loop_closure_error(document)
    if closure > 0.001 {
      issues.push(
        blocking_profile_issue(
          "profile-loop-not-closed",
          "loop closure error \{closure} exceeds 0.001",
        ),
      )
    }
  }
}

///|
fn push_quality_profile_issues(
  quality : BvhQualityReport,
  profile : BvhImportProfile,
  issues : Array[BvhValidationIssue],
) -> Unit {
  if quality.score < profile.min_quality_score {
    issues.push(
      blocking_profile_issue(
        "profile-quality-score-low",
        "quality score \{quality.score} is below profile minimum \{profile.min_quality_score}",
      ),
    )
  }
}

///|
pub fn import_profile_issues(
  document : BvhDocument,
  profile : BvhImportProfile,
) -> Array[BvhValidationIssue] {
  let issues : Array[BvhValidationIssue] = []
  push_baseline_validation_issues(document, profile, issues)
  push_shape_profile_issues(document, profile, issues)
  push_motion_profile_issues(document, profile, issues)
  push_quality_profile_issues(quality_report(document), profile, issues)
  issues
}

///|
fn count_issues_with_severity(
  issues : Array[BvhValidationIssue],
  severity : BvhIssueSeverity,
) -> Int {
  for issue in issues; count = 0 {
    if issue.severity == severity {
      continue count + 1
    } else {
      continue count
    }
  } nobreak {
    count
  }
}

///|
pub(all) struct BvhImportDecision {
  profile_name : String
  accepted : Bool
  action : String
  quality : BvhQualityReport
  issues : Array[BvhValidationIssue]
} derive(Eq, @debug.Debug)

///|
pub fn BvhImportDecision::error_count(self : BvhImportDecision) -> Int {
  count_issues_with_severity(self.issues, IssueError)
}

///|
pub fn BvhImportDecision::warning_count(self : BvhImportDecision) -> Int {
  count_issues_with_severity(self.issues, IssueWarning)
}

///|
pub fn BvhImportDecision::issue_count(self : BvhImportDecision) -> Int {
  self.issues.length()
}

///|
pub fn BvhImportDecision::ok(self : BvhImportDecision) -> Bool {
  self.accepted
}

///|
fn import_action(
  accepted : Bool,
  errors : Int,
  warnings : Int,
  quality : BvhQualityReport,
) -> String {
  if accepted {
    if warnings > 0 || quality.grade == QualityUsable {
      "accept-with-notes"
    } else {
      "accept"
    }
  } else if errors > 0 || quality.grade == QualityBlocked {
    "block"
  } else {
    "review"
  }
}

///|
pub fn evaluate_import_profile(
  document : BvhDocument,
  profile : BvhImportProfile,
) -> BvhImportDecision {
  let quality = quality_report(document)
  let issues = import_profile_issues(document, profile)
  let errors = count_issues_with_severity(issues, IssueError)
  let warnings = count_issues_with_severity(issues, IssueWarning)
  let accepted = errors == 0 &&
    quality.score >= profile.min_quality_score &&
    quality.ok_for_import()
  {
    profile_name: profile.name,
    accepted,
    action: import_action(accepted, errors, warnings, quality),
    quality,
    issues,
  }
}

///|
pub fn evaluate_bvh_import_profile(
  input : String,
  profile : BvhImportProfile,
) -> BvhImportDecision {
  let parsed = parse_bvh(input)
  if parsed.ok {
    evaluate_import_profile(parsed.document, profile)
  } else {
    let quality = quality_report_from_bvh(input)
    let issues = [
      BvhValidationIssue::error(
        parsed.error.code(),
        parsed.error.message,
        path="\{parsed.error.line}:\{parsed.error.column}",
      ),
    ]
    {
      profile_name: profile.name,
      accepted: false,
      action: "block",
      quality,
      issues,
    }
  }
}

///|
pub fn evaluate_default_import_profiles(
  document : BvhDocument,
) -> Array[BvhImportDecision] {
  let out : Array[BvhImportDecision] = []
  for profile in default_import_profiles() {
    out.push(evaluate_import_profile(document, profile))
  }
  out
}

///|
pub fn import_profile_to_json(profile : BvhImportProfile) -> String {
  let result =
    $|{"kind":\{json_string(profile.kind.to_string())},"name":\{json_string(profile.name)},"maxJointCount":\{profile.max_joint_count},"maxDepth":\{profile.max_depth},"maxDurationSeconds":\{profile.max_duration_seconds},"minFrameRate":\{profile.min_frame_rate},"maxFrameRate":\{profile.max_frame_rate},"maxRootSpeed":\{profile.max_root_speed},"maxRootTeleport":\{profile.max_root_teleport},"minQualityScore":\{profile.min_quality_score},"requireRootPosition":\{profile.require_root_position},"allowUnknownChannels":\{profile.allow_unknown_channels},"allowChildPositionChannels":\{profile.allow_child_position_channels},"requireLoopClosure":\{profile.require_loop_closure}}
  result
}

///|
fn profile_issue_to_json(issue : BvhValidationIssue) -> String {
  let result =
    $|{"severity":\{json_string(issue.severity.to_string())},"path":\{json_string(issue.path)},"code":\{json_string(issue.code)},"message":\{json_string(issue.message)}}
  result
}

///|
pub fn import_decision_to_json(decision : BvhImportDecision) -> String {
  let issues : Array[String] = []
  for issue in decision.issues {
    issues.push(profile_issue_to_json(issue))
  }
  let result =
    $|{"profile":\{json_string(decision.profile_name)},"accepted":\{decision.accepted},"action":\{json_string(decision.action)},"quality":\{quality_report_to_json(decision.quality)},"issueCount":\{decision.issue_count()},"errors":\{decision.error_count()},"warnings":\{decision.warning_count()},"issues":[\{issues.join(",")}]}
  result
}

///|
pub fn import_decision_to_text(decision : BvhImportDecision) -> String {
  let lines : Array[String] = [
    "profile=\{decision.profile_name}",
    "accepted=\{decision.accepted}",
    "action=\{decision.action}",
    "quality=\{decision.quality.grade.to_string()}",
    "score=\{decision.quality.score}",
  ]
  for issue in decision.issues {
    lines.push(
      "- \{issue.severity.to_string()} \{issue.code}: \{issue.message}",
    )
  }
  lines.join("\n")
}

///|
pub fn import_decisions_to_csv(decisions : Array[BvhImportDecision]) -> String {
  let rows : Array[String] = [
    "profile,accepted,action,quality,score,errors,warnings,issues",
  ]
  for decision in decisions {
    rows.push(
      "\{decision.profile_name},\{decision.accepted},\{decision.action},\{decision.quality.grade.to_string()},\{decision.quality.score},\{decision.error_count()},\{decision.warning_count()},\{decision.issue_count()}",
    )
  }
  rows.join("\n")
}

///|
pub fn import_profiles_to_csv(profiles : Array[BvhImportProfile]) -> String {
  let rows : Array[String] = [
    "kind,name,max_joint_count,max_depth,max_duration_seconds,min_frame_rate,max_frame_rate,max_root_speed,max_root_teleport,min_quality_score,require_root_position,allow_unknown_channels,allow_child_position_channels,require_loop_closure",
  ]
  for profile in profiles {
    rows.push(
      "\{profile.kind.to_string()},\{profile.name},\{profile.max_joint_count},\{profile.max_depth},\{profile.max_duration_seconds},\{profile.min_frame_rate},\{profile.max_frame_rate},\{profile.max_root_speed},\{profile.max_root_teleport},\{profile.min_quality_score},\{profile.require_root_position},\{profile.allow_unknown_channels},\{profile.allow_child_position_channels},\{profile.require_loop_closure}",
    )
  }
  rows.join("\n")
}