///|
/// The MoonBit type of a shape value, and how the generated code encodes and
/// decodes it.
priv enum Ty {
  Str
  Boolean
  ByteT
  ShortT
  IntT
  Int64T
  FloatT
  DoubleT
  BigDec
  BigInt
  Blob
  Doc
  Time(String)
  Named(String)
  ListOf(Ty)
  MapOf(Ty)
  Opt(Ty)
  UnitT
} derive(Eq, Debug)

///|
/// The wrappers of a type, outer first, and the leaf type inside them. The
/// walk keeps its own state, so a deep type does not recurse.
fn Ty::layers(self : Ty) -> (Array[Ty], Ty) {
  let wrappers : Array[Ty] = []
  let mut ty = self
  for ;; {
    match ty {
      ListOf(inner) | MapOf(inner) | Opt(inner) => {
        wrappers.push(ty)
        ty = inner
      }
      leaf => break (wrappers, leaf)
    }
  }
}

///|
fn Ty::text(self : Ty) -> String {
  let (wrappers, leaf) = self.layers()
  let mut text = leaf.leaf_text()
  for index = wrappers.length() - 1; index >= 0; index = index - 1 {
    text = match wrappers[index] {
      ListOf(_) => "Array[\{text}]"
      MapOf(_) => "Map[String, \{text}]"
      _ => "\{text}?"
    }
  }
  text
}

///|
fn Ty::leaf_text(self : Ty) -> String {
  match self {
    Str => "String"
    Boolean => "Bool"
    ByteT | ShortT | IntT => "Int"
    Int64T => "Int64"
    FloatT => "Float"
    DoubleT => "Double"
    BigDec => "@rpc.Decimal"
    BigInt => "@rpc.BigInteger"
    Blob => "Bytes"
    Doc => "Json"
    Time(_) => "@rpc.Timestamp"
    Named(name) => name
    UnitT => "Unit"
    ListOf(_) | MapOf(_) | Opt(_) => ""
  }
}

///|
fn Ty::leaf_encode(self : Ty, expr : String) -> String {
  match self {
    Str => "Json::string(\{expr})"
    Boolean => "Json::boolean(\{expr})"
    ByteT | ShortT | IntT => "Json::number(\{expr}.to_double())"
    Int64T => "@rpc.encode_int64(\{expr})"
    FloatT => "@rpc.encode_float(\{expr})"
    DoubleT => "@rpc.encode_double(\{expr})"
    BigDec | BigInt | Time(_) | Named(_) => "\{expr}.to_json()"
    Blob => "@rpc.encode_blob(\{expr})"
    Doc => expr
    UnitT => "Json::empty_object()"
    ListOf(_) | MapOf(_) | Opt(_) => expr
  }
}

///|
/// The expression that encodes `expr` as a `Json` value. Each inner layer
/// names its value `item`.
fn Ty::encode(self : Ty, expr : String) -> String {
  let (wrappers, leaf) = self.layers()
  if wrappers.is_empty() {
    return leaf.leaf_encode(expr)
  }
  let mut text = leaf.leaf_encode("item")
  for index = wrappers.length() - 1; index >= 0; index = index - 1 {
    let value = if index == 0 { expr } else { "item" }
    text = match wrappers[index] {
      ListOf(_) => "@rpc.encode_array(\{value}, item => \{text})"
      MapOf(_) => "@rpc.encode_map(\{value}, item => \{text})"
      _ => "match \{value} { Some(item) => \{text}; None => Json::null() }"
    }
  }
  text
}

///|
fn Ty::leaf_decode(self : Ty, json : String, path : String) -> String {
  match self {
    Str => "@rpc.decode_string(\{json}, path=\{path})"
    Boolean => "@rpc.decode_bool(\{json}, path=\{path})"
    ByteT => "@rpc.decode_byte(\{json}, path=\{path})"
    ShortT => "@rpc.decode_short(\{json}, path=\{path})"
    IntT => "@rpc.decode_int(\{json}, path=\{path})"
    FloatT => "@rpc.decode_float(\{json}, path=\{path})"
    Int64T => "@rpc.decode_int64(\{json}, path=\{path})"
    DoubleT => "@rpc.decode_double(\{json}, path=\{path})"
    BigDec => "@rpc.Decimal::from_json(\{json}, path=\{path})"
    BigInt => "@rpc.BigInteger::from_json(\{json}, path=\{path})"
    Time(format) =>
      "@rpc.Timestamp::from_json(\{json}, format=@rpc.\{format}, path=\{path})"
    Blob => "@rpc.decode_blob(\{json}, path=\{path})"
    Doc => json
    UnitT => "()"
    Named(name) => "\{name}::from_json(\{json}, path=\{path})"
    ListOf(_) | MapOf(_) | Opt(_) => json
  }
}

///|
/// The expression that decodes `json` at the JSON Pointer expression `path`.
/// A list or map layer names its item `item` and the item path `path`.
fn Ty::decode(self : Ty, json : String, path : String) -> String {
  let (wrappers, leaf) = self.layers()
  // The value and the path expression of each layer; the leaf is the last.
  let values = [json]
  let paths = [path]
  for index, wrapper in wrappers {
    values.push("item")
    paths.push(if wrapper is Opt(_) { paths[index] } else { "path" })
  }
  let depth = wrappers.length()
  let mut text = leaf.leaf_decode(values[depth], paths[depth])
  for index = depth - 1; index >= 0; index = index - 1 {
    let label = if text.contains("path") { "path" } else { "_path" }
    text = match wrappers[index] {
      ListOf(_) =>
        "@rpc.decode_array(\{values[index]}, path=\{paths[index]}, (item, \{label}) => \{text})"
      MapOf(_) =>
        "@rpc.decode_map(\{values[index]}, path=\{paths[index]}, (item, \{label}) => \{text})"
      _ => "match \{values[index]} { Null => None; item => Some(\{text}) }"
    }
  }
  text
}

///|
let unit_id : @smithy.ShapeId = @smithy.ShapeId::parse("smithy.api#Unit").unwrap()

///|
fn prelude_ty(name : String, format : String) -> Ty? {
  match name {
    "String" => Some(Str)
    "Boolean" | "PrimitiveBoolean" => Some(Boolean)
    "Byte" | "PrimitiveByte" => Some(ByteT)
    "Short" | "PrimitiveShort" => Some(ShortT)
    "Integer" | "PrimitiveInteger" => Some(IntT)
    "Long" | "PrimitiveLong" => Some(Int64T)
    "Float" | "PrimitiveFloat" => Some(FloatT)
    "Double" | "PrimitiveDouble" => Some(DoubleT)
    "BigInteger" => Some(BigInt)
    "BigDecimal" => Some(BigDec)
    "Blob" => Some(Blob)
    "Document" => Some(Doc)
    "Timestamp" => Some(Time(format))
    "Unit" => Some(UnitT)
    _ => None
  }
}

///|
fn simple_ty(simple : @smithy.SimpleType, format : String) -> Ty {
  match simple {
    String => Str
    Boolean => Boolean
    Byte => ByteT
    Short => ShortT
    Integer => IntT
    Long => Int64T
    Float => FloatT
    Double => DoubleT
    BigInteger => BigInt
    BigDecimal => BigDec
    Blob => Blob
    Document => Doc
    Timestamp => Time(format)
  }
}