///|
/// The first line of each generated MoonBit file.
let generated_header : String = "// Code generated by moonbit-jsonrpc-codegen. DO NOT EDIT."

///|
/// The version of moonbitlang/async that the generated module imports. The
/// runtime in `runtimes/moonbit` is tested with this version.
let async_version : String = "0.22.5"

///|
/// The state of one generator run.
priv struct Gen {
  model : @smithy.Model
  settings : @jsonrpc.PeerSettings
  names : @codegen.ServiceNames
  module_name : String
  delegator : @codegen.WriterDelegator
  // The prepared structure contracts of the peer's services, by shape.
  values : Map[@smithy.ShapeId, @validation.StructureContract]
  value_config : @validation.Config
  // The shapes that I wrote, and the error shapes in the order that I wrote them.
  written : Map[@smithy.ShapeId, Unit]
  errors : Array[@smithy.ShapeId]
  // The generated type names, to find two shapes with one name.
  type_names : Map[String, @smithy.ShapeId]
  // The type names that the generator itself declares.
  generated_names : Array[String]
}

///|
fn module_namespace(module_name : String) -> @codegen.Namespace {
  @codegen.Namespace::parse(module_name, separator="/").unwrap()
}

///|
/// I accept a module name such as `example/terminal`: two or more segments of
/// lower-case ASCII letters, digits, `_` and `-`, joined by `/`.
fn check_module_name(text : String) -> Unit raise @codegen.CodegenError {
  let segments = text.split("/").map(part => part.to_owned()).collect()
  let valid = segments.length() >= 2 &&
    segments
    .iter()
    .all(segment => {
      !segment.is_empty() &&
      segment
      .iter()
      .all(c => {
        c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-'
      })
    })
  if !valid {
    raise @codegen.InvalidSettings(
      "I expected setting `module` to be a MoonBit module name such as `example/terminal`, found `\{text}`.",
    )
  }
}

///|
fn Gen::shape(
  self : Gen,
  id : @smithy.ShapeId,
) -> @smithy.Shape raise @codegen.CodegenError {
  match self.model.shapes.get(id) {
    Some(shape) => shape
    None =>
      raise @codegen.InvalidSettings(
        "I could not find shape `\{id}` in the model.",
      )
  }
}

///|
/// The type name of a generated shape.
fn Gen::type_name_of(self : Gen, id : @smithy.ShapeId) -> String {
  type_name(self.names.shape_name(id))
}

///|
/// The timestamp format of a member and its target, as a runtime constructor.
fn Gen::timestamp_format(
  self : Gen,
  member_shape : @smithy.Shape?,
  target : @smithy.Shape?,
) -> String {
  let from_trait = (shape : @smithy.Shape?) => {
    match shape {
      Some(shape) =>
        match shape.trait_value("smithy.api#timestampFormat") {
          Some(String("date-time")) => Some("DateTime")
          Some(String("http-date")) => Some("HttpDate")
          Some(String("epoch-seconds")) => Some("EpochSeconds")
          _ => None
        }
      None => None
    }
  }
  match (from_trait(member_shape), from_trait(target)) {
    (Some(format), _) | (None, Some(format)) => format
    (None, None) =>
      match self.value_config.timestamp_format {
        DateTime => "DateTime"
        EpochSeconds => "EpochSeconds"
      }
  }
}

///|
/// The type of the value of a member or of a target shape. A list, a map and
/// a sparse wrapper become layers; the walk keeps its own stack of layers.
fn Gen::member_ty(
  self : Gen,
  member_id : @smithy.ShapeId,
) -> Ty raise @codegen.CodegenError {
  // Each step finds the target of a member and records a wrapper layer.
  let layers : Array[Int32] = [] // 0 list, 1 map, 2 optional item
  let mut member_shape = self.shape(member_id)
  let leaf = for ;; {
    guard member_shape.body is Member(target~) else {
      raise @codegen.InvalidSettings(
        "I expected `\{member_shape.id}` to be a member shape.",
      )
    }
    if target.namespace() == "smithy.api" {
      let target_shape = self.model.shapes.get(target)
      let format = self.timestamp_format(Some(member_shape), target_shape)
      match prelude_ty(target.name(), format) {
        Some(ty) => break ty
        None =>
          raise @codegen.InvalidSettings(
            "I cannot map prelude shape `\{target}` to a MoonBit type.",
          )
      }
    }
    let target_shape = self.shape(target)
    match target_shape.body {
      Simple(simple) =>
        break simple_ty(
          simple,
          self.timestamp_format(Some(member_shape), Some(target_shape)),
        )
      List(item) => {
        layers.push(0)
        if target_shape.trait_value("smithy.api#sparse") is Some(_) {
          layers.push(2)
        }
        member_shape = self.shape(item)
      }
      Map(key~, value~) => {
        let key_shape = self.shape(key)
        if key_shape.body is Member(target=key_target) &&
          !self.is_string(key_target) {
          raise @codegen.InvalidSettings(
            "I expected the key of map `\{target}` to be a string.",
          )
        }
        layers.push(1)
        if target_shape.trait_value("smithy.api#sparse") is Some(_) {
          layers.push(2)
        }
        member_shape = self.shape(value)
      }
      Structure(_) | Union(_) | Enum(_) | IntEnum(_) =>
        break Named(self.type_name_of(target))
      _ =>
        raise @codegen.InvalidSettings(
          "I cannot use shape `\{target}` (\{target_shape.body.keyword()}) as a value in MoonBit.",
        )
    }
  }
  let mut ty = leaf
  for index = layers.length() - 1; index >= 0; index = index - 1 {
    ty = match layers[index] {
      0 => ListOf(ty)
      1 => MapOf(ty)
      _ => Opt(ty)
    }
  }
  ty
}

///|
fn Gen::is_string(self : Gen, id : @smithy.ShapeId) -> Bool {
  if id == @smithy.ShapeId::parse("smithy.api#String").unwrap() {
    return true
  }
  match self.model.shapes.get(id) {
    Some({ body: Simple(String) | Enum(_), .. }) => true
    _ => false
  }
}

///|
/// A field of a generated structure.
priv struct Field {
  field_name : String
  wire_name : String
  ty : Ty
  required : Bool
}

///|
/// The fields of a structure, in model order, with the wire names and the
/// required flags of the prepared contract.
fn Gen::fields(
  self : Gen,
  shape : @smithy.Shape,
) -> Array[Field] raise @codegen.CodegenError {
  guard shape.body is Structure(members) else { return [] }
  let contract = self.values.get(shape.id)
  let out : Array[Field] = []
  let seen : Map[String, String] = Map([])
  for member_id in members {
    let member_name = member_id.member_name().unwrap_or("")
    let member_contract = contract.bind(c => {
      c.members.iter().find_first(m => m.shape == member_id)
    })
    let wire_name = match member_contract {
      Some(m) => m.wire_name
      None => member_name
    }
    let required = match member_contract {
      Some(m) => m.required
      None =>
        self.shape(member_id).trait_value("smithy.api#required") is Some(_)
    }
    let field_name = snake_name(member_name)
    if seen.get(field_name) is Some(other) {
      raise @codegen.InvalidSettings(
        "I found members `\{other}` and `\{member_name}` of `\{shape.id}` with the same MoonBit field name `\{field_name}`.",
      )
    }
    seen[field_name] = member_name
    let ty = self.member_ty(member_id)
    out.push({ field_name, wire_name, ty, required, })
  }
  out
}

///|
fn Gen::reject_unknown(self : Gen, shape : @smithy.Shape) -> Bool {
  match self.values.get(shape.id) {
    Some(contract) => contract.unknown_members is Reject
    None => false
  }
}

///|
fn Gen::claim_type_name(
  self : Gen,
  shape : @smithy.Shape,
) -> String raise @codegen.CodegenError {
  let name = self.type_name_of(shape.id)
  if self.generated_names.contains(name) {
    raise @codegen.InvalidSettings(
      "I cannot use the MoonBit name `\{name}` for shape `\{shape.id}`: the generated code uses it. Add a rename to the service.",
    )
  }
  match self.type_names.get(name) {
    Some(other) if other != shape.id =>
      raise @codegen.InvalidSettings(
        "I found shapes `\{other}` and `\{shape.id}` with the same MoonBit type name `\{name}`.",
      )
    _ => self.type_names[name] = shape.id
  }
  name
}

///|
fn types_path() -> @codegen.OutputPath {
  @codegen.OutputPath::parse("types.mbt").unwrap()
}

///|
fn errors_path() -> @codegen.OutputPath {
  @codegen.OutputPath::parse("errors.mbt").unwrap()
}

///|
fn Gen::with_file(
  self : Gen,
  path : @codegen.OutputPath,
  body : (@codegen.CodeWriter) -> Unit raise @codegen.CodegenError,
) -> Unit raise @codegen.CodegenError {
  self.delegator.use_file_writer(path, module_namespace(self.module_name), body)
}

///|
/// Write the declaration, `to_json` and `from_json` of a structure.
fn Gen::write_structure(
  self : Gen,
  shape : @smithy.Shape,
  path : @codegen.OutputPath,
) -> Unit raise @codegen.CodegenError {
  let name = self.claim_type_name(shape)
  let fields = self.fields(shape)
  let reject = self.reject_unknown(shape)
  self.with_file(path, w => {
    w.write("///|")
    if fields.is_empty() {
      w.write("pub(all) struct $L {} derive(Eq, Debug)", args=[Text(name)])
    } else {
      w.write("pub(all) struct $L {", args=[Text(name)])
      w.indent()
      for field in fields {
        let text = if field.required {
          field.ty.text()
        } else {
          Opt(field.ty).text()
        }
        w.write("$L : $L", args=[Text(field.field_name), Text(text)])
      }
      w.dedent()
      w.write("} derive(Eq, Debug)")
    }
    w.blank_line()
    // to_json
    w.write("///|")
    let self_name = if fields.is_empty() { "_self" } else { "self" }
    w.write("pub fn $L::to_json($L : $L) -> Json {", args=[
      Text(name),
      Text(self_name),
      Text(name),
    ])
    w.indent()
    if fields.is_empty() {
      w.write("Json::empty_object()")
    } else {
      w.write("let fields : Map[String, Json] = Map([])")
      for field in fields {
        let key = mbt_string(field.wire_name)
        if field.required {
          w.write("fields[$L] = $L", args=[
            Text(key),
            Text(field.ty.encode("self." + field.field_name)),
          ])
        } else {
          w.write("if self.$L is Some(value) {", args=[Text(field.field_name)])
          w.indent()
          w.write("fields[$L] = $L", args=[
            Text(key),
            Text(field.ty.encode("value")),
          ])
          w.dedent()
          w.write("}")
        }
      }
      w.write("Json::object(fields)")
    }
    w.dedent()
    w.write("}")
    w.blank_line()
    // from_json
    w.write("///|")
    w.write(
      "pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
      args=[Text(name), Text(name)],
    )
    w.indent()
    if fields.is_empty() && !reject {
      w.write("let _ = @rpc.object_of(json, path~)")
      w.write("$L::{  }", args=[Text(name)])
    } else {
      w.write("let fields = @rpc.object_of(json, path~)")
      if reject {
        let known = fields.map(field => mbt_string(field.wire_name)).join(", ")
        w.write("@rpc.reject_unknown(fields, [$L], path~)", args=[Text(known)])
      }
      if fields.is_empty() {
        w.write("$L::{  }", args=[Text(name)])
      } else {
        w.write("$L::{", args=[Text(name)])
        w.indent()
        for field in fields {
          let key = mbt_string(field.wire_name)
          let member_path = "path + " +
            mbt_string("/" + pointer_token(field.wire_name))
          if field.required {
            w.write("$L: $L,", args=[
              Text(field.field_name),
              Text(
                field.ty.decode(
                  "@rpc.required(fields, \{key}, path~)",
                  member_path,
                ),
              ),
            ])
          } else {
            w.write("$L: match fields.get($L) {", args=[
              Text(field.field_name),
              Text(key),
            ])
            w.indent()
            w.write("None | Some(Null) => None")
            w.write("Some(value) => Some($L)", args=[
              Text(field.ty.decode("value", member_path)),
            ])
            w.dedent()
            w.write("},")
          }
        }
        w.dedent()
        w.write("}")
      }
    }
    w.dedent()
    w.write("}")
    w.blank_line()
  })
}

///|
/// The wire name of a union member: its member name, or its `jsonName` when
/// the value configuration uses JSON names.
fn Gen::union_tag(
  self : Gen,
  member_id : @smithy.ShapeId,
) -> String raise @codegen.CodegenError {
  let member_name = member_id.member_name().unwrap_or("")
  if self.value_config.member_names is JsonName &&
    self.shape(member_id).trait_value("smithy.api#jsonName")
    is Some(String(name)) {
    return name
  }
  member_name
}

///|
fn Gen::write_union(
  self : Gen,
  shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
  guard shape.body is Union(members) else { return }
  let name = self.claim_type_name(shape)
  let variants : Array[(String, String, Ty)] = []
  let seen : Map[String, Unit] = Map([])
  for member_id in members {
    let variant = variant_name(member_id.member_name().unwrap_or(""))
    if seen.contains(variant) {
      raise @codegen.InvalidSettings(
        "I found two members of union `\{shape.id}` with the variant name `\{variant}`.",
      )
    }
    seen[variant] = ()
    variants.push(
      (variant, self.union_tag(member_id), self.member_ty(member_id)),
    )
  }
  let tags = variants.map(v => v.1)
  tags.sort_by((a, b) => a.lexical_compare(b))
  self.with_file(types_path(), w => {
    w.write("///|")
    w.write("pub(all) enum $L {", args=[Text(name)])
    w.indent()
    for variant in variants {
      if variant.2 is UnitT {
        w.write("$L", args=[Text(variant.0)])
      } else {
        w.write("$L($L)", args=[Text(variant.0), Text(variant.2.text())])
      }
    }
    w.dedent()
    w.write("} derive(Eq, Debug)")
    w.blank_line()
    w.write("///|")
    w.write("pub fn $L::to_json(self : $L) -> Json {", args=[
      Text(name),
      Text(name),
    ])
    w.indent()
    w.write("match self {")
    w.indent()
    for variant in variants {
      let tag = mbt_string(variant.1)
      if variant.2 is UnitT {
        w.write("$L => Json::object({ $L: Json::empty_object() })", args=[
          Text(variant.0),
          Text(tag),
        ])
      } else {
        w.write("$L(value) => Json::object({ $L: $L })", args=[
          Text(variant.0),
          Text(tag),
          Text(variant.2.encode("value")),
        ])
      }
    }
    w.dedent()
    w.write("}")
    w.dedent()
    w.write("}")
    w.blank_line()
    w.write("///|")
    w.write(
      "pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
      args=[Text(name), Text(name)],
    )
    w.indent()
    let uses_value = variants.iter().any(v => !(v.2 is UnitT))
    let value_name = if uses_value { "value" } else { "_" }
    w.write("let (tag, $L) = @rpc.union_member(json, path~)", args=[
      Text(value_name),
    ])
    w.write("match tag {")
    w.indent()
    for variant in variants {
      let tag = mbt_string(variant.1)
      if variant.2 is UnitT {
        w.write("$L => $L::$L", args=[Text(tag), Text(name), Text(variant.0)])
      } else {
        let member_path = "path + " + mbt_string("/" + pointer_token(variant.1))
        w.write("$L => $L::$L($L)", args=[
          Text(tag),
          Text(name),
          Text(variant.0),
          Text(variant.2.decode("value", member_path)),
        ])
      }
    }
    let message = "I expected a member of union `\{shape.id.name()}` (\{tags.join(", ")}), found `"
    w.write("_ =>", args=[])
    w.indent()
    w.write("raise @rpc.DecodeError(path~, message=$L + tag + $L)", args=[
      Text(mbt_string(message)),
      Text(mbt_string("`.")),
    ])
    w.dedent()
    w.dedent()
    w.write("}")
    w.dedent()
    w.write("}")
    w.blank_line()
  })
}

///|
fn Gen::write_enum(
  self : Gen,
  shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
  let (members, int_enum) = match shape.body {
    Enum(members) => (members, false)
    IntEnum(members) => (members, true)
    _ => return
  }
  let name = self.claim_type_name(shape)
  let variants : Array[(String, String)] = []
  let seen : Map[String, Unit] = Map([])
  for member_id in members {
    let member_name = member_id.member_name().unwrap_or("")
    let variant = variant_name(member_name)
    if seen.contains(variant) {
      raise @codegen.InvalidSettings(
        "I found two members of enum `\{shape.id}` with the variant name `\{variant}`.",
      )
    }
    seen[variant] = ()
    let value = match
      self.shape(member_id).trait_value("smithy.api#enumValue") {
      Some(String(text)) => mbt_string(text)
      Some(Number(number)) => number.to_string()
      _ => mbt_string(member_name)
    }
    variants.push((variant, value))
  }
  let values = variants.map(v => v.1)
  values.sort_by((a, b) => a.lexical_compare(b))
  self.with_file(types_path(), w => {
    w.write("///|")
    w.write("pub(all) enum $L {", args=[Text(name)])
    w.indent()
    for variant in variants {
      w.write("$L", args=[Text(variant.0)])
    }
    w.dedent()
    w.write("} derive(Eq, Debug)")
    w.blank_line()
    w.write("///|")
    w.write("pub fn $L::to_json(self : $L) -> Json {", args=[
      Text(name),
      Text(name),
    ])
    w.indent()
    let wrap = if int_enum { "Json::number" } else { "Json::string" }
    w.write("$L(", args=[Text(wrap)])
    w.indent()
    w.write("match self {")
    w.indent()
    for variant in variants {
      let value = if int_enum { variant.1 + ".0" } else { variant.1 }
      w.write("$L => $L", args=[Text(variant.0), Text(value)])
    }
    w.dedent()
    w.write("},")
    w.dedent()
    w.write(")")
    w.dedent()
    w.write("}")
    w.blank_line()
    w.write("///|")
    w.write(
      "pub fn $L::from_json(json : Json, path~ : String = \"\") -> $L raise @rpc.DecodeError {",
      args=[Text(name), Text(name)],
    )
    w.indent()
    let decode = if int_enum { "@rpc.decode_int" } else { "@rpc.decode_string" }
    w.write("let value = $L(json, path~)", args=[Text(decode)])
    w.write("match value {")
    w.indent()
    for variant in variants {
      w.write("$L => $L::$L", args=[
        Text(variant.1),
        Text(name),
        Text(variant.0),
      ])
    }
    let message = "I expected a value of enum `\{shape.id.name()}` (\{values.join(", ")}), found `"
    let shown = if int_enum { "value.to_string()" } else { "value" }
    w.write("_ =>")
    w.indent()
    w.write("raise @rpc.DecodeError(path~, message=$L + $L + $L)", args=[
      Text(mbt_string(message)),
      Text(shown),
      Text(mbt_string("`.")),
    ])
    w.dedent()
    w.dedent()
    w.write("}")
    w.dedent()
    w.write("}")
    w.blank_line()
  })
}

///|
fn Gen::write_shape(
  self : Gen,
  shape : @smithy.Shape,
) -> Unit raise @codegen.CodegenError {
  if self.written.contains(shape.id) || shape.id.namespace() == "smithy.api" {
    return
  }
  match shape.body {
    Structure(_) =>
      if shape.trait_value("smithy.api#error") is Some(_) {
        self.written[shape.id] = ()
        self.errors.push(shape.id)
        self.write_structure(shape, errors_path())
      } else {
        self.written[shape.id] = ()
        self.write_structure(shape, types_path())
      }
    Union(_) => {
      self.written[shape.id] = ()
      self.write_union(shape)
    }
    Enum(_) | IntEnum(_) => {
      self.written[shape.id] = ()
      self.write_enum(shape)
    }
    _ => ()
  }
}