///|
fn is_public_declaration(line : String) -> Bool {
  let text = line.trim().to_owned()
  text.has_prefix("pub fn ") ||
  text.has_prefix("pub async fn ") ||
  text.has_prefix("pub struct ") ||
  text.has_prefix("pub enum ") ||
  text.has_prefix("pub type ") ||
  text.has_prefix("pub trait ") ||
  text.has_prefix("pub suberror ") ||
  text.has_prefix("pub impl ") ||
  text.has_prefix("pub(all) fn ") ||
  text.has_prefix("pub(all) struct ") ||
  text.has_prefix("pub(all) enum ") ||
  text.has_prefix("declare pub ")
}

///|
fn declaration_kind(line : String) -> String {
  let text = line.trim().to_owned()
  if text.has_prefix("pub async fn ") {
    "async function"
  } else if text.has_prefix("pub fn ") && text.contains("::") {
    "method"
  } else if text.has_prefix("pub fn ") || text.has_prefix("pub(all) fn ") {
    "function"
  } else if text.has_prefix("pub struct ") ||
    text.has_prefix("pub(all) struct ") {
    "struct"
  } else if text.has_prefix("pub enum ") || text.has_prefix("pub(all) enum ") {
    "enum"
  } else if text.has_prefix("pub type ") {
    "type"
  } else if text.has_prefix("pub trait ") {
    "trait"
  } else if text.has_prefix("pub suberror ") {
    "suberror"
  } else if text.has_prefix("pub impl ") {
    "impl"
  } else if text.has_prefix("declare pub ") {
    "declaration"
  } else {
    "unknown"
  }
}

///|
fn strip_leading_keyword(text : String, prefix : String) -> String {
  match text.strip_prefix(prefix) {
    Some(rest) => rest.to_owned().trim().to_owned()
    None => text
  }
}

///|
fn impl_name(text : String) -> String {
  match text.find(" with ") {
    Some(index) => text[:index].to_owned().trim().to_owned()
    None =>
      match text.find("{") {
        Some(index) => text[:index].to_owned().trim().to_owned()
        None => text.trim().to_owned()
      }
  }
}

///|
fn declaration_name(line : String) -> String {
  let mut text = line.trim().to_owned()
  text = strip_leading_keyword(text, "declare pub ")
  text = strip_leading_keyword(text, "pub(all) ")
  text = strip_leading_keyword(text, "pub ")
  text = strip_leading_keyword(text, "async ")
  text = strip_leading_keyword(text, "fn ")
  text = strip_leading_keyword(text, "struct ")
  text = strip_leading_keyword(text, "enum ")
  text = strip_leading_keyword(text, "type ")
  text = strip_leading_keyword(text, "trait ")
  text = strip_leading_keyword(text, "suberror ")
  text = strip_leading_keyword(text, "impl ")
  if line.trim().to_owned().has_prefix("pub impl ") {
    impl_name(text)
  } else {
    let delimiters = if text.contains("::") {
      ["(", "{", " ", "\t"]
    } else {
      ["(", "{", ":", " ", "\t"]
    }
    let mut end = text.length()
    for delimiter in delimiters {
      match text.find(delimiter) {
        Some(index) => if index < end { end = index }
        None => ()
      }
    }
    text[:end].to_owned()
  }
}

///|
fn is_doc_line(line : String) -> Bool {
  let text = line.trim().to_owned()
  text.has_prefix("///") && text != "///|"
}

///|
fn is_blank_or_block_separator(line : String) -> Bool {
  let text = line.trim().to_owned()
  text.is_empty() || text == "///|"
}

///|
fn doc_text(line : String) -> String {
  let text = line.trim().to_owned()
  match text.strip_prefix("///") {
    Some(rest) => rest.to_owned().trim().to_owned()
    None => ""
  }
}

///|
fn has_weak_doc(doc_lines : Array[String], api_name : String) -> Bool {
  if doc_lines.is_empty() {
    false
  } else {
    let joined = doc_lines.join(" ").trim().to_owned()
    let lower = joined.to_lower()
    lower.contains("todo") ||
    lower.contains("fixme") ||
    lower.contains("tbd") ||
    joined.length() < 8 ||
    joined == api_name
  }
}

///|
fn collect_previous_doc(lines : Array[String], index : Int) -> Array[String] {
  let docs : Array[String] = []
  for cursor = index - 1; cursor >= 0; cursor = cursor - 1 {
    let line = lines[cursor]
    if is_doc_line(line) {
      docs.insert(0, doc_text(line))
    } else if is_blank_or_block_separator(line) && docs.is_empty() {
      continue
    } else {
      break
    }
  }
  docs
}

///|
fn declaration_header(lines : Array[String], index : Int) -> String {
  let parts : Array[String] = []
  let limit = if index + 8 < lines.length() {
    index + 8
  } else {
    lines.length() - 1
  }
  for cursor in index..<=limit {
    let text = lines[cursor].trim().to_owned()
    if text.is_empty() {
      break
    }
    parts.push(text)
    if text.contains("(") || text.contains("{") || text.contains(":") {
      break
    }
  }
  parts.join(" ")
}

///|
/// Scan a MoonBit source text and return public API items found in it.
pub fn scan_moon_source(file : String, source : String) -> Array[ApiItem] {
  let lines = source.split("\n").map(line => line.to_owned()).collect()
  let items : Array[ApiItem] = []
  for index, line in lines {
    if is_public_declaration(line) {
      let header = declaration_header(lines, index)
      let name = declaration_name(header)
      let docs = collect_previous_doc(lines, index)
      let documented = !docs.is_empty()
      items.push({
        name,
        kind: declaration_kind(header),
        file,
        line: index + 1,
        documented,
        weak_doc: documented && has_weak_doc(docs, name),
      })
    }
  }
  items
}