///|
fn port_exists(ports : Array[Port], name : String) -> Bool {
  ports.any(fn(port) { port.name == name || port_exists(port.ports, name) })
}

///|
fn rename_ports(
  ports : Array[Port],
  old_name : String,
  new_name : String,
) -> Array[Port] {
  ports.map(fn(port) {
    {
      ..port,
      name: if port.name == old_name {
        new_name
      } else {
        port.name
      },
      ports: rename_ports(port.ports, old_name, new_name),
    }
  })
}

///|
fn renamed_port_reference(
  node : String,
  port : String?,
  target : String,
  old_name : String,
  new_name : String,
) -> String? {
  if node == target && port == Some(old_name) {
    Some(new_name)
  } else {
    port
  }
}

///|
fn rename_port_graph(
  graph : Graph,
  target : String,
  old_name : String,
  new_name : String,
) -> Graph {
  {
    ..graph,
    nodes: graph.nodes.map(fn(node) {
      {
        ..node,
        ports: if node.id == target {
          rename_ports(node.ports, old_name, new_name)
        } else {
          node.ports
        },
        graph: node.graph.map(fn(nested) {
          rename_port_graph(nested, target, old_name, new_name)
        }),
      }
    }),
    edges: graph.edges.map(fn(edge) {
      {
        ..edge,
        source_port: renamed_port_reference(
          edge.source,
          edge.source_port,
          target,
          old_name,
          new_name,
        ),
        target_port: renamed_port_reference(
          edge.target,
          edge.target_port,
          target,
          old_name,
          new_name,
        ),
        graph: edge.graph.map(fn(nested) {
          rename_port_graph(nested, target, old_name, new_name)
        }),
      }
    }),
    hyperedges: graph.hyperedges.map(fn(hyperedge) {
      {
        ..hyperedge,
        endpoints: hyperedge.endpoints.map(fn(endpoint) {
          {
            ..endpoint,
            port: renamed_port_reference(
              endpoint.node,
              endpoint.port,
              target,
              old_name,
              new_name,
            ),
          }
        }),
        graph: hyperedge.graph.map(fn(nested) {
          rename_port_graph(nested, target, old_name, new_name)
        }),
      }
    }),
  }
}

///|
/// Rename a hierarchical port and update binary edges and hyperedge endpoints atomically.
pub fn Document::rename_port(
  self : Document,
  root : Int32,
  node_id : String,
  old_name : String,
  new_name : String,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  bounded_index(root, self.graphs.length(), "rename.root")
  valid_token(new_name, "port.name")
  let node = GraphIndex::new(self)
    .node(root, node_id)
    .unwrap_or_else(() => {
      raise InvalidGraph(path="rename.node", reason="node does not exist")
    })
  require(
    port_exists(node.ports, old_name),
    "port.name",
    "port to rename does not exist",
  )
  require(
    old_name == new_name || !port_exists(node.ports, new_name),
    "port.name",
    "new port name already exists",
  )
  self.replace_graph(
    GraphAddress::root(root),
    rename_port_graph(self.graphs[root], node_id, old_name, new_name),
    limits~,
  )
}