///|
priv struct LocalNames {
  names : Map[@ir.Name, String]
  used : NameSet
  plan : TargetNames
  owner : String
}

///|
fn collect_pattern_names(
  pattern : @ir.Pattern,
  names : Map[@ir.Name, Bool],
) -> Unit {
  match pattern {
    AsPattern(_, inner, name) => {
      names[name] = true
      collect_pattern_names(inner, names)
    }
    TuplePattern(_, ps) | ConstructorPattern(_, _, ps) =>
      for p in ps {
        collect_pattern_names(p, names)
      }
    HeadTailPattern(_, a, b) => {
      collect_pattern_names(a, names)
      collect_pattern_names(b, names)
    }
    _ => ()
  }
}

///|
fn collect_local_names(value : @ir.Value, names : Map[@ir.Name, Bool]) -> Unit {
  match value {
    Variable(_, name) => names[name] = true
    Lambda(_, pattern, body) => {
      collect_pattern_names(pattern, names)
      collect_local_names(body, names)
    }
    LetDefinition(_, name, definition, body) => {
      names[name] = true
      if definition is ExpressionBody(inputs, _, value) {
        for (name, _) in inputs {
          names[name] = true
        }
        collect_local_names(value, names)
      }
      collect_local_names(body, names)
    }
    Apply(_, a, b) => {
      collect_local_names(a, names)
      collect_local_names(b, names)
    }
    Tuple(_, xs) | List(_, xs) =>
      for x in xs {
        collect_local_names(x, names)
      }
    Record(_, fields) =>
      for _, value in fields {
        collect_local_names(value, names)
      }
    Field(_, value, _) => collect_local_names(value, names)
    Destructure(_, pattern, value, body) => {
      collect_pattern_names(pattern, names)
      collect_local_names(value, names)
      collect_local_names(body, names)
    }
    IfThenElse(_, a, b, c) => {
      collect_local_names(a, names)
      collect_local_names(b, names)
      collect_local_names(c, names)
    }
    PatternMatch(_, value, cases) => {
      collect_local_names(value, names)
      for (pattern, body) in cases {
        collect_pattern_names(pattern, names)
        collect_local_names(body, names)
      }
    }
    UpdateRecord(_, value, fields) => {
      collect_local_names(value, names)
      for _, v in fields {
        collect_local_names(v, names)
      }
    }
    _ => ()
  }
}

///|
fn LocalNames::new(
  plan : TargetNames,
  declaration : Declaration,
) -> LocalNames raise {
  let owner = value_node(declaration.fqname)
  let keys = Map::from_array(declaration.inputs.map(p => (p.0, true)))
  collect_local_names(declaration.body, keys)
  let names = keys.keys().collect()
  names.sort()
  let hints : Map[String, (String, @diagnostics.Span?)] = Map([])
  let ambiguous : Map[String, Bool] = Map([])
  fn save_hint(
    id : String,
    text : String,
    source : @diagnostics.Span?,
  ) -> Unit {
    if hints.contains(id) {
      hints.remove(id)
      ambiguous[id] = true
    } else if !ambiguous.contains(id) {
      hints[id] = (text, source)
    }
  }
  if plan.origins.get(owner) is Some(data) {
    if hint_field(data, "parameters") is Some(v) && v.kind() is List(parameters) {
      for i, p in parameters {
        let identity = match hint_text(p, "identity") {
          Some(id) => Some(id)
          None if parameters.length() == declaration.inputs.length() =>
            Some(declaration.inputs[i].0.to_canonical())
          None => None
        }
        if identity is Some(id) && hint_text(p, "name") is Some(text) {
          save_hint(id, text, hint_span(p))
        }
      }
    }
  }
  if plan.origins.get(owner + "/body") is Some(data) {
    if hint_field(data, "nodes") is Some(v) && v.kind() is List(nodes) {
      for n in nodes {
        if hint_field(n, "binding") is Some(binding) &&
          hint_text(n, "identity") is Some(id) &&
          hint_text(n, "allocation_reason") != Some("lexical-shadow") {
          if hint_text(binding, "name") is Some(text) {
            save_hint(id, text, hint_span(binding))
          }
        }
      }
    }
  }
  let result : LocalNames = {
    names: Map([]),
    used: NameSet::over(plan.reserved),
    plan,
    owner,
  }
  let candidates = []
  for name in names {
    validate_name(name)
    let id = owner + "/local/" + name.to_canonical()
    let hint = hints.get(name.to_canonical())
    let c = candidate(
      id,
      "local",
      name,
      declaration.module_name,
      hint.map(h => h.0),
      hint.map(h => h.1).unwrap_or(None),
      false,
    )
    let c = if plan.style == LegacyHex {
      {
        ..c,
        candidate: local_name(name),
        qualified: local_name(name) + "_local",
        reason: "legacy",
      }
    } else {
      { ..c, qualified: c.candidate + "_local", }
    }
    candidates.push(c)
  }
  allocate_namespace(plan, candidates, result.used, reserve_global=false)
  for name in names {
    result.names[name] = plan.names[owner + "/local/" + name.to_canonical()]
    result.used.add(result.names[name])
  }
  result
}

///|
fn LocalNames::fresh(self : LocalNames, role : String) -> String {
  let mut ordinal = 1
  while true {
    let name = role + "_" + ordinal.to_string()
    if !self.used.contains(name) {
      self.used.add(name)
      self.plan.allocations.push({
        owner: self.owner + "/temporary/" + name,
        role: "temporary",
        original: None,
        requested: None,
        candidate: role,
        allocated: name,
        reason: "fresh",
      })
      return name
    }
    ordinal += 1
  } nobreak {
    abort("unreachable")
  }
}