///|
/// DOCTYPE 序区扫描:底座的 DocType 事件只带根名,SYSTEM/PUBLIC id 与
/// 内部子集边界由本扫描自行提取。返回 None = 无 doctype 或序区畸形
/// (畸形留给底座报错——本扫描只取件,不做判官)。
/// 施工账:ctx.md §11(役B2)。
priv struct DoctypeScan {
  /// 声明结束('>')之后(预组合恢复内容扫描处)
  decl_end : Int32
  system_id : String?
  public_id : String?
  /// 文档 XMLDecl 的版次值(版次闸基准;无 XMLDecl = None = 1.0)。
  /// 只取值不判官——XMLDecl 语法面归底座。施工账:ctx.md §12(役B3)。
  doc_version : String?
  /// 内部子集起点('[' 之后);无子集 = -1
  subset_start : Int32
  /// 内部子集终点(']' 处);无子集 = -1
  subset_end : Int32
  /// XMLDecl standalone=='yes'(闸 S 基准,闸补役);只取值不判官。
  standalone_yes : Bool
}

///|
/// 序区扫描:越过空白 / PI / 注释找  DoctypeScan? {
  let limit = text.length()
  let doc_version = peek_decl_version(text)
  let standalone_yes = peek_decl_standalone(text)
  let mut pos = skip_doctype_space(text, 0)
  while pos < limit {
    let ch = text[pos]
    if ch == '<' {
      if text[pos + 1] == '?' {
        pos = skip_past(text, pos + 2, '?', '>')
        continue
      }
      if text[pos + 1] == '!' && text[pos + 2] == '-' && text[pos + 3] == '-' {
        pos = skip_comment(text, pos + 4)
        continue
      }
      if text[pos + 1] == '!' &&
        text[pos + 2] == 'D' &&
        text[pos + 3] == 'O' &&
        text[pos + 4] == 'C' &&
        text[pos + 5] == 'T' &&
        text[pos + 6] == 'Y' &&
        text[pos + 7] == 'P' &&
        text[pos + 8] == 'E' {
        return scan_doctype_body(text, pos + 9, doc_version, standalone_yes)
      }
      // 根元素起始:序区尽,无 doctype
      return None
    }
    if is_doctype_space(ch) {
      pos = pos + 1
      continue
    }
    return None
  }
  None
}

///|
/// '。畸形(缺名 / 缺引号 / 未闭合)一律 None——判官权在底座。
fn scan_doctype_body(
  text : String,
  start : Int32,
  doc_version : String?,
  standalone_yes : Bool,
) -> DoctypeScan? {
  let limit = text.length()
  let mut pos = skip_doctype_space(text, start)
  let name_start = pos
  while pos < limit && !is_doctype_stop(text[pos]) {
    pos = pos + 1
  }
  if pos == name_start {
    return None // 缺根名
  }
  pos = skip_doctype_space(text, pos)
  let mut system_id : String? = None
  let mut public_id : String? = None
  if has_word(text, pos, "SYSTEM") {
    match read_doctype_quoted(text, pos + 6) {
      Some((value, next)) => {
        system_id = Some(value)
        pos = next
      }
      None => return None
    }
  } else if has_word(text, pos, "PUBLIC") {
    match read_doctype_quoted(text, pos + 6) {
      Some((pubid, next)) =>
        match read_doctype_quoted(text, skip_doctype_space(text, next)) {
          Some((sysid, next2)) => {
            public_id = Some(pubid)
            system_id = Some(sysid)
            pos = next2
          }
          None => return None
        }
      None => return None
    }
  }
  pos = skip_doctype_space(text, pos)
  if pos < limit && text[pos] == '[' {
    let subset_start = pos + 1
    match scan_subset_end(text, subset_start) {
      Some(subset_end) => {
        pos = skip_doctype_space(text, subset_end + 1)
        if pos < limit && text[pos] == '>' {
          Some({
            decl_end: pos + 1,
            system_id,
            public_id,
            doc_version,
            subset_start,
            subset_end,
            standalone_yes,
          })
        } else {
          None // 子集后无收口 '>'
        }
      }
      None => None
    }
  } else if pos < limit && text[pos] == '>' {
    Some({
      decl_end: 0,
      system_id,
      public_id,
      doc_version,
      subset_start: -1,
      subset_end: -1,
      standalone_yes,
    })
  } else {
    None // 声明未收口
  }
}

///|
/// 子集括号配对:quote 态(单双引号字面量)与注释态感知;'|"' 值内
/// 与注释内的 ']' 不结束。返回 ']' 处位;未闭合 = None。
fn scan_subset_end(text : String, start : Int32) -> Int32? {
  let limit = text.length()
  let mut pos = start
  let mut quote : UInt16? = None
  let mut in_comment = false
  let mut in_pi = false
  while pos < limit {
    let ch = text[pos]
    if in_comment {
      if ch == '-' && text[pos + 1] == '-' && text[pos + 2] == '>' {
        pos = pos + 3
        in_comment = false
        continue
      }
      pos = pos + 1
      continue
    }
    if in_pi {
      if ch == '?' && text[pos + 1] == '>' {
        pos = pos + 2
        in_pi = false
        continue
      }
      pos = pos + 1
      continue
    }
    match quote {
      Some(open) => {
        if ch == open {
          quote = None
        }
        pos = pos + 1
        continue
      }
      None => ()
    }
    if ch == '<' &&
      text[pos + 1] == '!' &&
      text[pos + 2] == '-' &&
      text[pos + 3] == '-' {
      in_comment = true
      pos = pos + 4
      continue
    }
    if ch == '<' && text[pos + 1] == '?' {
      in_pi = true
      pos = pos + 2
      continue
    }
    if ch == '\'' || ch == '"' {
      quote = Some(ch)
      pos = pos + 1
      continue
    }
    if ch == ']' {
      return Some(pos)
    }
    pos = pos + 1
  }
  None
}

///|
/// 引号字面量读取(quote 为开引号处);未闭合 = None。
fn read_doctype_quoted(text : String, start : Int32) -> (String, Int32)? {
  let limit = text.length()
  let mut pos = skip_doctype_space(text, start)
  if pos >= limit {
    return None
  }
  let open = text[pos]
  if !(open == '\'' || open == '"') {
    return None
  }
  pos = pos + 1
  let value_start = pos
  while pos < limit && !(text[pos] == open) {
    pos = pos + 1
  }
  if pos >= limit {
    return None
  }
  Some((text[value_start:pos].to_owned(), pos + 1))
}

///|
/// 词前缀核对(区分边界:后续不得为名字续字——"SYSTEMX" 不咬 SYSTEM)。
fn has_word(text : String, pos : Int32, word : String) -> Bool {
  let word_length = word.length()
  if pos + word_length > text.length() {
    return false
  }
  if text[pos:pos + word_length].to_owned() != word {
    return false
  }
  // 边界:词后必须是空白或结构符(引号/'['/'>'/'='/文末;Eq 紧随
  // version 词——`version='1.1'` 形)
  let after = pos + word_length
  after >= text.length() ||
  is_doctype_stop(text[after]) ||
  is_doctype_space(text[after]) ||
  text[after] == '='
}

///|
/// 名字停字:空白与结构符('[' '>' 引号)终止根名扫描。
fn is_doctype_stop(ch : UInt16) -> Bool {
  is_doctype_space(ch) || ch == '[' || ch == '>' || ch == '\'' || ch == '"'
}

///|
fn is_doctype_space(ch : UInt16) -> Bool {
  ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
}

///|
fn skip_doctype_space(text : String, start : Int32) -> Int32 {
  let mut pos = start
  while pos < text.length() && is_doctype_space(text[pos]) {
    pos = pos + 1
  }
  pos
}

///|
/// 越过直到 close1+close2 二字组(如 "?>"/"-->" 前二字);未闭合 =
/// 文末。序区容错扫描——畸形留给底座判。
fn skip_past(
  text : String,
  start : Int32,
  close1 : UInt16,
  close2 : UInt16,
) -> Int32 {
  let limit = text.length()
  let mut pos = start
  while pos < limit {
    if text[pos] == close1 && text[pos + 1] == close2 {
      return pos + 2
    }
    pos = pos + 1
  }
  limit
}

///|
/// 注释跳扫(以三字组 `-->` 结束;skip_past 的二字组吃不掉注释——`--`
/// 后还余 `>`,下轮咬 `>` 报 unexpected content)。未闭合 = 文末
/// (容错扫描,判官在底座)。
fn skip_comment(text : String, start : Int32) -> Int32 {
  let limit = text.length()
  let mut pos = start
  while pos + 2 < limit {
    if text[pos] == '-' && text[pos + 1] == '-' && text[pos + 2] == '>' {
      return pos + 3
    }
    pos = pos + 1
  }
  limit
}

///|
/// XMLDecl 版次值只读取:` String? {
  if !text.has_prefix(" 5 && is_doctype_space(text[5])) {
    return None
  }
  let mut cursor : Int32 = 5
  let mut close : Int32 = -1
  while cursor + 1 < limit {
    if text[cursor] == '?' && text[cursor + 1] == '>' {
      close = cursor
      break
    }
    cursor = cursor + 1
  }
  if close < 0 {
    return None
  }
  let head = text[5:close].to_owned()
  let pos = skip_doctype_space(head, 0)
  if !has_word(head, pos, "version") {
    return None
  }
  // Eq(S? = S?):词后先越空白与 '=',再读引号值(read_doctype_quoted
  // 自带前导空白跳)
  let cursor = skip_doctype_space(head, pos + 7)
  if cursor >= head.length() || head[cursor] != '=' {
    return None
  }
  match read_doctype_quoted(head, cursor + 1) {
    Some((value, _)) => Some(value)
    None => None
  }
}

///|
/// XMLDecl standalone 值探测(P32;闸 S 基准):'standalone' 词后引号值
/// =="yes" 判真;词缺/值 "no"/畸形 = 假(判官权在底座,本扫描只取件)。
fn peek_decl_standalone(text : String) -> Bool {
  if !text.has_prefix(" 5 && is_doctype_space(text[5])) {
    return false
  }
  let mut cursor : Int32 = 5
  let mut close : Int32 = -1
  while cursor + 1 < limit {
    if text[cursor] == '?' && text[cursor + 1] == '>' {
      close = cursor
      break
    }
    cursor = cursor + 1
  }
  if close < 0 {
    return false
  }
  let head = text[5:close].to_owned()
  let head_limit = head.length()
  let mut pos = skip_doctype_space(head, 0)
  while pos < head_limit {
    if has_word(head, pos, "standalone") {
      let cursor = skip_doctype_space(head, pos + 10)
      if cursor < head_limit && head[cursor] == '=' {
        match read_doctype_quoted(head, cursor + 1) {
          Some((value, _)) => return value == "yes"
          None => return false
        }
      }
      return false
    }
    while pos < head_limit && !is_doctype_space(head[pos]) {
      pos = pos + 1
    }
    pos = skip_doctype_space(head, pos)
  }
  false
}