/// 结构操作 v2:打组/解组(包围盒换算)、多节点对齐、翻转、节点复制。
/// 语义对齐 Figma:
/// - group:同父(绝对定位容器)下多节点 → Group,坐标换算到组内相对
/// - ungroup:组员坐标还原回父坐标系,组移除
/// - align:多节点向包围盒边缘/中心对齐
/// - flip:水平/垂直镜像(渲染 transform)
/// - copy:子树克隆 + 偏移放置

///|
/// 打组:同父绝对容器下的多个节点 → Group
pub fn Project::group_nodes(
  self : Project,
  artboard~ : String,
  node_ids~ : Array[String],
  group_id~ : String,
) -> Result[String, String] {
  match self.artboards.get(artboard) {
    None => Err("unknown_artboard: \{artboard}")
    Some(ab) => {
      if node_ids.length() < 2 {
        return Err("need_two_nodes")
      }
      if ab.doc.has(group_id) {
        return Err("id_exists: \{group_id}")
      }
      let layout = solve(ab.doc)
      let mut parent = ""
      for nid in node_ids.iter() {
        match ab.doc.get(nid) {
          None => return Err("unknown_node: \{nid}")
          Some(_) => {
            let p = match ab.doc.find_parent(nid) {
              Some(p) => p
              None => return Err("orphan_node: \{nid}")
            }
            if parent == "" {
              parent = p
            } else if parent != p {
              return Err("mixed_parents")
            }
          }
        }
      }
      let pn = match ab.doc.get(parent) {
        Some(p) => p
        None => return Err("orphan_parent")
      }
      match pn.layout {
        Some(_) => return Err("unsupported_parent_layout")
        None => ()
      }
      let parent_rect = match layout.rect_of(parent) {
        Some(r) => r
        None => Rect::new(0.0, 0.0, 0.0, 0.0)
      }
      // 包围盒(画布绝对)
      let mut min_x = 1000000000000000000.0
      let mut min_y = 1000000000000000000.0
      let mut max_x = -1000000000000000000.0
      let mut max_y = -1000000000000000000.0
      for nid in node_ids.iter() {
        match layout.rect_of(nid) {
          Some(r) => {
            if r.x < min_x {
              min_x = r.x
            }
            if r.y < min_y {
              min_y = r.y
            }
            if r.x + r.width > max_x {
              max_x = r.x + r.width
            }
            if r.y + r.height > max_y {
              max_y = r.y + r.height
            }
          }
          None => ()
        }
      }
      if max_x < 0.0 {
        return Err("no_geometry")
      }
      let bw = max_x - min_x
      let bh = max_y - min_y
      let group = {
        ..Node::make(group_id, Group),
        name: group_id,
        width: Fixed(bw),
        height: Fixed(bh),
        x: Fixed(min_x - parent_rect.x),
        y: Fixed(min_y - parent_rect.y),
      }
      let _ = ab.doc.insert_node(group, parent)
      for nid in node_ids.iter() {
        let _ = ab.doc.update_node(nid, fn(x) {
          let nx = pos_offset_v(x.x) - (min_x - parent_rect.x)
          let ny = pos_offset_v(x.y) - (min_y - parent_rect.y)
          { ..x, x: Fixed(nx), y: Fixed(ny), }
        })
        let _ = ab.doc.update_node(parent, fn(p) {
          { ..p, children: p.children.filter(c => c != nid), }
        })
        match ab.doc.get(group_id) {
          Some(g) => g.children.push(nid)
          None => ()
        }
      }
      self.revision = self.revision + 1
      Ok(
        "{\"ok\":true,\"op\":\"group\",\"id\":\"\{group_id}\",\"members\":\{node_ids.length()},\"bounds\":{\"x\":\{min_x},\"y\":\{min_y},\"w\":\{bw},\"h\":\{bh}}}",
      )
    }
  }
}

///|
fn pos_offset_v(p : PosSpec) -> Double {
  match p {
    Fixed(v) => v
    AlignStart(v) => v
    AlignEnd(v) => v
    CenterInParent => 0.0
  }
}

///|
/// 解组:组员坐标还原回父坐标系
pub fn Project::ungroup_node(
  self : Project,
  artboard~ : String,
  group_id~ : String,
) -> Result[String, String] {
  match self.artboards.get(artboard) {
    None => Err("unknown_artboard: \{artboard}")
    Some(ab) => {
      let g = match ab.doc.get(group_id) {
        Some(g) => g
        None => return Err("unknown_node: \{group_id}")
      }
      let is_group = match g.kind {
        Group => true
        _ => false
      }
      if !is_group {
        return Err("not_a_group")
      }
      let parent = match ab.doc.find_parent(group_id) {
        Some(p) => p
        None => return Err("orphan_node")
      }
      let gx = pos_offset_v(g.x)
      let gy = pos_offset_v(g.y)
      let members = g.children.copy()
      for mid in members.iter() {
        let _ = ab.doc.update_node(mid, fn(x) {
          {
            ..x,
            x: Fixed(gx + pos_offset_v(x.x)),
            y: Fixed(gy + pos_offset_v(x.y)),
          }
        })
        match ab.doc.get(parent) {
          Some(pn) => pn.children.push(mid)
          None => ()
        }
      }
      let _ = ab.doc.update_node(parent, fn(p) {
        { ..p, children: p.children.filter(c => c != group_id), }
      })
      ab.doc.content_seq = ab.doc.content_seq + 1
      ab.doc.nodes.remove(group_id)
      self.revision = self.revision + 1
      Ok(
        "{\"ok\":true,\"op\":\"ungroup\",\"id\":\"\{group_id}\",\"members\":\{members.length()}}",
      )
    }
  }
}

///|
/// 多节点对齐:left|right|top|bottom|hcenter|vcenter(相对节点集合包围盒)
pub fn Project::align_nodes(
  self : Project,
  artboard~ : String,
  mode~ : String,
  node_ids~ : Array[String],
) -> Result[String, String] {
  match self.artboards.get(artboard) {
    None => Err("unknown_artboard: \{artboard}")
    Some(ab) => {
      if node_ids.length() < 2 {
        return Err("need_two_nodes")
      }
      let layout = solve(ab.doc)
      let mut min_x = 1000000000000000000.0
      let mut min_y = 1000000000000000000.0
      let mut max_r = -1000000000000000000.0
      let mut max_b = -1000000000000000000.0
      for nid in node_ids.iter() {
        match layout.rect_of(nid) {
          Some(r) => {
            if r.x < min_x {
              min_x = r.x
            }
            if r.y < min_y {
              min_y = r.y
            }
            if r.x + r.width > max_r {
              max_r = r.x + r.width
            }
            if r.y + r.height > max_b {
              max_b = r.y + r.height
            }
          }
          None => ()
        }
      }
      if max_r < 0.0 {
        return Err("no_geometry")
      }
      let mut moved = 0
      for nid in node_ids.iter() {
        let r = match layout.rect_of(nid) {
          Some(r) => r
          None => continue
        }
        let (tx, ty) : (Double, Double) = match mode {
          "left" => (min_x, r.y)
          "right" => (max_r - r.width, r.y)
          "top" => (r.x, min_y)
          "bottom" => (r.x, max_b - r.height)
          "hcenter" => ((min_x + max_r - r.width) / 2.0, r.y)
          "vcenter" => (r.x, (min_y + max_b - r.height) / 2.0)
          _ => return Err("unknown_mode: \{mode}")
        }
        let dx = tx - r.x
        let dy = ty - r.y
        if dx * dx + dy * dy < 0.0001 {
          continue
        }
        let _ = ab.doc.update_node(nid, fn(x) {
          {
            ..x,
            x: Fixed(pos_offset_v(x.x) + dx),
            y: Fixed(pos_offset_v(x.y) + dy),
          }
        })
        moved = moved + 1
      }
      self.revision = self.revision + 1
      Ok(
        "{\"ok\":true,\"op\":\"align\",\"mode\":\"\{mode}\",\"moved\":\{moved}}",
      )
    }
  }
}

///|
/// 翻转:flip h|v|both|none
pub fn Project::flip_node(
  self : Project,
  artboard~ : String,
  node_id~ : String,
  axis~ : String,
) -> Result[String, String] {
  match self.artboards.get(artboard) {
    None => Err("unknown_artboard: \{artboard}")
    Some(ab) => {
      if !ab.doc.has(node_id) {
        return Err("unknown_node: \{node_id}")
      }
      let _ = ab.doc.update_node(node_id, fn(n) {
        { ..n, style: { ..n.style, flip: axis, }, }
      })
      self.revision = self.revision + 1
      Ok(
        "{\"ok\":true,\"op\":\"flip\",\"node\":\"\{node_id}\",\"axis\":\"\{axis}\"}",
      )
    }
  }
}

///|
/// 复制节点:子树克隆 + 偏移
pub fn Project::copy_node(
  self : Project,
  artboard~ : String,
  node_id~ : String,
  new_id~ : String,
  dx~ : Double,
  dy~ : Double,
) -> Result[String, String] {
  match self.artboards.get(artboard) {
    None => Err("unknown_artboard: \{artboard}")
    Some(ab) => {
      if ab.doc.has(new_id) {
        return Err("id_exists: \{new_id}")
      }
      let src = match ab.doc.get(node_id) {
        Some(n) => n
        None => return Err("unknown_node: \{node_id}")
      }
      let parent = match ab.doc.find_parent(node_id) {
        Some(p) => p
        None => return Err("orphan_node")
      }
      let copied = clone_recursive(ab.doc, src, new_id, new_id, dx, dy)
      match copied {
        None => Err("clone_failed")
        Some(root) => {
          let ok = ab.doc.insert_node(root, parent)
          if !ok {
            return Err("insert_failed")
          }
          let count = count_subtree(ab.doc, new_id)
          Ok(
            "{\"ok\":true,\"op\":\"copy\",\"node\":\"\{new_id}\",\"from\":\"\{node_id}\",\"nodes\":\{count}}",
          )
        }
      }
    }
  }
}

///|
/// 深克隆:根用 root_id,子孙 id = prefix + 原id;根偏移 (dx,dy)
fn clone_recursive(
  doc : Document,
  n : Node,
  root_id : String,
  prefix : String,
  dx : Double,
  dy : Double,
) -> Node? {
  let new_id = root_id
  if doc.has(new_id) {
    return None
  }
  let shift_x = match n.x {
    Fixed(v) => v + dx
    AlignStart(v) => v + dx
    _ => dx
  }
  let shift_y = match n.y {
    Fixed(v) => v + dy
    AlignStart(v) => v + dy
    _ => dy
  }
  let out = {
    ..n,
    id: new_id,
    children: [],
    x: Fixed(shift_x),
    y: Fixed(shift_y),
  }
  for cid in n.children.iter() {
    match doc.get(cid) {
      None => ()
      Some(child) =>
        match clone_recursive(doc, child, prefix + cid, prefix, 0.0, 0.0) {
          None => ()
          Some(cc) => {
            let _ = doc.register(cc)
            out.children.push(cc.id)
          }
        }
    }
  }
  Some(out)
}

///|
fn count_subtree(doc : Document, id : String) -> Int {
  match doc.get(id) {
    None => 0
    Some(n) => {
      let mut c = 1
      for cid in n.children.iter() {
        c = c + count_subtree(doc, cid)
      }
      c
    }
  }
}