///|
fn reference_hidden_member_name(name : String) -> String {
  hidden_member_name("__ref_" + name)
}

///|
fn is_reference_value_members(members : Array[ValueMember]) -> Bool {
  object_class_tag_matches(members, "Reference") &&
  lookup_member(members, "__ref_domain") is Some(_)
}

///|
fn reference_hidden_value(
  members : Array[ValueMember],
  name : String,
) -> Value? {
  lookup_member(members, "__ref_" + name)
}

///|
fn reference_referent_type(members : Array[ValueMember]) -> String? {
  match reference_hidden_value(members, "type") {
    Some(StringValue(type_name)) => Some(type_name)
    _ => None
  }
}

///|
fn reference_path_values(members : Array[ValueMember]) -> Array[Value] {
  match reference_hidden_value(members, "path") {
    Some(ListValue(values)) => values
    Some(ListingValue(values)) => values
    _ => []
  }
}

///|
fn reference_value(
  domain : Value,
  referent_type : String,
  data : Value,
  path : Array[Value],
) -> Value {
  ObjectValue(
    tag_object_with_class(
      [
        {
          name: reference_hidden_member_name("domain"),
          value: domain,
          source: None,
          annotations: [],
        },
        {
          name: reference_hidden_member_name("type"),
          value: StringValue(referent_type),
          source: None,
          annotations: [],
        },
        {
          name: reference_hidden_member_name("data"),
          value: data,
          source: None,
          annotations: [],
        },
        {
          name: reference_hidden_member_name("path"),
          value: ListValue(path),
          source: None,
          annotations: [],
        },
      ],
      "ref.Reference",
    ),
  )
}

///|
fn reference_rebind_data(value : Value, data : Value) -> Value {
  match value {
    ObjectValue(members) if is_reference_value_members(members) => {
      let next : Array[ValueMember] = []
      let data_name = reference_hidden_member_name("data")
      for value_member in members {
        if value_member.name == data_name {
          next.push({
            name: value_member.name,
            value: data,
            source: value_member.source,
            annotations: value_member.annotations,
          })
        } else {
          next.push(value_member)
        }
      }
      ObjectValue(next)
    }
    _ => value
  }
}

///|
fn reference_access_value(property : String?, key : Value) -> Value {
  ObjectValue(
    tag_object_with_class(
      [
        {
          name: "isProperty",
          value: BoolValue(property is Some(_)),
          source: None,
          annotations: [],
        },
        {
          name: "isSubscript",
          value: BoolValue(property is None),
          source: None,
          annotations: [],
        },
        {
          name: "property",
          value: match property {
            Some(name) => StringValue(name)
            None => NullValue
          },
          source: None,
          annotations: [],
        },
        { name: "key", value: key, source: None, annotations: [] },
      ],
      "ref.Access",
    ),
  )
}

///|
fn reference_simple_type_name(type_name : String) -> String {
  let base = match pkl_constrained_type_base_name(type_name) {
    Some(name) => name
    None => type_name
  }
  let without_module = match base.find("#") {
    Some(index) =>
      String::unsafe_substring(base, start=index + 1, end=base.length())
    None => base
  }
  match without_module.rev_find(".") {
    Some(index) =>
      String::unsafe_substring(
        without_module,
        start=index + 1,
        end=without_module.length(),
      )
    None => without_module
  }
}

///|
fn reference_annotation_head(type_name : String) -> String {
  let trimmed = pkl_strip_default_type_marker(trim_spaces(type_name))
  let base = match pkl_constrained_type_base_name(trimmed) {
    Some(name) => name
    None => trimmed
  }
  match base.find("<") {
    Some(index) => String::unsafe_substring(base, start=0, end=index)
    None => base
  }
}

///|
fn reference_value_satisfies_annotation(
  type_name : String,
  value : Value,
  declarations : Array[Declaration],
) -> Bool {
  guard value is ObjectValue(members) && is_reference_value_members(members) else {
    return false
  }
  for choice in split_top_level_union_choices(type_name) {
    let head = reference_annotation_head(choice)
    if head == "Reference" ||
      head == "ref.Reference" ||
      head == "Ref" ||
      head.has_suffix("Reference") {
      return true
    }
    for declaration in declarations {
      match declaration {
        TypeAliasDeclaration(alias_decl) if alias_decl.name == head => {
          let target_head = reference_annotation_head(alias_decl.target)
          if target_head == "Reference" || target_head == "ref.Reference" {
            return true
          }
        }
        _ => ()
      }
    }
  }
  false
}

///|
fn reference_normalize_type(
  raw : String,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
  cache : Array[ValueBinding],
) -> String {
  let aliases = eval_type_alias_bindings(declarations)
  let resolved = eval_resolved_type_alias(trim_spaces(raw), aliases)
  let unconstrained = match pkl_constrained_type_base_name(resolved) {
    Some(base) => base
    None => resolved
  }
  if unconstrained.has_suffix("?") {
    let inner = String::unsafe_substring(
      unconstrained,
      start=0,
      end=unconstrained.length() - 1,
    )
    let normalized_inner = reference_normalize_type(
      inner, class_env, declarations, cache,
    )
    return if normalized_inner == "String" {
      "Null | String"
    } else {
      normalized_inner + " | Null"
    }
  }
  let choices = split_top_level_union_choices(unconstrained)
  if choices.length() > 1 {
    let normalized : Array[String] = []
    for choice in choices {
      normalized.push(
        reference_normalize_type(choice, class_env, declarations, cache),
      )
    }
    return evaluator_path_join(normalized, " | ")
  }
  match unconstrained.find("<") {
    Some(index) if unconstrained.has_suffix(">") => {
      let head = String::unsafe_substring(unconstrained, start=0, end=index)
      let inner = String::unsafe_substring(
        unconstrained,
        start=index + 1,
        end=unconstrained.length() - 1,
      )
      let args = split_top_level_generic_arguments(inner)
      let normalized : Array[String] = []
      for arg in args {
        normalized.push(
          reference_normalize_type(arg, class_env, declarations, cache),
        )
      }
      return head + "<" + evaluator_path_join(normalized, ", ") + ">"
    }
    _ => ()
  }
  let simple = reference_simple_type_name(unconstrained)
  if is_stdlib_class_name(simple) || is_stdlib_type_alias_name(simple) {
    simple
  } else {
    match lookup_class_binding(class_env, simple) {
      Some(_) =>
        match lookup_value(cache, "@__module_name") {
          Some(StringValue(module_name)) if module_name != "" =>
            module_name + "#" + simple
          _ => simple
        }
      None => unconstrained
    }
  }
}

///|
fn reference_property_referent_type(
  members : Array[ValueMember],
  property_name : String,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
  cache : Array[ValueBinding],
) -> String {
  let current = match reference_referent_type(members) {
    Some(type_name) => type_name
    None => "Any"
  }
  let simple = reference_simple_type_name(current)
  match
    class_property_type_annotation_from_class_env(
      simple, property_name, class_env,
    ) {
    Some(type_name) =>
      reference_normalize_type(type_name, class_env, declarations, cache)
    None => reference_normalize_type(current, class_env, declarations, cache)
  }
}

///|
fn reference_subscript_referent_type(
  members : Array[ValueMember],
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
  cache : Array[ValueBinding],
) -> String {
  let current = match reference_referent_type(members) {
    Some(type_name) => type_name
    None => return "Any"
  }
  let normalized = reference_normalize_type(
    current, class_env, declarations, cache,
  )
  for head in ["Mapping", "Map"] {
    match generic_argument_text(normalized, head) {
      Some(inner) => {
        let args = split_top_level_generic_arguments(inner)
        if args.length() == 2 {
          return reference_normalize_type(
            args[1],
            class_env,
            declarations,
            cache,
          )
        }
      }
      None => ()
    }
  }
  for head in ["Listing", "List", "Set", "Collection"] {
    match generic_argument_text(normalized, head) {
      Some(inner) =>
        return reference_normalize_type(inner, class_env, declarations, cache)
      None => ()
    }
  }
  normalized
}

///|
fn reference_append_access(
  members : Array[ValueMember],
  referent_type : String,
  access : Value,
) -> Value? {
  let domain = match reference_hidden_value(members, "domain") {
    Some(value) => value
    None => return None
  }
  let data = match reference_hidden_value(members, "data") {
    Some(value) => value
    None => return None
  }
  let path = reference_path_values(members)
  let next_path : Array[Value] = []
  for item in path {
    next_path.push(item)
  }
  next_path.push(access)
  Some(reference_value(domain, referent_type, data, next_path))
}

///|
fn eval_reference_property_access(
  members : Array[ValueMember],
  property_name : String,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
  cache : Array[ValueBinding],
) -> Value? {
  if !is_reference_value_members(members) {
    return None
  }
  reference_append_access(
    members,
    reference_property_referent_type(
      members, property_name, class_env, declarations, cache,
    ),
    reference_access_value(Some(property_name), NullValue),
  )
}

///|
fn eval_reference_subscript_access(
  members : Array[ValueMember],
  key : Value,
  class_env : Array[ClassBinding],
  declarations : Array[Declaration],
  cache : Array[ValueBinding],
) -> Value? {
  if !is_reference_value_members(members) {
    return None
  }
  reference_append_access(
    members,
    reference_subscript_referent_type(members, class_env, declarations, cache),
    reference_access_value(None, key),
  )
}

///|
fn reference_values_equal(
  left : Array[ValueMember],
  right : Array[ValueMember],
) -> Bool {
  match
    (
      reference_hidden_value(left, "domain"),
      reference_hidden_value(right, "domain"),
      reference_hidden_value(left, "type"),
      reference_hidden_value(right, "type"),
      reference_hidden_value(left, "data"),
      reference_hidden_value(right, "data"),
      reference_hidden_value(left, "path"),
      reference_hidden_value(right, "path"),
    ) {
    (
      Some(left_domain),
      Some(right_domain),
      Some(left_type),
      Some(right_type),
      Some(left_data),
      Some(right_data),
      Some(left_path),
      Some(right_path),
    ) =>
      values_equal(left_domain, right_domain) &&
      values_equal(left_type, right_type) &&
      values_equal(left_data, right_data) &&
      values_equal(left_path, right_path)
    _ => false
  }
}

///|
fn eval_reference_to_string(
  reference : Array[ValueMember],
  bindings : Array[Binding],
  env : Array[ValueBinding],
  class_env : Array[ClassBinding],
  cache : Array[ValueBinding],
  stack : Array[String],
  declarations : Array[Declaration],
  diagnostics : Array[Diagnostic],
  resolve_import : (String) -> EvalResult?,
) -> Value? {
  let domain_members = match reference_hidden_value(reference, "domain") {
    Some(ObjectValue(members)) => members
    _ => {
      diagnostics.push(diag("Reference domain must be an object"))
      return None
    }
  }
  let domain_class = match find_object_class_tag(domain_members) {
    Some(name) => name
    None => {
      diagnostics.push(diag("Reference domain must extend ref.Domain"))
      return None
    }
  }
  let render_method = match
    lookup_class_method(class_env, domain_class, "renderReference") {
    Some(function_decl) => function_decl
    None => {
      diagnostics.push(diag("Reference domain must implement renderReference"))
      return None
    }
  }
  let body = match render_method.body {
    Some(body) => body
    None => {
      diagnostics.push(diag("Reference domain renderReference has no body"))
      return None
    }
  }
  if render_method.parameters.length() != 1 {
    diagnostics.push(
      diag("Reference domain renderReference expects one parameter"),
    )
    return None
  }
  let method_cache = copy_value_bindings(cache)
  push_receiver_method_bindings(method_cache, domain_members)
  push_super_dispatch_marker(method_cache, domain_class)
  push_sibling_class_methods(method_cache, domain_class, class_env, env, cache)
  method_cache.push({
    name: render_method.parameters[0].name,
    value: ObjectValue(reference),
  })
  eval_expr_with_bindings(
    body, bindings, env, class_env, method_cache, stack, declarations, diagnostics,
    resolve_import,
  )
}

///|
fn render_reference_embedded_value(value : Value, buf : StringBuilder) -> Unit {
  match value {
    ObjectValue(members) =>
      match find_object_class_tag(members) {
        Some(class_name) => render_pcf_class_inline(class_name, members, buf)
        None => render_pcf_inline(value, 0, false, buf)
      }
    _ => render_pcf_inline(value, 0, false, buf)
  }
}

///|
fn render_reference_constructor(
  members : Array[ValueMember],
  buf : StringBuilder,
) -> Bool {
  if !is_reference_value_members(members) {
    return false
  }
  let domain = match reference_hidden_value(members, "domain") {
    Some(value) => value
    None => return false
  }
  let referent_type = match reference_referent_type(members) {
    Some(value) => value
    None => return false
  }
  let data = match reference_hidden_value(members, "data") {
    Some(value) => value
    None => return false
  }
  buf.write_string("Reference(")
  render_reference_embedded_value(domain, buf)
  buf.write_string(", ")
  buf.write_string(referent_type)
  buf.write_string(", ")
  render_reference_embedded_value(data, buf)
  buf.write_char(')')
  for access in reference_path_values(members) {
    match access {
      ObjectValue(access_members) =>
        match lookup_member(access_members, "property") {
          Some(StringValue(name)) => {
            buf.write_char('.')
            buf.write_string(name)
          }
          _ => {
            buf.write_char('[')
            match lookup_member(access_members, "key") {
              Some(key) => render_reference_embedded_value(key, buf)
              None => buf.write_string("null")
            }
            buf.write_char(']')
          }
        }
      _ => ()
    }
  }
  true
}