///|
/// 组合面补丁(错误位面重映射账本):顶层引用一级记账——组合文里替换
/// 段跨度 ↔ 原文里引用跨度。嵌套展开不计账(账本不重叠;错误报位归
/// 顶层引用位)。按组合序push,天然升序。
priv struct SpanPatch {
  composed_start : Int32
  composed_end : Int32
  source_start : Int32
  source_length : Int32
}

///|
/// 组合缓冲(带已写长度——StringBuilder 无 O(1) 长度查询)。
priv struct ComposeBuffer {
  inner : StringBuilder
  mut length : Int32
}

///|
fn write_slice(
  buffer : ComposeBuffer,
  text : String,
  from : Int32,
  to : Int32,
) -> Unit {
  if to > from {
    buffer.inner.write_string(text[from:to].to_owned())
    buffer.length = buffer.length + (to - from)
  }
}

///|
/// 单字照抄(String 索引 = UInt16,write_char 要 Char——切片写绕开型差)。
fn write_unit(buffer : ComposeBuffer, text : String, i : Int32) -> Unit {
  buffer.inner.write_string(text[i:i + 1].to_owned())
  buffer.length = buffer.length + 1
}

///|
/// 预组合入口:无标记 / 含标记的外取实体统一走本层预组合(底座注入径已弃用):
/// doctype 扫描 → 内部子集外取形收编(内部优先)→ 外取子集装载 → 内容预展开。
/// 实体表为空 = 无外挂实体,原文直通(逐字节零漂移)。
/// 施工账:ctx.md §11(役B2,R-B1b)。
fn prepare_external(
  source : String,
  resolver : &EntityResolver,
  edition : XmlEdition,
) -> (String, Array[SpanPatch]) raise XmlError {
  match scan_doctype(source) {
    None => (source, [])
    Some(doctype) => {
      let load : ExternalLoad = {
        resolver,
        table: Map([]),
        internal_literals: Map([]),
        pe_values: Map([]),
        pe_externals: Map([]),
        pe_depth: 0,
        doc_version: doctype.doc_version,
        standalone_yes: doctype.standalone_yes,
        edition,
        consumed: 0,
      }
      if doctype.subset_start >= 0 {
        let subset = source[doctype.subset_start:doctype.subset_end].to_owned()
        collect_internal_external_entities(subset, load)
      }
      match doctype.system_id {
        Some(sysid) =>
          // 解析面拒:外取子集不装 → 引用处底座 InvalidEntity 自然拒
          // (fetch-fail = reject,B1 事实 F2 同款)
          match resolver.resolve(None, sysid, doctype.public_id) {
            Some(bytes) => {
              let subset_text = match decode_source(bytes) {
                Ok(raw) => admit_external_text(raw, load.doc_version)
                Err(why) => raise why
              }
              load_external_subset(subset_text, Some(sysid), load)
            }
            None => ()
          }
        None => ()
      }
      claim_internal_chains(load)
      if load.table.length() == 0 {
        (source, [])
      } else {
        preexpand(source, doctype, load)
      }
    }
  }
}

///|
/// 预展开:序区与声明区逐字照抄(序区无引用位;声明区引用是 DTD 面);
/// 内容区上下文感知组合(注释/CDATA/PI 内不展开;属性值位按实体类别
/// 分型——外取件引用 = WFC 违例原样留底座拒)。
fn preexpand(
  source : String,
  doctype : DoctypeScan,
  load : ExternalLoad,
) -> (String, Array[SpanPatch]) raise XmlError {
  let buffer : ComposeBuffer = { inner: StringBuilder(), length: 0, }
  let patches : Array[SpanPatch] = []
  write_slice(buffer, source, 0, doctype.decl_end)
  compose_content(
    buffer,
    patches,
    source,
    doctype.decl_end,
    doctype.decl_end,
    load,
    0,
    false,
  )
  (buffer.inner.to_string(), patches)
}

///|
/// 内容组合器:引用展开 + 区域保护。attr_face = 文本处于属性值位
/// (外取件引用不展开;字面量实体引用合法,照展)。depth>0 = 实体
/// 内容再扫(origin 指向顶层引用在原文的起点——错误与账本坐标归一)。
fn compose_content(
  buffer : ComposeBuffer,
  patches : Array[SpanPatch],
  text : String,
  from : Int32,
  origin : Int32,
  load : ExternalLoad,
  depth : Int32,
  attr_face : Bool,
) -> Unit raise XmlError {
  let limit = text.length()
  let mut i : Int32 = from
  while i < limit {
    let ch = text[i]
    if ch == '&' {
      match read_general_reference(text, i) {
        Some((name, ref_end)) =>
          match load.table.get(name) {
            Some(entry) =>
              match entry {
                InternalText(content) => {
                  expand_entry(
                    buffer,
                    patches,
                    name,
                    content,
                    origin + i,
                    ref_end - i,
                    load,
                    depth,
                    attr_face,
                  )
                  i = ref_end
                }
                ExternalPiece(content) =>
                  if attr_face {
                    // §3.1 WFC:属性值位外取引用 = 违例,我层直拒
                    // (底座对属性位未声明实体放行,不能赖它)
                    raise expansion_error(
                      origin + i,
                      "external entity reference in attribute value at '&\{name};'",
                    )
                  } else {
                    expand_entry(
                      buffer,
                      patches,
                      name,
                      content,
                      origin + i,
                      ref_end - i,
                      load,
                      depth,
                      attr_face,
                    )
                    i = ref_end
                  }
              }
            // 预定义/内部/字符引用:原样留底座展开
            None => {
              buffer.inner.write_char('&')
              buffer.length = buffer.length + 1
              i = i + 1
            }
          }
        None => {
          buffer.inner.write_char('&')
          buffer.length = buffer.length + 1
          i = i + 1
        }
      }
      continue
    }
    if ch == '<' && !attr_face {
      if text[i + 1] == '?' {
        let end = skip_past(text, i + 2, '?', '>')
        write_slice(buffer, text, i, end)
        i = end
        continue
      }
      if text[i + 1] == '!' && text[i + 2] == '-' && text[i + 3] == '-' {
        let end = skip_comment(text, i + 4)
        write_slice(buffer, text, i, end)
        i = end
        continue
      }
      if text[i + 1] == '!' &&
        text[i + 2] == '[' &&
        text[i + 3] == 'C' &&
        text[i + 4] == 'D' &&
        text[i + 5] == 'A' &&
        text[i + 6] == 'T' &&
        text[i + 7] == 'A' &&
        text[i + 8] == '[' {
        i = copy_cdata(buffer, text, i)
        continue
      }
      i = copy_tag(buffer, patches, text, i, origin, load, depth)
      continue
    }
    write_unit(buffer, text, i)
    i = i + 1
  }
}

///|
/// 引用展开:深度闸 + 预算闸(billion-laughs 防御;底座预算闸管它自己
/// 的展开,我层展开自担);顶层(depth 0)记补丁账,嵌套不记(账本
/// 不重叠,错误报位归顶层引用位)。
fn expand_entry(
  buffer : ComposeBuffer,
  patches : Array[SpanPatch],
  name : String,
  content : String,
  source_start : Int32,
  source_length : Int32,
  load : ExternalLoad,
  depth : Int32,
  attr_face : Bool,
) -> Unit raise XmlError {
  if depth >= EXTERNAL_EXPANSION_DEPTH {
    raise expansion_error(
      source_start,
      "entity expansion too deep at '&\{name};'",
    )
  }
  // 闸 S(闸补役,WFC 2.9 Entity Declared):standalone='yes' 时正文/
  // 属性位出现本表实体引用即拒——本表实体皆外取件声明(内部子集字面量
  // 链闭合收归者亦传递含外取内容)。预定义/纯内部实体不入本表,底座
  // 自持,零误伤。
  if depth == 0 && load.standalone_yes {
    raise expansion_error(
      source_start,
      "entity '\{name}' declared in external subset but document declares standalone='yes'",
    )
  }
  load.consumed = load.consumed + content.length()
  if load.consumed > EXTERNAL_EXPANSION_BUDGET {
    raise expansion_error(
      source_start,
      "entity expansion budget exceeded at '&\{name};'",
    )
  }
  if depth == 0 {
    let composed_start = buffer.length
    compose_content(
      buffer,
      patches,
      content,
      0,
      source_start,
      load,
      depth + 1,
      attr_face,
    )
    patches.push({
      composed_start,
      composed_end: buffer.length,
      source_start,
      source_length,
    })
  } else {
    compose_content(
      buffer,
      patches,
      content,
      0,
      source_start,
      load,
      depth + 1,
      attr_face,
    )
  }
}

///|
/// 标签照抄(quote 态感知):标签名/属性名区不展开;引号值区按属性值
/// 位组合(外取件引用原样 = WFC 拒;字面量实体引用展开)。
fn copy_tag(
  buffer : ComposeBuffer,
  patches : Array[SpanPatch],
  text : String,
  start : Int32,
  origin : Int32,
  load : ExternalLoad,
  depth : Int32,
) -> Int32 raise XmlError {
  let limit = text.length()
  buffer.inner.write_char('<')
  buffer.length = buffer.length + 1
  let mut i : Int32 = start + 1
  while i < limit {
    let ch = text[i]
    if ch == '>' {
      buffer.inner.write_char('>')
      buffer.length = buffer.length + 1
      return i + 1
    }
    if ch == '\'' || ch == '"' {
      let open = ch
      write_unit(buffer, text, i)
      i = i + 1
      let value_start = i
      while i < limit && text[i] != open {
        i = i + 1
      }
      let value_end = if i < limit { i } else { limit }
      let value = text[value_start:value_end].to_owned()
      compose_content(
        buffer,
        patches,
        value,
        0,
        origin + value_start,
        load,
        depth,
        true,
      )
      if i < limit {
        write_unit(buffer, text, i)
        i = i + 1
      }
      continue
    }
    write_unit(buffer, text, i)
    i = i + 1
  }
  limit
}

///|
/// CDATA 照抄(以 `]]>` 结束;未闭合 = 到文末,由底座判)。
fn copy_cdata(buffer : ComposeBuffer, text : String, start : Int32) -> Int32 {
  let limit = text.length()
  let mut j : Int32 = start + 9
  let mut end : Int32 = limit
  while j + 2 < limit {
    if text[j] == ']' && text[j + 1] == ']' && text[j + 2] == '>' {
      end = j + 3
      break
    }
    j = j + 1
  }
  write_slice(buffer, text, start, end)
  end
}

///|
/// GE 引用读取(&name;;&# 字符引用与残 '&' 不收)。名字停字 = ';'
/// / 空白 / '<' / '&'。
fn read_general_reference(text : String, start : Int32) -> (String, Int32)? {
  let limit = text.length()
  let mut i : Int32 = start + 1
  if i < limit && text[i] == '#' {
    return None
  }
  while i < limit {
    let ch = text[i]
    if ch == ';' {
      let name = text[start + 1:i].to_owned()
      if name.length() == 0 {
        return None
      }
      return Some((name, i + 1))
    }
    if ch == ' ' ||
      ch == '\t' ||
      ch == '\r' ||
      ch == '\n' ||
      ch == '<' ||
      ch == '&' {
      return None
    }
    i = i + 1
  }
  None
}

///|
/// 展开闸错误(span = 原文引用位 offset;行列零位报——病态路径,
/// 消息带引用名,E1 IllegalName 零位报同款)。
fn expansion_error(offset : Int32, message : String) -> XmlError {
  let position : SourcePosition = { offset, line: 0, column: 0, }
  XmlError::At(error=XmlErrorKind::External(message), span={
    start: position,
    end: position,
  })
}

///|
/// 补丁账本重映射:组合文 offset → 原文 offset。账前段累计平移;
/// 账内 = 归引用位(报位诚实:错误在该次插入的源头)。
fn remap_position(offset : Int32, patches : Array[SpanPatch]) -> Int32 {
  let mut position = offset
  for patch in patches {
    if patch.composed_end <= position {
      position = position +
        patch.source_length -
        (patch.composed_end - patch.composed_start)
    } else if patch.composed_start <= position {
      return patch.source_start
    } else {
      break
    }
  }
  position
}

///|
/// 错误重定位:补丁非空时组合文坐标 → 原文坐标(行列按原文重算,
/// char_span 规则);补丁为空 = 原样透传(零漂移保底)。
fn relocate_error(
  error : XmlError,
  patches : Array[SpanPatch],
  source : String,
) -> XmlError {
  if patches.is_empty() {
    return error
  }
  match error {
    At(error~, span~) => {
      let start_offset = remap_position(span.start.offset, patches)
      let end_offset = remap_position(span.end.offset, patches)
      let start_position = char_span(source, start_offset).start
      let end_position = char_span(source, end_offset).start
      At(error~, span={ start: start_position, end: end_position, })
    }
  }
}

///|
/// 链所有权闭合:内部子集的字面量实体若(传递地)引用到本层收编的实体,
/// 须整链收归本表展开——否则底座展开内部链时撞上本层取走的外取名即 InvEnt。
/// 收归后底座在内容里再也见不到该名(组合面已整链替换);不动链的内部字面量
/// 留给底座,内部优先语义不变。不动点迭代(与账序无关)。
/// 施工账:ctx.md §11(役B2)。
fn claim_internal_chains(load : ExternalLoad) -> Unit {
  let mut changed = true
  while changed {
    changed = false
    for name in load.internal_literals.keys() {
      if load.table.contains(name) {
        continue
      }
      match load.internal_literals.get(name) {
        Some(value) =>
          if literal_touches_table(value, load) {
            load.table.set(name, InternalText(value))
            changed = true
          }
        None => ()
      }
    }
  }
}

///|
/// 字面量值引用探测:值内 `&name;` 形引用(跳 `&#` 字符引用)是否
/// 撞到本表实体。PE 引用(`%..;`)在一般实体值内按 bypass 语义不展开,
/// 不入探。
fn literal_touches_table(value : String, load : ExternalLoad) -> Bool {
  let limit = value.length()
  let mut i : Int32 = 0
  while i < limit {
    if value[i] == '&' && !(i + 1 < limit && value[i + 1] == '#') {
      match read_general_reference(value, i) {
        Some((name, next)) => {
          if load.table.contains(name) {
            return true
          }
          i = next
          continue
        }
        None => ()
      }
    }
    i = i + 1
  }
  false
}