// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Variable substitution for diago IR (reference behavior)
///
/// This pass resolves `${var}` substitution segments that the parser stores in
/// `@ast.StringValue::Unquoted` interpolation segments, and also handles
/// spread substitutions (`...$var`) compiled as placeholder nodes.

///|
fn Map::resolve_substitutions(self : Map) -> Unit raise CompileError {
  self.resolve_substitutions_with_stack([], false)
}

///|
fn Map::resolve_substitutions_with_stack(
  self : Map,
  vars_stack : Array[Map],
  in_vars_map : Bool,
) -> Unit raise CompileError {
  let local_stack = prepend_vars_stack(self, vars_stack)

  // Fields (allow removal for spread placeholders)
  let mut i = 0
  while i < self.fields.length() {
    let name_lower = self.fields[i].name.to_lower()
    if name_lower == "vars" {
      // Still resolve substitutions inside vars blocks (with itself at top of stack).
      match self.fields[i].composite {
        Some(Map(m)) => m.resolve_substitutions_with_stack(local_stack, true)
        Some(Array(a)) => a.resolve_substitutions_with_stack(local_stack)
        None => ()
      }
      i = i + 1
      continue
    }

    // Resolve primary substitutions first. Spread substitutions can remove the field.
    if self.fields[i].primary is Some(_) {
      let removed = resolve_field_primary_substitutions(
        self, i, local_stack, in_vars_map,
      )
      if removed {
        continue
      }
    }

    // Recurse into composite after primary resolution.
    match self.fields[i].composite {
      Some(Map(m)) => m.resolve_substitutions_with_stack(local_stack, false)
      Some(Array(a)) => a.resolve_substitutions_with_stack(local_stack)
      None => ()
    }
    i = i + 1
  }

  // Edges
  for ei, edge in self.edges {
    let (primary, map) = resolve_edge_primary_and_map(
      edge.primary,
      edge.map,
      local_stack,
    )
    match map {
      Some(m) => m.resolve_substitutions_with_stack(local_stack, false)
      None => ()
    }
    self.edges[ei] = {
      id: edge.id,
      primary,
      map,
      references: edge.references,
      import_ast: edge.import_ast,
      path: edge.path,
    }
  }
}

///|
fn IRArray::resolve_substitutions_with_stack(
  self : IRArray,
  vars_stack : Array[Map],
) -> Unit raise CompileError {
  let mut i = 0
  while i < self.values.length() {
    match self.values[i] {
      Scalar(s) => {
        let action = resolve_scalar_in_array(self, i, s, vars_stack)
        if action {
          // Element was replaced/removed, stay at same index.
          continue
        }
      }
      Map(m) => m.resolve_substitutions_with_stack(vars_stack, false)
      Array(a) => a.resolve_substitutions_with_stack(vars_stack)
    }
    i = i + 1
  }
}

// ============================================================================
// Field substitution resolution
// ============================================================================

///|
fn resolve_field_primary_substitutions(
  m : Map,
  field_index : Int,
  vars_stack : Array[Map],
  in_vars_map : Bool,
) -> Bool raise CompileError {
  let field = m.fields[field_index]
  guard field.primary is Some(p) else { return false }
  let (removed, updated_field) = resolve_scalar_in_field(
    m,
    field_index,
    field.name,
    p,
    field.composite,
    vars_stack,
    in_vars_map,
  )
  if removed {
    return true
  }
  m.fields[field_index] = {
    name: field.name,
    name_syntax: field.name_syntax(),
    primary: updated_field.0,
    composite: updated_field.1,
    references: field.references,
    import_ast: field.import_ast,
    path: field.path,
  }
  false
}

///|
fn resolve_scalar_in_field(
  parent_map : Map,
  field_index : Int,
  field_name : String,
  primary : Scalar,
  composite : Composite?,
  vars_stack : Array[Map],
  in_vars_map : Bool,
) -> (Bool, (Scalar?, Composite?)) raise CompileError {
  match primary.value {
    String(sv) =>
      match sv {
        Unquoted(us) =>
          resolve_unquoted_in_field(
            parent_map,
            field_index,
            field_name,
            primary.range,
            us,
            composite,
            vars_stack,
            in_vars_map,
          )
        DoubleQuoted(dqs) => {
          let resolved = resolve_double_quoted_in_field(
            primary.range,
            dqs,
            vars_stack,
            field_name,
            in_vars_map,
          )
          (false, (Some(resolved), composite))
        }
        Block(bs) => {
          let resolved = resolve_block_string_in_scalar(
            primary.range,
            bs,
            vars_stack,
          )
          (false, (Some(resolved), composite))
        }
        _ => (false, (Some(primary), composite))
      }
    _ => (false, (Some(primary), composite))
  }
}

///|
fn resolve_double_quoted_in_field(
  range : @lexer.Range,
  dqs : @ast.DoubleQuotedString,
  vars_stack : Array[Map],
  field_name : String,
  in_vars_map : Bool,
) -> Scalar raise CompileError {
  // reference behavior: substitutions in quotes always resolve to a string; composites are invalid.
  let buf = StringBuilder::new()
  for seg in dqs.value {
    match seg {
      Text(s) => buf.write_string(s)
      Sub(sub) => {
        let resolved = resolve_from_stack(
          vars_stack,
          sub,
          Some(field_name),
          in_vars_map,
        )
        match resolved.primary {
          Some(rp) => buf.write_string(rp.to_string())
          None =>
            match resolved.composite {
              Some(_) =>
                raise InvalidSubstitution(
                  "cannot substitute map variable \"\{sub_path_string(sub)}\" in quotes",
                )
              None =>
                raise InvalidSubstitution(
                  "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
                )
            }
        }
      }
    }
  }
  let new_sv = @ast.StringValue::DoubleQuoted(
    @ast.DoubleQuotedString::new(range, [Text(buf.to_string())]),
  )
  Scalar::at(range, String(new_sv))
}

///|
fn resolve_unquoted_in_field(
  parent_map : Map,
  field_index : Int,
  field_name : String,
  range : @lexer.Range,
  us : @ast.UnquotedString,
  composite : Composite?,
  vars_stack : Array[Map],
  in_vars_map : Bool,
) -> (Bool, (Scalar?, Composite?)) raise CompileError {
  // Whole substitution (`${x}`) is handled specially (may set composite or spread).
  if us.value.length() == 1 {
    match us.value[0] {
      Sub(sub) =>
        return resolve_single_sub_in_field(
          parent_map, field_index, field_name, range, sub, composite, vars_stack,
          in_vars_map,
        )
      _ => ()
    }
  }

  // Otherwise, treat it as a string with substitutions inside.
  let buf = StringBuilder::new()
  let mut updated_composite = composite
  for seg in us.value {
    match seg {
      Text(s) => buf.write_string(s)
      Sub(sub) => {
        let resolved = resolve_from_stack(
          vars_stack,
          sub,
          Some(field_name),
          in_vars_map,
        )
        guard resolved.primary is Some(rp) else {
          if resolved.composite is None {
            raise InvalidSubstitution(
              "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
            )
          }
          raise InvalidSubstitution(
            "cannot substitute composite variable \"\{sub_path_string(sub)}\" as part of a string",
          )
        }
        buf.write_string(rp.to_string())
        match resolved.composite {
          Some(c) => updated_composite = Some(clone_composite(c))
          None => ()
        }
      }
    }
  }
  let new_sv = @ast.StringValue::Unquoted(
    @ast.UnquotedString::new(range, [Text(buf.to_string())], []),
  )
  (false, (Some(Scalar::at(range, String(new_sv))), updated_composite))
}

///|
fn resolve_single_sub_in_field(
  parent_map : Map,
  field_index : Int,
  field_name : String,
  _range : @lexer.Range,
  sub : @ast.Substitution,
  composite : Composite?,
  vars_stack : Array[Map],
  in_vars_map : Bool,
) -> (Bool, (Scalar?, Composite?)) raise CompileError {
  let resolved = resolve_from_stack(
    vars_stack,
    sub,
    Some(field_name),
    in_vars_map,
  )
  if sub.spread {
    guard resolved.composite is Some(rc) else {
      raise InvalidSubstitution("cannot spread non-composite")
    }
    match rc {
      Map(rm) => {
        let inserted = expand_substitution(parent_map, rm, field_index)
        let _ = parent_map.fields.remove(field_index + inserted)
        return (true, (None, None))
      }
      Array(_) => {
        // reference behavior: placeholder is removed even if nothing can be expanded into a map.
        let _ = parent_map.fields.remove(field_index)
        return (true, (None, None))
      }
    }
  }
  match resolved.primary {
    Some(rp) => {
      let new_primary = Some(clone_scalar(rp))
      let new_composite = match resolved.composite {
        Some(c) => Some(clone_composite(c))
        None => composite
      }
      (false, (new_primary, new_composite))
    }
    None =>
      match resolved.composite {
        Some(c) => (false, (None, Some(clone_composite(c))))
        None =>
          raise InvalidSubstitution(
            "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
          )
      }
  }
}

// ============================================================================
// Edge substitution resolution
// ============================================================================

///|
fn resolve_edge_primary_and_map(
  primary : Scalar?,
  edge_map : Map?,
  vars_stack : Array[Map],
) -> (Scalar?, Map?) raise CompileError {
  let mut out_primary = primary
  let mut out_map = edge_map
  guard primary is Some(p) else { return (out_primary, out_map) }
  match p.value {
    String(sv) =>
      match sv {
        Unquoted(us) =>
          if us.value.length() == 1 {
            match us.value[0] {
              Sub(sub) => {
                let resolved = resolve_from_stack(vars_stack, sub, None, false)
                match resolved.primary {
                  Some(rp) => out_primary = Some(clone_scalar(rp))
                  None => out_primary = None
                }
                match resolved.composite {
                  Some(Map(m)) => out_map = Some(clone_map(m))
                  Some(Array(_)) =>
                    raise InvalidSubstitution(
                      "cannot substitute array variable \"\{sub_path_string(sub)}\" to an edge",
                    )
                  None => ()
                }
                if resolved.primary is None && resolved.composite is None {
                  raise InvalidSubstitution(
                    "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
                  )
                }
              }
              _ => ()
            }
          } else {
            let buf = StringBuilder::new()
            for seg in us.value {
              match seg {
                Text(s) => buf.write_string(s)
                Sub(sub) => {
                  let resolved = resolve_from_stack(
                    vars_stack,
                    sub,
                    None,
                    false,
                  )
                  guard resolved.primary is Some(rp) else {
                    if resolved.composite is None {
                      raise InvalidSubstitution(
                        "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
                      )
                    }
                    raise InvalidSubstitution(
                      "cannot substitute composite variable \"\{sub_path_string(sub)}\" as part of a string",
                    )
                  }
                  buf.write_string(rp.to_string())
                  match resolved.composite {
                    Some(Map(m)) => out_map = Some(clone_map(m))
                    Some(Array(_)) =>
                      raise InvalidSubstitution(
                        "cannot substitute array variable \"\{sub_path_string(sub)}\" to an edge",
                      )
                    None => ()
                  }
                }
              }
            }
            let r = p.range
            out_primary = Some(
              Scalar::at(
                r,
                String(
                  Unquoted(
                    @ast.UnquotedString::new(r, [Text(buf.to_string())], []),
                  ),
                ),
              ),
            )
          }
        DoubleQuoted(dqs) =>
          out_primary = Some(
            resolve_double_quoted_in_field(p.range, dqs, vars_stack, "", false),
          )
        Block(bs) =>
          out_primary = Some(
            resolve_block_string_in_scalar(p.range, bs, vars_stack),
          )
        _ => ()
      }
    _ => ()
  }
  (out_primary, out_map)
}

// ============================================================================
// Array substitution resolution (including spread)
// ============================================================================

///|
fn resolve_scalar_in_array(
  arr : IRArray,
  index : Int,
  scalar : Scalar,
  vars_stack : Array[Map],
) -> Bool raise CompileError {
  match scalar.value {
    String(sv) =>
      match sv {
        Unquoted(us) =>
          resolve_unquoted_in_array(arr, index, scalar.range, us, vars_stack)
        DoubleQuoted(dqs) => {
          arr.values[index] = Scalar(
            resolve_double_quoted_in_field(
              scalar.range,
              dqs,
              vars_stack,
              "",
              false,
            ),
          )
          false
        }
        Block(bs) => {
          arr.values[index] = Scalar(
            resolve_block_string_in_scalar(scalar.range, bs, vars_stack),
          )
          false
        }
        _ => false
      }
    _ => false
  }
}

///|
fn resolve_unquoted_in_array(
  arr : IRArray,
  index : Int,
  range : @lexer.Range,
  us : @ast.UnquotedString,
  vars_stack : Array[Map],
) -> Bool raise CompileError {
  if us.value.length() == 1 {
    match us.value[0] {
      Sub(sub) => {
        let resolved = resolve_from_stack(vars_stack, sub, None, false)
        if sub.spread {
          guard resolved.composite is Some(rc) else {
            raise InvalidSubstitution("cannot spread non-composite")
          }
          match rc {
            Array(a) => {
              let out : Array[Value] = []
              for v in a.values {
                out.push(clone_value(v))
              }
              // Splice values into array in place of the placeholder element.
              let _ = arr.values.remove(index)
              for j, v in out {
                arr.values.insert(index + j, v)
              }
              return true
            }
            Map(_) =>
              raise InvalidSubstitution("cannot spread non-array into array")
          }
        }
        match resolved.primary {
          Some(rp) => {
            arr.values[index] = Scalar(clone_scalar(rp))
            false
          }
          None =>
            // reference behavior: composite variables substituted as a whole array element are ignored.
            false
        }
      }
      _ => false
    }
  } else {
    let buf = StringBuilder::new()
    for seg in us.value {
      match seg {
        Text(s) => buf.write_string(s)
        Sub(sub) => {
          let resolved = resolve_from_stack(vars_stack, sub, None, false)
          guard resolved.primary is Some(rp) else {
            if resolved.composite is None {
              raise InvalidSubstitution(
                "cannot substitute variable without value: \"\{sub_path_string(sub)}\"",
              )
            }
            raise InvalidSubstitution(
              "cannot substitute composite variable \"\{sub_path_string(sub)}\" as part of a string",
            )
          }
          buf.write_string(rp.to_string())
        }
      }
    }
    let new_sv = @ast.StringValue::Unquoted(
      @ast.UnquotedString::new(range, [Text(buf.to_string())], []),
    )
    arr.values[index] = Scalar(Scalar::at(range, String(new_sv)))
    false
  }
}

// ============================================================================
// Block string substitution resolution (reference behavior)
// ============================================================================

///|
fn resolve_block_string_in_scalar(
  range : @lexer.Range,
  bs : @ast.BlockString,
  vars_stack : Array[Map],
) -> Scalar {
  let variables : Array[(String, String)] = []
  // Our vars_stack is current-scope-first. Collect outer-to-inner so inner wins.
  let mut i = vars_stack.length() - 1
  while i >= 0 {
    collect_block_string_variables(vars_stack[i], variables)
    i = i - 1
  }
  let replaced = replace_substitutions_markdown(bs.value, variables)
  let new_bs = @ast.BlockString::new(range, bs.quote, bs.tag, replaced)
  Scalar::at(range, String(Block(new_bs)))
}

///|
fn collect_block_string_variables(
  vars : Map,
  out : Array[(String, String)],
) -> Unit {
  for f in vars.fields {
    match f.primary {
      Some(p) => vars_put(out, f.name, p.to_string())
      None => ()
    }
    match f.composite {
      Some(Map(m)) => {
        let nested : Array[(String, String)] = []
        collect_block_string_variables(m, nested)
        for kv in nested {
          vars_put(out, "\{f.name}.\{kv.0}", kv.1)
        }
        // Also expose nested keys without prefix.
        collect_block_string_variables(m, out)
      }
      _ => ()
    }
  }
}

///|
fn vars_put(
  out : Array[(String, String)],
  key : String,
  value : String,
) -> Unit {
  for i, kv in out {
    if kv.0 == key {
      out[i] = (key, value)
      return
    }
  }
  out.push((key, value))
}

///|
fn replace_substitutions_markdown(
  md_text : String,
  variables : Array[(String, String)],
) -> String {
  // Best-effort port of upstream reference lib/textmeasure/substitutions.go:
  // replace ${var} outside inline/fenced code spans.
  let chars = md_text.to_array()
  let out = StringBuilder::new()
  let seg = StringBuilder::new()
  let mut in_fenced = false
  let mut in_inline = false
  let mut i = 0
  while i < chars.length() {
    // Toggle fenced code blocks on ``` when not in inline code.
    if !in_inline &&
      i + 2 < chars.length() &&
      chars[i] == '`' &&
      chars[i + 1] == '`' &&
      chars[i + 2] == '`' {
      out.write_string(replace_variables(seg.to_string(), variables))
      seg.reset()
      out.write_string("```")
      in_fenced = !in_fenced
      i = i + 3
      continue
    }
    // Toggle inline code on ` when not in fenced code.
    if !in_fenced && chars[i] == '`' {
      out.write_string(replace_variables(seg.to_string(), variables))
      seg.reset()
      out.write_char('`')
      in_inline = !in_inline
      i = i + 1
      continue
    }
    if in_fenced || in_inline {
      out.write_char(chars[i])
    } else {
      seg.write_char(chars[i])
    }
    i = i + 1
  }
  out.write_string(replace_variables(seg.to_string(), variables))
  out.to_string()
}

///|
fn replace_variables(
  text : String,
  variables : Array[(String, String)],
) -> String {
  let mut out = text
  for kv in variables {
    // Avoid embedding a raw "${" substring in JS backend template literals.
    out = out.replace_all(old=substitution_pattern(kv.0), new=kv.1)
  }
  out
}

///|
fn substitution_pattern(key : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('$')
  buf.write_char('{')
  buf.write_string(key)
  buf.write_char('}')
  buf.to_string()
}

// ============================================================================
// Vars lookup helpers
// ============================================================================

///|
fn prepend_vars_stack(m : Map, vars_stack : Array[Map]) -> Array[Map] {
  match m.get_field("vars") {
    Some(f) =>
      match f.composite {
        Some(Map(vm)) => {
          let out : Array[Map] = [vm]
          for v in vars_stack {
            out.push(v)
          }
          out
        }
        _ => vars_stack
      }
    None => vars_stack
  }
}

///|
fn resolve_from_stack(
  vars_stack : Array[Map],
  sub : @ast.Substitution,
  field_name : String?,
  in_vars_map : Bool,
) -> Field raise CompileError {
  let mut found : Field? = None
  for i, vars in vars_stack {
    match resolve_in_vars(vars, sub, field_name, in_vars_map, i == 0) {
      Some(f) => {
        // reference behavior: a found `null` shadows outer scopes and behaves like unresolved.
        match f.primary {
          Some(p) =>
            if p.value is Null {
              raise InvalidSubstitution(
                "could not resolve variable \"\{sub_path_string(sub)}\"",
              )
            }
          None => ()
        }
        found = Some(f)
        break
      }
      None => ()
    }
  }
  match found {
    Some(f) => f
    None =>
      raise InvalidSubstitution(
        "could not resolve variable \"\{sub_path_string(sub)}\"",
      )
  }
}

///|
fn resolve_in_vars(
  vars : Map,
  sub : @ast.Substitution,
  field_name : String?,
  in_vars_map : Bool,
  is_current_scope_vars : Bool,
) -> Field? {
  let mut current = vars
  for i, part in sub.path {
    let key = part.content()
    match current.get_field(key) {
      Some(f) => {
        match field_name {
          Some(n) =>
            if in_vars_map &&
              is_current_scope_vars &&
              n.to_lower() == key.to_lower() {
              return None
            }
          None => ()
        }
        if i == sub.path.length() - 1 {
          return Some(f)
        }
        match f.composite {
          Some(Map(m)) => current = m
          _ => return None
        }
      }
      None => return None
    }
  }
  None
}

///|
fn sub_path_string(sub : @ast.Substitution) -> String {
  let parts = sub.path.map(fn(p) { p.content() })
  join(parts, ".")
}

///|
fn join(parts : Array[String], sep : String) -> String {
  let buf = StringBuilder::new()
  for i, s in parts {
    if i > 0 {
      buf.write_string(sep)
    }
    buf.write_string(s)
  }
  buf.to_string()
}

// ============================================================================
// Spread expansion
// ============================================================================

///|
fn expand_substitution(m : Map, resolved : Map, placeholder_index : Int) -> Int {
  let mut insert_at = placeholder_index
  let mut inserted = 0
  for of in resolved.fields {
    match find_field_index(m, of.name) {
      Some(bi) => {
        let base = m.fields[bi]
        m.fields[bi] = overlay_field(base, of)
      }
      None => {
        m.fields.insert(insert_at, clone_field(of))
        insert_at = insert_at + 1
        inserted = inserted + 1
      }
    }
  }
  for oe in resolved.edges {
    match find_edge_index(m, oe.id) {
      Some(ei) => m.edges[ei] = overlay_edge(m.edges[ei], oe)
      None => m.edges.push(clone_edge(oe))
    }
  }
  inserted
}

///|
fn find_field_index(m : Map, name : String) -> Int? {
  let lower = name.to_lower()
  for i, f in m.fields {
    if f.name.to_lower() == lower {
      return Some(i)
    }
  }
  None
}

///|
fn find_edge_index(m : Map, id : EdgeID) -> Int? {
  for i, e in m.edges {
    if e.id.matches(id) {
      return Some(i)
    }
  }
  None
}

///|
/// Return a detached map created by overlaying `overlay` onto `self`.
pub fn Map::overlay(self : Map, overlay : Map) -> Map {
  let merged = clone_map(self)
  overlay_map(merged, overlay)
  merged.reindex_paths()
}

///|
fn overlay_field(base : Field, overlay : Field) -> Field {
  let mut primary = base.primary
  if overlay.primary is Some(op) {
    primary = Some(clone_scalar(op))
  }
  let mut composite = base.composite
  match overlay.composite {
    Some(oc) =>
      match oc {
        Map(om) =>
          match composite {
            Some(Map(bm)) => {
              overlay_map(bm, om)
              composite = Some(Map(bm))
            }
            _ => composite = Some(Map(clone_map(om)))
          }
        Array(oa) => composite = Some(Array(clone_array(oa)))
      }
    None => ()
  }
  let refs = base.references.copy()
  for r in overlay.references {
    refs.push(r)
  }
  {
    name: base.name,
    name_syntax: match overlay.name_syntax() {
      Some(name_syntax) => Some(name_syntax)
      None => base.name_syntax()
    },
    primary,
    composite,
    references: refs,
    import_ast: match overlay.import_ast {
      Some(node) => Some(node)
      None => base.import_ast
    },
    path: base.path,
  }
}

///|
fn overlay_edge(base : Edge, overlay : Edge) -> Edge {
  let mut primary = base.primary
  if overlay.primary is Some(op) {
    primary = Some(clone_scalar(op))
  }
  let mut map = base.map
  match overlay.map {
    Some(om) =>
      match map {
        Some(bm) => {
          overlay_map(bm, om)
          map = Some(bm)
        }
        None => map = Some(clone_map(om))
      }
    None => ()
  }
  let refs = base.references.copy()
  for r in overlay.references {
    refs.push(r)
  }
  {
    id: clone_edge_id(base.id),
    primary,
    map,
    references: refs,
    import_ast: match overlay.import_ast {
      Some(node) => Some(node)
      None => base.import_ast
    },
    path: base.path,
  }
}

///|
fn overlay_map(base : Map, overlay : Map) -> Unit {
  for of in overlay.fields {
    match find_field_index(base, of.name) {
      Some(i) => base.fields[i] = overlay_field(base.fields[i], of)
      None => base.fields.push(clone_field(of))
    }
  }
  for oe in overlay.edges {
    match find_edge_index(base, oe.id) {
      Some(i) => base.edges[i] = overlay_edge(base.edges[i], oe)
      None => base.edges.push(clone_edge(oe))
    }
  }
}

// ============================================================================
// Clone helpers
// ============================================================================

///|
fn clone_scalar(s : Scalar) -> Scalar {
  {
    value: s.value,
    range: s.range,
    ast: s.ast,
    source_path: s.source_path,
    path: s.path,
  }
}

///|
fn clone_composite(c : Composite) -> Composite {
  match c {
    Map(m) => Map(clone_map(m))
    Array(a) => Array(clone_array(a))
  }
}

///|
fn clone_map(m : Map) -> Map {
  let out : Map = {
    fields: [],
    edges: [],
    ast: m.ast,
    import_ast: m.import_ast,
    source_path: m.source_path,
    path: m.path,
  }
  for f in m.fields {
    out.fields.push(clone_field(f))
  }
  for e in m.edges {
    out.edges.push(clone_edge(e))
  }
  out
}

///|
fn clone_field(f : Field) -> Field {
  let primary = match f.primary {
    Some(p) => Some(clone_scalar(p))
    None => None
  }
  let composite = match f.composite {
    Some(c) => Some(clone_composite(c))
    None => None
  }
  {
    name: f.name,
    name_syntax: f.name_syntax(),
    primary,
    composite,
    references: f.references.copy(),
    import_ast: f.import_ast,
    path: f.path,
  }
}

///|
fn clone_edge_id(id : EdgeID) -> EdgeID {
  {
    src_path: id.src_path.copy(),
    src_path_syntax: id.src_path_syntax(),
    src_arrow: id.src_arrow,
    dst_path: id.dst_path.copy(),
    dst_path_syntax: id.dst_path_syntax(),
    dst_arrow: id.dst_arrow,
    index: id.index,
    glob: id.glob,
  }
}

///|
fn clone_edge(e : Edge) -> Edge {
  let primary = match e.primary {
    Some(p) => Some(clone_scalar(p))
    None => None
  }
  let map = match e.map {
    Some(m) => Some(clone_map(m))
    None => None
  }
  {
    id: clone_edge_id(e.id),
    primary,
    map,
    references: e.references.copy(),
    import_ast: e.import_ast,
    path: e.path,
  }
}

///|
fn clone_value(v : Value) -> Value {
  match v {
    Scalar(s) => Scalar(clone_scalar(s))
    Map(m) => Map(clone_map(m))
    Array(a) => Array(clone_array(a))
  }
}

///|
fn clone_array(a : IRArray) -> IRArray {
  let values : Array[Value] = []
  for v in a.values {
    values.push(clone_value(v))
  }
  { values, ast: a.ast, source_path: a.source_path, path: a.path }
}