// Schema inspection helpers (upstream typify-impl/src/util.rs).

///|
type Defs = Map[RefKey, @schema.Schema]

///|
/// Whether all subschemas are pairwise mutually exclusive (conservatively).
fn all_mutually_exclusive(
  subschemas : Array[@schema.Schema],
  defs : Defs,
) -> Bool raise TypifyError {
  let len = subschemas.length()
  if len < 2 {
    return true
  }
  for ii in 0..<(len - 1) {
    for jj in (ii + 1).. @schema.SubschemaValidation? {
  match schema {
    Object(o) if shape(o, some=F_SUB) => o.subschemas
    _ => None
  }
}

///|
fn schemas_mutually_exclusive(
  a : @schema.Schema,
  b : @schema.Schema,
  defs : Defs,
) -> Bool raise TypifyError {
  match (a, b) {
    (Bool(false), _) | (_, Bool(false)) => return true
    (Bool(true), _) | (_, Bool(true)) => return false
    _ => ()
  }
  // (other, subschemas) | (subschemas, other): the left alternative first.
  let sub_case = match pure_subschemas(b) {
    Some(s) => Some((a, s))
    None =>
      match pure_subschemas(a) {
        Some(s) => Some((b, s))
        None => None
      }
  }
  if sub_case is Some((other, subschemas)) {
    let m = sub_present(subschemas)
    return if m == S_ALL {
      let mut any = false
      for sub in subschemas.all_of.unwrap() {
        if schemas_mutually_exclusive(sub, other, defs) {
          any = true
          break
        }
      }
      any
    } else if m == S_ANY || m == S_ONE {
      let list = if m == S_ANY {
        subschemas.any_of.unwrap()
      } else {
        subschemas.one_of.unwrap()
      }
      let mut all = true
      for sub in list {
        if !schemas_mutually_exclusive(sub, other, defs) {
          all = false
          break
        }
      }
      all
    } else if m == S_NOT {
      !schemas_mutually_exclusive(subschemas.not.unwrap(), other, defs)
    } else {
      false
    }
  }
  // Enumerated values without a type.
  let enum_case = match b {
    Object({ instance_type: None, enum_values: Some(values), .. }) =>
      Some((a, values))
    _ =>
      match a {
        Object({ instance_type: None, enum_values: Some(values), .. }) =>
          Some((b, values))
        _ => None
      }
  }
  if enum_case is Some((schema, values)) {
    return values.iter().all(v => schema_value_validate(schema, v) is Some(_))
  }
  // Constant values.
  let const_case = match b {
    Object({ const_value: Some(value), .. }) => Some((a, value))
    _ =>
      match a {
        Object({ const_value: Some(value), .. }) => Some((b, value))
        _ => None
      }
  }
  if const_case is Some((schema, value)) {
    return schema_value_validate(schema, value) is Some(_)
  }
  guard (a, b) is (Object(a), Object(b)) else { abort("unreachable") }
  match (a.instance_type, b.instance_type) {
    (None, _) | (_, None) => false
    (Some(Single(x)), Some(Single(y))) if x != y => true
    (Some(Single(Object)), Some(Single(Object))) =>
      if shape(a, some=F_OBJ, any=F_META | F_TYPE) &&
        shape(b, some=F_OBJ, any=F_META | F_TYPE) {
        object_schemas_mutually_exclusive(a.object.unwrap(), b.object.unwrap())
      } else {
        false
      }
    (Some(Single(Array)), Some(Single(Array))) =>
      if shape(a, some=F_ARR, any=F_META | F_TYPE) &&
        shape(b, some=F_ARR, any=F_META | F_TYPE) {
        array_schemas_mutually_exclusive(
          a.array.unwrap(),
          b.array.unwrap(),
          defs,
        )
      } else {
        false
      }
    (Some(Single(x)), Some(Single(y))) => x != y
    (Some(Vec(xs)), Some(Vec(ys))) => xs.iter().all(t => !ys.contains(t))
    (Some(Single(single)), Some(Vec(vec)))
    | (Some(Vec(vec)), Some(Single(single))) => !vec.contains(single)
  }
}

///|
fn object_schemas_mutually_exclusive(
  a : @schema.ObjectValidation,
  b : @schema.ObjectValidation,
) -> Bool {
  if a.properties.is_empty() || b.properties.is_empty() {
    return false
  }
  let a_props = @collections.StrSet::from_array(a.properties.keys())
  let b_props = @collections.StrSet::from_array(b.properties.keys())
  if !a.required.is_subset(b_props) || !b.required.is_subset(a_props) {
    return true
  }
  // Fixed-value required properties (e.g. enum tags) may still differ.
  let fixed = (v : @schema.ObjectValidation) => {
    let out : Array[(String, String)] = []
    for name in v.required.iter() {
      let t = v.properties.get(name).unwrap()
      if constant_string_value(t) is Some(s) && !out.contains((name, s)) {
        out.push((name, s))
      }
    }
    out
  }
  let aa = fixed(a)
  let bb = fixed(b)
  let subset = (x : Array[(String, String)], y : Array[(String, String)]) => {
    x.iter().all(e => y.contains(e))
  }
  !subset(aa, bb) && !subset(bb, aa)
}

///|
fn array_schemas_mutually_exclusive(
  a : @schema.ArrayValidation,
  b : @schema.ArrayValidation,
  defs : Defs,
) -> Bool raise TypifyError {
  let tuple_vs_single = (
    single : @schema.ArrayValidation,
    tuple : @schema.ArrayValidation,
  ) => {
    match (single, tuple) {
      (
        { items: Some(Single(s)), additional_items: None, .. },
        {
          items: Some(Vec(vec)),
          additional_items: None,
          max_items: Some(max_items),
          min_items: Some(min_items),
          unique_items: None,
          contains: None,
        },
      ) if max_items == min_items &&
        max_items.reinterpret_as_int() == vec.length() => Some((s, vec))
      _ => None
    }
  }
  let pair = match tuple_vs_single(a, b) {
    Some(p) => Some(p)
    None => tuple_vs_single(b, a)
  }
  if pair is Some((single, vec)) {
    for schema in vec {
      if schemas_mutually_exclusive(schema, single, defs) {
        return true
      }
    }
    return false
  }
  if (a.max_items, b.min_items) is (Some(max), Some(min)) && min > max {
    return true
  }
  if (b.max_items, a.min_items) is (Some(max), Some(min)) && min > max {
    return true
  }
  if (a.items, b.items) is (Some(Single(ai)), Some(Single(bi))) &&
    schemas_mutually_exclusive(ai, bi, defs) {
    return true
  }
  false
}

///|
/// If the schema is a constant string (a singleton `enum` or a `const`),
/// return it.
fn constant_string_value(schema : @schema.Schema) -> String? {
  guard schema is Object(o) else { return None }
  let is_string_or_none = match o.instance_type {
    Some(Single(String)) => true
    None => true
    _ => false
  }
  if !is_string_or_none {
    return None
  }
  if shape(o, some=F_ENUM, any=F_META | F_TYPE) {
    let values = o.enum_values.unwrap()
    if values.length() == 1 {
      return values[0].as_str()
    }
    return None
  }
  if shape(o, some=F_CONST, any=F_META | F_TYPE) {
    return o.const_value.unwrap().as_str()
  }
  None
}

///|
fn decode_segment(segment : String) -> String {
  segment.replace_all(old="~1", new="/").replace_all(old="~0", new="~")
}

///|
/// The definition key referenced by a `$ref` string.
fn ref_key(ref_name : String) -> RefKey raise TypifyError {
  if ref_name == "#" {
    return Root
  }
  match ref_name.rev_find("/") {
    Some(idx) =>
      Def(
        decode_segment(
          ref_name.unsafe_substring(start=idx + 1, end=ref_name.length()),
        ),
      )
    None => raise panic_with("expected a '/' in $ref: \{ref_name}")
  }
}

///|
/// Resolve a bare `$ref` schema to its definition.
fn resolve(
  schema : @schema.Schema,
  defs : Defs,
) -> @schema.Schema raise TypifyError {
  match schema {
    Bool(_) => schema
    // Metadata is ignored, as in upstream's pattern (`metadata: _`).
    Object(o) if shape(o, some=F_REF, any=F_META) => {
      let key = ref_key(o.reference.unwrap())
      match defs.get(key) {
        Some(s) => s
        None =>
          raise panic_with("unresolved reference: \{o.reference.unwrap()}")
      }
    }
    Object({ reference: None, .. }) => schema
    Object(_) => raise panic_with("not yet implemented")
  }
}

///|
/// A name for a schema, if one can be inferred (for untagged enum variants).
fn schema_is_named(schema : @schema.Schema) -> String? {
  guard schema is Object(o) else { return None }
  let raw_name = if shape(o, some=F_REF, any=F_META) {
    let reference = o.reference.unwrap()
    guard reference.rev_find("/") is Some(idx) else { return None }
    reference.unsafe_substring(start=idx + 1, end=reference.length())
  } else if o.metadata is Some({ title: Some(title), .. }) {
    title
  } else if shape(o, some=F_SUB, any=F_META | F_TYPE) {
    match singleton_subschema(o.subschemas.unwrap()) {
      Some(sub) => return schema_is_named(sub)
      None => return None
    }
  } else if o.instance_type is Some(Single(single)) {
    match (single, o.format) {
      (_, Some(format)) => @heck.to_pascal_case(format)
      (Boolean, _) => "Boolean"
      (Integer, _) => "Integer"
      (Number, _) => "Number"
      (String, _) => "String"
      (Array, _) => "Array"
      (Object, _) => "Object"
      (Null, _) => "Null"
    }
  } else {
    return None
  }
  Some(sanitize(raw_name, Pascal))
}

///|
/// Whether `additionalProperties` is absent or `false`.
fn schema_none_or_false(additional : @schema.Schema?) -> Bool {
  additional is (None | Some(Bool(false)))
}

///|
fn simple_object_validation(v : @schema.ObjectValidation) -> Bool {
  schema_none_or_false(v.additional_properties) &&
  v.max_properties is None &&
  v.min_properties is None &&
  v.pattern_properties.is_empty() &&
  v.property_names is None
}

///|
/// Return the metadata and object validation of an object schema of the
/// shape we know how to handle.
fn get_object(
  schema : @schema.Schema,
) -> (@schema.Metadata?, @schema.ObjectValidation)? {
  guard schema is Object(o) else { return None }
  if o.instance_type is Some(Single(Object)) &&
    shape(o, some=F_TYPE | F_OBJ, any=F_META | F_NUM | F_STR | F_ARR) &&
    simple_object_validation(o.object.unwrap()) {
    return Some((o.metadata, o.object.unwrap()))
  }
  if shape(o, some=F_OBJ, any=F_META) &&
    simple_object_validation(o.object.unwrap()) {
    return Some((o.metadata, o.object.unwrap()))
  }
  if shape(o, some=F_SUB, any=F_META | F_TYPE) {
    guard singleton_subschema(o.subschemas.unwrap()) is Some(sub) else {
      return None
    }
    return match get_object(sub) {
      Some((Some(_), validation)) => Some((o.metadata, validation))
      Some((None, validation)) => Some((None, validation))
      None => None
    }
  }
  None
}

///|
/// The lone schema of a one-element `allOf`, `anyOf` or `oneOf`.
fn singleton_subschema(s : @schema.SubschemaValidation) -> @schema.Schema? {
  let list = match sub_present(s) {
    m if m == S_ALL => s.all_of
    m if m == S_ANY => s.any_of
    m if m == S_ONE => s.one_of
    _ => None
  }
  match list {
    Some([only]) => Some(only)
    _ => None
  }
}

///|
/// Validates strings against `maxLength`, `minLength` and `pattern`.
priv struct StringValidator {
  max_length : UInt?
  min_length : UInt?
  pattern : @regex.Regex?
}

///|
fn StringValidator::new(
  type_name : Name,
  validation : @schema.StringValidation?,
) -> StringValidator raise TypifyError {
  match validation {
    None => { max_length: None, min_length: None, pattern: None, }
    Some(v) => {
      let pattern = match v.pattern {
        None => None
        Some(p) =>
          Some(
            @regex.Regex::new(p) catch {
              e =>
                raise InvalidSchema(
                  type_name=type_name.into_option(),
                  reason="invalid pattern '\{p}' \{e}",
                )
            },
          )
      }
      { max_length: v.max_length, min_length: v.min_length, pattern, }
    }
  }
}

///|
fn StringValidator::is_valid(self : StringValidator, s : String) -> Bool {
  // Lengths count code points.
  let len = s.char_length().reinterpret_as_uint()
  self.max_length.map_or(true, max => len <= max) &&
  self.min_length.map_or(true, min => len >= min) &&
  self.pattern.map_or(true, p => p.find(s) is Some(_))
}

///|
/// Instance types reordered so integers come before numbers.
fn reordered_rank(t : @schema.InstanceType) -> Int {
  match t {
    Null => 0
    Boolean => 1
    Integer => 2
    Number => 3
    String => 4
    Array => 5
    Object => 6
  }
}