// Constructors and queries for type entries (upstream type_entry.rs).

///|
fn variants_unique(variants : Array[Variant]) -> Bool {
  unique(variants.map(v => v.ident_name.unwrap()))
}

///|
fn TypeEntryEnum::from_metadata(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  tag_type : EnumTagType,
  variants : Array[Variant],
  deny_unknown_fields : Bool,
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  // First try simple sanitization of the raw names.
  let mut variants = variants.map(v => {
    ..v,
    ident_name: Some(sanitize(v.raw_name, Pascal)),
  })
  // If they collide, turn elided characters into 'X's.
  if !variants_unique(variants) {
    variants = variants.map(v => {
      let buf = StringBuilder()
      for c in v.raw_name {
        buf.write_char(
          if c == '_' || !@unicode.is_xid_continue(c) {
            'X'
          } else {
            c
          },
        )
      }
      { ..v, ident_name: Some(sanitize(buf.to_string(), Pascal)), }
    })
  }
  if !variants_unique(variants) {
    let counts : Map[String, Int] = Map([])
    for v in variants {
      let n = v.ident_name.unwrap()
      counts[n] = match counts.get(n) {
        Some(c) => c + 1
        None => 0
      }
    }
    let dups = variants
      .filter(v => counts.get(v.ident_name.unwrap()).unwrap() > 0)
      .map(v => v.raw_name)
      .join(",")
    raise panic_with("Failed to make unique variant names for [\{dups}]")
  }
  guard get_type_name(type_name, metadata) is Some(name) else {
    raise panic_with("called `Option::unwrap()` on a `None` value (enum name)")
  }
  let patch = TypePatch::new(space.settings, name)
  {
    details: Enum({
      name: patch.name,
      rename: None,
      description: metadata_description(metadata),
      default: None,
      tag_type,
      variants,
      deny_unknown_fields,
      bespoke_impls: [],
      schema,
    }),
    extra_derives: patch.derives,
    extra_attrs: patch.attrs,
  }
}

///|
fn TypeEntryEnum::finalize(
  self : TypeEntryEnum,
  space : TypeSpace,
) -> TypeEntryEnum raise TypifyError {
  let impls = []
  // Not untagged, with all simple variants.
  if self.tag_type != Untagged &&
    !self.variants.is_empty() &&
    self.variants.iter().all(v => v.details is Simple) {
    impls.push(TypeEntryEnumImpl::AllSimpleVariants)
  }
  // Untagged and all variants impl FromStr, but none is irrefutably a String.
  if untagged_newtype_variants(
      space,
      self.tag_type,
      self.variants,
      FromStr,
      Some(FromStringIrrefutable),
    ) {
    impls.push(UntaggedFromStr)
  }
  if untagged_newtype_variants(
      space,
      self.tag_type,
      self.variants,
      Display,
      None,
    ) {
    impls.push(UntaggedDisplay)
  }
  if untagged_newtype_string(space, self.tag_type, self.variants) {
    impls.push(UntaggedFromStringIrrefutable)
  }
  { ..self, bespoke_impls: impls, }
}

///|
/// Untagged enum whose variants are all single-item newtypes implementing
/// `req_impl` (and, if given, none implementing `neg_impl`).
fn untagged_newtype_variants(
  space : TypeSpace,
  tag_type : EnumTagType,
  variants : Array[Variant],
  req_impl : TypeSpaceImpl,
  neg_impl : TypeSpaceImpl?,
) -> Bool raise TypifyError {
  if tag_type != Untagged {
    return false
  }
  for variant in variants {
    match variant.details {
      Item(type_id) => {
        let entry = space.entry_unchecked(type_id)
        if !entry.has_impl(space, req_impl) {
          return false
        }
        if neg_impl is Some(n) && entry.has_impl(space, n) {
          return false
        }
      }
      _ => return false
    }
  }
  true
}

///|
/// Untagged enum where some variant is irrefutably a string.
fn untagged_newtype_string(
  space : TypeSpace,
  tag_type : EnumTagType,
  variants : Array[Variant],
) -> Bool raise TypifyError {
  if tag_type != Untagged {
    return false
  }
  for variant in variants {
    if variant.details is Item(type_id) &&
      space.entry_unchecked(type_id).has_impl(space, FromStringIrrefutable) {
      return true
    }
  }
  false
}

///|
fn TypeEntryStruct::from_metadata(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  properties : Array[StructProperty],
  deny_unknown_fields : Bool,
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  guard get_type_name(type_name, metadata) is Some(name) else {
    raise panic_with(
      "called `Option::unwrap()` on a `None` value (struct name)",
    )
  }
  let patch = TypePatch::new(space.settings, name)
  {
    details: Struct({
      name: patch.name,
      rename: None,
      description: metadata_description(metadata),
      default: metadata_default(metadata),
      properties,
      deny_unknown_fields,
      schema,
    }),
    extra_derives: patch.derives,
    extra_attrs: patch.attrs,
  }
}

///|
fn TypeEntryNewtype::make(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  type_id : TypeId,
  constraints : TypeEntryNewtypeConstraints,
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  guard get_type_name(type_name, metadata) is Some(name) else {
    raise panic_with(
      "called `Option::unwrap()` on a `None` value (newtype name)",
    )
  }
  let patch = TypePatch::new(space.settings, name)
  {
    details: Newtype({
      name: patch.name,
      rename: None,
      description: metadata_description(metadata),
      default: None,
      type_id,
      constraints,
      schema,
    }),
    extra_derives: patch.derives,
    extra_attrs: patch.attrs,
  }
}

///|
fn TypeEntryNewtype::from_metadata(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  type_id : TypeId,
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  TypeEntryNewtype::make(
    space,
    type_name,
    metadata,
    type_id,
    NoConstraints,
    schema,
  )
}

///|
fn TypeEntryNewtype::from_metadata_with_enum_values(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  type_id : TypeId,
  enum_values : Array[@serde_json.Value],
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  TypeEntryNewtype::make(
    space,
    type_name,
    metadata,
    type_id,
    EnumValue(enum_values),
    schema,
  )
}

///|
fn TypeEntryNewtype::from_metadata_with_deny_values(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  type_id : TypeId,
  enum_values : Array[@serde_json.Value],
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  TypeEntryNewtype::make(
    space,
    type_name,
    metadata,
    type_id,
    DenyValue(enum_values),
    schema,
  )
}

///|
fn TypeEntryNewtype::from_metadata_with_string_validation(
  space : TypeSpace,
  type_name : Name,
  metadata : @schema.Metadata?,
  type_id : TypeId,
  validation : @schema.StringValidation,
  schema : @schema.Schema,
) -> TypeEntry raise TypifyError {
  TypeEntryNewtype::make(
    space,
    type_name,
    metadata,
    type_id,
    TypeEntryNewtypeConstraints::String(
      max_length=validation.max_length,
      min_length=validation.min_length,
      pattern=validation.pattern,
    ),
    schema,
  )
}

///|
/// Whether the type is known to implement `impl_name`.
pub fn TypeEntry::has_impl(
  self : TypeEntry,
  space : TypeSpace,
  impl_name : TypeSpaceImpl,
) -> Bool raise TypifyError {
  match self.details {
    Enum(details) =>
      match impl_name {
        Default => details.default is Some(_)
        FromStr =>
          details.bespoke_impls.contains(AllSimpleVariants) ||
          details.bespoke_impls.contains(UntaggedFromStr)
        Display =>
          details.bespoke_impls.contains(AllSimpleVariants) ||
          details.bespoke_impls.contains(UntaggedDisplay)
        FromStringIrrefutable =>
          details.bespoke_impls.contains(UntaggedFromStringIrrefutable)
      }
    Struct(details) => impl_name == Default && details.default is Some(_)
    Newtype(details) =>
      match (details.constraints, impl_name) {
        (_, Default) => details.default is Some(_)
        (String(..), FromStr) => true
        (String(..), Display) => true
        (NoConstraints, _) =>
          space.entry_unchecked(details.type_id).has_impl(space, impl_name)
        _ => false
      }
    Native(details) => details.impls.contains(impl_name)
    Box(type_id) =>
      if impl_name == Default {
        space.entry_unchecked(type_id).has_impl(space, impl_name)
      } else {
        false
      }
    JsonValue => false
    Unit | Option(_) | Vec(_) | Map(_, _) | Set(_) => impl_name == Default
    Tuple(type_ids) => {
      if impl_name != Default || type_ids.length() > 12 {
        return false
      }
      for id in type_ids {
        if !space.entry_unchecked(id).has_impl(space, Default) {
          return false
        }
      }
      true
    }
    Array(item_id, length) =>
      if length <= 32 && impl_name == Default {
        space.entry_unchecked(item_id).has_impl(space, impl_name)
      } else {
        false
      }
    Boolean | Integer(_) | Float(_) => impl_name != FromStringIrrefutable
    String => true
    Reference(_) => raise panic_with("internal error: entered unreachable code")
  }
}