///|
fn encoded_path_segment(value : String) -> String {
  let output = StringBuilder(size_hint=value.length())
  for character in value.iter() {
    if (character >= 'a' && character <= 'z') ||
      (character >= 'A' && character <= 'Z') ||
      (character >= '0' && character <= '9') ||
      character == '_' ||
      character == '-' {
      output.write_char(character)
    } else {
      output.write_char('_')
      output.write_string(character.to_int().to_string(radix=16))
      output.write_char('_')
    }
  }
  output.to_string()
}

///|
fn index_bindings(
  bindings : Array[NodeBinding],
  output : Map[String, @renderer.PageEvent[Cmd]],
) -> Unit {
  for binding in bindings {
    match binding {
      DirectBinding(event) => {
        guard !output.contains(event.key()) else {
          abort("duplicate event binding marker: " + event.key())
        }
        output[event.key()] = event
      }
      NestedBindings(children) => index_bindings(children, output)
    }
  }
}

///|
fn materialized_event_key(
  binding : @renderer.PageEvent[Cmd],
  path : String,
) -> String {
  let parts = binding.key().split("|").collect()
  if parts.length() > 1 && parts[1] != "" {
    parts[1].to_owned()
  } else {
    "v/" + path + "/" + binding.kind().host_name()
  }
}

///|
fn materialize_node(
  node : @ui.Node,
  path : String,
  bindings : Map[String, @renderer.PageEvent[Cmd]],
  events : @renderer.Events[GraphMessage],
) -> @ui.Node {
  match node {
    @ui.Text(_) | @ui.Empty => node
    @ui.Retained(identity, child) =>
      @ui.retained(identity, materialize_node(child, path, bindings, events))
    @ui.Fragment(children) => {
      let output : Array[@ui.Node] = []
      for index = 0; index < children.length(); index = index + 1 {
        output.push(
          materialize_node(
            children[index],
            path + "/i:" + index.to_string(),
            bindings,
            events,
          ),
        )
      }
      @ui.fragment(output)
    }
    @ui.KeyedFragment(children) => {
      let output : Array[@ui.KeyedNode] = []
      for child in children {
        output.push(
          @ui.keyed(
            child.key(),
            materialize_node(
              child.node(),
              path + "/k:" + encoded_path_segment(child.key()),
              bindings,
              events,
            ),
          ),
        )
      }
      @ui.keyed_fragment(output)
    }
    @ui.Element(tag, attrs, children) => {
      let next_attrs = attrs.map(attr => {
        if attr.name.has_prefix("on_") && attr.value.has_prefix("$minimoon:") {
          let binding = bindings
            .get(attr.value)
            .unwrap_or_else(fn() {
              abort("missing event binding: " + attr.value)
            })
          let key = materialized_event_key(binding, path)
          let _ = events.custom(key, binding.kind(), payload => {
            match binding.decode(payload) {
              Ok(command) => Ok(GraphMessage(command))
              Err(error) => Err(error)
            }
          })
          @ui.attr(attr.name, key)
        } else {
          attr
        }
      })
      let next_children : Array[@ui.Node] = []
      for index = 0; index < children.length(); index = index + 1 {
        next_children.push(
          materialize_node(
            children[index],
            path + "/i:" + index.to_string(),
            bindings,
            events,
          ),
        )
      }
      @ui.element(tag, attrs=next_attrs, children=next_children)
    }
  }
}

///|
fn materialize(
  node : Node,
  events : @renderer.Events[GraphMessage],
) -> @ui.Node {
  let bindings : Map[String, @renderer.PageEvent[Cmd]] = Map([])
  index_bindings(node.bindings, bindings)
  materialize_node(node.raw, "root", bindings, events)
}