///|
let encoding_trait : String = "smithy.serialization#jsonEncoding"

///|
let profiles_trait : String = "smithy.serialization#jsonProfiles"

///|
let untagged_trait : String = "smithy.serialization#untagged"

///|
let empty_as_absent_trait : String = "smithy.serialization#emptyAsAbsent"

///|
let bundle_trait : String = "smithy.serialization#jsonBundle"

///|
/// The capabilities that this contract preparation implements.
pub let capabilities : Array[String] = [
  "external", "internal", "adjacent", "tagArray", "untagged", "tuple", "dictionary",
  "entries", "forms", "adapters", "aliases", "bundles", "expandedWriter",
]

///|
priv struct RawUnion {
  style : String
  tag : String?
  content : String?
  unit : String?
  accept : Array[RawUnion]
}

///|
priv struct RawTuple {
  members : Array[String]
  rest : String?
  optional : String?
}

///|
priv struct RawDictionary {
  layout : String
  key : String?
  order : String?
  accept : Array[String]
}

///|
priv struct RawForm {
  union_member : String?
  kind : String
  adapter : String?
  field : String?
  absent_field : String?
  bind : String?
  positions : Array[String]?
  rest : String?
  write : (Array[String], Array[String])?
}

///|
priv struct RawRule {
  union_ : RawUnion?
  tuple : RawTuple?
  dictionary : RawDictionary?
  adapter : String?
  name : String?
  tag : String?
  aliases : Array[String]?
  absent : String?
  flatten : Bool
  forms : Array[RawForm]?
}

///|
fn empty_rule() -> RawRule {
  {
    union_: None,
    tuple: None,
    dictionary: None,
    adapter: None,
    name: None,
    tag: None,
    aliases: None,
    absent: None,
    flatten: false,
    forms: None,
  }
}

///|
fn fields(
  owner : @smithy.ShapeId,
  value : @smithy.NodeValue,
  what : String,
  allowed : Array[String],
) -> Map[String, @smithy.NodeValue] raise ContractError {
  guard value is Object(fields) else {
    raise InvalidDeclaration(owner, "I expected \{what} to be an object.")
  }
  for name, _ in fields {
    if !allowed.contains(name) {
      raise UnsupportedDeclaration(
        owner,
        "I do not implement the \{what} setting \{name.escape()}.",
      )
    }
  }
  fields
}

///|
fn text_field(
  owner : @smithy.ShapeId,
  fields : Map[String, @smithy.NodeValue],
  name : String,
  what : String,
) -> String? raise ContractError {
  match fields.get(name) {
    None => None
    Some(String(text)) => Some(text)
    Some(_) =>
      raise InvalidDeclaration(
        owner,
        "I expected the \{what} setting \{name} to be a string.",
      )
  }
}

///|
fn texts_field(
  owner : @smithy.ShapeId,
  fields : Map[String, @smithy.NodeValue],
  name : String,
  what : String,
) -> Array[String]? raise ContractError {
  match fields.get(name) {
    None => None
    Some(Array(items)) => {
      let out : Array[String] = []
      for item in items {
        guard item is String(text) else {
          raise InvalidDeclaration(
            owner,
            "I expected the \{what} setting \{name} to list strings.",
          )
        }
        out.push(text)
      }
      Some(out)
    }
    Some(_) =>
      raise InvalidDeclaration(
        owner,
        "I expected the \{what} setting \{name} to be a list.",
      )
  }
}

///|
fn parse_union_rule(
  owner : @smithy.ShapeId,
  value : @smithy.NodeValue,
) -> RawUnion raise ContractError {
  let f = fields(owner, value, "union rule", [
    "style", "tag", "content", "unit", "accept",
  ])
  guard text_field(owner, f, "style", "union rule") is Some(style) else {
    raise InvalidDeclaration(owner, "I expected a union rule style.")
  }
  let accept : Array[RawUnion] = []
  match f.get("accept") {
    None => ()
    Some(Array(items)) =>
      for item in items {
        let alternative = parse_union_rule(owner, item)
        if !alternative.accept.is_empty() {
          raise InvalidDeclaration(
            owner, "An accepted union layout cannot accept further layouts.",
          )
        }
        accept.push(alternative)
      }
    Some(_) =>
      raise InvalidDeclaration(
        owner, "I expected a list of accepted union layouts.",
      )
  }
  {
    style,
    tag: text_field(owner, f, "tag", "union rule"),
    content: text_field(owner, f, "content", "union rule"),
    unit: text_field(owner, f, "unit", "union rule"),
    accept,
  }
}

///|
fn parse_dictionary_rule(
  owner : @smithy.ShapeId,
  value : @smithy.NodeValue,
) -> RawDictionary raise ContractError {
  let f = fields(owner, value, "dictionary rule", [
    "layout", "key", "order", "accept",
  ])
  guard text_field(owner, f, "layout", "dictionary rule") is Some(layout) else {
    raise InvalidDeclaration(owner, "I expected a dictionary layout.")
  }
  {
    layout,
    key: text_field(owner, f, "key", "dictionary rule"),
    order: text_field(owner, f, "order", "dictionary rule"),
    accept: texts_field(owner, f, "accept", "dictionary rule").unwrap_or([]),
  }
}

///|
fn parse_rule(
  owner : @smithy.ShapeId,
  value : @smithy.NodeValue,
) -> RawRule raise ContractError {
  let f = fields(owner, value, "encoding rule", [
    "union", "tuple", "dictionary", "adapter", "name", "tag", "aliases", "absent",
    "flatten", "forms",
  ])
  let tuple = match f.get("tuple") {
    None => None
    Some(v) => {
      let t = fields(owner, v, "tuple rule", ["members", "rest", "optional"])
      guard texts_field(owner, t, "members", "tuple rule") is Some(members) else {
        raise InvalidDeclaration(
          owner, "I expected the tuple member positions.",
        )
      }
      Some({
        members,
        rest: text_field(owner, t, "rest", "tuple rule"),
        optional: text_field(owner, t, "optional", "tuple rule"),
      })
    }
  }
  let forms = match f.get("forms") {
    None => None
    Some(Array(items)) => {
      let out : Array[RawForm] = []
      for item in items {
        let g = fields(owner, item, "form", [
          "member", "read", "bind", "positions", "rest", "write",
        ])
        guard g.get("read") is Some(read) else {
          raise InvalidDeclaration(owner, "I expected a form reader guard.")
        }
        let r = fields(owner, read, "form reader guard", [
          "kind", "adapter", "field", "absent",
        ])
        guard text_field(owner, r, "kind", "form reader guard") is Some(kind) else {
          raise InvalidDeclaration(
            owner, "I expected the JSON kind of a form reader guard.",
          )
        }
        let write = match g.get("write") {
          None => None
          Some(w) => {
            let w = fields(owner, w, "form writer guard", ["empty", "present"])
            Some(
              (
                texts_field(owner, w, "empty", "form writer guard").unwrap_or([]),
                texts_field(owner, w, "present", "form writer guard").unwrap_or([],
                ),
              ),
            )
          }
        }
        out.push({
          union_member: text_field(owner, g, "member", "form"),
          kind,
          adapter: text_field(owner, r, "adapter", "form reader guard"),
          field: text_field(owner, r, "field", "form reader guard"),
          absent_field: text_field(owner, r, "absent", "form reader guard"),
          bind: text_field(owner, g, "bind", "form"),
          positions: texts_field(owner, g, "positions", "form"),
          rest: text_field(owner, g, "rest", "form"),
          write,
        })
      }
      Some(out)
    }
    Some(_) => raise InvalidDeclaration(owner, "I expected a list of forms.")
  }
  {
    union_: match f.get("union") {
      None => None
      Some(v) => Some(parse_union_rule(owner, v))
    },
    tuple,
    dictionary: match f.get("dictionary") {
      None => None
      Some(v) => Some(parse_dictionary_rule(owner, v))
    },
    adapter: text_field(owner, f, "adapter", "encoding rule"),
    name: text_field(owner, f, "name", "encoding rule"),
    tag: text_field(owner, f, "tag", "encoding rule"),
    aliases: texts_field(owner, f, "aliases", "encoding rule"),
    absent: text_field(owner, f, "absent", "encoding rule"),
    flatten: match f.get("flatten") {
      None | Some(Boolean(false)) => false
      Some(Boolean(true)) => true
      Some(_) =>
        raise InvalidDeclaration(owner, "I expected flatten to be a boolean.")
    },
    forms,
  }
}

///|
/// The base rule and the profile rules of one shape or member.
priv struct Encoding {
  base : RawRule?
  profiles : Map[String, RawRule]
}

///|
fn shape_encoding(
  shape : @smithy.Shape,
  declared : Map[ProfileId, ProfileDeclaration],
) -> Encoding raise ContractError {
  guard shape.trait_value(encoding_trait) is Some(value) else {
    return { base: None, profiles: Map([]), }
  }
  let f = fields(shape.id, value, "jsonEncoding trait", ["base", "profiles"])
  let profiles : Map[String, RawRule] = Map([])
  match f.get("profiles") {
    None => ()
    Some(Object(rules)) =>
      for name, rule in rules {
        let id = ProfileId::parse(name) catch {
          _ =>
            raise InvalidDeclaration(
              shape.id,
              "I expected a profile identifier, but found \{name.escape()}.",
            )
        }
        guard declared.contains(id) else {
          raise InvalidDeclaration(
            shape.id,
            "I found rules for the undeclared profile \{id}.",
          )
        }
        profiles[name] = parse_rule(shape.id, rule)
      }
    Some(_) =>
      raise InvalidDeclaration(
        shape.id,
        "I expected the profile rules to be an object.",
      )
  }
  {
    base: match f.get("base") {
      None => None
      Some(v) => Some(parse_rule(shape.id, v))
    },
    profiles,
  }
}

///|
/// A declared serialization profile.
pub struct ProfileDeclaration {
  id : ProfileId
  declared_on : @smithy.ShapeId
  /// The profile whose shape rules this profile uses. It is `id` unless the declaration names another.
  rule_set : ProfileId
  writer : WriterMode
  unknown_members : UnknownMemberPolicy
  requires : Array[String]
  priv defaults : RawRule
}

///|
fn parse_profile(
  owner : @smithy.ShapeId,
  value : @smithy.NodeValue,
) -> ProfileDeclaration raise ContractError {
  let f = fields(owner, value, "profile declaration", [
    "id", "documentation", "ruleSet", "writer", "unknownMembers", "defaults", "requires",
  ])
  let parse_id = fn(text : String) raise ContractError {
    ProfileId::parse(text) catch {
      InvalidIdentity(_, message) => raise InvalidDeclaration(owner, message)
    }
  }
  guard text_field(owner, f, "id", "profile declaration") is Some(id) else {
    raise InvalidDeclaration(owner, "I expected a profile identifier.")
  }
  let id = parse_id(id)
  let rule_set = match text_field(owner, f, "ruleSet", "profile declaration") {
    None => id
    Some(text) => parse_id(text)
  }
  let writer = match text_field(owner, f, "writer", "profile declaration") {
    None | Some("canonical") => Canonical
    Some("expanded") => Expanded
    Some(other) =>
      raise UnsupportedDeclaration(
        owner,
        "I do not implement the writer mode \{other.escape()} of profile \{id}.",
      )
  }
  let unknown_members = match
    text_field(owner, f, "unknownMembers", "profile declaration") {
    None | Some("reject") => RejectUnknown
    Some("ignore") => IgnoreUnknown
    Some(other) =>
      raise UnsupportedDeclaration(
        owner,
        "I do not implement the unknown-member policy \{other.escape()} of profile \{id}.",
      )
  }
  let defaults = match f.get("defaults") {
    None => empty_rule()
    Some(v) => {
      let d = fields(owner, v, "profile defaults", ["union", "dictionary"])
      {
        ..empty_rule(),
        union_: match d.get("union") {
          None => None
          Some(u) => Some(parse_union_rule(owner, u))
        },
        dictionary: match d.get("dictionary") {
          None => None
          Some(u) => Some(parse_dictionary_rule(owner, u))
        },
      }
    }
  }
  let requires = texts_field(owner, f, "requires", "profile declaration").unwrap_or([],
  )
  for capability in requires {
    guard capabilities.contains(capability) else {
      raise UnsupportedDeclaration(
        owner,
        "Profile \{id} requires the capability \{capability.escape()}, which I do not implement.",
      )
    }
  }
  if rule_set != id && f.get("defaults") is Some(_) {
    raise InvalidDeclaration(
      owner,
      "Profile \{id} uses the rules of \{rule_set}, so it cannot declare its own defaults.",
    )
  }
  {
    id,
    declared_on: owner,
    rule_set,
    writer,
    unknown_members,
    requires,
    defaults,
  }
}

///|
/// Every profile declared in the model, by identifier. Two declarations of one identifier are a conflict.
pub fn declared_profiles(
  model : @smithy.Model,
) -> Map[ProfileId, ProfileDeclaration] raise ContractError {
  let out : Map[ProfileId, ProfileDeclaration] = Map([])
  let ids = model.shapes.keys().to_array()
  ids.sort_by((a, b) => a.to_string().compare(b.to_string()))
  for id in ids {
    let shape = model.shapes.get(id).unwrap()
    guard shape.trait_value(profiles_trait) is Some(value) else { continue }
    guard value is Array(items) else {
      raise InvalidDeclaration(id, "I expected a list of profile declarations.")
    }
    for item in items {
      let declaration = parse_profile(id, item)
      if out.get(declaration.id) is Some(previous) {
        raise ConflictingDeclaration(
          id,
          "Profile \{declaration.id} is already declared on \{previous.declared_on}.",
        )
      }
      out[declaration.id] = declaration
    }
  }
  for _, declaration in out {
    if declaration.rule_set != declaration.id {
      guard out.get(declaration.rule_set) is Some(base) else {
        raise InvalidDeclaration(
          declaration.declared_on,
          "Profile \{declaration.id} uses the rules of the undeclared profile \{declaration.rule_set}.",
        )
      }
      guard base.rule_set == base.id else {
        raise InvalidDeclaration(
          declaration.declared_on,
          "Profile \{declaration.id} uses the rules of \{base.id}, which uses the rules of another profile.",
        )
      }
    }
  }
  out
}