///|
using @cmd {trait Scheduler}

///|
#warnings("-unused_constructor")
pub enum VNode {
  Elem(String, Props, Children[VNode], namespace_uri~ : String?)
  Text(String)
  Frag(Array[VNode])
  Thunk(Int, () -> VNode)
}

///|
pub impl Eq for VNode with fn equal(a, b) {
  physical_equal(a, b)
}

///|
pub extend VNode with Eq::{equal, not_equal}

///|
const CAPTURED_LINK_TAG : String = "RABBITA_CAPTURED_LINK"

///|
const FRAGMENT_START_MARKER : String = "["

///|
const FRAGMENT_END_MARKER : String = "]"

///|
fn rendered_tag(tag : String) -> String {
  if tag == CAPTURED_LINK_TAG {
    "a"
  } else {
    tag
  }
}

///|
#cfg(target="js")
fn is_captured_link_tag(tag : String) -> Bool {
  tag == CAPTURED_LINK_TAG
}

///|
fn is_html_void_element(tag : String) -> Bool {
  match tag {
    "area"
    | "base"
    | "br"
    | "col"
    | "embed"
    | "hr"
    | "img"
    | "input"
    | "link"
    | "meta"
    | "param"
    | "source"
    | "track"
    | "wbr" => true
    _ => false
  }
}

///|
pub fn VNode::thunk(hash : Int, f : () -> VNode) -> VNode {
  VNode::Thunk(hash, f)
}

///|
pub fn VNode::elem(
  tag : String,
  props : Props,
  children : Children[VNode],
  namespace_uri? : String,
) -> VNode {
  Elem(tag, props, children, namespace_uri~)
}

///|
pub fn VNode::text(s : String) -> VNode {
  Text(s)
}

///|
pub fn VNode::link(
  props : Props,
  children : Children[VNode],
  escape? : Bool = false,
) -> VNode {
  let tag = if escape { "a" } else { CAPTURED_LINK_TAG }
  VNode::elem(tag, props, children)
}

///|
pub fn VNode::fragment(childs : Array[VNode]) -> VNode {
  Frag(childs)
}

///|
pub fn VNode::document(props : Props, head : VNode, body : VNode) -> VNode {
  Elem("html", props, Array([head, body]), namespace_uri=None)
}

///|
pub(all) enum Children[T] {
  Array(Array[T])
  Map(Map[String, T])
  RawHtml(String)
}

///|
/// ★ moobile 改造:事件值是**后端无关的不透明类型**,不再别名为 `@dom.Event`。
///
/// 为什么必须改:原来的别名让 `Props.handlers` 的**公开签名**带着 `@dom`,
/// 而 `@dom` 是 js-only 的 DOM 绑定包。后果是外部包既命名不了这个类型、
/// 也换不掉它 —— 这正是当初不得不加 `Props::each_handler` 的原因。
///
/// 现在:DOM 后端在监听器边界处把 `@dom.Event` 强转进来(`dom_event`),
/// React 后端直接把合成事件透传。语义上两边都是"一个事件值",
/// 谁也不需要认识对方的具体类型。
#external
pub type Event

///|
/// `@dom.Event` → `Event`。DOM 后端在监听器边界处使用。
#cfg(target="js")
fn dom_event(value : @dom.Event) -> Event = "%identity"

///|
/// `Event` → `@dom.Event`。DOM 侧的辅助函数(从事件里取值)使用。
#cfg(target="js")
pub fn as_dom_event(value : Event) -> @dom.Event = "%identity"

///|
/// ★ moobile 改造(D 轨道性能,见 `docs/PERF.md` §8):属性表 = 字符串键的**不可变**表。
///
/// **为什么必须有这个包装**:`Props` 的四张表在**每个元素每帧**都会被 `Props::copy()`
/// 整份复制一遍 —— 因为元素构造器要一份"私有草稿纸"把自己那些显式参数
/// (`class` / `style` / `on_click` …)写进去(`html.mbt` 里 `resolve_attrs` 之后那串
/// `push_*`)。用可变 `Map` 时"复制"只能逐条重插 ⇒ **实测 0.64 µs/元素**,
/// 占每帧翻译成本的 **28%(N=5000)~36%(N=1000)**(消融探针实测,`PERF.md` §7.3)。
///
/// 换成不可变表之后,`Props::copy()` 退化成**指针拷贝:O(1)、零分配**,
/// 而**"每个元素拿到独立副本"这条语义一点没变** —— 不可变结构的共享是安全的,
/// "改副本"等于用一张新表替换掉那一格,原件不受影响。
///
/// ⚠️ **为什么做成包装类型、而不是把四处字段直接换成 `HashMap`**:
/// 这样 `attrs[k] = v` / `.get(k)` / `.contains(k)` / `for k, v in attrs` 这些写法
/// 在上游那 **~200 处调用点一个字都不用改**(`#alias("_[_]=_")` 接住索引赋值)。
/// 换字段类型的代价是编译器**强制**的(漏一处就编译不过),比手工改写入点安全得多。
pub struct PropsTable[V] {
  mut entries : @hashmap.HashMap[String, V]
  /// ★ **moobile 追加(2026-10-03):让"空不空"变成 O(1)。**
  ///
  /// 为什么需要:core 的 `immut/hashmap` 把 `HashMap::length()` **明确标成 O(N)**
  /// (`HAMT.mbt` 的文档原话:`WARNING: this operation is O(N) in map size`,实现是递归数节点 + 一个闭包)。
  /// 而"这张表空不空"在**每个元素每帧**都要问一次 —— `render.mbt` 的 `render_props` 靠它决定
  /// **要不要写 `style` 键**(空样式表不写,那是给第三方组件库的契约)。
  /// 实测这一项占翻译层 **3.0%**(`PERF.md` §12)。
  ///
  /// 为什么是"保守非空位"而不是缓存精确长度:`set` 走的是 `HashMap::add`、**不查旧值**
  /// (要查就得多一趟哈希,反而更贵),所以精确长度算不出来;而调用点要的**只是"空不空"**。
  /// `remove` 是罕见路径(只有 `html.mbt` 剥 `innerHTML` 那两处)⇒ 在那里重算一次,
  /// 语义仍然**精确**,不是近似。
  mut nonempty : Bool
}

///|
/// 空表。
pub fn[V] PropsTable::new() -> PropsTable[V] {
  { entries: @hashmap.HashMap::new(), nonempty: false }
}

///|
/// 索引赋值 —— 接住上游 `self.0.attrs["name"] = value` 那种写法(约 200 处)。
#alias("_[_]=_")
pub fn[V] PropsTable::set(self : PropsTable[V], key : String, value : V) -> Unit {
  self.entries = self.entries.add(key, value)
  self.nonempty = true
}

///|
/// **独立句柄** —— 一个新的 `PropsTable` 对象,**共享同一份不可变数据**。
///
/// ★ **为什么必须有它(2026-10-03 修的真 bug)**:`PropsTable` 是**可变对象**
/// (`set` 原地改写 `entries` 那一格),而 P1 的第一版 `Props::copy()` 把那四张表**原样共享** ——
/// 于是"复制"出来的 Props 与原件**是同一批表对象**,往副本里写会**漏回原件**。
/// 这不是理论风险:它正是 `docs/PERF.md` §0 警告过的那类**静默污染**
/// (同一个 `Attrs` 传给第二个元素,第二个元素就把自己的属性写进了第一个)。
///
/// 上游自带的回归测试 `vendor/rabbita/html/attrs_copy_test.mbt`
/// (`"Attrs::copy isolates later mutations"`)**抓到了它** ——
/// ⚠️ 而它**没在门里跑**(`tools/verify_all.sh` 不跑 `moon test`),所以一直没被发现。
///
/// 正确做法:**只共享不可变数据(HAMT 根),不共享那个可写的格子** ——
/// 复制仍然是 **O(1)、不逐条重插**(这是 P1 的收益),但副本与原件从此互不可见。
pub fn[V] PropsTable::share(self : PropsTable[V]) -> PropsTable[V] {
  { entries: self.entries, nonempty: self.nonempty }
}

///|
pub fn[V] PropsTable::get(self : PropsTable[V], key : String) -> V? {
  self.entries.get(key)
}

///|
pub fn[V] PropsTable::contains(self : PropsTable[V], key : String) -> Bool {
  self.entries.contains(key)
}

///|
pub fn[V] PropsTable::remove(self : PropsTable[V], key : String) -> Unit {
  self.entries = self.entries.remove(key)
  // 罕见路径:只有在这里才需要重算(`length()` 是 O(N))
  self.nonempty = self.entries.length() > 0
}

///|
/// ⚠️ **仍然是 O(N)**(core 的不可变表没有 O(1) 的 size)—— 别把它放进"每元素每帧"的路径。
/// 要问"空不空"请用 `is_empty()`。
pub fn[V] PropsTable::length(self : PropsTable[V]) -> Int {
  self.entries.length()
}

///|
/// **O(1)**(见 `nonempty` 字段的注释)。
pub fn[V] PropsTable::is_empty(self : PropsTable[V]) -> Bool {
  !self.nonempty
}

///|
pub fn[V] PropsTable::iter2(self : PropsTable[V]) -> Iter2[String, V] {
  self.entries.iter2()
}

///|
/// **样式表:扁平数组**(moobile 改造,2026-10-03)。
///
/// ## 为什么不再用 `PropsTable`(不可变 HAMT)
///
/// 这条路**每个元素每帧**要插 4 条样式(N=1000 时 4005 次/帧),而 HAMT 的 `add` 每次都要
/// **哈希 + 路径拷贝 + 分配**。消融探针(把"插入"做 3 遍、输出逐项不变)实测
/// `min` **+61.4%(区间不重叠)** ⇒ 反推现存那一遍约 **1.8 ms/帧 ≈ 翻译层的 30%**(`PERF.md` §12.3)。
/// ⚠️ 同一处的 profile 份额只报 3.2% —— **profile 估大小会低一个数量级**。
///
/// 换成数组之后:写入 = 一次数组拷贝(没有哈希),读取/迭代 = 顺序扫,`is_empty` = O(1)。
/// 表的规模在这里天然很小(一条 `Style` 几条到十几条),所以线性扫描比哈希更快。
///
/// ## 三条不变式(`Props::copy` 的 O(1) 与"副本独立"全靠它们)
///
/// 1. **`entries` 指向的那个数组永不原地修改** —— 所有"改"都**换成新数组**。
///    所以两个句柄可以安全共享同一个数组。
///    ⚠️ 这条是**约定**,不是类型保证:曾试过用 `ArrayView` 把它变成类型保证,
///    实测**更慢 8~15%**(§14.2)⇒ 留在约定这一侧,靠"不留可变别名的访问器"+ 测试守着。
/// 2. **句柄本身是可变的**(`mut` 字段 ⇒ `Styles` 是引用类型),
///    这样 `Props::styles()` 才能像原来那样"就地"把新样式写进这一格;
///    而 `Props::copy()` 走 `share()` 给**新句柄** ⇒ 副本怎么改都碰不到原件。
///    (⚠️ 这正是 `PropsTable` 第一版栽过的地方:句柄共享 + 原地改 = 串味。)
/// 3. **存进来的数组是从调用方复制来的**:`@style.Style` 的构造器是**可变**的,
///    调用方之后继续用那份 `Style`,不能影响这里已经存下的内容。
pub struct Styles {
  /// ⚠️ **裸 `Array`,不是 `ArrayView`** —— 试过后者(让"只读"由类型保证):
  /// 实测 `translate` **+10~15%**、`dom` **+8~9.5%**(六个统计量方向一致,堆增量 dom +86%)
  /// ⇒ **回退**。见 `docs/PERF.md` §14.2。
  /// 换来的安全收益用另一条路拿:**不给任何会漏出这个可变数组的访问器**
  /// (`Styles::entries()` 已删掉),不变式 + 下面的测试守着它。
  mut entries : Array[(String, @style.StyleValue)]
}

///|
/// 空样式表。
pub fn Styles::empty() -> Styles {
  { entries: [] }
}

///|
/// 从应用给的 `Style` 复制一份(不变式 3)。
pub fn Styles::from_style(s : @style.Style) -> Styles {
  { entries: s.entries().copy() }
}

///|
/// **新句柄、同一个数组** —— `Props::copy()` 用它。
///
/// 之所以安全:写入一律**换新数组**(不变式 1),所以两个句柄永远不会互相看见对方的改动。
pub fn Styles::share(self : Styles) -> Styles {
  { entries: self.entries }
}

///|
/// 就地合并一份样式(**后来的赢**,与原来的 HAMT 覆盖语义一致)。
///
/// 只改**本句柄**那一格;那个数组本身不动(不变式 1)。
pub fn Styles::apply(self : Styles, s : @style.Style) -> Unit {
  let incoming = s.entries()
  if incoming.length() == 0 {
    return
  }
  // ★ 快路径:表本来就是空的(**新建元素时就是这种情况**)⇒ 直接拿 incoming 的副本,不必走通用路径。
  if self.entries.length() == 0 {
    self.entries = incoming.copy()
    return
  }
  // 通用路径:先把自己这份拷成**新数组**(`out` 是局部量,函数返回后没人持有这个旧数组的别名),
  // 再把 incoming 合进去(同键**后来的赢**,与原来 HAMT 覆盖的语义一致),最后整体换上去。
  let out : Array[(String, @style.StyleValue)] = self.entries.copy()
  for entry in incoming {
    let mut replaced = false
    for i = 0; i < out.length(); i = i + 1 {
      if out[i].0 == entry.0 {
        out[i] = entry
        replaced = true
        break
      }
    }
    if !replaced {
      out.push(entry)
    }
  }
  self.entries = out
}

///|
/// **O(1)**(数组长度)。
pub fn Styles::is_empty(self : Styles) -> Bool {
  self.entries.length() == 0
}

///|
pub fn Styles::length(self : Styles) -> Int {
  self.entries.length()
}

///|
/// 线性扫描:表很小(≤ 十几条),比哈希更快。
pub fn Styles::get(self : Styles, key : String) -> @style.StyleValue? {
  for entry in self.entries {
    if entry.0 == key {
      return Some(entry.1)
    }
  }
  None
}

///|
pub fn Styles::contains(self : Styles, key : String) -> Bool {
  for entry in self.entries {
    if entry.0 == key {
      return true
    }
  }
  false
}

///|
/// **热路径用的按序号取**(`styles_to_js` 每帧要遍历 4005 条)。
///
/// 为什么不直接用 `for k, v in styles`(`iter2`):那一条走的是**闭包迭代器**
/// —— 每个元素都要一次间接调用 + 一个 `Option[(K,V)]`。含时归因(`PERF.md` §14.5)里
/// `Iter2::next` 在 `styles_to_js` 下就占 1.3~2.4%,而这条按序号循环是纯索引。
/// (仍然只读:`at` 返回的是值,拿不到那个数组。)
pub fn Styles::at(self : Styles, i : Int) -> (String, @style.StyleValue) {
  self.entries[i]
}

///|
pub fn Styles::iter2(self : Styles) -> Iter2[String, @style.StyleValue] {
  let mut i = 0
  let n = self.entries.length()
  Iter2::new(
    () => {
      guard i < n else { None }
      let entry = self.entries[i]
      i += 1
      Some((entry.0, entry.1))
    },
    size_hint=n,
  ).iter2()
}

///|
pub struct Props {
  handlers : PropsTable[(Event, &Scheduler) -> Unit]
  attrs : PropsTable[String]
  props : PropsTable[@variant.Variant]
  // ★ moobile 改造:样式值从一开始就是**类型化**的,不再是"待解析的字符串"。
  // 旧的 `Map[String, String]` 通道已删除 —— 实测零调用点。
  // ★ 2026-10-03:再改成**扁平数组**(`Styles`)—— 见 `Styles` 的注释(插入是这笔开销的大头)。
  styles : Styles
}

///|
pub fn Props::new(
  attrs : PropsTable[String],
  props : PropsTable[@variant.Variant],
  styles : Styles,
  handlers : PropsTable[(Event, &Scheduler) -> Unit],
) -> Props {
  { handlers, attrs, styles, props, }
}

///|
/// **独立副本** —— 语义与上游逐条重插**等价**,但**不逐条重插**:
/// 四张表的**不可变数据**(HAMT 根)共享,四张表的**句柄**各自新建
/// (`PropsTable::share`)⇒ `O(1)`、与元素数无关,而"副本与原件互不可见"这条语义是真的。
///
/// ⚠️ **这里修正过一个真 bug**:第一版把四张表**原样共享**(连句柄也共享)——
/// 因为 `PropsTable` 是可变的(`set` 原地改 `entries`),于是往副本里写会**漏回原件**,
/// 上游的回归测试 `attrs_copy_test.mbt` 直接红(详见 `PropsTable::share` 的注释)。
///
/// 上游这一处曾用 `copy_map` 逐条重插(四张表 × 每元素每帧)—— 那是被替换掉的那笔开销。
pub fn Props::copy(self : Props) -> Props {
  {
    handlers: self.handlers.share(),
    attrs: self.attrs.share(),
    props: self.props.share(),
    styles: self.styles.share(),
  }
}

// ============================================================================
// ★ moobile 扩展(fork 的一部分)
//
// 存在的理由:`Event` 在本包内**不是 pub**,`Attrs::handler` 也是私有的,
// 所以外部包既没法给处理器标注类型、也没法注册处理器。
// 这三个方法把 `Event` 藏在签名里,调用方靠类型推断即可使用:
//
//     props.on("click", _ => dispatch(Msg::Inc))
//
// 这正是 README 说的「fork 的第二档:改造 rabbita 的类型支持」。
// ============================================================================

///|
/// 空属性集。
pub fn Props::empty() -> Props {
  {
    handlers: PropsTable::new(),
    attrs: PropsTable::new(),
    props: PropsTable::new(),
    styles: Styles::empty(),
  }
}

///|
/// 合并一份**类型化**样式。
pub fn Props::styles(self : Props, s : @style.Style) -> Props {
  // 就地写进**本句柄**那一格(`Styles::apply` 里面是"换新数组",不改共享的那个数组)
  self.styles.apply(s)
  self
}

///|
/// 注册事件处理器。`Event` 由签名提供,调用方不必命名它。
pub fn Props::on(
  self : Props,
  event : String,
  f : (Event, &Scheduler) -> Unit,
) -> Props {
  self.handlers[event] = f
  self
}

///|
/// 设置字符串属性(`placeholder` / `value` 这类)。
pub fn Props::attr(self : Props, key : String, value : String) -> Props {
  self.attrs[key] = value
  self
}

///|
/// 设置非字符串属性(bool / int / float)。
pub fn Props::prop(self : Props, key : String, value : @variant.Variant) -> Props {
  self.props[key] = value
  self
}

///|
/// 类型化样式表(只读)。
pub fn Props::styles_map(self : Props) -> Styles {
  self.styles
}

///|
/// 字符串属性表(只读)。
pub fn Props::attrs_map(self : Props) -> PropsTable[String] {
  self.attrs
}

///|
/// 非字符串属性表(只读)。
pub fn Props::props_map(self : Props) -> PropsTable[@variant.Variant] {
  self.props
}

///|
/// 把事件处理器包成"只吃一个 JS 值"的形式,供非 DOM 后端(React Native)调用。
///|
/// **有没有 handler**(O(1))—— 给热路径用来"没 handler 就别建迭代器"。
///
/// 为什么值这一行:`each_handler` 里的 `for event, handler in self.handlers` 每次都要
/// **先构造一个 HAMT 迭代器**;而绝大多数元素根本没有 handler
/// (基准负载:7007 个元素里只有 1001 个有)。含时归因里 `each_handler` 占 4.9%(`PERF.md` §14.5)。
pub fn Props::has_handlers(self : Props) -> Bool {
  !self.handlers.is_empty()
}

///|
pub fn Props::each_handler(
  self : Props,
  scheduler : &Scheduler,
  f : (String, (Event) -> Unit) -> Unit,
) -> Unit {
  for event, handler in self.handlers {
    f(event, (v) => handler(v, scheduler))
  }
}