///|
pub const NAMING_POLICY : String = "moonbit-readable-targets-v1"

///|
#derive(Eq, Debug)
pub(all) enum NamingStyle {
  Readable
  LegacyHex
}

///|
pub extend NamingStyle with Eq::{equal, not_equal}

///|
pub extend NamingStyle with @debug.Debug::{to_repr}

///|
#derive(Eq, Debug)
pub(all) struct NamingAllocation {
  owner : String
  role : String
  original : String?
  requested : String?
  candidate : String
  allocated : String
  reason : String
}

///|
pub extend NamingAllocation with Eq::{equal, not_equal}

///|
pub extend NamingAllocation with @debug.Debug::{to_repr}

///|
priv struct NamingCandidate {
  owner : String
  role : String
  original : String?
  requested : String?
  candidate : String
  qualified : String
  source : @diagnostics.Span?
  reason : String
}

///|
priv struct TargetNames {
  style : NamingStyle
  facts : @linked_metadata.Interpretation
  origins : Map[String, @ion.IonValue]
  names : Map[String, String]
  fields : Map[String, Map[@ir.Name, String]]
  reserved : Map[String, Bool]
  reserved_locations : Map[String, Array[@diagnostics.Location]]
  allocations : Array[NamingAllocation]
  warnings : Array[@diagnostics.Warning]
}

///|
fn keyword(text : String) -> Bool {
  [
    "as", "else", "extern", "fn", "if", "let", "const", "match", "using", "mut",
    "type", "struct", "enum", "extenum", "trait", "derive", "while", "break", "continue",
    "import", "return", "throw", "raise", "try", "catch", "pub", "priv", "readonly",
    "true", "false", "_", "test", "loop", "for", "in", "impl", "with", "guard", "async",
    "is", "suberror", "and", "letrec", "enumview", "noraise", "nocancel", "defer",
    "lexmatch", "lexscan", "where", "declare", "nobreak", "proof_assert", "proof_let",
    "main", "init", "module", "move", "ref", "static", "super", "unsafe", "use",
    "await", "dyn", "abstract", "do", "final", "macro", "override", "typeof", "virtual",
    "yield", "local", "method", "alias", "assert", "package", "recur", "isnot", "define",
    "downcast", "inherit", "member", "namespace", "upcast", "void", "lazy", "include",
    "mixin", "protected", "sealed", "constructor", "atomic", "volatile", "anyframe",
    "anytype", "asm", "comptime", "errdefer", "export", "opaque", "orelse", "resume",
    "threadlocal", "unreachable", "dynclass", "dynobj", "dynrec", "var", "finally",
    "noasync", "assume", "extend",
  ].contains(text)
}

///|
fn usable_identifier(text : String, upper : Bool) -> Bool {
  let chars = text.to_array()
  if chars.is_empty() {
    return false
  }
  if upper {
    if chars[0] < 'A' || chars[0] > 'Z' {
      return false
    }
  } else if (chars[0] < 'a' || chars[0] > 'z') && chars[0] != '_' {
    return false
  }
  if text == "_" {
    return false
  }
  chars.all(c => {
    (c >= 'a' && c <= 'z') ||
    (c >= 'A' && c <= 'Z') ||
    (c >= '0' && c <= '9') ||
    c == '_'
  })
}

///|
fn render_name(name : @ir.Name, upper : Bool) -> String {
  let parts = name.to_canonical().split("-").map(StringView::to_owned).collect()
  let text = if upper {
    parts
    .map(p => {
      let chars = p.to_array()
      chars[0].to_string().to_upper() +
      chars[1:].iter().map(Char::to_string).collect().join("")
    })
    .join("")
  } else {
    parts.join("_")
  }
  if usable_identifier(text, upper) {
    text
  } else {
    (if upper { "Type_" } else { "value_" }) + text
  }
}

///|
/// `owned` holds the node identities of the input, from `@identities.nodes`.
fn TargetNames::new(
  input : Input,
  style : NamingStyle,
  capabilities : @linked_metadata.Capabilities,
  owned : Map[String, Bool],
) -> TargetNames raise {
  let plan : TargetNames = {
    style,
    facts: @linked_metadata.interpret(
      input.ir,
      input.unit_id,
      input.metadata.map(m => { node_id: m.node_id, data: m.data, }),
      capabilities~,
      owned~,
    ),
    origins: Map([]),
    names: Map([]),
    fields: Map([]),
    reserved: Map([]),
    reserved_locations: Map([]),
    allocations: [],
    warnings: [],
  }
  let same_language = moonbit_origins(input)
  let facts_by_owner = Map::from_array(plan.facts.facts.map(f => (f.owner, f)))
  if same_language {
    let ambiguous : Map[String, Bool] = Map([])
    for metadata in input.metadata {
      if hint_field(metadata.data, "declaration") is None &&
        hint_field(metadata.data, "parameters") is None &&
        hint_field(metadata.data, "nodes") is None {
        continue
      }
      if plan.origins.contains(metadata.node_id) {
        plan.origins.remove(metadata.node_id)
        ambiguous[metadata.node_id] = true
      } else if !ambiguous.contains(metadata.node_id) {
        plan.origins[metadata.node_id] = metadata.data
      }
    }
  }
  guard input.ir.distribution is Library(package_name, _, definition) else {
    return plan
  }
  let values = []
  let types = []
  let constructors = []
  for module_name, access in definition.modules {
    for name, _ in access.value.values {
      let fqname : @ir.FQName = { package_name, module_name, local_name: name, }
      validate_fqname(fqname)
      let owner = value_node(fqname)
      let (hint, source) = declaration_hint(plan, owner)
      let fact = facts_by_owner.get(owner)
      let original = if same_language {
        match fact.map(f => f.names.frontend.get("moonbit")).unwrap_or(None) {
          Some(value) => Some(value)
          None => hint
        }
      } else {
        hint
      }
      let requested = fact
        .map(f => f.names.backend.get("moonbit"))
        .unwrap_or(None)
      if requested is Some(text) &&
        (!usable_identifier(text, false) || keyword(text)) {
        raise Invalid(
          "metadata.invalid_target_request",
          owner,
          "Requested MoonBit name '" +
          text +
          "' must be a portable lowercase or underscore identifier and a non-keyword; edit backend.moonbit",
        )
      }
      let c = candidate(
        owner,
        "value",
        name,
        module_name,
        original,
        source,
        false,
        requested~,
      )
      values.push(
        if style == LegacyHex {
          {
            ..c,
            candidate: value_name(fqname),
            qualified: "value_" + value_name(fqname),
            reason: "legacy",
          }
        } else {
          c
        },
      )
    }
    for name, entry in access.value.types {
      let fqname : @ir.FQName = { package_name, module_name, local_name: name, }
      validate_fqname(fqname)
      if entry.value.value is CustomTypeDefinition(_, cases) {
        let owner = "type:" + fqname.to_canonical()
        if style == LegacyHex {
          plan.names[owner] = "T_" + hex(fqname.to_canonical())
        } else {
          types.push(
            candidate(owner, "type", name, module_name, None, None, true),
          )
        }
        for name, _ in cases.value {
          let ctor = { ..fqname, local_name: name, }
          let owner = "constructor:" + ctor.to_canonical()
          if style == LegacyHex {
            plan.names[owner] = "C_" + hex(ctor.to_canonical())
          } else {
            constructors.push(
              candidate(
                owner,
                "constructor",
                name,
                module_name,
                None,
                None,
                true,
              ),
            )
          }
        }
      }
    }
  }
  values.sort_by(fn(a, b) { a.owner.compare(b.owner) })
  let requests : Map[String, String] = Map([])
  for c in values {
    if c.requested is Some(text) {
      if requests.get(text) is Some(previous) {
        raise Invalid(
          "metadata.conflicting_target_request",
          c.owner,
          "Exact name '" +
          text +
          "' is also requested by " +
          previous +
          "; set distinct backend.moonbit names for both owners",
        )
      }
      requests[text] = c.owner
    }
  }
  allocate_namespace(plan, values, NameSet::of([]))
  allocate_namespace(
    plan,
    types,
    NameSet::of([
      "Int", "Int16", "Int32", "Int64", "UInt", "UInt16", "UInt32", "UInt64", "Bool",
      "Double", "Float", "String", "StringView", "Char", "Byte", "Bytes", "BytesView",
      "Unit", "Array", "FixedArray", "Option", "Result",
    ]),
  )
  allocate_namespace(
    plan,
    constructors,
    NameSet::of(["Some", "None", "Ok", "Err"]),
  )
  plan
}