// CSS 选择器子集与声明式字段提取。
//
// 采集器的「提什么」不该写死在代码里。本模块在 moonbit-community/html
// 解析出的 DOM 上做选择器匹配,把「容器 + 字段 → 记录」表达成数据,
// 调用方换站点只需要换选择器,不必改代码、不必重新编译引擎。
//
// 选择器语法(CSS Selectors Level 4 的可实现子集):
//   selector-list := complex ("," complex)*
//   complex       := compound (combinator compound)*
//   combinator    := " "(后代) | ">"(子代)
//   compound      := (type | "*")? simple*
//   simple        := "." name | "#" name
//                  | "[" attr (op value)? "]"
//                  | ":first-child"
//   op            := "=" | "~=" | "^=" | "$=" | "*=" | "|="
//
// 明确不支持(不声明、不假装支持):伪元素、`:not()`/`:has()`、
// 兄弟组合子 `+`/`~`、`nth-child` 表达式、CSS 转义序列。遇到无法识别的
// 语法时 `Selector::parse` 返回空选择器——匹配 0 个节点,而不是猜。

///|
/// 属性谓词:`[name]` 仅判存在,其余按 op 比较。
pub(all) struct AttrPredicate {
  name : String
  op : String // "" | "=" | "~=" | "^=" | "$=" | "*=" | "|="
  value : String
}

///|
/// 单个简单选择器。
pub(all) enum SimpleSelector {
  TagSel(String) // 含 "*"
  ClassSel(String)
  IdSel(String)
  AttrSel(AttrPredicate)
  FirstChildSel
}

///|
/// 复合选择器:一串简单选择器 + 它与前一个复合选择器之间的组合子。
/// `combinator` 取 ""(链首)、" "(后代)、">"(子代)。
pub(all) struct CompoundSelector {
  simples : Array[SimpleSelector]
  combinator : String
}

///|
/// 解析后的选择器:多个以逗号分隔的复杂选择器。
pub(all) struct Selector {
  chains : Array[Array[CompoundSelector]]
}

///|
/// 解析失败或空输入时返回的选择器(匹配 0 个节点)。
pub fn Selector::empty() -> Selector {
  { chains: [], }
}

///|
pub fn Selector::is_empty(self : Selector) -> Bool {
  self.chains.length() == 0
}

///|
fn is_ws(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
fn is_name_char(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9') ||
  c == '-' ||
  c == '_' ||
  c.to_int() > 127
}

///|
fn read_name(cs : Array[Char], from : Int32) -> (String, Int32) {
  let buf = StringBuilder()
  let mut i : Int32 = from
  let n : Int32 = cs.length()
  while i < n && is_name_char(cs[i]) {
    buf.write_char(cs[i])
    i = i + 1
  }
  (buf.to_string(), i)
}

///|
fn trim_ws(s : String) -> String {
  let cs = chars_of(s)
  let n : Int32 = cs.length()
  let mut a : Int32 = 0
  let mut b : Int32 = n
  while a < b && is_ws(cs[a]) {
    a = a + 1
  }
  while b > a && is_ws(cs[b - 1]) {
    b = b - 1
  }
  slice(cs, a, b)
}

///|
fn unquote(s : String) -> String {
  let cs = chars_of(s)
  let n : Int32 = cs.length()
  if n >= 2 &&
    ((cs[0] == '"' && cs[n - 1] == '"') || (cs[0] == '\'' && cs[n - 1] == '\'')) {
    slice(cs, 1, n - 1)
  } else {
    s
  }
}

///|
/// 解析 `[name op value]` 的内部文本。
fn parse_attr_body(body : String) -> AttrPredicate? {
  let cs = chars_of(body)
  let n : Int32 = cs.length()
  let (name, after_name) = read_name(cs, 0)
  if name == "" {
    return None
  }
  let mut i : Int32 = after_name
  while i < n && is_ws(cs[i]) {
    i = i + 1
  }
  if i >= n {
    return Some({ name, op: "", value: "", })
  }
  // 读取操作符:可能是一到两个字符
  let mut op = ""
  if cs[i] == '=' {
    op = "="
    i = i + 1
  } else if i + 1 < n && cs[i + 1] == '=' {
    let c = cs[i]
    if c == '~' || c == '^' || c == '$' || c == '*' || c == '|' {
      op = slice(cs, i, i + 2)
      i = i + 2
    }
  }
  if op == "" {
    return None
  }
  while i < n && is_ws(cs[i]) {
    i = i + 1
  }
  Some({ name, op, value: unquote(trim_ws(slice(cs, i, n))), })
}

///|
/// 扫描出的选择器词法单元。
priv enum SelTok {
  TName(String)
  TClass(String)
  TId(String)
  TAttr(AttrPredicate)
  TFirstChild
  TDesc
  TChild
  TComma
}

///|
fn tokenize_selector(input : String) -> Array[SelTok]? {
  let cs = chars_of(input)
  let n : Int32 = cs.length()
  let out : Array[SelTok] = []
  let mut i : Int32 = 0
  while i < n {
    let c = cs[i]
    if is_ws(c) {
      // 连续空白折叠成一个后代组合子;紧跟在逗号/子代符后不重复记录
      while i < n && is_ws(cs[i]) {
        i = i + 1
      }
      let last = out.length() - 1
      let redundant = match out.length() {
        0 => true
        _ =>
          match out[last] {
            TComma | TChild | TDesc => true
            _ => false
          }
      }
      if !redundant {
        out.push(TDesc)
      }
    } else if c == ',' {
      out.push(TComma)
      i = i + 1
    } else if c == '>' {
      // 覆盖掉紧邻的后代组合子
      match out.length() {
        0 => ()
        _ =>
          match out[out.length() - 1] {
            TDesc => out[out.length() - 1] = TChild
            _ => out.push(TChild)
          }
      }
      i = i + 1
    } else if c == '.' {
      let (name, next) = read_name(cs, i + 1)
      if name == "" {
        return None
      }
      out.push(TClass(name))
      i = next
    } else if c == '#' {
      let (name, next) = read_name(cs, i + 1)
      if name == "" {
        return None
      }
      out.push(TId(name))
      i = next
    } else if c == '[' {
      let mut j : Int32 = i + 1
      let mut closed : Bool = false
      while j < n {
        if cs[j] == ']' {
          closed = true
          break
        }
        j = j + 1
      }
      if !closed {
        return None
      }
      match parse_attr_body(slice(cs, i + 1, j)) {
        Some(pred) => out.push(TAttr(pred))
        None => return None
      }
      i = j + 1
    } else if c == ':' {
      let (name, next) = read_name(cs, i + 1)
      // 只支持 :first-child;其余伪类视为无法识别
      if name != "first-child" {
        return None
      }
      out.push(TFirstChild)
      i = next
    } else if c == '*' {
      out.push(TName("*"))
      i = i + 1
    } else if is_name_char(c) {
      let (name, next) = read_name(cs, i)
      out.push(TName(name))
      i = next
    } else {
      return None
    }
  }
  Some(out)
}

///|
/// 解析选择器字符串。非法语法返回 `Selector::empty()`。
pub fn Selector::parse(input : String) -> Selector {
  let tokens = match tokenize_selector(input) {
    Some(t) => t
    None => return Selector::empty()
  }
  let chains : Array[Array[CompoundSelector]] = []
  let mut chain : Array[CompoundSelector] = []
  let mut simples : Array[SimpleSelector] = []
  let mut pending : String = "" // 待挂到下一个复合选择器上的组合子
  let mut next_comb : String = ""
  for tok in tokens {
    let is_combinator = match tok {
      TComma | TDesc | TChild => true
      _ => false
    }
    if is_combinator {
      match tok {
        TComma => {
          if simples.length() > 0 {
            chain.push({ simples, combinator: pending, })
            simples = []
          }
          if chain.length() > 0 {
            chains.push(chain)
            chain = []
          }
          pending = ""
          next_comb = ""
        }
        TDesc => next_comb = " "
        _ => next_comb = ">"
      }
      continue
    }
    if next_comb != "" && simples.length() > 0 {
      chain.push({ simples, combinator: pending, })
      simples = []
      pending = next_comb
      next_comb = ""
    }
    match tok {
      TName(name) => simples.push(TagSel(name))
      TClass(name) => simples.push(ClassSel(name))
      TId(name) => simples.push(IdSel(name))
      TAttr(pred) => simples.push(AttrSel(pred))
      _ => simples.push(FirstChildSel)
    }
  }
  if simples.length() > 0 {
    chain.push({ simples, combinator: pending, })
  }
  if chain.length() > 0 {
    chains.push(chain)
  }
  { chains, }
}

///|
fn attr_value(el : @html.Element, name : String) -> String? {
  for pair in el.attributes {
    let (k, v) = pair
    if k == name {
      return Some(v)
    }
  }
  None
}

///|
fn split_words(s : String) -> Array[String] {
  let out : Array[String] = []
  let cs = chars_of(s)
  let n : Int32 = cs.length()
  let mut start : Int32 = 0
  let mut i : Int32 = 0
  while i <= n {
    if i == n || is_ws(cs[i]) {
      if i > start {
        out.push(slice(cs, start, i))
      }
      i = i + 1
      start = i
    } else {
      i = i + 1
    }
  }
  out
}

///|
fn starts_with_str(s : String, prefix : String) -> Bool {
  let sc = chars_of(s)
  let pc = chars_of(prefix)
  if pc.length() > sc.length() {
    return false
  }
  let mut i : Int32 = 0
  while i < pc.length() {
    if sc[i] != pc[i] {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn ends_with_str(s : String, suffix : String) -> Bool {
  let sc = chars_of(s)
  let pc = chars_of(suffix)
  if pc.length() > sc.length() {
    return false
  }
  let off = sc.length() - pc.length()
  let mut i : Int32 = 0
  while i < pc.length() {
    if sc[off + i] != pc[i] {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn contains_str(s : String, needle : String) -> Bool {
  let sc = chars_of(s)
  let pc = chars_of(needle)
  if pc.length() == 0 {
    return true
  }
  if pc.length() > sc.length() {
    return false
  }
  let mut i : Int32 = 0
  while i + pc.length() <= sc.length() {
    let mut j : Int32 = 0
    let mut ok : Bool = true
    while j < pc.length() {
      if sc[i + j] != pc[j] {
        ok = false
        break
      }
      j = j + 1
    }
    if ok {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn attr_matches(actual : String, pred : AttrPredicate) -> Bool {
  match pred.op {
    "" => true
    "=" => actual == pred.value
    "~=" => {
      let words = split_words(actual)
      let mut hit : Bool = false
      for w in words {
        if w == pred.value {
          hit = true
          break
        }
      }
      hit
    }
    "^=" => pred.value != "" && starts_with_str(actual, pred.value)
    "$=" => pred.value != "" && ends_with_str(actual, pred.value)
    "*=" => pred.value != "" && contains_str(actual, pred.value)
    "|=" => actual == pred.value || starts_with_str(actual, pred.value + "-")
    _ => false
  }
}

///|
fn is_first_element_child(doc : @html.Document, node : Int32) -> Bool {
  let p = doc.get_parent(node)
  if p < 0 || p == node {
    return false
  }
  for child in doc.get_children(p) {
    match doc.get_node(child) {
      Some(@html.ElementNode(_)) => return child == node
      _ => ()
    }
  }
  false
}

///|
fn matches_simple(
  doc : @html.Document,
  node : Int32,
  el : @html.Element,
  s : SimpleSelector,
) -> Bool {
  match s {
    TagSel(tag) => tag == "*" || el.tag_name == tag
    ClassSel(cls) =>
      match attr_value(el, "class") {
        Some(v) => {
          let words = split_words(v)
          let mut hit : Bool = false
          for w in words {
            if w == cls {
              hit = true
              break
            }
          }
          hit
        }
        None => false
      }
    IdSel(id) =>
      match attr_value(el, "id") {
        Some(v) => v == id
        None => false
      }
    AttrSel(pred) =>
      match attr_value(el, pred.name) {
        Some(v) => attr_matches(v, pred)
        None => false
      }
    FirstChildSel => is_first_element_child(doc, node)
  }
}

///|
fn matches_compound(
  doc : @html.Document,
  node : Int32,
  c : CompoundSelector,
) -> Bool {
  match doc.get_node(node) {
    Some(@html.ElementNode(el)) => {
      for s in c.simples {
        if !matches_simple(doc, node, el, s) {
          return false
        }
      }
      true
    }
    _ => false
  }
}

///|
/// node 是否位于 root 的子树内(含 root 自身)。用来把匹配限制在
/// `extract_records` 的容器内部,避免字段选择器越过容器边界。
fn is_within(doc : @html.Document, node : Int32, root : Int32) -> Bool {
  let mut p = node
  let mut steps : Int32 = 0
  while p >= 0 && steps < 512 {
    if p == root {
      return true
    }
    let q = doc.get_parent(p)
    if q == p {
      return false
    }
    p = q
    steps = steps + 1
  }
  false
}

///|
fn matches_chain(
  doc : @html.Document,
  node : Int32,
  chain : Array[CompoundSelector],
  idx : Int32,
  root : Int32,
) -> Bool {
  if !is_within(doc, node, root) {
    return false
  }
  if !matches_compound(doc, node, chain[idx]) {
    return false
  }
  if idx == 0 {
    return true
  }
  let combinator = chain[idx].combinator
  if combinator == ">" {
    let p = doc.get_parent(node)
    if p < 0 || p == node {
      return false
    }
    return matches_chain(doc, p, chain, idx - 1, root)
  }
  // 后代:沿祖先链逐个尝试
  let mut p = doc.get_parent(node)
  while p >= 0 && p != node {
    if matches_chain(doc, p, chain, idx - 1, root) {
      return true
    }
    p = doc.get_parent(p)
  }
  false
}

///|
fn walk_elements(
  doc : @html.Document,
  node : Int32,
  visit : (Int32) -> Unit,
) -> Unit {
  if node < 0 {
    return
  }
  match doc.get_node(node) {
    Some(@html.ElementNode(el)) => {
      visit(node)
      for child in el.children {
        walk_elements(doc, child, visit)
      }
    }
    _ => ()
  }
}

///|
/// 在整篇文档内按选择器收集节点(文档顺序,不重复)。
fn select_nodes(
  doc : @html.Document,
  sel : Selector,
  root : Int32,
) -> Array[Int32] {
  let out : Array[Int32] = []
  if sel.is_empty() {
    return out
  }
  walk_elements(doc, root, fn(node) {
    for chain in sel.chains {
      if matches_chain(doc, node, chain, chain.length() - 1, root) {
        let _ = out.push(node)
        return
      }
    }
  })
  out
}

///|
/// 选中节点的可见文本(去 script/style/noscript,折叠空白)。
fn node_text(doc : @html.Document, node : Int32) -> String {
  let parts : Array[String] = []
  collect_text(doc, node, parts)
  join_collapsed(parts)
}

///|
/// 按选择器提取文本,返回文档顺序的数组。
pub fn select_texts(html : String, selector : String) -> Array[String] {
  let doc = @html.parse(html)
  let sel = Selector::parse(selector)
  let out : Array[String] = []
  for node in select_nodes(doc, sel, doc.document_element) {
    out.push(node_text(doc, node))
  }
  out
}

///|
/// 按选择器提取指定属性的值;缺该属性的节点被跳过。
pub fn select_attrs(
  html : String,
  selector : String,
  attr : String,
) -> Array[String] {
  let doc = @html.parse(html)
  let sel = Selector::parse(selector)
  let out : Array[String] = []
  for node in select_nodes(doc, sel, doc.document_element) {
    match doc.get_node(node) {
      Some(@html.ElementNode(el)) =>
        match attr_value(el, attr) {
          Some(v) => out.push(v)
          None => ()
        }
      _ => ()
    }
  }
  out
}

///|
/// 按选择器统计匹配节点数。
pub fn select_count(html : String, selector : String) -> Int32 {
  let doc = @html.parse(html)
  select_nodes(doc, Selector::parse(selector), doc.document_element).length()
}

///|
/// 一个待提取字段:名称 + 选择器 + 可选取属性(None 取文本)。
pub(all) struct FieldSpec {
  name : String
  selector : String
  attr : String?
}

///|
/// 文本字段构造。
pub fn FieldSpec::text(name : String, selector : String) -> FieldSpec {
  { name, selector, attr: None, }
}

///|
/// 属性字段构造(如 `FieldSpec::attr("link", "a.next", "href")`)。
pub fn FieldSpec::attr(
  name : String,
  selector : String,
  attr : String,
) -> FieldSpec {
  { name, selector, attr: Some(attr), }
}

///|
/// 在给定节点上取一个字段的值;无匹配返回空串。
fn field_value(doc : @html.Document, root : Int32, spec : FieldSpec) -> String {
  let nodes = select_nodes(doc, Selector::parse(spec.selector), root)
  if nodes.length() == 0 {
    return ""
  }
  let first = nodes[0]
  match spec.attr {
    None => node_text(doc, first)
    Some(name) =>
      match doc.get_node(first) {
        Some(@html.ElementNode(el)) =>
          match attr_value(el, name) {
            Some(v) => v
            None => ""
          }
        _ => ""
      }
  }
}

///|
/// 从整页提取一组字段(每个字段取首个匹配),返回 (字段名, 值) 对。
/// 字段无匹配时值为空串,保证 CSV 列数稳定。
pub fn extract_fields(
  html : String,
  fields : Array[FieldSpec],
) -> Array[(String, String)] {
  let doc = @html.parse(html)
  let out : Array[(String, String)] = []
  for spec in fields {
    out.push((spec.name, field_value(doc, doc.document_element, spec)))
  }
  out
}

///|
/// 列表页采集:对每个容器匹配,在容器子树内提取字段,得到一张表。
///
/// 字段选择器不会越过容器边界(`matches_chain` 的 root 约束),因此
/// 容器内的相对选择器不会误取兄弟容器的内容。
pub fn extract_records(
  html : String,
  container : String,
  fields : Array[FieldSpec],
) -> Array[Array[(String, String)]] {
  let doc = @html.parse(html)
  let out : Array[Array[(String, String)]] = []
  let roots = select_nodes(
    doc,
    Selector::parse(container),
    doc.document_element,
  )
  for root in roots {
    let row : Array[(String, String)] = []
    for spec in fields {
      row.push((spec.name, field_value(doc, root, spec)))
    }
    out.push(row)
  }
  out
}