// =============================================================================
// Readability-style Scoring
// =============================================================================

///|
fn build_readability_scores(root : @aom.AccessibilityNode) -> ReadabilityScores {
  let scores : Map[String, Double] = {}
  let candidates : Array[@aom.AccessibilityNode] = []
  let seen : Map[String, Bool] = {}
  let ancestors : Array[@aom.AccessibilityNode] = []
  collect_readability_candidates(root, ancestors, scores, candidates, seen)
  adjust_readability_scores(candidates, scores)
  { scores, candidates }
}

///|
fn collect_readability_candidates(
  node : @aom.AccessibilityNode,
  ancestors : Array[@aom.AccessibilityNode],
  scores : Map[String, Double],
  candidates : Array[@aom.AccessibilityNode],
  seen : Map[String, Bool],
) -> Unit {
  if should_score_for_readability(node) {
    let text_content = get_text_content(node)
    if text_content.length() >= 25 {
      let content_score = calculate_readability_content_score(text_content)
      let depth = ancestors.length()
      let max_depth = if depth < 3 { depth } else { 3 }
      for i in 0.. Bool {
  match node.tag_name {
    Some(tag) =>
      match tag {
        "section" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "td" | "pre" =>
          true
        _ => node.role == @aom.Paragraph || node.role == @aom.Heading
      }
    None => node.role == @aom.Paragraph || node.role == @aom.Heading
  }
}

///|
fn calculate_readability_content_score(text_content : String) -> Double {
  let mut score = 1.0
  let comma_count = count_readability_commas(text_content)
  score = score + comma_count.to_double()
  let text_len = text_content.length()
  let length_bonus = if text_len / 100 > 3 { 3 } else { text_len / 100 }
  score = score + length_bonus.to_double()
  score
}

///|
fn count_readability_commas(text_content : String) -> Int {
  let mut count = 0
  for c in text_content {
    if c == ',' {
      count = count + 1
    }
  }
  count
}

///|
fn ensure_readability_initialized(
  scores : Map[String, Double],
  candidates : Array[@aom.AccessibilityNode],
  seen : Map[String, Bool],
  node : @aom.AccessibilityNode,
) -> Unit {
  match scores.get(node.id) {
    Some(_) => ()
    None => {
      scores[node.id] = initial_readability_score(node)
      match seen.get(node.id) {
        Some(_) => ()
        None => {
          seen[node.id] = true
          candidates.push(node)
        }
      }
    }
  }
}

///|
fn initial_readability_score(node : @aom.AccessibilityNode) -> Double {
  let tag_score = match node.tag_name {
    Some(tag) =>
      match tag {
        "article" | "main" => 10.0
        "div" => 5.0
        "pre" | "td" | "blockquote" => 3.0
        "address" | "ol" | "ul" | "dl" | "dd" | "dt" | "li" | "form" => -3.0
        "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "th" => -5.0
        _ => 0.0
      }
    None =>
      match node.role {
        @aom.Article | @aom.Main => 10.0
        @aom.Blockquote => 3.0
        @aom.Heading => -5.0
        _ => 0.0
      }
  }
  tag_score + calculate_readability_class_weight(node)
}

///|
fn calculate_readability_class_weight(node : @aom.AccessibilityNode) -> Double {
  let mut weight = 0.0
  let selector_lower = match node.selector {
    Some(selector_str) => selector_str.to_lower()
    None => ""
  }
  if selector_lower.length() > 0 {
    if contains_any(selector_lower, readability_negative_patterns()) {
      weight = weight - 25.0
    }
    if contains_any(selector_lower, readability_positive_patterns()) {
      weight = weight + 25.0
    }
  }
  if node.id.length() > 0 {
    let id_lower = node.id.to_lower()
    if contains_any(id_lower, readability_negative_patterns()) {
      weight = weight - 25.0
    }
    if contains_any(id_lower, readability_positive_patterns()) {
      weight = weight + 25.0
    }
  }
  weight
}

///|
fn readability_positive_patterns() -> FixedArray[String] {
  [
    "article", "body", "content", "entry", "hentry", "h-entry", "main", "page", "pagination",
    "post", "text", "blog", "story", "body-text", "bodytext", "articlebody", "article-body",
    "article__body", "entry-content", "post-content", "post-body", "story-body",
  ]
}

///|
fn readability_negative_patterns() -> FixedArray[String] {
  [
    "-ad-", "hidden", " hid ", "banner", "combx", "comment", "com-", "contact", "footer",
    "gdpr", "masthead", "media", "meta", "outbrain", "promo", "related", "scroll",
    "share", "shoutbox", "sidebar", "skyscraper", "sponsor", "shopping", "tags",
    "widget", "newsletter", "subscribe", "signup", "sign-up", "signin", "sign-in",
    "login", "privacy", "terms", "cookie", "policy", "legal",
  ]
}

///|
fn readability_unlikely_patterns() -> FixedArray[String] {
  [
    "-ad-", "ai2html", "banner", "breadcrumbs", "combx", "comment", "community",
    "cover-wrap", "disqus", "extra", "footer", "gdpr", "header", "legends", "menu",
    "related", "remark", "replies", "rss", "shoutbox", "sidebar", "skyscraper", "social",
    "sponsor", "supplemental", "ad-break", "agegate", "pagination", "pager", "popup",
    "yom-remote", "newsletter", "subscribe", "signup", "sign-up", "signin", "sign-in",
    "login", "privacy", "terms", "cookie", "policy", "legal",
  ]
}

///|
fn readability_ok_patterns() -> FixedArray[String] {
  ["and", "article", "body", "column", "content", "main", "shadow"]
}

///|
fn adjust_readability_scores(
  candidates : Array[@aom.AccessibilityNode],
  scores : Map[String, Double],
) -> Unit {
  for candidate in candidates {
    let base_score = get_readability_score(scores, candidate)
    let text_content = get_text_content(candidate)
    let link_density = calculate_link_density(candidate, text_content)
    let mut score = base_score * (1.0 - link_density)
    let text_density = calculate_readability_text_density(candidate)
    if text_density > 0.0 {
      let density_bonus = text_density / 10.0
      let bonus = if density_bonus > 0.1 { 0.1 } else { density_bonus }
      score = score * (1.0 + bonus)
    }
    let repetition_penalty = calculate_repetition_penalty(text_content)
    score = score * repetition_penalty
    let navigation_penalty = calculate_navigation_penalty(
      candidate, text_content,
    )
    score = score * navigation_penalty
    let numeric_penalty = calculate_numeric_penalty(text_content)
    score = score * numeric_penalty
    let table_penalty = calculate_table_penalty(candidate, text_content)
    score = score * table_penalty
    let share_penalty = calculate_share_penalty(text_content)
    score = score * share_penalty
    let caption_penalty = calculate_caption_penalty(text_content)
    score = score * caption_penalty
    let related_penalty = calculate_related_penalty(candidate, text_content)
    score = score * related_penalty
    let heading_bonus = calculate_heading_bonus(candidate)
    score = score * heading_bonus
    let paragraph_factor = calculate_readability_paragraph_factor(candidate)
    score = score * paragraph_factor
    let punctuation_factor = calculate_readability_punctuation_factor(
      text_content,
    )
    score = score * punctuation_factor
    let length_penalty = calculate_readability_length_penalty(
      text_content.length(),
    )
    score = score * length_penalty
    scores[candidate.id] = score
  }
}

///|
fn calculate_readability_punctuation_factor(text_content : String) -> Double {
  let density = get_punctuation_density(text_content)
  if density <= 0.5 {
    0.6
  } else if density <= 1.0 {
    0.8
  } else if density <= 2.0 {
    1.1
  } else if density <= 5.0 {
    1.25
  } else if density <= 8.0 {
    1.05
  } else {
    0.9
  }
}

///|
fn calculate_readability_paragraph_factor(
  node : @aom.AccessibilityNode,
) -> Double {
  let count = count_paragraph_elements(node)
  if count >= 6 {
    1.3
  } else if count >= 4 {
    1.2
  } else if count >= 2 {
    1.1
  } else if count == 1 {
    1.0
  } else {
    0.8
  }
}

///|
fn calculate_readability_length_penalty(text_len : Int) -> Double {
  if text_len < 120 {
    0.4
  } else if text_len < 200 {
    0.6
  } else if text_len < 300 {
    0.8
  } else if text_len <= 2000 {
    1.0
  } else if text_len <= 4000 {
    0.95
  } else if text_len <= 8000 {
    0.9
  } else if text_len <= 16000 {
    0.7
  } else if text_len <= 32000 {
    0.5
  } else {
    0.3
  }
}

///|
fn calculate_readability_text_density(node : @aom.AccessibilityNode) -> Double {
  let text_len = get_text_content(node).length()
  if text_len == 0 {
    return 0.0
  }
  let child_count = node.children.length()
  let denom = if child_count > 0 { child_count.to_double() } else { 1.0 }
  text_len.to_double() / denom
}

///|
fn add_readability_score(
  scores : Map[String, Double],
  node : @aom.AccessibilityNode,
  delta : Double,
) -> Unit {
  let current = match scores.get(node.id) {
    Some(v) => v
    None => 0.0
  }
  scores[node.id] = current + delta
}

///|
fn get_readability_score(
  scores : Map[String, Double],
  node : @aom.AccessibilityNode,
) -> Double {
  match scores.get(node.id) {
    Some(v) => v
    None => 0.0
  }
}

///|
fn is_probably_content(node : @aom.AccessibilityNode) -> Bool {
  if !node.visible {
    return false
  }
  if is_unlikely_candidate(node) {
    return false
  }
  let text_content = get_text_content(node)
  if text_content.length() < 140 {
    return false
  }
  let link_density = calculate_link_density(node, text_content)
  if link_density > 0.5 {
    return false
  }
  let text_density = calculate_readability_text_density(node)
  if text_density < 0.1 {
    return false
  }
  true
}

///|
fn is_unlikely_candidate(node : @aom.AccessibilityNode) -> Bool {
  let selector_lower = match node.selector {
    Some(selector_str) => selector_str.to_lower()
    None => ""
  }
  let id_lower = node.id.to_lower()
  let match_string = if selector_lower.is_empty() {
    id_lower
  } else {
    selector_lower + " " + id_lower
  }
  if contains_any(match_string, readability_unlikely_patterns()) &&
    !contains_any(match_string, readability_ok_patterns()) {
    true
  } else {
    false
  }
}

///|
fn calculate_readability_bonus(readability_score : Double) -> Double {
  if readability_score <= 0.0 {
    1.0
  } else {
    let bonus = 1.0 + readability_score / 8.0
    if bonus > 3.0 {
      3.0
    } else {
      bonus
    }
  }
}

///|
fn calculate_navigation_penalty(
  node : @aom.AccessibilityNode,
  text_content : String,
) -> Double {
  let text_len = text_content.length()
  if text_len == 0 {
    return 1.0
  }
  let text_lower = text_content.to_lower()
  let patterns : FixedArray[String] = [
    "read more", "continue reading", "next", "previous", "next post", "previous post",
    "older", "newer", "related", "recommended", "see also", "more stories", "newsletter",
    "subscribe", "sign in", "sign-in", "signin", "login", "privacy policy", "terms of use",
    "all rights reserved", "copyright", "cookie policy", "terms", "privacy", "you may like",
    "you might also like", "you may also like", "related posts", "related articles",
    "more like this", "vocĂȘ pode gostar", "voce pode gostar", "talvez voce goste",
    "share", "facebook", "twitter", "whatsapp", "pinterest", "linkedin", "instagram",
    "telegram", "email", "compartilhar", "compartilhe", "enviar", "filed under",
    "comments", "comment", "replies", "reply", "securedrop", "secure drop", "signal",
  ]
  let nav_hits = count_any_occurrences(text_lower, patterns)
  if nav_hits == 0 {
    return 1.0
  }
  let social_hits = count_any_occurrences(text_lower, [
    "share", "facebook", "twitter", "whatsapp", "pinterest", "linkedin", "instagram",
    "telegram", "email", "compartilhar", "compartilhe", "enviar", "pint", "baixar",
    "download", "you may like", "you might also like", "voce pode gostar", "vocĂȘ pode gostar",
  ])
  let link_density = calculate_link_density(node, text_content)
  let mut penalty = if nav_hits >= 6 {
    0.4
  } else if nav_hits >= 3 {
    0.6
  } else {
    0.8
  }
  if nav_hits >= 12 && text_len > 3000 {
    penalty = 0.2
  } else if nav_hits >= 6 && text_len > 1000 && penalty > 0.3 {
    penalty = 0.3
  }
  if text_len < 200 {
    if penalty > 0.3 {
      penalty = 0.3
    }
  } else if text_len < 400 && link_density > 0.2 {
    if penalty > 0.5 {
      penalty = 0.5
    }
  } else if link_density > 0.5 {
    if penalty > 0.6 {
      penalty = 0.6
    }
  }
  if social_hits >= 10 {
    penalty = 0.2
  } else if social_hits >= 6 && penalty > 0.3 {
    penalty = 0.3
  }
  penalty
}

///|
fn calculate_repetition_penalty(text_content : String) -> Double {
  let parts = text_content.to_lower().split(" ")
  let mut total = 0
  let mut unique = 0
  let seen : Map[String, Bool] = {}
  for part in parts {
    let word = part.trim().to_owned()
    if word.length() < 3 {
      continue
    }
    total = total + 1
    match seen.get(word) {
      Some(_) => ()
      None => {
        seen[word] = true
        unique = unique + 1
      }
    }
  }
  if total < 12 {
    return 1.0
  }
  let ratio = unique.to_double() / total.to_double()
  if ratio < 0.15 {
    0.2
  } else if ratio < 0.25 {
    0.3
  } else if ratio < 0.35 {
    0.5
  } else if ratio < 0.45 {
    0.8
  } else {
    1.0
  }
}