///|
fn basename(path : String) -> String {
  let parts = path.split("/").to_array()
  let name = parts[parts.length() - 1].to_owned()
  if name.to_lower().has_suffix(".md") {
    name[:name.length() - 3].to_owned()
  } else if name.to_lower().has_suffix(".txt") {
    name[:name.length() - 4].to_owned()
  } else {
    name
  }
}

///|
fn heading(line : String) -> (Int32, String)? {
  if line.has_prefix("    ") || line.has_prefix("\t") {
    return None
  }
  let chars = line.trim().to_owned().to_array()
  let mut count = 0
  while count < chars.length() && chars[count] == '#' {
    count += 1
  }
  if count == 0 ||
    count > 6 ||
    count >= chars.length() ||
    (chars[count] != ' ' && chars[count] != '\t') {
    return None
  }
  let title = String::from_array(chars[count:]).trim().to_owned()
  if title == "" {
    None
  } else {
    Some((count, title))
  }
}

///|
/// Supported metadata is deliberately limited to topic: and tags: [a, b].
pub fn parse_document(doc : Document, preferences : Array[Preference]) -> Page {
  let lines = doc.content
    .split("\n")
    .map(s => s.trim_end(chars="\r").to_owned())
    .to_array()
  let mut front_end = -1
  if doc.path.to_lower().has_suffix(".md") && lines[0].trim() == "---" {
    for i = 1; i < lines.length() && i < 100; i = i + 1 {
      if lines[i].trim() == "---" {
        front_end = i
        break
      }
    }
  }
  let mut topic = "未分类"
  let mut assignment = "unclassified"
  let folders = doc.path.split("/").to_array()
  if folders.length() > 1 {
    topic = folders[0].to_owned()
    assignment = "folder"
  }
  let tags : Array[String] = []
  if front_end >= 0 {
    for i = 1; i < front_end; i = i + 1 {
      let line = lines[i].trim().to_owned()
      if line.has_prefix("topic:") {
        let value = line[6:].trim().to_owned()
        if value != "" {
          topic = value
          assignment = "metadata"
        }
      }
      if line.has_prefix("tags:") {
        let value = line[5:].trim().to_owned()
        if value.has_prefix("[") && value.has_suffix("]") {
          for item in value[1:value.length() - 1].split(",") {
            let tag = item.trim().to_owned()
            if tag != "" && !tags.contains(tag) {
              tags.push(tag)
            }
          }
        }
      }
    }
  }
  let mut title = basename(doc.path)
  let mut title_found = false
  let marks : Array[(Int32, Int32, String)] = []
  let mut fence_char : Char = ' '
  let mut fence_length = 0
  for i, line in lines {
    if i <= front_end {
      continue
    }
    let trimmed = line.trim_start().to_owned()
    let chars = trimmed.to_array()
    if chars.length() >= 3 && (chars[0] == '`' || chars[0] == '~') {
      let mut n = 0
      while n < chars.length() && chars[n] == chars[0] {
        n += 1
      }
      if fence_length == 0 && n >= 3 {
        fence_char = chars[0]
        fence_length = n
        continue
      }
      if chars[0] == fence_char &&
        n >= fence_length &&
        String::from_array(chars[n:]).trim() == "" {
        fence_length = 0
        continue
      }
    }
    if fence_length > 0 {
      continue
    }
    // TXT is literal text: hashes do not become Markdown headings.
    if doc.path.to_lower().has_suffix(".md") {
      if heading(line) is Some((level, text)) {
        marks.push((i, level, text))
        if level == 1 && !title_found {
          title = text
          title_found = true
        }
      }
    }
  }
  let occurrences : Map[String, Int32] = Map([])
  let sections : Array[Section] = []
  for i, mark in marks {
    let (start, level, text) = mark
    let end = if i + 1 < marks.length() {
      marks[i + 1].0
    } else {
      lines.length()
    }
    let normalized = normalize_name(text)
    let occurrence = occurrences.get(normalized).unwrap_or(0)
    occurrences[normalized] = occurrence + 1
    let chunk = lines[start:end].to_owned().join("\n")
    sections.push({
      id: "section:" +
      Json([doc.path, normalized, occurrence.to_string()]).stringify(),
      title: text,
      level,
      evidence: {
        path: doc.path,
        start_line: start + 1,
        end_line: end,
        quote: clip_text(chunk, 1200),
      },
    })
  }
  for pref in preferences {
    if pref.path == doc.path {
      if pref.topic.trim() != "" {
        topic = pref.topic.trim().to_owned()
        assignment = "user"
      }
      if pref.title.trim() != "" {
        title = pref.title.trim().to_owned()
      }
    }
  }
  let body = lines[front_end + 1:].to_owned().join("\n").trim().to_owned()
  {
    id: "doc:" + doc.path,
    path: doc.path,
    title,
    topic,
    assignment,
    sections,
    tags,
    excerpt: clip_text(body, 220),
  }
}

///|
fn clip_text(text : String, limit : Int32) -> String {
  let chars = text.to_array()
  if chars.length() <= limit {
    text
  } else {
    String::from_array(chars[:limit])
  }
}