///|
fn Client::prepare_request(
  self : Client,
  meth : S3Method,
  bucket : String,
  key : String,
  body : &@io.Data,
  options? : RequestOptions = RequestOptions(),
) -> Request raise {
  if bucket == "" {
    raise S3Error::InvalidInput("bucket must not be empty")
  }
  let timestamp = match options.timestamp {
    Some(value) => value
    None => now_timestamp()
  }
  let payload_hash = match options.payload_hash {
    Some(value) => value
    None => sha256_data_hex(body)
  }
  let (origin, host) = endpoint_origin_and_host(self.config, bucket)
  let path = canonical_object_path(self.config, bucket, key)
  let headers = copy_user_headers(options.headers)
  if meth is Put && !contains_header(headers, "content-length") {
    headers["Content-Length"] = body.binary().length().to_string()
  }
  headers["Accept-Encoding"] = "identity"
  headers["Host"] = host
  headers["x-amz-date"] = timestamp
  headers["x-amz-content-sha256"] = payload_hash
  if self.config.credentials.session_token is Some(token) {
    headers["x-amz-security-token"] = token
  }
  let headers = sign_headers(
    meth,
    path,
    options.query,
    headers,
    payload_hash,
    self.config.credentials,
    self.config.region,
    timestamp,
  )
  { origin, target: append_query(path, options.query), headers }
}

///|
fn ensure_object_key(key : String) -> Unit raise {
  if key == "" {
    raise S3Error::InvalidInput("key must not be empty")
  }
}

///|
fn copy_user_headers(headers : Map[String, String]) -> Map[String, String] {
  let copied : Map[String, String] = {}
  for key, value in headers {
    if !is_signer_controlled_header(key) {
      copied[key] = value
    }
  }
  copied
}

///|
fn is_signer_controlled_header(name : String) -> Bool {
  name.compare_ignore_ascii_case("accept-encoding") == 0 ||
  name.compare_ignore_ascii_case("authorization") == 0 ||
  name.compare_ignore_ascii_case("host") == 0 ||
  name.compare_ignore_ascii_case("transfer-encoding") == 0 ||
  name.compare_ignore_ascii_case("x-amz-content-sha256") == 0 ||
  name.compare_ignore_ascii_case("x-amz-date") == 0 ||
  name.compare_ignore_ascii_case("x-amz-security-token") == 0
}

///|
fn contains_header(headers : Map[String, String], name : String) -> Bool {
  for key, _ in headers {
    if key.compare_ignore_ascii_case(name) == 0 {
      return true
    }
  }
  false
}

///|
fn http_method(meth : S3Method) -> @http.RequestMethod {
  match meth {
    Get => @http.Get
    Head => @http.Head
    Put => @http.Put
    Delete => @http.Delete
  }
}

///|
async fn[T] Client::send(
  self : Client,
  meth : S3Method,
  bucket : String,
  key : String,
  body : &@io.Data,
  options? : RequestOptions = RequestOptions(),
  f : async (@http.Response, @http.Client) -> T,
) -> T {
  let request = self.prepare_request(meth, bucket, key, body, options~)
  let client = @http.Client(request.origin)
  defer client.close()
  client.request(
    http_method(meth),
    request.target,
    extra_headers=request.extra_headers(),
  )
  if meth is Put {
    client.write(body)
  }
  let response = client.end_request()
  f(response, client)
}

///|
fn Request::extra_headers(self : Request) -> Map[String, String] {
  let headers : Map[String, String] = {}
  for key, value in self.headers {
    if key.compare_ignore_ascii_case("host") != 0 {
      headers[key] = value
    }
  }
  headers
}

///|
fn ensure_success_response(
  response : @http.Response,
  body : String,
) -> Unit raise {
  if !is_success_response(response) {
    raise S3Error::ServiceError(response, body)
  }
}

///|
fn is_success_response(response : @http.Response) -> Bool {
  response.code >= 200 && response.code < 300
}

///|
async fn read_error_body(client : @http.Client) -> String {
  client.read_all().text()
}

///|
async fn raise_service_error_response(
  response : @http.Response,
  client : @http.Client,
) -> Unit {
  let body = read_error_body(client)
  raise S3Error::ServiceError(response, body)
}

///|
fn raise_service_error_response_without_body(
  response : @http.Response,
) -> Unit raise {
  raise S3Error::ServiceError(response, "")
}

///|
pub async fn Client::get_object(
  self : Client,
  bucket : String,
  key : String,
  options? : RequestOptions = RequestOptions(),
) -> ObjectResult {
  ensure_object_key(key)
  let request = self.prepare_request(Get, bucket, key, b"", options~)
  let client = @http.Client(request.origin)
  let response = client
    ..request(@http.Get, request.target, extra_headers=request.extra_headers())
    .end_request() catch {
      err => {
        client.close()
        raise err
      }
    }
  if !is_success_response(response) {
    defer client.close()
    raise_service_error_response(response, client)
  }
  { response, body: client }
}

///|
pub async fn Client::put_object(
  self : Client,
  bucket : String,
  key : String,
  body : &@io.Data,
  options? : RequestOptions = RequestOptions(),
) -> @http.Response {
  ensure_object_key(key)
  self.send(Put, bucket, key, body, options~, (response, client) => {
    if !is_success_response(response) {
      raise_service_error_response(response, client)
    }
    response
  })
}

///|
pub async fn Client::delete_object(
  self : Client,
  bucket : String,
  key : String,
  options? : RequestOptions = RequestOptions(),
) -> @http.Response {
  ensure_object_key(key)
  self.send(Delete, bucket, key, b"", options~, (response, client) => {
    if !is_success_response(response) {
      raise_service_error_response(response, client)
    }
    response
  })
}

///|
pub async fn Client::head_object(
  self : Client,
  bucket : String,
  key : String,
  options? : RequestOptions = RequestOptions(),
) -> @http.Response {
  ensure_object_key(key)
  self.send(Head, bucket, key, b"", options~, (response, client) => {
    if !is_success_response(response) {
      ignore(client)
      raise_service_error_response_without_body(response)
    }
    response
  })
}

///|
pub async fn Client::list_objects_v2(
  self : Client,
  bucket : String,
  prefix? : String,
  delimiter? : String,
  continuation_token? : String,
  max_keys? : Int,
  options? : RequestOptions = RequestOptions(),
) -> ListObjectsV2Result {
  let query = options.query.copy()
  query["list-type"] = "2"
  if prefix is Some(value) {
    query["prefix"] = value
  }
  if delimiter is Some(value) {
    query["delimiter"] = value
  }
  if continuation_token is Some(value) {
    query["continuation-token"] = value
  }
  if max_keys is Some(value) {
    query["max-keys"] = value.to_string()
  }
  let options = {
    headers: options.headers,
    query,
    timestamp: options.timestamp,
    payload_hash: options.payload_hash,
  }
  self.send(Get, bucket, "", b"", options~, (response, client) => {
    let body = client.read_all().text()
    ensure_success_response(response, body)
    parse_list_objects_v2(body)
  })
}

///|
impl ReadCloser for @http.Client

///|
impl ReadCloser for @http.Client with fn close(self) {
  self.close()
}