///|
pub(all) struct SimilarityResult {
  func1 : FunctionInfo
  func2 : FunctionInfo
  similarity : Double
  score : Double
}

///|
pub impl Show for SimilarityResult with output(self, logger) {
  logger.write_string(self.func1.name)
  logger.write_string(" <-> ")
  logger.write_string(self.func2.name)
  logger.write_string(" (similarity: ")
  let pct = (self.similarity * 100.0).to_int()
  logger.write_string(pct.to_string())
  logger.write_string("%, score: ")
  let score_int = (self.score * 10.0).to_int()
  logger.write_string((score_int / 10).to_string())
  logger.write_string(".")
  logger.write_string((score_int % 10).to_string())
  logger.write_string(")")
}

///|
pub(all) struct DetectorOptions {
  threshold : Double
  min_lines : Int
  size_penalty : Bool
}

///|
pub fn DetectorOptions::default() -> DetectorOptions {
  { threshold: 0.88, min_lines: 3, size_penalty: true }
}

///|
fn create_tsed_options(options : DetectorOptions) -> TSEDOptions {
  {
    apted_options: APTEDOptions::default(),
    min_lines: options.min_lines,
    min_tokens: None,
    size_penalty: options.size_penalty,
  }
}

///|
fn compare_functions(
  func1 : FunctionInfo,
  func2 : FunctionInfo,
  options : DetectorOptions,
  tsed_options : TSEDOptions,
) -> SimilarityResult? {
  let lines1 = func1.end_line - func1.start_line + 1
  let lines2 = func2.end_line - func2.start_line + 1
  if lines1 < options.min_lines || lines2 < options.min_lines {
    return None
  }
  // Early pruning: skip if tree sizes are too different
  let size1 = func1.tree.get_subtree_size()
  let size2 = func2.tree.get_subtree_size()
  let size_ratio = if size1 > size2 {
    size2.to_double() / size1.to_double()
  } else {
    size1.to_double() / size2.to_double()
  }
  // If size ratio is below threshold, similarity will be low
  if size_ratio < options.threshold * 0.5 {
    return None
  }
  let similarity = calculate_tsed(func1.tree, func2.tree, tsed_options)
  if similarity >= options.threshold {
    let avg_lines = (lines1 + lines2) / 2
    let score = similarity * avg_lines.to_double()
    Some({ func1, func2, similarity, score })
  } else {
    None
  }
}

///|
pub fn detect_similarities(
  source : String,
  options : DetectorOptions,
) -> Array[SimilarityResult] {
  // Delegate to cross-file detection with single file
  let cross_results = detect_cross_file_similarities(
    [("", source)],
    options,
  )
  cross_results.map(fn(r) {
    let (_, _, result) = r
    result
  })
}

///|
pub fn detect_cross_file_similarities(
  sources : Array[(String, String)],
  options : DetectorOptions,
) -> Array[(String, String, SimilarityResult)] {
  let all_functions : Array[(String, FunctionInfo)] = []
  for pair in sources {
    let (filename, source) = pair
    let functions = extract_functions(source)
    for func in functions {
      all_functions.push((filename, func))
    }
  }
  let tsed_options = create_tsed_options(options)
  let results : Array[(String, String, SimilarityResult)] = []
  let n = all_functions.length()
  for i = 0; i < n; i = i + 1 {
    for j = i + 1; j < n; j = j + 1 {
      let (file1, func1) = all_functions[i]
      let (file2, func2) = all_functions[j]
      match compare_functions(func1, func2, options, tsed_options) {
        Some(result) => results.push((file1, file2, result))
        None => ()
      }
    }
  }
  results.sort_by(fn(a, b) {
    let (_, _, res_a) = a
    let (_, _, res_b) = b
    res_b.score.compare(res_a.score)
  })
  results
}