/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <jni.h>

#include "crt.h"
#include "http_connection_manager.h"
#include "http_request_response.h"
#include "http_request_utils.h"
#include "java_class_ids.h"

#include <aws/common/atomics.h>
#include <aws/common/mutex.h>
#include <aws/http/connection.h>
#include <aws/http/http.h>
#include <aws/http/request_response.h>
#include <aws/io/logging.h>
#include <aws/io/stream.h>

#if _MSC_VER
#    pragma warning(disable : 4204) /* non-constant aggregate initializer */
#endif

/* 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 *' */
#    else
#        pragma GCC diagnostic push
#        pragma GCC diagnostic ignored "-Wpointer-to-int-cast"
#        pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
#    endif
#endif

jobject aws_java_http_stream_from_native_new(JNIEnv *env, void *opaque, int version) {
    jlong jni_native_ptr = (jlong)opaque;
    AWS_ASSERT(jni_native_ptr);
    jobject stream = NULL;
    switch (version) {
        case AWS_HTTP_VERSION_2:
            stream = (*env)->NewObject(
                env, http2_stream_properties.stream_class, http2_stream_properties.constructor, jni_native_ptr);
            break;
        case AWS_HTTP_VERSION_1_0:
        case AWS_HTTP_VERSION_1_1:
            stream = (*env)->NewObject(
                env, http_stream_properties.stream_class, http_stream_properties.constructor, jni_native_ptr);
            break;
        default:
            aws_jni_throw_runtime_exception(env, "Unsupported HTTP protocol.");
            aws_raise_error(AWS_ERROR_UNIMPLEMENTED);
    }
    return stream;
}

void aws_java_http_stream_from_native_delete(JNIEnv *env, jobject jHttpStream) {
    /* Delete our reference to the HttpStream Object from the JVM. */
    (*env)->DeleteGlobalRef(env, jHttpStream);
}

/*******************************************************************************
 * http_stream_binding - Jni native represent of the Java HTTP stream object
 ******************************************************************************/

static void s_http_stream_binding_destroy(JNIEnv *env, struct http_stream_binding *binding) {

    if (binding->java_http_stream_base) {
        aws_java_http_stream_from_native_delete(env, binding->java_http_stream_base);
    }

    if (binding->java_http_response_stream_handler != NULL) {
        (*env)->DeleteGlobalRef(env, binding->java_http_response_stream_handler);
    }

    if (binding->native_request) {
        aws_http_message_release(binding->native_request);
    }
    aws_byte_buf_clean_up(&binding->headers_buf);
    aws_mem_release(aws_jni_get_allocator(), binding);
}

void *aws_http_stream_binding_acquire(struct http_stream_binding *binding) {
    if (binding == NULL) {
        return NULL;
    }
    aws_atomic_fetch_add(&binding->ref, 1);
    return binding;
}

void *aws_http_stream_binding_release(JNIEnv *env, struct http_stream_binding *binding) {
    if (binding == NULL) {
        return NULL;
    }
    size_t pre_ref = aws_atomic_fetch_sub(&binding->ref, 1);
    AWS_ASSERT(pre_ref > 0 && "stream binding refcount has gone negative");
    if (pre_ref == 1) {
        s_http_stream_binding_destroy(env, binding);
    }
    return NULL;
}

// If error occurs, A Java exception is thrown and NULL is returned.
struct http_stream_binding *aws_http_stream_binding_new(JNIEnv *env, jobject java_callback_handler) {

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct http_stream_binding *binding = aws_mem_calloc(allocator, 1, sizeof(struct http_stream_binding));
    AWS_FATAL_ASSERT(binding);

    // GetJavaVM() reference doesn't need a NewGlobalRef() call since it's global by default
    jint jvmresult = (*env)->GetJavaVM(env, &binding->jvm);
    (void)jvmresult;
    AWS_FATAL_ASSERT(jvmresult == 0);

    binding->java_http_response_stream_handler = (*env)->NewGlobalRef(env, java_callback_handler);
    AWS_FATAL_ASSERT(binding->java_http_response_stream_handler);
    AWS_FATAL_ASSERT(!aws_byte_buf_init(&binding->headers_buf, allocator, 1024));

    aws_atomic_init_int(&binding->ref, 1);

    return binding;
}

int aws_java_http_stream_on_incoming_headers_fn(
    struct aws_http_stream *stream,
    enum aws_http_header_block block_type,
    const struct aws_http_header *header_array,
    size_t num_headers,
    void *user_data) {
    (void)block_type;

    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;
    int resp_status = -1;
    int err_code = aws_http_stream_get_incoming_response_status(stream, &resp_status);
    if (err_code != AWS_OP_SUCCESS) {
        AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Invalid Incoming Response Status", (void *)stream);
        return AWS_OP_ERR;
    }

    binding->response_status = resp_status;

    if (aws_marshal_http_headers_array_to_dynamic_buffer(&binding->headers_buf, header_array, num_headers)) {
        AWS_LOGF_ERROR(
            AWS_LS_HTTP_STREAM, "id=%p: Failed to allocate buffer space for incoming headers", (void *)stream);
        return AWS_OP_ERR;
    }

    return AWS_OP_SUCCESS;
}

int aws_java_http_stream_on_incoming_header_block_done_fn(
    struct aws_http_stream *stream,
    enum aws_http_header_block block_type,
    void *user_data) {
    (void)stream;

    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(binding->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;
    }

    int result = AWS_OP_ERR;
    jint jni_block_type = block_type;

    jobject jni_headers_buf =
        aws_jni_direct_byte_buffer_from_raw_ptr(env, binding->headers_buf.buffer, binding->headers_buf.len);

    (*env)->CallVoidMethod(
        env,
        binding->java_http_response_stream_handler,
        http_stream_response_handler_properties.onResponseHeaders,
        binding->java_http_stream_base,
        (jint)binding->response_status,
        (jint)block_type,
        jni_headers_buf);

    if (aws_jni_check_and_clear_exception(env)) {
        (*env)->DeleteLocalRef(env, jni_headers_buf);
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
        goto done;
    }

    /* instead of cleaning it up here, reset it in case another block is encountered */
    aws_byte_buf_reset(&binding->headers_buf, false);
    (*env)->DeleteLocalRef(env, jni_headers_buf);

    (*env)->CallVoidMethod(
        env,
        binding->java_http_response_stream_handler,
        http_stream_response_handler_properties.onResponseHeadersDone,
        binding->java_http_stream_base,
        jni_block_type);

    if (aws_jni_check_and_clear_exception(env)) {
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
        goto done;
    }

    result = AWS_OP_SUCCESS;

done:

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

    return result;
}

int aws_java_http_stream_on_incoming_body_fn(
    struct aws_http_stream *stream,
    const struct aws_byte_cursor *data,
    void *user_data) {
    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;

    size_t total_window_increment = 0;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(binding->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;
    }

    int result = AWS_OP_ERR;

    jobject jni_payload = aws_jni_direct_byte_buffer_from_raw_ptr(env, data->ptr, data->len);

    jint window_increment = (*env)->CallIntMethod(
        env,
        binding->java_http_response_stream_handler,
        http_stream_response_handler_properties.onResponseBody,
        binding->java_http_stream_base,
        jni_payload);

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

    if (aws_jni_check_and_clear_exception(env)) {
        AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Received Exception from onResponseBody", (void *)stream);
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
        goto done;
    }

    if (window_increment < 0) {
        AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Window Increment from onResponseBody < 0", (void *)stream);
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
        goto done;
    }

    total_window_increment += window_increment;

    if (total_window_increment > 0) {
        aws_http_stream_update_window(stream, total_window_increment);
    }

    result = AWS_OP_SUCCESS;

done:

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

    return result;
}

void aws_java_http_stream_on_stream_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data) {
    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;

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

    /* Don't invoke Java callbacks if Java HttpStream failed to completely setup */
    jint jErrorCode = error_code;
    (*env)->CallVoidMethod(
        env,
        binding->java_http_response_stream_handler,
        http_stream_response_handler_properties.onResponseComplete,
        binding->java_http_stream_base,
        jErrorCode);

    if (aws_jni_check_and_clear_exception(env)) {
        /* Close the Connection if the Java Callback throws an Exception */
        aws_http_connection_close(aws_http_stream_get_connection(stream));
    }

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

void aws_java_http_stream_on_stream_destroy_fn(void *user_data) {
    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;

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

void aws_java_http_stream_on_stream_metrics_fn(
    struct aws_http_stream *stream,
    const struct aws_http_stream_metrics *metrics,
    void *user_data) {
    struct http_stream_binding *binding = (struct http_stream_binding *)user_data;

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

    /* Convert metrics to Java HttpStreamMetrics obj */
    jobject jni_metrics = (*env)->NewObject(
        env,
        http_stream_metrics_properties.http_stream_metrics_class,
        http_stream_metrics_properties.constructor_id,
        (jlong)metrics->send_start_timestamp_ns,
        (jlong)metrics->send_end_timestamp_ns,
        (jlong)metrics->sending_duration_ns,
        (jlong)metrics->receive_start_timestamp_ns,
        (jlong)metrics->receive_end_timestamp_ns,
        (jlong)metrics->receiving_duration_ns,

        /* Stream IDs are 31-bit unsigned integers, which fits into Java's regular (signed) 32-bit int */
        (jint)metrics->stream_id);

    (*env)->CallVoidMethod(
        env,
        binding->java_http_response_stream_handler,
        http_stream_response_handler_properties.onMetrics,
        binding->java_http_stream_base,
        jni_metrics);

    /* Delete local reference to metrics object */
    (*env)->DeleteLocalRef(env, jni_metrics);

    if (aws_jni_check_and_clear_exception(env)) {
        /* Close the Connection if the Java Callback throws an Exception */
        aws_http_connection_close(aws_http_stream_get_connection(stream));

        AWS_LOGF_ERROR(AWS_LS_HTTP_STREAM, "id=%p: Received Exception from onMetrics", (void *)stream);
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
    }

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

jobjectArray aws_java_http_headers_from_native(JNIEnv *env, struct aws_http_headers *headers) {
    (void)headers;
    jobjectArray ret;
    const size_t header_count = aws_http_headers_count(headers);

    ret = (jobjectArray)(*env)->NewObjectArray(
        env, (jsize)header_count, http_header_properties.http_header_class, (void *)NULL);

    for (size_t index = 0; index < header_count; index += 1) {
        struct aws_http_header header;
        aws_http_headers_get_index(headers, index, &header);
        jbyteArray header_name = aws_jni_byte_array_from_cursor(env, &header.name);
        jbyteArray header_value = aws_jni_byte_array_from_cursor(env, &header.value);

        jobject java_http_header = (*env)->NewObject(
            env,
            http_header_properties.http_header_class,
            http_header_properties.constructor_method_id,
            header_name,
            header_value);

        (*env)->SetObjectArrayElement(env, ret, (jsize)index, java_http_header);
    }

    return (ret);
}

static jobject s_make_request_general(
    JNIEnv *env,
    jlong jni_connection,
    jbyteArray marshalled_request,
    jobject jni_http_request_body_stream,
    jobject jni_http_response_callback_handler,
    enum aws_http_version version) {

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_null_pointer_exception(env, "HttpClientConnection.MakeRequest: Invalid aws_http_connection");
        return (jobject)NULL;
    }

    if (!jni_http_response_callback_handler) {
        aws_jni_throw_illegal_argument_exception(
            env, "HttpClientConnection.MakeRequest: Invalid jni_http_response_callback_handler");
        return (jobject)NULL;
    }

    /* initial refcount created for the Java object */
    struct http_stream_binding *stream_binding = aws_http_stream_binding_new(env, jni_http_response_callback_handler);
    if (!stream_binding) {
        /* Exception already thrown */
        return (jobject)NULL;
    }

    stream_binding->native_request =
        aws_http_request_new_from_java_http_request(env, marshalled_request, jni_http_request_body_stream);
    if (stream_binding->native_request == NULL) {
        /* Exception already thrown */
        goto error;
    }

    struct aws_http_make_request_options request_options = {
        .self_size = sizeof(request_options),
        .request = stream_binding->native_request,
        /* Set Callbacks */
        .on_response_headers = aws_java_http_stream_on_incoming_headers_fn,
        .on_response_header_block_done = aws_java_http_stream_on_incoming_header_block_done_fn,
        .on_response_body = aws_java_http_stream_on_incoming_body_fn,
        .on_complete = aws_java_http_stream_on_stream_complete_fn,
        .on_destroy = aws_java_http_stream_on_stream_destroy_fn,
        .on_metrics = aws_java_http_stream_on_stream_metrics_fn,
        .user_data = stream_binding,
    };

    stream_binding->native_stream = aws_http_connection_make_request(native_conn, &request_options);
    if (stream_binding->native_stream == NULL) {
        AWS_LOGF_ERROR(AWS_LS_HTTP_CONNECTION, "Stream Request Failed. conn: %p", (void *)native_conn);
        aws_jni_throw_runtime_exception(env, "HttpClientConnection.MakeRequest: Unable to Execute Request");
        goto error;
    }

    /* Stream created successfully, acquire on binding for the native stream lifetime. */
    aws_http_stream_binding_acquire(stream_binding);

    jobject jHttpStreamBase = aws_java_http_stream_from_native_new(env, stream_binding, version);
    if (jHttpStreamBase == NULL) {
        goto error;
    }

    AWS_LOGF_TRACE(
        AWS_LS_HTTP_CONNECTION,
        "Opened new Stream on Connection. conn: %p, stream: %p",
        (void *)native_conn,
        (void *)stream_binding->native_stream);

    return jHttpStreamBase;

error:
    aws_http_stream_release(stream_binding->native_stream);
    aws_http_stream_binding_release(env, stream_binding);
    return NULL;
}

JNIEXPORT jobject JNICALL Java_software_amazon_awssdk_crt_http_HttpClientConnection_httpClientConnectionMakeRequest(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection,
    jbyteArray marshalled_request,
    jobject jni_http_request_body_stream,
    jobject jni_http_response_callback_handler) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    return s_make_request_general(
        env,
        jni_connection,
        marshalled_request,
        jni_http_request_body_stream,
        jni_http_response_callback_handler,
        AWS_HTTP_VERSION_1_1);
}

JNIEXPORT jobject JNICALL Java_software_amazon_awssdk_crt_http_Http2ClientConnection_http2ClientConnectionMakeRequest(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection,
    jbyteArray marshalled_request,
    jobject jni_http_request_body_stream,
    jobject jni_http_response_callback_handler) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    return s_make_request_general(
        env,
        jni_connection,
        marshalled_request,
        jni_http_request_body_stream,
        jni_http_response_callback_handler,
        AWS_HTTP_VERSION_2);
}

struct http_stream_chunked_callback_data {
    struct http_stream_binding *stream_cb_data;
    struct aws_byte_buf chunk_data;
    struct aws_input_stream *chunk_stream;
    jobject completion_callback;
};

static void s_cleanup_chunked_callback_data(
    JNIEnv *env,
    struct http_stream_chunked_callback_data *chunked_callback_data) {
    aws_input_stream_destroy(chunked_callback_data->chunk_stream);
    aws_byte_buf_clean_up(&chunked_callback_data->chunk_data);
    (*env)->DeleteGlobalRef(env, chunked_callback_data->completion_callback);
    aws_mem_release(aws_jni_get_allocator(), chunked_callback_data);
}

static void s_write_chunk_complete(struct aws_http_stream *stream, int error_code, void *user_data) {
    (void)stream;

    struct http_stream_chunked_callback_data *chunked_callback_data = user_data;

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

    (*env)->CallVoidMethod(
        env,
        chunked_callback_data->completion_callback,
        http_stream_write_chunk_completion_properties.callback,
        error_code);
    aws_jni_check_and_clear_exception(env);

    JavaVM *jvm = chunked_callback_data->stream_cb_data->jvm;
    s_cleanup_chunked_callback_data(env, chunked_callback_data);
    aws_jni_release_thread_env(jvm, env);
    /********** JNI ENV RELEASE **********/
}

JNIEXPORT jint JNICALL Java_software_amazon_awssdk_crt_http_HttpStream_httpStreamWriteChunk(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_cb_data,
    jbyteArray chunk_data,
    jboolean is_final_chunk,
    jobject completion_callback) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *cb_data = (struct http_stream_binding *)jni_cb_data;
    struct aws_http_stream *stream = cb_data->native_stream;

    struct http_stream_chunked_callback_data *chunked_callback_data =
        aws_mem_calloc(aws_jni_get_allocator(), 1, sizeof(struct http_stream_chunked_callback_data));

    chunked_callback_data->stream_cb_data = cb_data;
    chunked_callback_data->completion_callback = (*env)->NewGlobalRef(env, completion_callback);

    struct aws_byte_cursor chunk_cur = aws_jni_byte_cursor_from_jbyteArray_acquire(env, chunk_data);
    aws_byte_buf_init_copy_from_cursor(&chunked_callback_data->chunk_data, aws_jni_get_allocator(), chunk_cur);
    aws_jni_byte_cursor_from_jbyteArray_release(env, chunk_data, chunk_cur);

    struct aws_http1_chunk_options chunk_options = {
        .chunk_data_size = chunked_callback_data->chunk_data.len,
        .user_data = chunked_callback_data,
        .on_complete = s_write_chunk_complete,
    };

    chunk_cur = aws_byte_cursor_from_buf(&chunked_callback_data->chunk_data);
    chunked_callback_data->chunk_stream = aws_input_stream_new_from_cursor(aws_jni_get_allocator(), &chunk_cur);
    chunk_options.chunk_data = chunked_callback_data->chunk_stream;

    if (aws_http1_stream_write_chunk(stream, &chunk_options)) {
        s_cleanup_chunked_callback_data(env, chunked_callback_data);
        return AWS_OP_ERR;
    }

    if (is_final_chunk) {
        struct aws_http1_chunk_options final_chunk_options = {
            .chunk_data_size = 0,
        };

        if (aws_http1_stream_write_chunk(stream, &final_chunk_options)) {
            return AWS_OP_ERR;
        }
    }

    return AWS_OP_SUCCESS;
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_HttpStreamBase_httpStreamBaseActivate(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_stream_binding,
    jobject j_http_stream_base) {
    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *binding = (struct http_stream_binding *)jni_stream_binding;
    struct aws_http_stream *stream = binding->native_stream;

    if (stream == NULL) {
        aws_jni_throw_runtime_exception(env, "HttpStream is null.");
        return;
    }

    AWS_LOGF_TRACE(AWS_LS_HTTP_STREAM, "Activating Stream. stream: %p", (void *)stream);

    /* global ref this because now the callbacks will be firing, and they will release their reference when the
     * stream callback sequence completes. */
    binding->java_http_stream_base = (*env)->NewGlobalRef(env, j_http_stream_base);
    if (aws_http_stream_activate(stream)) {
        (*env)->DeleteGlobalRef(env, binding->java_http_stream_base);
        aws_jni_throw_runtime_exception(
            env, "HttpStream activate failed with error %s\n", aws_error_str(aws_last_error()));
    }
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_HttpStreamBase_httpStreamBaseRelease(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_binding) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *binding = (struct http_stream_binding *)jni_binding;
    struct aws_http_stream *stream = binding->native_stream;

    if (stream == NULL) {
        aws_jni_throw_runtime_exception(env, "HttpStream is null.");
        return;
    }
    AWS_LOGF_TRACE(AWS_LS_HTTP_STREAM, "Releasing Stream. stream: %p", (void *)stream);
    aws_http_stream_release(stream);

    aws_http_stream_binding_release(env, binding);
}

JNIEXPORT jint JNICALL Java_software_amazon_awssdk_crt_http_HttpStreamBase_httpStreamBaseGetResponseStatusCode(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_binding) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *binding = (struct http_stream_binding *)jni_binding;
    struct aws_http_stream *stream = binding->native_stream;

    if (stream == NULL) {
        aws_jni_throw_runtime_exception(env, "HttpStream is null.");
        return -1;
    }

    int status = -1;
    int err_code = aws_http_stream_get_incoming_response_status(stream, &status);

    if (err_code != AWS_OP_SUCCESS) {
        aws_jni_throw_runtime_exception(env, "Error Getting Response Status Code from HttpStream.");
        return -1;
    }

    return (jint)status;
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_HttpStreamBase_httpStreamBaseIncrementWindow(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_binding,
    jint window_update) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *binding = (struct http_stream_binding *)jni_binding;
    struct aws_http_stream *stream = binding->native_stream;

    if (stream == NULL) {
        aws_jni_throw_runtime_exception(env, "HttpStream is null.");
        return;
    }

    if (window_update < 0) {
        aws_jni_throw_runtime_exception(env, "Window Update is < 0");
        return;
    }

    AWS_LOGF_TRACE(
        AWS_LS_HTTP_STREAM, "Updating Stream Window. stream: %p, update: %d", (void *)stream, (int)window_update);
    aws_http_stream_update_window(stream, window_update);
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_Http2Stream_http2StreamResetStream(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_cb_data,
    jint error_code) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct http_stream_binding *binding = (struct http_stream_binding *)jni_cb_data;
    struct aws_http_stream *stream = binding->native_stream;

    if (stream == NULL) {
        aws_jni_throw_null_pointer_exception(env, "Http2Stream is null.");
        return;
    }

    AWS_LOGF_TRACE(AWS_LS_HTTP_STREAM, "Resetting Stream. stream: %p", (void *)stream);
    if (aws_http2_stream_reset(stream, error_code)) {
        aws_jni_throw_runtime_exception(
            env, "reset stream failed with error %d(%s).", aws_last_error(), aws_error_debug_str(aws_last_error()));
        return;
    }
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_HttpClientConnection_httpClientConnectionShutdown(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_runtime_exception(env, "HttpClientConnection.Shutdown: Invalid aws_http_connection");
        return;
    }

    aws_http_connection_close(native_conn);
}

JNIEXPORT jboolean JNICALL Java_software_amazon_awssdk_crt_http_HttpClientConnection_httpClientConnectionIsOpen(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_runtime_exception(env, "HttpClientConnection.isOpen: Invalid aws_http_connection");
        return false;
    }

    return aws_http_connection_is_open(native_conn);
}

JNIEXPORT jshort JNICALL Java_software_amazon_awssdk_crt_http_HttpClientConnection_httpClientConnectionGetVersion(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_runtime_exception(env, "HttpClientConnection.getVersion: Invalid aws_http_connection");
        return 0;
    }
    return (jshort)aws_http_connection_get_version(native_conn);
}

JNIEXPORT jboolean JNICALL Java_software_amazon_awssdk_crt_http_HttpClientConnection_isErrorRetryable(
    JNIEnv *env,
    jclass jni_class,
    jint error_code) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    switch (error_code) {
        case AWS_ERROR_HTTP_HEADER_NOT_FOUND:
        case AWS_ERROR_HTTP_INVALID_HEADER_FIELD:
        case AWS_ERROR_HTTP_INVALID_HEADER_NAME:
        case AWS_ERROR_HTTP_INVALID_HEADER_VALUE:
        case AWS_ERROR_HTTP_INVALID_METHOD:
        case AWS_ERROR_HTTP_INVALID_PATH:
        case AWS_ERROR_HTTP_INVALID_STATUS_CODE:
        case AWS_ERROR_HTTP_MISSING_BODY_STREAM:
        case AWS_ERROR_HTTP_INVALID_BODY_STREAM:
        case AWS_ERROR_HTTP_OUTGOING_STREAM_LENGTH_INCORRECT:
        case AWS_ERROR_HTTP_CALLBACK_FAILURE:
        case AWS_ERROR_HTTP_STREAM_MANAGER_SHUTTING_DOWN:
        case AWS_HTTP2_ERR_CANCEL:
            return false;
        default:
            return true;
    }
}

struct aws_http2_callback_data {
    JavaVM *jvm;
    jobject async_callback;
};

static void s_cleanup_http2_callback_data(struct aws_http2_callback_data *callback_data, JNIEnv *env) {
    if (callback_data == NULL || env == NULL) {
        return;
    }

    if (callback_data->async_callback) {
        (*env)->DeleteGlobalRef(env, callback_data->async_callback);
    }

    aws_mem_release(aws_jni_get_allocator(), callback_data);
}

static struct aws_http2_callback_data *s_new_http2_callback_data(
    JNIEnv *env,
    struct aws_allocator *allocator,
    jobject async_callback) {
    struct aws_http2_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_http2_callback_data));

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);
    callback_data->async_callback = async_callback ? (*env)->NewGlobalRef(env, async_callback) : NULL;
    AWS_FATAL_ASSERT(callback_data->async_callback != NULL);

    return callback_data;
}

static void s_on_settings_completed(struct aws_http_connection *http2_connection, int error_code, void *user_data) {
    (void)http2_connection;
    struct aws_http2_callback_data *callback_data = user_data;

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

    if (error_code) {
        jobject crt_exception = aws_jni_new_crt_exception_from_error_code(env, error_code);
        (*env)->CallVoidMethod(env, callback_data->async_callback, async_callback_properties.on_failure, crt_exception);
        (*env)->DeleteLocalRef(env, crt_exception);
    } else {
        (*env)->CallVoidMethod(env, callback_data->async_callback, async_callback_properties.on_success);
    }
    AWS_FATAL_ASSERT(!aws_jni_check_and_clear_exception(env));
    s_cleanup_http2_callback_data(callback_data, env);

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

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_Http2ClientConnection_http2ClientConnectionUpdateSettings(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection,
    jobject java_async_callback,
    jlongArray java_marshalled_settings) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_null_pointer_exception(
            env, "Http2ClientConnection.http2ClientConnectionUpdateSettings: Invalid aws_http_connection");
        return;
    }
    if (!java_async_callback) {
        aws_jni_throw_illegal_argument_exception(
            env, "Http2ClientConnection.http2ClientConnectionUpdateSettings: Invalid async callback");
        return;
    }
    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_http2_callback_data *callback_data = s_new_http2_callback_data(env, allocator, java_async_callback);

    /* We marshalled each setting to two long integers, the long list will be number of settings times two */
    const size_t len = (*env)->GetArrayLength(env, java_marshalled_settings);
    AWS_ASSERT(len % 2 == 0);
    const size_t settings_len = len / 2;
    struct aws_http2_setting *settings =
        settings_len ? aws_mem_calloc(allocator, settings_len, sizeof(struct aws_http2_setting)) : NULL;
    int success = false;
    jlong *marshalled_settings = (*env)->GetLongArrayElements(env, java_marshalled_settings, NULL);
    for (size_t i = 0; i < settings_len; i++) {
        jlong id = marshalled_settings[i * 2];
        settings[i].id = id;
        jlong value = marshalled_settings[i * 2 + 1];
        settings[i].value = (uint32_t)value;
    }

    if (aws_http2_connection_change_settings(
            native_conn, settings, settings_len, s_on_settings_completed, callback_data)) {
        aws_jni_throw_runtime_exception(
            env, "Http2ClientConnection.http2ClientConnectionUpdateSettings: failed to change settings");
        goto done;
    }
    success = true;
done:
    aws_mem_release(allocator, settings);
    (*env)->ReleaseLongArrayElements(env, java_marshalled_settings, (jlong *)marshalled_settings, JNI_ABORT);
    if (!success) {
        s_cleanup_http2_callback_data(callback_data, env);
    }
    return;
}

static void s_on_ping_completed(
    struct aws_http_connection *http2_connection,
    uint64_t round_trip_time_ns,
    int error_code,
    void *user_data) {
    (void)http2_connection;
    struct aws_http2_callback_data *callback_data = user_data;

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

    if (error_code) {
        jobject crt_exception = aws_jni_new_crt_exception_from_error_code(env, error_code);
        (*env)->CallVoidMethod(env, callback_data->async_callback, async_callback_properties.on_failure, crt_exception);
        (*env)->DeleteLocalRef(env, crt_exception);
    } else {
        jobject java_round_trip_time_ns = (*env)->NewObject(
            env, boxed_long_properties.long_class, boxed_long_properties.constructor, (jlong)round_trip_time_ns);
        (*env)->CallVoidMethod(
            env,
            callback_data->async_callback,
            async_callback_properties.on_success_with_object,
            java_round_trip_time_ns);
        (*env)->DeleteLocalRef(env, java_round_trip_time_ns);
    }
    AWS_FATAL_ASSERT(!aws_jni_check_and_clear_exception(env));
    s_cleanup_http2_callback_data(callback_data, env);

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

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_Http2ClientConnection_http2ClientConnectionSendPing(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection,
    jobject java_async_callback,
    jbyteArray ping_data) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_null_pointer_exception(
            env, "Http2ClientConnection.http2ClientConnectionSendPing: Invalid aws_http_connection");
        return;
    }
    if (!java_async_callback) {
        aws_jni_throw_illegal_argument_exception(
            env, "Http2ClientConnection.http2ClientConnectionSendPing: Invalid async callback");
        return;
    }
    bool success = false;
    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_byte_cursor *ping_cur_pointer = NULL;
    struct aws_byte_cursor ping_cur;
    AWS_ZERO_STRUCT(ping_cur);
    struct aws_http2_callback_data *callback_data = s_new_http2_callback_data(env, allocator, java_async_callback);

    if (ping_data) {
        ping_cur = aws_jni_byte_cursor_from_jbyteArray_acquire(env, ping_data);
        ping_cur_pointer = &ping_cur;
    }
    if (aws_http2_connection_ping(native_conn, ping_cur_pointer, s_on_ping_completed, callback_data)) {
        aws_jni_throw_runtime_exception(env, "Failed to send ping");
        goto done;
    }
    success = true;
done:
    if (ping_cur_pointer) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, ping_data, ping_cur);
    }
    if (!success) {
        s_cleanup_http2_callback_data(callback_data, env);
    }
    return;
}

JNIEXPORT void JNICALL Java_software_amazon_awssdk_crt_http_Http2ClientConnection_http2ClientConnectionSendGoAway(
    JNIEnv *env,
    jclass jni_class,
    jlong jni_connection,
    jlong h2_error_code,
    jboolean allow_more_streams,
    jbyteArray debug_data) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;
    struct aws_byte_cursor *debug_cur_pointer = NULL;
    struct aws_byte_cursor debug_cur;
    AWS_ZERO_STRUCT(debug_cur);

    if (!native_conn) {
        aws_jni_throw_runtime_exception(
            env, "Http2ClientConnection.http2ClientConnectionSendGoAway: Invalid aws_http_connection");
        return;
    }
    if (debug_data) {
        debug_cur = aws_jni_byte_cursor_from_jbyteArray_acquire(env, debug_data);
        debug_cur_pointer = &debug_cur;
    }
    aws_http2_connection_send_goaway(native_conn, (uint32_t)h2_error_code, allow_more_streams, debug_cur_pointer);
    if (debug_cur_pointer) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, debug_data, debug_cur);
    }
    return;
}

JNIEXPORT void JNICALL
    Java_software_amazon_awssdk_crt_http_Http2ClientConnection_http2ClientConnectionUpdateConnectionWindow(
        JNIEnv *env,
        jclass jni_class,
        jlong jni_connection,
        jlong increment_size) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_http_connection_binding *connection_binding = (struct aws_http_connection_binding *)jni_connection;
    struct aws_http_connection *native_conn = connection_binding->connection;

    if (!native_conn) {
        aws_jni_throw_runtime_exception(
            env, "Http2ClientConnection.http2ClientConnectionUpdateConnectionWindow: Invalid aws_http_connection");
        return;
    }
    /* We did range check in Java already. */
    aws_http2_connection_update_window(native_conn, (uint32_t)increment_size);
    return;
}

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