use boa_engine::{Context, JsNativeError, JsResult, JsValue};
use crate::{script::ScriptState, tdom::node::NodeId};
use super::{mark_dirty, node_wrapper, this_node_id, with_state};
fn reject_cycle(
st: &ScriptState,
parent_id: NodeId,
node_id: NodeId,
method: &str,
) -> JsResult<()> {
let mut current = Some(parent_id);
while let Some(id) = current {
if id == node_id {
return Err(JsNativeError::error()
.with_message(format!(
"Failed to execute '{method}' on 'Node': The new child element contains the parent."
))
.into());
}
current = st.arena[id].parent;
}
Ok(())
}
pub(super) fn native_insert_before(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let parent_id = this_node_id(this, context)?;
let child_id = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context)?;
let reference_id = this_node_id(args.get(1).unwrap_or(&JsValue::undefined()), context).ok();
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
reject_cycle(&st, parent_id, child_id, "insertBefore")?;
let position = reference_id.and_then(|ref_id| {
st.arena
.get(parent_id)
.and_then(|p| p.children.iter().position(|&c| c == ref_id))
});
let old_parent = st.arena.get(child_id).and_then(|c| c.parent);
if let Some(op) = old_parent
&& let Some(opn) = st.arena.get_mut(op)
{
opn.children.retain(|c| *c != child_id);
}
if let Some(child) = st.arena.get_mut(child_id) {
child.parent = Some(parent_id);
}
if let Some(parent) = st.arena.get_mut(parent_id) {
match position {
Some(pos) => parent.children.insert(pos, child_id),
None => parent.children.push(child_id),
}
mark_dirty(&mut st, parent_id);
mark_dirty(&mut st, child_id);
}
drop(st);
node_wrapper(context, child_id)
}
pub(super) fn native_replace_child(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let parent_id = this_node_id(this, context)?;
let new_child = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context)?;
let old_child = this_node_id(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let mut st = rc.borrow_mut();
reject_cycle(&st, parent_id, new_child, "replaceChild")?;
let old_parent = st.arena.get(new_child).and_then(|c| c.parent);
if let Some(op) = old_parent
&& let Some(opn) = st.arena.get_mut(op)
{
opn.children.retain(|c| *c != new_child);
}
if let Some(parent) = st.arena.get_mut(parent_id) {
match parent.children.iter().position(|&c| c == old_child) {
Some(pos) => parent.children[pos] = new_child,
None => parent.children.push(new_child),
}
}
if let Some(c) = st.arena.get_mut(new_child) {
c.parent = Some(parent_id);
}
if let Some(c) = st.arena.get_mut(old_child) {
c.parent = None;
}
mark_dirty(&mut st, parent_id);
mark_dirty(&mut st, new_child);
mark_dirty(&mut st, old_child);
drop(st);
node_wrapper(context, old_child)
}
pub(super) fn native_clone_node(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let deep = args.first().map(|v| v.to_boolean()).unwrap_or(false);
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let mut st = rc.borrow_mut();
let Some(clone_id) = clone_subtree(&mut st, node_id, deep) else {
return Ok(JsValue::null());
};
drop(st);
node_wrapper(context, clone_id)
}
fn clone_subtree(st: &mut ScriptState, node_id: NodeId, deep: bool) -> Option<NodeId> {
let source = st.arena.get(node_id)?.clone();
let mut clone = source.clone();
clone.parent = None;
clone.children.clear();
let clone_id = st.arena.insert(clone);
if deep {
for &child in &source.children {
let child_clone = clone_subtree(st, child, true)?;
st.arena[child_clone].parent = Some(clone_id);
st.arena[clone_id].children.push(child_clone);
}
}
Some(clone_id)
}
pub(super) fn native_append_child(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let parent_id = this_node_id(this, context)?;
let child_id = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
reject_cycle(&st, parent_id, child_id, "appendChild")?;
let old_parent = st.arena.get(child_id).and_then(|c| c.parent);
if let Some(old_parent) = old_parent
&& let Some(op) = st.arena.get_mut(old_parent)
{
op.children.retain(|c| *c != child_id);
}
if let Some(child) = st.arena.get_mut(child_id) {
child.parent = Some(parent_id);
}
if let Some(parent) = st.arena.get_mut(parent_id) {
parent.children.push(child_id);
mark_dirty(&mut st, parent_id);
mark_dirty(&mut st, child_id);
}
Ok(JsValue::undefined())
}
pub(super) fn native_remove_child(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let parent_id = this_node_id(this, context)?;
let child_id = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
if let Some(child) = st.arena.get_mut(child_id) {
child.parent = None;
}
if let Some(parent) = st.arena.get_mut(parent_id) {
parent.children.retain(|c| *c != child_id);
mark_dirty(&mut st, parent_id);
mark_dirty(&mut st, child_id);
}
Ok(JsValue::undefined())
}