/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */
#include "aws_signing.h"
#include "credentials.h"
#include "crt.h"
#include "http_request_utils.h"
#include "java_class_ids.h"
#include "retry_utils.h"
#include <aws/common/string.h>
#include <aws/http/connection.h>
#include <aws/http/proxy.h>
#include <aws/http/request_response.h>
#include <aws/io/channel_bootstrap.h>
#include <aws/io/retry_strategy.h>
#include <aws/io/stream.h>
#include <aws/io/tls_channel_handler.h>
#include <aws/io/uri.h>
#include <aws/s3/s3_client.h>
#include <aws/s3/s3express_credentials_provider.h>
#include <http_proxy_options.h>
#include <http_proxy_options_environment_variable.h>
#include <jni.h>

/* on 32-bit platforms, casting pointers to longs throws a warning we don't need */
#if UINTPTR_MAX == 0xffffffff
#    if defined(_MSC_VER)
#        pragma warning(push)
#        pragma warning(disable : 4305) /* 'type cast': truncation from 'jlong' to 'jni_tls_ctx_options *' */
#        pragma warning(disable : 4221)
#    else
#        pragma GCC diagnostic push
#        pragma GCC diagnostic ignored "-Wpointer-to-int-cast"
#        pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
#    endif
#endif

struct s3_client_callback_data {
    JavaVM *jvm;
    jobject java_s3_client;
    struct aws_signing_config_data signing_config_data;
    jobject java_s3express_provider_factory;
};

struct s3_client_make_meta_request_callback_data {
    JavaVM *jvm;
    jobject java_s3_meta_request;
    jobject java_s3_meta_request_response_handler_native_adapter;
    struct aws_input_stream *input_stream;
    struct aws_signing_config_data signing_config_data;
    jthrowable java_exception;
};

static void s_on_s3_client_shutdown_complete_callback(void *user_data);
static void s_on_s3_meta_request_shutdown_complete_callback(void *user_data);

int aws_s3_tcp_keep_alive_options_from_java(
    JNIEnv *env,
    jobject jni_s3_tcp_keep_alive_options,
    struct aws_s3_tcp_keep_alive_options *s3_tcp_keep_alive_options) {

    uint16_t jni_keep_alive_interval_sec = (*env)->GetShortField(
        env, jni_s3_tcp_keep_alive_options, s3_tcp_keep_alive_options_properties.keep_alive_interval_sec_field_id);

    uint16_t jni_keep_alive_timeout_sec = (*env)->GetShortField(
        env, jni_s3_tcp_keep_alive_options, s3_tcp_keep_alive_options_properties.keep_alive_timeout_sec_field_id);

    uint16_t jni_keep_alive_max_failed_probes = (*env)->GetShortField(
        env, jni_s3_tcp_keep_alive_options, s3_tcp_keep_alive_options_properties.keep_alive_max_failed_probes_field_id);

    AWS_ZERO_STRUCT(*s3_tcp_keep_alive_options);

    s3_tcp_keep_alive_options->keep_alive_interval_sec = jni_keep_alive_interval_sec;
    s3_tcp_keep_alive_options->keep_alive_timeout_sec = jni_keep_alive_timeout_sec;
    s3_tcp_keep_alive_options->keep_alive_max_failed_probes = jni_keep_alive_max_failed_probes;

    return AWS_OP_SUCCESS;
}

struct s3_client_s3express_provider_java_impl {
    JavaVM *jvm;
    jobject java_s3express_provider;
};

struct s3_client_s3express_provider_callback_data {
    void *log_id;
    aws_on_get_credentials_callback_fn *get_cred_callback;
    void *get_cred_user_data;
};

JNIEXPORT void JNICALL
    Java_software_amazon_awssdk_crt_s3_S3ExpressCredentialsProvider_s3expressCredentialsProviderGetCredentialsCompleted(
        JNIEnv *env,
        jclass jni_class,
        jlong nativeHandler,
        jobject java_credentials) {

    (void)jni_class;
    struct s3_client_s3express_provider_callback_data *callback_data = (void *)nativeHandler;
    struct aws_credentials *native_credentials = NULL;
    int error_code = AWS_ERROR_SUCCESS;

    if (!java_credentials || aws_jni_check_and_clear_exception(env)) {
        /* TODO: a separate error code?? */
        AWS_LOGF_ERROR(
            AWS_LS_S3_META_REQUEST,
            "id=%p: Failed to get S3Express credentials from Java",
            (void *)callback_data->log_id);
        error_code = AWS_ERROR_HTTP_CALLBACK_FAILURE;
        goto done;
    }

    native_credentials = aws_credentials_new_from_java_credentials(env, java_credentials);
    if (!native_credentials) {
        aws_jni_throw_runtime_exception(env, "Failed to create native credentials");
        AWS_LOGF_ERROR(
            AWS_LS_S3_META_REQUEST,
            "id=%p: Failed to create native credentials from Java",
            (void *)callback_data->log_id);
        goto done;
    }
    struct aws_byte_cursor session_token = aws_credentials_get_session_token(native_credentials);

    if (session_token.len == 0) {
        aws_jni_throw_runtime_exception(env, "S3ExpressCredentialsProvider - sessionToken must be non-null");
        error_code = AWS_ERROR_HTTP_CALLBACK_FAILURE;
        aws_credentials_release(native_credentials);
        native_credentials = NULL;
        goto done;
    }

done:
    callback_data->get_cred_callback(native_credentials, error_code, callback_data->get_cred_user_data);
    aws_credentials_release(native_credentials);
    aws_mem_release(aws_jni_get_allocator(), callback_data);
}

static int s_s3express_get_creds_java(
    struct aws_s3express_credentials_provider *provider,
    const struct aws_credentials *original_credentials,
    const struct aws_credentials_properties_s3express *s3express_properties,
    aws_on_get_credentials_callback_fn callback,
    void *user_data) {

    struct s3_client_s3express_provider_java_impl *impl = provider->impl;
    int result = AWS_OP_ERR;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(impl->jvm);
    if (!env) {
        /* If we can't get an environment, then the JVM is probably shutting down. Don't crash. */
        return AWS_OP_SUCCESS;
    }
    jobject properties_object = NULL;
    jobject original_credentials_object = NULL;

    properties_object = (*env)->NewObject(
        env,
        s3express_credentials_properties_properties.s3express_credentials_properties_class,
        s3express_credentials_properties_properties.constructor_method_id);
    if ((*env)->ExceptionCheck(env) || properties_object == NULL) {
        aws_jni_throw_runtime_exception(
            env,
            "S3ExpressCredentialsProvider.getS3ExpressCredentials: Failed to create S3ExpressCredentialsProperties "
            "object.");
        goto done;
    }
    original_credentials_object = aws_java_credentials_from_native_new(env, original_credentials);
    if ((*env)->ExceptionCheck(env) || original_credentials_object == NULL) {
        aws_jni_throw_runtime_exception(
            env, "S3ExpressCredentialsProvider.getS3ExpressCredentials: Failed to create Credentials object.");
        goto done;
    }

    jstring jni_host = aws_jni_string_from_cursor(env, &s3express_properties->host);
    jstring jni_region = aws_jni_string_from_cursor(env, &s3express_properties->region);
    (*env)->SetObjectField(env, properties_object, s3express_credentials_properties_properties.host_field_id, jni_host);
    (*env)->SetObjectField(
        env, properties_object, s3express_credentials_properties_properties.region_field_id, jni_region);
    (*env)->DeleteLocalRef(env, jni_host);
    (*env)->DeleteLocalRef(env, jni_region);

    struct s3_client_s3express_provider_callback_data *callback_data =
        aws_mem_calloc(aws_jni_get_allocator(), 1, sizeof(struct s3_client_s3express_provider_callback_data));

    callback_data->get_cred_callback = callback;
    callback_data->get_cred_user_data = user_data;
    callback_data->log_id = impl->java_s3express_provider;

    /* Invoke the java call */
    (*env)->CallVoidMethod(
        env,
        impl->java_s3express_provider,
        s3express_credentials_provider_properties.getS3ExpressCredentials,
        properties_object,
        original_credentials_object,
        (jlong)callback_data);

    if (aws_jni_check_and_clear_exception(env)) {
        /* Check if any exception raised */
        AWS_LOGF_ERROR(
            AWS_LS_S3_META_REQUEST,
            "id=%p: S3ExpressCredentialsProvider.getS3ExpressCredentials failed",
            (void *)impl->java_s3express_provider);
        aws_mem_release(aws_jni_get_allocator(), callback_data);
        goto done;
    }
    result = AWS_OP_SUCCESS;
done:
    if (properties_object) {
        (*env)->DeleteLocalRef(env, properties_object);
    }
    if (original_credentials_object) {
        (*env)->DeleteLocalRef(env, original_credentials_object);
    }
    aws_jni_release_thread_env(impl->jvm, env);
    /********** JNI ENV RELEASE **********/
    return result;
}

static void s_s3express_destroy_java(struct aws_s3express_credentials_provider *provider) {
    struct s3_client_s3express_provider_java_impl *impl = provider->impl;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(impl->jvm);
    if (!env) {
        /* If we can't get an environment, then the JVM is probably shutting down. Don't crash. */
        return;
    }
    /* Invoke the java call */
    (*env)->CallVoidMethod(
        env, impl->java_s3express_provider, s3express_credentials_provider_properties.destroyProvider);

    (*env)->DeleteGlobalRef(env, impl->java_s3express_provider);

    aws_jni_release_thread_env(impl->jvm, env);
    /********** JNI ENV RELEASE **********/
    /* Once the java call returns, the java resource should be cleaned up already. We can finish up the shutdown
     * process. Clean up the native part. */
    aws_simple_completion_callback *callback = provider->shutdown_complete_callback;
    void *user_data = provider->shutdown_user_data;
    aws_mem_release(provider->allocator, provider);
    callback(user_data);
}

static struct aws_s3express_credentials_provider_vtable s_java_s3express_vtable = {
    .get_credentials = s_s3express_get_creds_java,
    .destroy = s_s3express_destroy_java,
};

struct aws_s3express_credentials_provider *s_s3express_provider_jni_factory(
    struct aws_allocator *allocator,
    struct aws_s3_client *client,
    aws_simple_completion_callback shutdown_complete_callback,
    void *shutdown_user_data,
    void *factory_user_data) {

    (void)client;
    struct s3_client_callback_data *client_data = factory_user_data;

    struct aws_s3express_credentials_provider *provider = NULL;
    struct s3_client_s3express_provider_java_impl *impl = NULL;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(client_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return NULL;
    }

    aws_mem_acquire_many(
        allocator,
        2,
        &provider,
        sizeof(struct aws_s3express_credentials_provider),
        &impl,
        sizeof(struct s3_client_s3express_provider_java_impl));
    /* Call into the java factory to create the java impl */
    AWS_FATAL_ASSERT(client_data->java_s3express_provider_factory != NULL);
    jobject java_s3express_provider = (*env)->CallObjectMethod(
        env,
        client_data->java_s3express_provider_factory,
        s3express_credentials_provider_factory_properties.createS3ExpressCredentialsProvider,
        client_data->java_s3_client);

    if (aws_jni_check_and_clear_exception(env)) {
        AWS_LOGF_ERROR(
            AWS_LS_S3_META_REQUEST,
            "id=%p: Failed to create Java S3Express Provider",
            (void *)client_data->java_s3express_provider_factory);
        aws_mem_release(allocator, provider);
        provider = NULL;
        goto done;
    }
    impl->java_s3express_provider = (*env)->NewGlobalRef(env, java_s3express_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &impl->jvm);
    (void)jvmresult;
    AWS_FATAL_ASSERT(jvmresult == 0);

    aws_s3express_credentials_provider_init_base(provider, allocator, &s_java_s3express_vtable, impl);
    provider->shutdown_complete_callback = shutdown_complete_callback;
    provider->shutdown_user_data = shutdown_user_data;

    /* We are done using the factory, when succeed, clean it up. TODO: we don't have to clean it up here */
    (*env)->DeleteGlobalRef(env, client_data->java_s3express_provider_factory);
    client_data->java_s3express_provider_factory = NULL;

done:
    aws_jni_release_thread_env(client_data->jvm, env);
    /********** JNI ENV RELEASE **********/
    return provider;
}

JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientNew(
    JNIEnv *env,
    jclass jni_class,
    jobject s3_client_jobject,
    jbyteArray jni_region,
    jlong jni_client_bootstrap,
    jlong jni_tls_ctx,
    jobject java_signing_config,
    jlong part_size_jlong,
    jlong multipart_upload_threshold_jlong,
    jdouble throughput_target_gbps,
    jboolean enable_read_backpressure,
    jlong initial_read_window_jlong,
    int max_connections,
    jobject jni_standard_retry_options,
    jboolean compute_content_md5,
    jint jni_proxy_connection_type,
    jbyteArray jni_proxy_host,
    jint jni_proxy_port,
    jlong jni_proxy_tls_context,
    jint jni_proxy_authorization_type,
    jbyteArray jni_proxy_authorization_username,
    jbyteArray jni_proxy_authorization_password,
    jint jni_environment_variable_proxy_connection_type,
    jlong jni_environment_variable_proxy_tls_connection_options,
    jint jni_environment_variable_type,
    int connect_timeout_ms,
    jobject jni_s3_tcp_keep_alive_options,
    jlong jni_monitoring_throughput_threshold_in_bytes_per_second,
    jint jni_monitoring_failure_interval_in_seconds,
    jboolean enable_s3express,
    jobject java_s3express_provider_factory,
    jlong jni_memory_limit_bytes_jlong) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();

    struct aws_client_bootstrap *client_bootstrap = (struct aws_client_bootstrap *)jni_client_bootstrap;

    if (!client_bootstrap) {
        aws_jni_throw_illegal_argument_exception(env, "Invalid Client Bootstrap");
        return (jlong)NULL;
    }

    uint64_t part_size = (uint64_t)part_size_jlong;
    uint64_t multipart_upload_threshold = (uint64_t)multipart_upload_threshold_jlong;
    uint64_t memory_limit_in_bytes = (uint64_t)jni_memory_limit_bytes_jlong;

    size_t initial_read_window;
    if (aws_size_t_from_java(env, &initial_read_window, initial_read_window_jlong, "Initial read window")) {
        return (jlong)NULL;
    }

    struct aws_retry_strategy *retry_strategy = NULL;

    if (jni_standard_retry_options != NULL) {
        struct aws_standard_retry_options retry_options;

        if (aws_standard_retry_options_from_java(env, jni_standard_retry_options, &retry_options)) {
            return (jlong)NULL;
        }

        if (retry_options.backoff_retry_options.el_group == NULL) {
            retry_options.backoff_retry_options.el_group = client_bootstrap->event_loop_group;
        }

        retry_strategy = aws_retry_strategy_new_standard(allocator, &retry_options);

        if (retry_strategy == NULL) {
            aws_jni_throw_runtime_exception(env, "Could not create retry strategy with standard-retry-options");
            return (jlong)NULL;
        }
    }
    struct aws_s3_tcp_keep_alive_options *s3_tcp_keep_alive_options = NULL;

    if (jni_s3_tcp_keep_alive_options != NULL) {
        s3_tcp_keep_alive_options = aws_mem_calloc(allocator, 1, sizeof(struct aws_s3_tcp_keep_alive_options));
        if (aws_s3_tcp_keep_alive_options_from_java(env, jni_s3_tcp_keep_alive_options, s3_tcp_keep_alive_options)) {
            aws_jni_throw_runtime_exception(env, "Could not create s3_tcp_keep_alive_options");
            return (jlong)NULL;
        }
    }

    struct aws_byte_cursor region = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_region);

    struct s3_client_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct s3_client_callback_data));

    struct aws_signing_config_aws signing_config;
    struct aws_credentials *anonymous_credentials = NULL;
    AWS_ZERO_STRUCT(signing_config);
    if (java_signing_config != NULL) {
        if (aws_build_signing_config(env, java_signing_config, &callback_data->signing_config_data, &signing_config)) {
            aws_jni_throw_runtime_exception(env, "Invalid signingConfig");
            aws_mem_release(allocator, callback_data);
            return (jlong)NULL;
        }
    } else {
        anonymous_credentials = aws_credentials_new_anonymous(allocator);
        signing_config.credentials = anonymous_credentials;
    }

    if (java_s3express_provider_factory != NULL && enable_s3express) {
        /* Create a JNI factory */
        callback_data->java_s3express_provider_factory = (*env)->NewGlobalRef(env, java_s3express_provider_factory);
    }

    AWS_FATAL_ASSERT(callback_data);
    callback_data->java_s3_client = (*env)->NewGlobalRef(env, s3_client_jobject);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    (void)jvmresult;
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_tls_connection_options *tls_options = NULL;
    struct aws_tls_connection_options tls_options_storage;
    AWS_ZERO_STRUCT(tls_options_storage);
    if (jni_tls_ctx) {
        struct aws_tls_ctx *tls_ctx = (void *)jni_tls_ctx;
        tls_options = &tls_options_storage;
        aws_tls_connection_options_init_from_ctx(tls_options, tls_ctx);
    }

    struct aws_s3_client_config client_config = {
        .max_active_connections_override = max_connections,
        .region = region,
        .client_bootstrap = client_bootstrap,
        .tls_connection_options = tls_options,
        .signing_config = &signing_config,
        .part_size = part_size,
        .multipart_upload_threshold = multipart_upload_threshold,
        .throughput_target_gbps = throughput_target_gbps,
        .enable_read_backpressure = enable_read_backpressure,
        .initial_read_window = initial_read_window,
        .retry_strategy = retry_strategy,
        .shutdown_callback = s_on_s3_client_shutdown_complete_callback,
        .shutdown_callback_user_data = callback_data,
        .compute_content_md5 = compute_content_md5 ? AWS_MR_CONTENT_MD5_ENABLED : AWS_MR_CONTENT_MD5_DISABLED,
        .connect_timeout_ms = connect_timeout_ms,
        .tcp_keep_alive_options = s3_tcp_keep_alive_options,
        .enable_s3express = enable_s3express,
        .s3express_provider_override_factory =
            java_s3express_provider_factory ? s_s3express_provider_jni_factory : NULL,
        .factory_user_data = callback_data,
        .memory_limit_in_bytes = memory_limit_in_bytes,
    };

    struct aws_http_connection_monitoring_options monitoring_options;
    AWS_ZERO_STRUCT(monitoring_options);
    if (jni_monitoring_throughput_threshold_in_bytes_per_second >= 0 &&
        jni_monitoring_failure_interval_in_seconds >= 2) {
        monitoring_options.minimum_throughput_bytes_per_second =
            jni_monitoring_throughput_threshold_in_bytes_per_second;
        monitoring_options.allowable_throughput_failure_interval_seconds = jni_monitoring_failure_interval_in_seconds;
        client_config.monitoring_options = &monitoring_options;
    }

    struct aws_http_proxy_options proxy_options;
    AWS_ZERO_STRUCT(proxy_options);

    struct aws_tls_connection_options proxy_tls_conn_options;
    AWS_ZERO_STRUCT(proxy_tls_conn_options);

    aws_http_proxy_options_jni_init(
        env,
        &proxy_options,
        jni_proxy_connection_type,
        &proxy_tls_conn_options,
        jni_proxy_host,
        jni_proxy_port,
        jni_proxy_authorization_username,
        jni_proxy_authorization_password,
        jni_proxy_authorization_type,
        (struct aws_tls_ctx *)jni_proxy_tls_context);

    if (jni_proxy_host != NULL) {
        client_config.proxy_options = &proxy_options;
    }

    struct proxy_env_var_settings proxy_ev_settings;
    AWS_ZERO_STRUCT(proxy_ev_settings);

    aws_http_proxy_environment_variable_setting_jni_init(
        &proxy_ev_settings,
        jni_environment_variable_proxy_connection_type,
        jni_environment_variable_type,
        (struct aws_tls_connection_options *)jni_environment_variable_proxy_tls_connection_options);

    client_config.proxy_ev_settings = &proxy_ev_settings;

    struct aws_s3_client *client = aws_s3_client_new(allocator, &client_config);
    if (!client) {
        aws_jni_throw_runtime_exception(env, "S3Client.aws_s3_client_new: creating aws_s3_client failed");
        /* Clean up stuff */
        aws_signing_config_data_clean_up(&callback_data->signing_config_data, env);
        aws_mem_release(allocator, callback_data);
    }
    aws_credentials_release(anonymous_credentials);

    aws_retry_strategy_release(retry_strategy);

    aws_jni_byte_cursor_from_jbyteArray_release(env, jni_region, region);

    aws_http_proxy_options_jni_clean_up(
        env, &proxy_options, jni_proxy_host, jni_proxy_authorization_username, jni_proxy_authorization_password);

    aws_mem_release(aws_jni_get_allocator(), s3_tcp_keep_alive_options);

    return (jlong)client;
}

JNIEXPORT void JNICALL
    Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientDestroy(JNIEnv *env, jclass jni_class, jlong jni_s3_client) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_s3_client *client = (struct aws_s3_client *)jni_s3_client;
    if (!client) {
        aws_jni_throw_runtime_exception(env, "S3Client.s3_client_clean_up: Invalid/null client");
        return;
    }

    aws_s3_client_release(client);
}

static void s_on_s3_client_shutdown_complete_callback(void *user_data) {
    struct s3_client_callback_data *callback = (struct s3_client_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    AWS_LOGF_DEBUG(AWS_LS_S3_CLIENT, "S3 Client Shutdown Complete");
    if (callback->java_s3_client != NULL) {
        (*env)->CallVoidMethod(env, callback->java_s3_client, s3_client_properties.onShutdownComplete);

        if (aws_jni_check_and_clear_exception(env)) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Ignored Exception from S3Client.onShutdownCompete callback",
                (void *)callback->java_s3_client);
        }
    }

    // We're done with this callback data, free it.
    if (callback->java_s3express_provider_factory) {
        /* Clean up factory data if still exist */
        (*env)->DeleteGlobalRef(env, callback->java_s3express_provider_factory);
        callback->java_s3express_provider_factory = NULL;
    }
    (*env)->DeleteGlobalRef(env, callback->java_s3_client);

    aws_signing_config_data_clean_up(&callback->signing_config_data, env);

    aws_jni_release_thread_env(callback->jvm, env);
    /********** JNI ENV RELEASE **********/

    aws_mem_release(aws_jni_get_allocator(), user_data);
}

static int s_on_s3_meta_request_body_callback(
    struct aws_s3_meta_request *meta_request,
    const struct aws_byte_cursor *body,
    uint64_t range_start,
    void *user_data) {
    (void)body;
    (void)range_start;
    int return_value = AWS_OP_ERR;

    uint64_t range_end = range_start + body->len;

    struct s3_client_make_meta_request_callback_data *callback_data =
        (struct s3_client_make_meta_request_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return AWS_OP_ERR;
    }

    jobject jni_payload = aws_jni_byte_array_from_cursor(env, body);
    if (jni_payload == NULL) {
        /* JVM is out of memory, but native code can still have memory available, handle it and don't crash. */
        aws_jni_check_and_clear_exception(env);
        aws_jni_release_thread_env(callback_data->jvm, env);
        /********** JNI ENV RELEASE **********/
        return aws_raise_error(AWS_ERROR_JAVA_CRT_JVM_OUT_OF_MEMORY);
    }

    jint body_response_result = 0;

    if (callback_data->java_s3_meta_request_response_handler_native_adapter != NULL) {
        body_response_result = (*env)->CallIntMethod(
            env,
            callback_data->java_s3_meta_request_response_handler_native_adapter,
            s3_meta_request_response_handler_native_adapter_properties.onResponseBody,
            jni_payload,
            range_start,
            range_end);

        if (aws_jni_get_and_clear_exception(env, &(callback_data->java_exception))) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Received exception from S3MetaRequest.onResponseBody callback",
                (void *)meta_request);
            aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
            goto cleanup;
        }

        /* The Java onResponseBody API lets users return a size for auto-incrementing the read window */
        if (body_response_result > 0) {
            aws_s3_meta_request_increment_read_window(meta_request, (uint64_t)body_response_result);
        }
    }
    return_value = AWS_OP_SUCCESS;

cleanup:
    (*env)->DeleteLocalRef(env, jni_payload);

    aws_jni_release_thread_env(callback_data->jvm, env);
    /********** JNI ENV RELEASE **********/

    return return_value;
}

static int s_marshal_http_headers_to_buf(const struct aws_http_headers *headers, struct aws_byte_buf *out_headers_buf) {
    /* calculate initial header capacity */
    size_t headers_initial_capacity = 0;
    for (size_t header_index = 0; header_index < aws_http_headers_count(headers); ++header_index) {
        struct aws_http_header header;
        aws_http_headers_get_index(headers, header_index, &header);
        /* aws_marshal_http_headers_array_to_dynamic_buffer() impl drives this calculation */
        headers_initial_capacity += header.name.len + header.value.len + 8;
    }

    struct aws_allocator *allocator = aws_jni_get_allocator();

    if (aws_byte_buf_init(out_headers_buf, allocator, headers_initial_capacity)) {
        return AWS_OP_ERR;
    }

    if (aws_marshal_http_headers_to_dynamic_buffer(out_headers_buf, headers)) {
        aws_byte_buf_clean_up(out_headers_buf);
        return AWS_OP_ERR;
    }

    return AWS_OP_SUCCESS;
}

static int s_on_s3_meta_request_headers_callback(
    struct aws_s3_meta_request *meta_request,
    const struct aws_http_headers *headers,
    int response_status,
    void *user_data) {
    int return_value = AWS_OP_ERR;
    struct s3_client_make_meta_request_callback_data *callback_data =
        (struct s3_client_make_meta_request_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return AWS_OP_ERR;
    }

    jobject java_headers_buffer = NULL;
    struct aws_byte_buf headers_buf;
    AWS_ZERO_STRUCT(headers_buf);

    if (s_marshal_http_headers_to_buf(headers, &headers_buf)) {
        goto cleanup;
    }

    java_headers_buffer = aws_jni_direct_byte_buffer_from_raw_ptr(env, headers_buf.buffer, headers_buf.len);
    if (java_headers_buffer == NULL) {
        aws_jni_check_and_clear_exception(env);
        aws_raise_error(AWS_ERROR_JAVA_CRT_JVM_OUT_OF_MEMORY);
        goto cleanup;
    }

    if (callback_data->java_s3_meta_request_response_handler_native_adapter != NULL) {
        (*env)->CallVoidMethod(
            env,
            callback_data->java_s3_meta_request_response_handler_native_adapter,
            s3_meta_request_response_handler_native_adapter_properties.onResponseHeaders,
            response_status,
            java_headers_buffer);

        if (aws_jni_get_and_clear_exception(env, &(callback_data->java_exception))) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Exception thrown from S3MetaRequest.onResponseHeaders callback",
                (void *)meta_request);

            aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
            goto cleanup;
        }
    }
    return_value = AWS_OP_SUCCESS;

cleanup:
    aws_byte_buf_clean_up(&headers_buf);
    if (java_headers_buffer) {
        (*env)->DeleteLocalRef(env, java_headers_buffer);
    }

    aws_jni_release_thread_env(callback_data->jvm, env);
    /********** JNI ENV RELEASE **********/

    return return_value;
}

static void s_on_s3_meta_request_finish_callback(
    struct aws_s3_meta_request *meta_request,
    const struct aws_s3_meta_request_result *meta_request_result,
    void *user_data) {

    (void)meta_request;

    struct s3_client_make_meta_request_callback_data *callback_data =
        (struct s3_client_make_meta_request_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    if (callback_data->java_s3_meta_request_response_handler_native_adapter != NULL) {
        struct aws_byte_buf *error_response_body = meta_request_result->error_response_body;
        struct aws_byte_cursor error_response_cursor;
        AWS_ZERO_STRUCT(error_response_cursor);
        if (error_response_body) {
            error_response_cursor = aws_byte_cursor_from_buf(error_response_body);
        }
        jbyteArray jni_payload = aws_jni_byte_array_from_cursor(env, &error_response_cursor);
        /* Only propagate java_exception if crt error code is callback failure */
        jthrowable java_exception =
            meta_request_result->error_code == AWS_ERROR_HTTP_CALLBACK_FAILURE ? callback_data->java_exception : NULL;

        jobject java_error_headers_buffer = NULL;
        struct aws_byte_buf headers_buf;
        AWS_ZERO_STRUCT(headers_buf);
        if (meta_request_result->error_response_headers) {
            /* Ignore any errors and just report the original error without headers */
            if (s_marshal_http_headers_to_buf(meta_request_result->error_response_headers, &headers_buf) ==
                AWS_OP_SUCCESS) {
                java_error_headers_buffer =
                    aws_jni_direct_byte_buffer_from_raw_ptr(env, headers_buf.buffer, headers_buf.len);
                if (java_error_headers_buffer == NULL) {
                    AWS_LOGF_ERROR(
                        AWS_LS_S3_META_REQUEST,
                        "id=%p: Ignored Exception from "
                        "S3MetaRequest.onFinished.aws_jni_direct_byte_buffer_from_raw_ptr",
                        (void *)meta_request);
                    aws_jni_check_and_clear_exception(env);
                }
            } else {
                AWS_LOGF_ERROR(
                    AWS_LS_S3_META_REQUEST,
                    "id=%p: Ignored Exception from S3MetaRequest.onFinished.s_marshal_http_headers_to_buf",
                    (void *)meta_request);
            }
        }

        (*env)->CallVoidMethod(
            env,
            callback_data->java_s3_meta_request_response_handler_native_adapter,
            s3_meta_request_response_handler_native_adapter_properties.onFinished,
            meta_request_result->error_code,
            meta_request_result->response_status,
            jni_payload,
            meta_request_result->validation_algorithm,
            meta_request_result->did_validate,
            java_exception,
            java_error_headers_buffer);

        if (aws_jni_check_and_clear_exception(env)) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Ignored Exception from S3MetaRequest.onFinished callback",
                (void *)meta_request);
        }
        if (jni_payload) {
            (*env)->DeleteLocalRef(env, jni_payload);
        }

        aws_byte_buf_clean_up(&headers_buf);
        if (java_error_headers_buffer) {
            (*env)->DeleteLocalRef(env, java_error_headers_buffer);
        }
    }

    aws_jni_release_thread_env(callback_data->jvm, env);
    /********** JNI ENV RELEASE **********/
}

static void s_on_s3_meta_request_progress_callback(
    struct aws_s3_meta_request *meta_request,
    const struct aws_s3_meta_request_progress *progress,
    void *user_data) {

    (void)meta_request;

    struct s3_client_make_meta_request_callback_data *callback_data =
        (struct s3_client_make_meta_request_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    jobject progress_object = (*env)->NewObject(
        env,
        s3_meta_request_progress_properties.s3_meta_request_progress_class,
        s3_meta_request_progress_properties.s3_meta_request_progress_constructor_method_id);
    if ((*env)->ExceptionCheck(env) || progress_object == NULL) {
        aws_jni_throw_runtime_exception(
            env, "S3MetaRequestResponseHandler.onProgress: Failed to create S3MetaRequestProgress object.");
        goto done;
    }

    (*env)->SetLongField(
        env,
        progress_object,
        s3_meta_request_progress_properties.bytes_transferred_field_id,
        progress->bytes_transferred);
    (*env)->SetLongField(
        env, progress_object, s3_meta_request_progress_properties.content_length_field_id, progress->content_length);

    if (callback_data->java_s3_meta_request_response_handler_native_adapter != NULL) {

        (*env)->CallVoidMethod(
            env,
            callback_data->java_s3_meta_request_response_handler_native_adapter,
            s3_meta_request_response_handler_native_adapter_properties.onProgress,
            progress_object);

        if (aws_jni_check_and_clear_exception(env)) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Ignored Exception from S3MetaRequest.onProgress callback",
                (void *)meta_request);
        }
    }

    (*env)->DeleteLocalRef(env, progress_object);

done:

    aws_jni_release_thread_env(callback_data->jvm, env);
    /********** JNI ENV RELEASE **********/
}

static void s_s3_meta_request_callback_cleanup(
    JNIEnv *env,
    struct s3_client_make_meta_request_callback_data *callback_data) {
    if (callback_data) {
        (*env)->DeleteGlobalRef(env, callback_data->java_s3_meta_request);
        (*env)->DeleteGlobalRef(env, callback_data->java_s3_meta_request_response_handler_native_adapter);
        (*env)->DeleteGlobalRef(env, callback_data->java_exception);
        aws_signing_config_data_clean_up(&callback_data->signing_config_data, env);
        aws_mem_release(aws_jni_get_allocator(), callback_data);
    }
}

static struct aws_s3_meta_request_resume_token *s_native_resume_token_from_java_new(
    JNIEnv *env,
    jobject resume_token_jni) {

    if (resume_token_jni == NULL) {
        return NULL;
    }

    struct aws_allocator *allocator = aws_jni_get_allocator();

    jint native_type =
        (*env)->GetIntField(env, resume_token_jni, s3_meta_request_resume_token_properties.native_type_field_id);

    if (native_type != AWS_S3_META_REQUEST_TYPE_PUT_OBJECT) {
        aws_jni_throw_illegal_argument_exception(
            env, "ResumeToken: Operations other than PutObject are not supported for resume.");
        return NULL;
    }

    jlong part_size_jni =
        (*env)->GetLongField(env, resume_token_jni, s3_meta_request_resume_token_properties.part_size_field_id);
    jlong total_num_parts_jni =
        (*env)->GetLongField(env, resume_token_jni, s3_meta_request_resume_token_properties.total_num_parts_field_id);
    jlong num_parts_completed_jni = (*env)->GetLongField(
        env, resume_token_jni, s3_meta_request_resume_token_properties.num_parts_completed_field_id);

    jstring upload_id_jni =
        (*env)->GetObjectField(env, resume_token_jni, s3_meta_request_resume_token_properties.upload_id_field_id);
    if (upload_id_jni == NULL) {
        aws_jni_throw_illegal_argument_exception(env, "ResumeToken: UploadId must not be NULL.");
        return NULL;
    }

    struct aws_string *upload_id = aws_jni_new_string_from_jstring(env, upload_id_jni);

    struct aws_s3_upload_resume_token_options upload_options = {
        .part_size = (uint64_t)part_size_jni,
        .total_num_parts = (size_t)total_num_parts_jni,
        .num_parts_completed = (size_t)num_parts_completed_jni,
        .upload_id = aws_byte_cursor_from_string(upload_id),
    };

    struct aws_s3_meta_request_resume_token *resume_token =
        aws_s3_meta_request_resume_token_new_upload(allocator, &upload_options);
    aws_string_destroy(upload_id);

    return resume_token;
}

JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_s3_S3Client_s3ClientMakeMetaRequest(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_s3_client,
    jobject java_s3_meta_request_jobject,
    jbyteArray jni_region,
    jint meta_request_type,
    jint checksum_location,
    jint checksum_algorithm,
    jboolean validate_response,
    jintArray jni_marshalled_validate_algorithms,
    jbyteArray jni_marshalled_message_data,
    jobject jni_http_request_body_stream,
    jbyteArray jni_request_filepath,
    jobject java_signing_config,
    jobject java_response_handler_jobject,
    jbyteArray jni_endpoint,
    jobject java_resume_token_jobject) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_s3_client *client = (struct aws_s3_client *)jni_s3_client;
    struct aws_byte_cursor request_filepath;
    AWS_ZERO_STRUCT(request_filepath);
    struct aws_s3_meta_request_resume_token *resume_token =
        s_native_resume_token_from_java_new(env, java_resume_token_jobject);
    struct aws_s3_meta_request *meta_request = NULL;
    bool success = false;
    struct aws_byte_cursor region = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_region);
    struct aws_http_message *request_message = NULL;

    struct s3_client_make_meta_request_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct s3_client_make_meta_request_callback_data));
    AWS_FATAL_ASSERT(callback_data);
    struct aws_signing_config_aws signing_config;
    AWS_ZERO_STRUCT(signing_config);
    if (java_signing_config != NULL) {
        if (aws_build_signing_config(env, java_signing_config, &callback_data->signing_config_data, &signing_config)) {
            goto done;
        }
    }

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    (void)jvmresult;
    AWS_FATAL_ASSERT(jvmresult == 0);

    callback_data->java_s3_meta_request = (*env)->NewGlobalRef(env, java_s3_meta_request_jobject);
    AWS_FATAL_ASSERT(callback_data->java_s3_meta_request != NULL);

    callback_data->java_s3_meta_request_response_handler_native_adapter =
        (*env)->NewGlobalRef(env, java_response_handler_jobject);
    AWS_FATAL_ASSERT(callback_data->java_s3_meta_request_response_handler_native_adapter != NULL);

    request_message = aws_http_message_new_request(allocator);
    AWS_FATAL_ASSERT(request_message);

    AWS_FATAL_ASSERT(
        AWS_OP_SUCCESS == aws_apply_java_http_request_changes_to_native_request(
                              env, jni_marshalled_message_data, jni_http_request_body_stream, request_message));

    if (jni_request_filepath) {
        request_filepath = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_request_filepath);
        if (request_filepath.ptr == NULL) {
            goto done;
        }
        if (request_filepath.len == 0) {
            aws_jni_throw_illegal_argument_exception(env, "Request file path cannot be empty");
            goto done;
        }
    }

    struct aws_uri endpoint;
    AWS_ZERO_STRUCT(endpoint);
    if (jni_endpoint != NULL) {
        struct aws_byte_cursor endpoint_str = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_endpoint);
        int uri_parse = aws_uri_init_parse(&endpoint, allocator, &endpoint_str);
        aws_jni_byte_cursor_from_jbyteArray_release(env, jni_endpoint, endpoint_str);
        if (uri_parse) {
            aws_jni_throw_runtime_exception(env, "S3Client.aws_s3_client_make_meta_request: failed to parse endpoint");
            goto done;
        }
    }

    struct aws_s3_checksum_config checksum_config = {
        .location = checksum_location,
        .checksum_algorithm = checksum_algorithm,
        .validate_response_checksum = validate_response,
    };

    struct aws_array_list response_checksum_list;
    AWS_ZERO_STRUCT(response_checksum_list);
    if (jni_marshalled_validate_algorithms != NULL) {
        jint *marshalled_algorithms = (*env)->GetIntArrayElements(env, jni_marshalled_validate_algorithms, NULL);
        const size_t marshalled_len = (*env)->GetArrayLength(env, jni_marshalled_validate_algorithms);
        aws_array_list_init_dynamic(&response_checksum_list, allocator, marshalled_len, sizeof(int));
        for (size_t i = 0; i < marshalled_len; ++i) {
            enum aws_s3_checksum_algorithm algorithm = (int)marshalled_algorithms[i];
            aws_array_list_push_back(&response_checksum_list, &algorithm);
        }
        checksum_config.validate_checksum_algorithms = &response_checksum_list;
    }

    struct aws_s3_meta_request_options meta_request_options = {
        .type = meta_request_type,
        .checksum_config = &checksum_config,
        .message = request_message,
        .send_filepath = request_filepath,
        .user_data = callback_data,
        .signing_config = java_signing_config ? &signing_config : NULL,
        .headers_callback = s_on_s3_meta_request_headers_callback,
        .body_callback = s_on_s3_meta_request_body_callback,
        .finish_callback = s_on_s3_meta_request_finish_callback,
        .progress_callback = s_on_s3_meta_request_progress_callback,
        .shutdown_callback = s_on_s3_meta_request_shutdown_complete_callback,
        .endpoint = jni_endpoint != NULL ? &endpoint : NULL,
        .resume_token = resume_token,
    };

    meta_request = aws_s3_client_make_meta_request(client, &meta_request_options);
    /* We are done using the list, it can be safely cleaned up now. */
    aws_array_list_clean_up(&response_checksum_list);
    if (!meta_request) {
        aws_jni_throw_runtime_exception(
            env, "S3Client.aws_s3_client_make_meta_request: creating aws_s3_meta_request failed");
        goto done;
    }

    success = true;

done:
    aws_s3_meta_request_resume_token_release(resume_token);
    aws_jni_byte_cursor_from_jbyteArray_release(env, jni_region, region);
    aws_http_message_release(request_message);
    aws_jni_byte_cursor_from_jbyteArray_release(env, jni_request_filepath, request_filepath);
    aws_uri_clean_up(&endpoint);
    if (success) {
        return (jlong)meta_request;
    }
    s_s3_meta_request_callback_cleanup(env, callback_data);
    return (jlong)0;
}

static void s_on_s3_meta_request_shutdown_complete_callback(void *user_data) {
    struct s3_client_make_meta_request_callback_data *callback_data =
        (struct s3_client_make_meta_request_callback_data *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    if (callback_data->java_s3_meta_request != NULL) {
        (*env)->CallVoidMethod(env, callback_data->java_s3_meta_request, s3_meta_request_properties.onShutdownComplete);

        if (aws_jni_check_and_clear_exception(env)) {
            AWS_LOGF_ERROR(
                AWS_LS_S3_META_REQUEST,
                "id=%p: Ignored Exception from S3MetaRequest.onShutdownCompete callback",
                (void *)callback_data->java_s3_meta_request);
        }
    }

    // We're done with this callback data, free it.
    JavaVM *jvm = callback_data->jvm;
    s_s3_meta_request_callback_cleanup(env, callback_data);

    aws_jni_release_thread_env(jvm, env);
    /********** JNI ENV RELEASE **********/
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_s3_S3MetaRequest_s3MetaRequestDestroy(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_s3_meta_request) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_s3_meta_request *meta_request = (struct aws_s3_meta_request *)jni_s3_meta_request;
    if (!meta_request) {
        aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
        aws_jni_throw_runtime_exception(env, "S3MetaRequest.s3MetaRequestDestroy: Invalid/null meta request");
        return;
    }

    aws_s3_meta_request_release(meta_request);
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_s3_S3MetaRequest_s3MetaRequestCancel(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_s3_meta_request) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_s3_meta_request *meta_request = (struct aws_s3_meta_request *)jni_s3_meta_request;
    if (!meta_request) {
        /* It's fine if this particular function does nothing when it's called
         * after CrtResource is closed and the handle is NULL */
        return;
    }

    aws_s3_meta_request_cancel(meta_request);
}

JNIEXPORT jobject JNICALL Java_software_amazon_awssdk_crt_s3_S3MetaRequest_s3MetaRequestPause(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_s3_meta_request) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_s3_meta_request *meta_request = (struct aws_s3_meta_request *)jni_s3_meta_request;
    if (!meta_request) {
        aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
        aws_jni_throw_illegal_argument_exception(env, "S3MetaRequest.s3MetaRequestPause: Invalid/null meta request");
        return NULL;
    }

    struct aws_s3_meta_request_resume_token *resume_token = NULL;

    if (aws_s3_meta_request_pause(meta_request, &resume_token)) {
        aws_jni_throw_runtime_exception(env, "S3MetaRequest.s3MetaRequestPause: Failed to pause request");
        return NULL;
    }

    jobject resume_token_jni = NULL;
    if (resume_token != NULL) {
        resume_token_jni = (*env)->NewObject(
            env,
            s3_meta_request_resume_token_properties.s3_meta_request_resume_token_class,
            s3_meta_request_resume_token_properties.s3_meta_request_resume_token_constructor_method_id);
        if ((*env)->ExceptionCheck(env) || resume_token_jni == NULL) {
            aws_jni_throw_runtime_exception(env, "S3MetaRequest.s3MetaRequestPause: Failed to create ResumeToken.");
            goto on_done;
        }

        enum aws_s3_meta_request_type type = aws_s3_meta_request_resume_token_type(resume_token);
        if (type != AWS_S3_META_REQUEST_TYPE_PUT_OBJECT) {
            aws_jni_throw_runtime_exception(env, "S3MetaRequest.s3MetaRequestPause: Failed to convert resume token.");
            goto on_done;
        }

        (*env)->SetIntField(env, resume_token_jni, s3_meta_request_resume_token_properties.native_type_field_id, type);
        (*env)->SetLongField(
            env,
            resume_token_jni,
            s3_meta_request_resume_token_properties.part_size_field_id,
            aws_s3_meta_request_resume_token_part_size(resume_token));
        (*env)->SetLongField(
            env,
            resume_token_jni,
            s3_meta_request_resume_token_properties.total_num_parts_field_id,
            aws_s3_meta_request_resume_token_total_num_parts(resume_token));
        (*env)->SetLongField(
            env,
            resume_token_jni,
            s3_meta_request_resume_token_properties.num_parts_completed_field_id,
            aws_s3_meta_request_resume_token_num_parts_completed(resume_token));

        struct aws_byte_cursor upload_id_cur = aws_s3_meta_request_resume_token_upload_id(resume_token);
        jstring upload_id_jni = aws_jni_string_from_cursor(env, &upload_id_cur);
        (*env)->SetObjectField(
            env, resume_token_jni, s3_meta_request_resume_token_properties.upload_id_field_id, upload_id_jni);

        (*env)->DeleteLocalRef(env, upload_id_jni);
    }

on_done:
    aws_s3_meta_request_resume_token_release(resume_token);
    return resume_token_jni;
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_s3_S3MetaRequest_s3MetaRequestIncrementReadWindow(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_s3_meta_request,
    jlong increment) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_s3_meta_request *meta_request = (struct aws_s3_meta_request *)jni_s3_meta_request;
    if (!meta_request) {
        /* It's fine if this particular function does nothing when it's called
         * after CrtResource is closed and the handle is NULL */
        return;
    }

    if (increment < 0) {
        aws_jni_throw_illegal_argument_exception(
            env, "S3MetaRequest.s3MetaRequestIncrementReadWindow: Number cannot be negative");
        return;
    }

    aws_s3_meta_request_increment_read_window(meta_request, (uint64_t)increment);
}

#if UINTPTR_MAX == 0xffffffff
#    if defined(_MSC_VER)
#        pragma warning(pop)
#    else
#        pragma GCC diagnostic pop
#    endif
#endif
