///|
/// Heading discovered from a Markdown document.
pub(all) struct MarkdownHeading {
  level : Int
  text : String
  line : Int
} derive(Debug, Eq)

///|
/// Fenced code block discovered from a Markdown document.
pub(all) struct MarkdownCodeBlock {
  language : String
  content : String
  start_line : Int
  end_line : Int
} derive(Debug, Eq)

///|
/// Static facts extracted from a repository Markdown document.
pub(all) struct MarkdownDocument {
  path : String
  headings : Array[MarkdownHeading]
  code_blocks : Array[MarkdownCodeBlock]
  links : Array[String]
  image_links : Array[String]
  command_examples : Array[String]
  line_count : Int
} derive(Debug, Eq)

///|
pub fn MarkdownDocument::has_heading(
  self : MarkdownDocument,
  expected : String,
) -> Bool {
  self.headings.any(heading => heading.text == expected)
}

///|
pub fn MarkdownDocument::has_heading_casefold(
  self : MarkdownDocument,
  expected : String,
) -> Bool {
  self.headings.any(heading => {
    heading.text == expected ||
    heading.text.contains(expected) ||
    expected.contains(heading.text)
  })
}

///|
pub fn MarkdownDocument::heading_location(
  self : MarkdownDocument,
  expected : String,
) -> SourceLocation? {
  for heading in self.headings {
    if heading.text == expected {
      return Some(SourceLocation::new(self.path, heading.line, 1))
    }
  }
  None
}

///|
pub fn MarkdownDocument::has_command(
  self : MarkdownDocument,
  command : String,
) -> Bool {
  self.command_examples.any(example => example.contains(command))
}

///|
pub fn MarkdownDocument::summary(self : MarkdownDocument) -> String {
  "\{self.headings.length()} headings, \{self.code_blocks.length()} code blocks, \{self.links.length()} links"
}

///|
fn heading_level(line : String) -> Int {
  let mut level = 0
  for character in line {
    if character == '#' {
      level += 1
    } else {
      break
    }
  }
  level
}

///|
fn heading_text(line : String, level : Int) -> String {
  if level == 0 || line.length() <= level {
    ""
  } else {
    line[level:].trim().to_owned()
  }
}

///|
fn fence_language(line : String) -> String? {
  let trimmed = line.trim().to_owned()
  if trimmed.has_prefix("```") {
    Some(trimmed[3:].trim().to_owned())
  } else {
    None
  }
}

///|
fn bracket_links(line : String) -> Array[String] {
  let links : Array[String] = []
  let pieces = line.split("](").to_array()
  let mut index = 1
  while index < pieces.length() {
    let piece = pieces[index]
    match piece.split(")").to_array() {
      [target, ..] => links.push(target.to_owned())
      [] => ()
    }
    index += 1
  }
  links
}

///|
fn looks_like_command(line : String) -> Bool {
  let trimmed = line.trim().to_owned()
  trimmed.has_prefix("moon ") ||
  trimmed.has_prefix("git ") ||
  trimmed.has_prefix("curl ") ||
  trimmed.has_prefix("powershell ")
}

///|
/// Parse common Markdown structures without depending on a browser or network.
pub fn parse_markdown(path : String, content : String) -> MarkdownDocument {
  let headings : Array[MarkdownHeading] = []
  let code_blocks : Array[MarkdownCodeBlock] = []
  let links : Array[String] = []
  let image_links : Array[String] = []
  let command_examples : Array[String] = []
  let mut line_number = 1
  let mut active_language : String? = None
  let mut active_start = 0
  let active_lines : Array[String] = []
  for raw in content.split("\n") {
    let line = raw.to_owned()
    match active_language {
      Some(language) =>
        if fence_language(line) is Some(_) {
          let block_content = active_lines.join("\n")
          code_blocks.push({
            language,
            content: block_content,
            start_line: active_start,
            end_line: line_number,
          })
          active_language = None
          active_start = 0
          active_lines.clear()
        } else {
          active_lines.push(line)
          if looks_like_command(line) {
            command_examples.push(line.trim().to_owned())
          }
        }
      None => {
        let level = heading_level(line)
        if level > 0 && line.get_char(level) is Some(' ') {
          headings.push({
            level,
            text: heading_text(line, level),
            line: line_number,
          })
        }
        match fence_language(line) {
          Some(language) => {
            active_language = Some(language)
            active_start = line_number
          }
          None => ()
        }
        let found_links = bracket_links(line)
        for link in found_links {
          if line.contains("![](") || line.contains("![]") {
            image_links.push(link)
          } else {
            links.push(link)
          }
        }
      }
    }
    line_number += 1
  }
  MarkdownDocument::{
    path,
    headings,
    code_blocks,
    links,
    image_links,
    command_examples,
    line_count: line_number - 1,
  }
}

///|
pub fn parse_inventory_markdown(
  inventory : ProjectInventory,
) -> Array[MarkdownDocument] {
  let documents : Array[MarkdownDocument] = []
  for file in inventory.files_with_suffix(".md") {
    documents.push(parse_markdown(file.path, file.content))
  }
  documents
}

///|
pub fn readme_document(inventory : ProjectInventory) -> MarkdownDocument? {
  match inventory.find_file("README.md") {
    Some(file) => Some(parse_markdown(file.path, file.content))
    None =>
      match inventory.find_file("README.mbt.md") {
        Some(file) => Some(parse_markdown(file.path, file.content))
        None => None
      }
  }
}

///|
pub fn documentation_health_report(inventory : ProjectInventory) -> String {
  let builder = StringBuilder()
  for document in parse_inventory_markdown(inventory) {
    builder.write_string(document.path + ": " + document.summary() + "\n")
  }
  builder.to_string()
}