///|
fn has_input(method_ : @rpc.MethodContract) -> Bool {
  method_.input.to_string() != "smithy.api#Unit"
}

///|
fn signature(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  method_ : @rpc.MethodContract,
  named : Bool,
  notification~ : Bool = false,
) -> Unit raise @codegen.CodegenError {
  w.write_inline("ctx $T", args=[ctx.foreign("Context", "context")])
  if has_input(method_) {
    w.write_inline(", input $C", args=[ctx.type_symbol(method_.input)])
  }
  w.write_inline(") ")
  if !notification &&
    method_.kind != @rpc.NotificationOnly &&
    result_shape(method_) is Some(id) {
    w.write_inline("($C, error)", args=[ctx.type_symbol(id)])
  } else {
    w.write_inline("error")
  }
  let _ = named
}

///|
fn write_params(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  method_ : @rpc.MethodContract,
) -> Unit raise @codegen.CodegenError {
  if method_.mode == @rpc.Positional {
    w.write_inline("\tparams := []any{")
    for id in method_.positional {
      w.write_inline("input.$L,", args=[Text(ctx.field(id))])
    }
    w.write("}")
    // A nil pointer stored in an interface is not a nil interface. Trim the
    // slots by their typed fields, from the last optional member.
    let mut stopped = false
    for id in method_.positional.rev() {
      if stopped {
        continue
      }
      let required = ctx.values
        .get(method_.input)
        .map(v => v.members.iter().any(m => m.shape == id && m.required))
        .unwrap_or(false)
      if required {
        stopped = true
        continue
      }
      let index = method_.positional.search(id).unwrap()
      w.write(
        "\tif len(params) == $L && input.$L == nil { params = params[:$L] }",
        args=[Int(index + 1), Text(ctx.field(id)), Int(index)],
      )
    }
  } else if has_input(method_) {
    w.write("\tparams := input")
  } else {
    w.write("\tparams := struct{}{}")
  }
}

///|
fn generate_call(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  method_ : @rpc.MethodContract,
  notification : Bool,
) -> Unit raise @codegen.CodegenError {
  let name = ctx.name(method_.operation)
  let name = if notification && method_.kind == @rpc.RequestOrNotification {
    "Notify" + name
  } else {
    name
  }
  let output = if notification { None } else { result_shape(method_) }
  w.write_inline("func (peer *Peer) $L(", args=[Text(name)])
  signature(ctx, w, method_, true, notification~)
  w.write(" {")
  write_params(ctx, w, method_)
  if notification {
    w.write("\treturn peer.Peer.Notify(ctx, $S, params)\n}\n", args=[
      Text(method_.name.to_string()),
    ])
    return
  }
  w.write("\tvar raw $T\n\terr := peer.Peer.Call(ctx, $S, params, &raw)", args=[
    ctx.foreign("RawMessage", "encoding/json"),
    Text(method_.name.to_string()),
  ])
  match output {
    None =>
      w.write("\treturn decode$LError(err)\n}\n", args=[
        Text(ctx.name(method_.operation)),
      ])
    Some(id) => {
      w.write(
        "\tif err != nil { var zero $C; return zero, decode$LError(err) }",
        args=[ctx.type_symbol(id), Text(ctx.name(method_.operation))],
      )
      w.write_inline("\treturn ")
      decode_expression(ctx, w, id, "raw")
      w.write("\n}\n")
    }
  }
  generate_error_decoder(ctx, w, method_)
}

///|
fn generate_error_decoder(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  method_ : @rpc.MethodContract,
) -> Unit raise @codegen.CodegenError {
  w.write("func decode$LError(err error) error {", args=[
    Text(ctx.name(method_.operation)),
  ])
  if method_.errors.is_empty() {
    w.write("\treturn err\n}\n")
    return
  }
  w.write(
    "\tvar wire *$T\n\tif !$T(err, &wire) { return err }\n\tswitch wire.Code {",
    args=[ctx.runtime("Error"), ctx.foreign("As", "errors")],
  )
  for error in method_.errors {
    w.write("\tcase $L:\n\t\tvar value $C", args=[
      Text(code_text(error.code, error.shape.to_string())),
      ctx.type_symbol(error.shape),
    ])
    match error.data {
      None => ()
      Some(data) => {
        let is_member = data.member_name() != None
        w.write("\t\tif len(wire.Data) != 0 {")
        w.write_inline("\t\t\tdecoded, decodeErr := ")
        decode_expression(ctx, w, data, "wire.Data")
        w.write("\n\t\t\tif decodeErr != nil { return err }")
        if is_member {
          w.write("\t\t\tvalue.$L = $Ldecoded", args=[
            Text(ctx.field(data)),
            Text(if member_required(ctx, data) { "" } else { "&" }),
          ])
        } else {
          w.write("\t\t\tvalue = decoded")
        }
        w.write("\t\t}")
        if error.data_required {
          w.write("\t\tif len(wire.Data) == 0 { return err }")
        }
      }
    }
    w.write("\t\treturn value")
  }
  w.write("\tdefault: return err\n\t}\n}\n")
}

///|
fn member_required(
  ctx : GoContext,
  id : @smithy.ShapeId,
) -> Bool raise @codegen.CodegenError {
  match ctx.values.get(id.root()) {
    Some(policy) =>
      policy.members
      .iter()
      .any(member_ => member_.shape == id && member_.required)
    None => ctx.shape(id).trait_value("smithy.api#required") != None
  }
}

///|
fn generate_peer(ctx : GoContext) -> String raise @codegen.CodegenError {
  let w = ctx.writer()
  w.write(
    "// Peer accepts local handlers and calls the other peer.\ntype Peer struct { *$T }\n\n// Handlers holds the typed functions that this peer accepts.\ntype Handlers struct {",
    args=[ctx.runtime("Peer")],
  )
  if ctx.selected.accepts is Some(service) {
    for method_ in service.methods {
      w.write_inline("\t$L func(", args=[Text(ctx.name(method_.operation))])
      signature(ctx, w, method_, false)
      w.write("")
    }
  }
  w.write(
    "}\n\nfunc NewPeer(handlers Handlers, options $T) (*Peer, error) {\n\truntime, err := $T(Contract(), $S, options)\n\tif err != nil { return nil, err }\n\tpeer := &Peer{Peer: runtime}",
    args=[
      ctx.runtime("Options"),
      ctx.runtime("NewPeer"),
      Text(ctx.selected.name.to_string()),
    ],
  )
  if ctx.selected.accepts is Some(service) {
    for method_ in service.methods {
      let name = ctx.name(method_.operation)
      w.write(
        "\tif handlers.$L != nil {\n\t\terr = runtime.Register($S, func(ctx $T, data $T) (any, error) {",
        args=[
          Text(name),
          Text(method_.name.to_string()),
          ctx.foreign("Context", "context"),
          ctx.foreign("RawMessage", "encoding/json"),
        ],
      )
      if has_input(method_) {
        w.write_inline("\t\t\tinput, err := ")
        decode_expression(ctx, w, method_.input, "data")
        w.write(
          "\n\t\t\tif err != nil { return nil, $T(-32602, err.Error(), nil) }",
          args=[ctx.runtime("NewError")],
        )
      }
      let args_ = if has_input(method_) { "ctx, input" } else { "ctx" }
      if method_.kind == @rpc.NotificationOnly ||
        method_.result == @rpc.NullResult {
        w.write("\t\t\treturn nil, handlers.$L($L)", args=[
          Text(name),
          Text(args_),
        ])
      } else {
        w.write("\t\t\treturn handlers.$L($L)", args=[Text(name), Text(args_)])
      }
      w.write("\t\t})\n\t\tif err != nil { return nil, err }\n\t}")
    }
  }
  w.write("\treturn peer, nil\n}\n")
  if ctx.selected.calls is Some(service) {
    for method_ in service.methods {
      generate_call(ctx, w, method_, method_.kind == @rpc.NotificationOnly)
      if method_.kind == @rpc.RequestOrNotification {
        generate_call(ctx, w, method_, true)
      }
    }
  }
  ctx.render(w)
}

///|
fn generate_errors(ctx : GoContext) -> String raise @codegen.CodegenError {
  let w = ctx.writer()
  let errors : Map[@smithy.ShapeId, @rpc.ErrorContract] = Map([])
  for peer in ctx.description.peers {
    if peer.accepts is Some(service) {
      for method_ in service.methods {
        for error in method_.errors {
          if errors.get(error.shape) is Some(previous) && previous != error {
            raise @codegen.InvalidSettings(
              "I found different prepared error mappings for shape `\{error.shape}`.",
            )
          }
          errors[error.shape] = error
        }
      }
    }
  }
  let ids = errors.keys().to_array()
  ids.sort_by((a, b) => a.to_string().lexical_compare(b.to_string()))
  for id in ids {
    let error = errors[id]
    w.write("func (value $C) Error() string { return $S }", args=[
      ctx.type_symbol(id),
      Text("I received modeled error `" + id.to_string() + "`."),
    ])
    w.write("func (value $C) RPCError() *$T {", args=[
      ctx.type_symbol(id),
      ctx.runtime("Error"),
    ])
    if error.data is Some(data) &&
      data.member_name() != None &&
      !error.data_required {
      w.write("\tif value.$L == nil { return $T($L, value.Error(), nil) }", args=[
        Text(ctx.field(data)),
        ctx.runtime("NewError"),
        Text(code_text(error.code, error.shape.to_string())),
      ])
    }
    w.write_inline("\treturn $T($L, value.Error(), ", args=[
      ctx.runtime("NewError"),
      Text(code_text(error.code, error.shape.to_string())),
    ])
    match error.data {
      None => w.write_inline("nil")
      Some(data) if data.member_name() != None =>
        w.write_inline("value.$L", args=[Text(ctx.field(data))])
      Some(_) => w.write_inline("value")
    }
    w.write(")\n}\n")
  }
  ctx.render(w)
}