// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "erl_nif.h"
#include "fdb.h"

#include "atoms.h"
#include "resources.h"
#include "util.h"

// FoundationDB can only be intiailized once
// in a given OS process. By creating a random
// atom we can flag whether erlfdb was previously
// initialized in a given Erlang VM.
const char *SENTINEL = "faaffadf46e64b87bdd0fedbddd97b1a";

typedef enum _ErlFDBLibState {
    ErlFDB_STATE_ERROR = 0,
    ErlFDB_LOADED,
    ErlFDB_API_SELECTED,
    ErlFDB_NETWORK_STARTING,
    ErlFDB_CONNECTED,
} ErlFDBLibState;

typedef struct _ErlFDBSt {
    ErlFDBLibState lib_state;
    ErlNifTid network_tid;
    ErlNifMutex *lock;
    ErlNifCond *cond;
} ErlFDBSt;

static void *erlfdb_network_thread(void *arg) {
    ErlFDBSt *st = (ErlFDBSt *)arg;
    fdb_error_t *err = enif_alloc(sizeof(fdb_error_t));

    enif_mutex_lock(st->lock);

    st->lib_state = ErlFDB_NETWORK_STARTING;

    enif_cond_signal(st->cond);
    enif_mutex_unlock(st->lock);

    *err = fdb_run_network();

    return (void *)err;
}

static void erlfdb_future_cb(FDBFuture *fdb_future, void *data) {
    ErlFDBFuture *future = (ErlFDBFuture *)data;
    ErlNifEnv *caller;
    ERL_NIF_TERM msg;

    // FoundationDB callbacks can fire from the thread
    // that created them. Check if we were actually
    // submitted to the network thread or not so that
    // we pass the correct environment to enif_send
    if (enif_thread_type() == ERL_NIF_THR_UNDEFINED) {
        caller = NULL;
    } else {
        caller = future->pid_env;
    }

    enif_mutex_lock(future->lock);

    if (!future->cancelled) {
        msg = T2(future->msg_env, future->msg_ref, ATOM_ready);
        enif_send(caller, &(future->pid), future->msg_env, msg);
    }

    enif_mutex_unlock(future->lock);

    // We're now done with this future which means we need
    // to release our handle to it. See erlfdb_create_future
    // for more on why this happens here.

    enif_release_resource(future);

    return;
}

static ERL_NIF_TERM erlfdb_create_future(ErlNifEnv *env, FDBFuture *future,
                                         ErlFDBFutureType ftype) {
    ErlFDBFuture *f;
    ERL_NIF_TERM ref = enif_make_ref(env);
    ERL_NIF_TERM ret;
    fdb_error_t err;

    f = enif_alloc_resource(ErlFDBFutureRes, sizeof(ErlFDBFuture));
    f->future = future;
    f->ftype = ftype;
    enif_self(env, &(f->pid));
    f->pid_env = env;
    f->msg_env = enif_alloc_env();
    f->msg_ref = enif_make_copy(f->msg_env, ref);
    f->lock = enif_mutex_create("fdb:future_lock");
    f->cancelled = false;

    // This resource reference counting dance is a bit
    // awkward as erlfdb_future_cb can be called both
    // synchronously and asynchronously.
    //
    // At this point the reference count is 1

    enif_keep_resource(f);

    // The reference count is now 2

    err = fdb_future_set_callback(f->future, erlfdb_future_cb, (void *)f);

    if (err != 0) {
        // If we failed to set the future callback function
        // then we assume the callback was not invoked and
        // we have to release twice
        enif_release_resource(f);
        enif_release_resource(f);
        return erlfdb_erlang_error(env, err);
    }

    // At this point our callback may have been called
    // which means we could have a reference count
    // of 1 or 2

    ret = enif_make_resource(env, f);

    // enif_make_resource increases the ref count
    // so now our reference count is either 2 or 3

    enif_release_resource(f);

    // Finally our reference count is now either
    // 1 or 2 depending on whether the callback
    // has already fired. If the ref count is
    // 2 then Erlang has a reference and the network
    // thread has a reference. If its 1 then only
    // Erlang has a reference.

    return T3(env, ATOM_erlfdb_future, ref, ret);
}

static inline ERL_NIF_TERM erlfdb_future_get_void(ErlNifEnv *env,
                                                  ErlFDBFuture *f) {
    fdb_error_t err;

    err = fdb_future_get_error(f->future);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    return ATOM_ok;
}

static inline ERL_NIF_TERM erlfdb_future_get_int64(ErlNifEnv *env,
                                                   ErlFDBFuture *f) {
    int64_t fdb_res;
    ErlNifSInt64 nif_res;
    fdb_error_t err;

    err = fdb_future_get_int64(f->future, &fdb_res);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    nif_res = fdb_res;

    return enif_make_int64(env, nif_res);
}

static inline ERL_NIF_TERM erlfdb_future_get_key(ErlNifEnv *env,
                                                 ErlFDBFuture *f) {
    const uint8_t *key;
    int len;
    unsigned char *buf;
    ERL_NIF_TERM ret;
    fdb_error_t err;

    err = fdb_future_get_key(f->future, &key, &len);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    buf = enif_make_new_binary(env, len, &ret);
    memcpy(buf, key, len);

    return ret;
}

static inline ERL_NIF_TERM erlfdb_future_get_value(ErlNifEnv *env,
                                                   ErlFDBFuture *f) {
    fdb_bool_t present;
    const uint8_t *val;
    int len;
    unsigned char *buf;
    ERL_NIF_TERM ret;
    fdb_error_t err;

    err = fdb_future_get_value(f->future, &present, &val, &len);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    if (!present) {
        return ATOM_not_found;
    }

    buf = enif_make_new_binary(env, len, &ret);
    memcpy(buf, val, len);

    return ret;
}

static inline ERL_NIF_TERM erlfdb_future_get_string_array(ErlNifEnv *env,
                                                          ErlFDBFuture *f) {
    const char **strings;
    int count;
    unsigned char *buf;
    ERL_NIF_TERM bin;
    ERL_NIF_TERM ret;
    fdb_error_t err;
    int i;

    err = fdb_future_get_string_array(f->future, &strings, &count);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    ret = enif_make_list(env, 0);

    for (i = count - 1; i >= 0; i--) {
        buf = enif_make_new_binary(env, strlen(strings[i]), &bin);
        memcpy(buf, strings[i], strlen(strings[i]));
        ret = enif_make_list_cell(env, bin, ret);
    }

    return ret;
}

static inline ERL_NIF_TERM erlfdb_future_get_keyvalue_array(ErlNifEnv *env,
                                                            ErlFDBFuture *f) {
    FDBKeyValue const *kvs;
    fdb_bool_t more;
    int count;
    unsigned char *buf;
    ERL_NIF_TERM key;
    ERL_NIF_TERM val;
    ERL_NIF_TERM ret;
    fdb_error_t err;
    int i;

    err = fdb_future_get_keyvalue_array(f->future, &kvs, &count, &more);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    ret = enif_make_list(env, 0);

    for (i = count - 1; i >= 0; i--) {
        buf = enif_make_new_binary(env, kvs[i].key_length, &key);
        memcpy(buf, kvs[i].key, kvs[i].key_length);
        buf = enif_make_new_binary(env, kvs[i].value_length, &val);
        memcpy(buf, kvs[i].value, kvs[i].value_length);
        ret = enif_make_list_cell(env, T2(env, key, val), ret);
    }

    if (more) {
        return T3(env, ret, enif_make_int(env, count), ATOM_true);
    } else {
        return T3(env, ret, enif_make_int(env, count), ATOM_false);
    }
}

static inline ERL_NIF_TERM
erlfdb_future_get_mappedkeyvalue_array(ErlNifEnv *env, ErlFDBFuture *f) {
    FDBMappedKeyValue const *mappedkvs;
    FDBKeyValue *data;
    fdb_bool_t more;
    int count;
    unsigned char *buf;
    ERL_NIF_TERM pq_key;
    ERL_NIF_TERM pq_val;
    ERL_NIF_TERM sq_begin;
    ERL_NIF_TERM sq_end;
    ERL_NIF_TERM key;
    ERL_NIF_TERM val;
    ERL_NIF_TERM ret;
    ERL_NIF_TERM kvs_ret;
    fdb_error_t err;
    int i, j;

    err = fdb_future_get_mappedkeyvalue_array(f->future, &mappedkvs, &count,
                                              &more);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    ret = enif_make_list(env, 0);

    for (i = count - 1; i >= 0; i--) {
        // pq_key
        buf = enif_make_new_binary(env, mappedkvs[i].key.key_length, &pq_key);
        memcpy(buf, mappedkvs[i].key.key, mappedkvs[i].key.key_length);

        // pq_val (which is also a key)
        buf = enif_make_new_binary(env, mappedkvs[i].value.key_length, &pq_val);
        memcpy(buf, mappedkvs[i].value.key, mappedkvs[i].value.key_length);

        const FDBGetRangeReqAndResult *getRange = &mappedkvs[i].getRange;

        // sq_begin
        buf = enif_make_new_binary(env, getRange->begin.key.key_length,
                                   &sq_begin);
        memcpy(buf, getRange->begin.key.key, getRange->begin.key.key_length);

        // sq_end
        buf = enif_make_new_binary(env, getRange->end.key.key_length, &sq_end);
        memcpy(buf, getRange->end.key.key, getRange->end.key.key_length);

        data = getRange->data;

        kvs_ret = enif_make_list(env, 0);
        for (j = getRange->m_size - 1; j >= 0; j--) {

            // mapped key
            buf = enif_make_new_binary(env, data[j].key_length, &key);
            memcpy(buf, data[j].key, data[j].key_length);

            // mapped val
            buf = enif_make_new_binary(env, data[j].value_length, &val);
            memcpy(buf, data[j].value, data[j].value_length);

            kvs_ret = enif_make_list_cell(env, T2(env, key, val), kvs_ret);
        }

        // {
        //   {PrimaryQueryKey, PrimaryQueryValue},
        //   {SecondaryQueryKeyBegin, SecondaryQueryKeyEnd},
        //   [
        //      {SecondaryQueryKey, SecondaryQueryValue},
        //      ...
        //   ]
        // }
        ret = enif_make_list_cell(env,
                                  T3(env, T2(env, pq_key, pq_val),
                                     T2(env, sq_begin, sq_end), kvs_ret),
                                  ret);
    }

    if (more) {
        return T3(env, ret, enif_make_int(env, count), ATOM_true);
    } else {
        return T3(env, ret, enif_make_int(env, count), ATOM_false);
    }
}

static int erlfdb_load(ErlNifEnv *env, void **priv,
                       ERL_NIF_TERM num_schedulers) {
    ErlFDBSt *st = (ErlFDBSt *)enif_alloc(sizeof(ErlFDBSt));

    erlfdb_init_atoms(env);

    if (!erlfdb_init_resources(env)) {
        return 1;
    }

    st->lock = enif_mutex_create("fdb:st_lock");
    st->cond = enif_cond_create("fdb:st_cond");

    st->lib_state = ErlFDB_LOADED;
    *priv = st;

    return 0;
}

static void erlfdb_unload(ErlNifEnv *env, void *priv) {
    ErlFDBSt *st = (ErlFDBSt *)priv;
    ErlFDBLibState lib_state = ErlFDB_STATE_ERROR;
    fdb_error_t err;
    void *tmp;
    fdb_error_t *net_err;
    int nif_err;

    enif_mutex_lock(st->lock);
    lib_state = st->lib_state;
    enif_mutex_unlock(st->lock);

    if (lib_state == ErlFDB_CONNECTED) {
        err = fdb_stop_network();
        assert(err == 0 && "Error disconnecting fdb client");

        nif_err = enif_thread_join(st->network_tid, &tmp);
        assert(nif_err == 0 && "Error joining network thread");

        net_err = (fdb_error_t *)tmp;
        assert(*net_err == 0 && "Error running network thread");
        enif_free(tmp);
    }

    enif_mutex_destroy(st->lock);
    enif_cond_destroy(st->cond);
    enif_free(priv);

    return;
}

static ERL_NIF_TERM erlfdb_can_initialize(ErlNifEnv *env, int argc,
                                          const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ERL_NIF_TERM atom;

    if (st->lib_state != ErlFDB_LOADED) {
        return enif_make_badarg(env);
    }

    if (argc != 0) {
        return enif_make_badarg(env);
    }

    if (enif_make_existing_atom(env, SENTINEL, &atom, ERL_NIF_LATIN1)) {
        return ATOM_false;
    }

    enif_make_atom(env, SENTINEL);

    return ATOM_true;
}

static ERL_NIF_TERM erlfdb_get_max_api_version(ErlNifEnv *env, int argc,
                                               const ERL_NIF_TERM argv[]) {
    int vsn = fdb_get_max_api_version();

    if (argc != 0) {
        return enif_make_badarg(env);
    }

    return enif_make_int(env, vsn);
}

static ERL_NIF_TERM erlfdb_select_api_version(ErlNifEnv *env, int argc,
                                              const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    int vsn;
    fdb_error_t err;

    if (st->lib_state != ErlFDB_LOADED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[0], &vsn)) {
        return enif_make_badarg(env);
    }

    err = fdb_select_api_version(vsn);

    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    st->lib_state = ErlFDB_API_SELECTED;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_network_set_option(ErlNifEnv *env, int argc,
                                              const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    FDBNetworkOption option;
    ErlNifBinary value;
    fdb_error_t err;

    if (st->lib_state != ErlFDB_API_SELECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[0], local_address)) {
        option = FDB_NET_OPTION_LOCAL_ADDRESS;
    } else if (IS_ATOM(argv[0], cluster_file)) {
        option = FDB_NET_OPTION_CLUSTER_FILE;
    } else if (IS_ATOM(argv[0], trace_enable)) {
        option = FDB_NET_OPTION_TRACE_ENABLE;
    } else if (IS_ATOM(argv[0], trace_format)) {
        option = FDB_NET_OPTION_TRACE_FORMAT;
    } else if (IS_ATOM(argv[0], trace_roll_size)) {
        option = FDB_NET_OPTION_TRACE_ROLL_SIZE;
    } else if (IS_ATOM(argv[0], trace_max_logs_size)) {
        option = FDB_NET_OPTION_TRACE_MAX_LOGS_SIZE;
    } else if (IS_ATOM(argv[0], trace_log_group)) {
        option = FDB_NET_OPTION_TRACE_LOG_GROUP;
    } else if (IS_ATOM(argv[0], knob)) {
        option = FDB_NET_OPTION_KNOB;
    } else if (IS_ATOM(argv[0], tls_plugin)) {
        option = FDB_NET_OPTION_TLS_PLUGIN;
    } else if (IS_ATOM(argv[0], tls_cert_bytes)) {
        option = FDB_NET_OPTION_TLS_CERT_BYTES;
    } else if (IS_ATOM(argv[0], tls_cert_path)) {
        option = FDB_NET_OPTION_TLS_CERT_PATH;
    } else if (IS_ATOM(argv[0], tls_key_bytes)) {
        option = FDB_NET_OPTION_TLS_KEY_BYTES;
    } else if (IS_ATOM(argv[0], tls_key_path)) {
        option = FDB_NET_OPTION_TLS_KEY_PATH;
    } else if (IS_ATOM(argv[0], tls_verify_peers)) {
        option = FDB_NET_OPTION_TLS_VERIFY_PEERS;
    } else if (IS_ATOM(argv[0], client_buggify_enable)) {
        option = FDB_NET_OPTION_CLIENT_BUGGIFY_ENABLE;
    } else if (IS_ATOM(argv[0], client_buggify_disable)) {
        option = FDB_NET_OPTION_CLIENT_BUGGIFY_DISABLE;
    } else if (IS_ATOM(argv[0], client_buggify_section_activated_probability)) {
        option = FDB_NET_OPTION_CLIENT_BUGGIFY_SECTION_ACTIVATED_PROBABILITY;
    } else if (IS_ATOM(argv[0], client_buggify_section_fired_probability)) {
        option = FDB_NET_OPTION_CLIENT_BUGGIFY_SECTION_FIRED_PROBABILITY;
    } else if (IS_ATOM(argv[0], tls_ca_bytes)) {
        option = FDB_NET_OPTION_TLS_CA_BYTES;
    } else if (IS_ATOM(argv[0], tls_password)) {
        option = FDB_NET_OPTION_TLS_PASSWORD;
    } else if (IS_ATOM(argv[0], disable_multi_version_client_api)) {
        option = FDB_NET_OPTION_DISABLE_MULTI_VERSION_CLIENT_API;
    } else if (IS_ATOM(argv[0], callbacks_on_external_threads)) {
        option = FDB_NET_OPTION_CALLBACKS_ON_EXTERNAL_THREADS;
    } else if (IS_ATOM(argv[0], external_client_library)) {
        option = FDB_NET_OPTION_EXTERNAL_CLIENT_LIBRARY;
    } else if (IS_ATOM(argv[0], external_client_directory)) {
        option = FDB_NET_OPTION_EXTERNAL_CLIENT_DIRECTORY;
    } else if (IS_ATOM(argv[0], disable_local_client)) {
        option = FDB_NET_OPTION_DISABLE_LOCAL_CLIENT;
    } else if (IS_ATOM(argv[0], disable_client_statistics_logging)) {
        option = FDB_NET_OPTION_DISABLE_CLIENT_STATISTICS_LOGGING;
    } else if (IS_ATOM(argv[0], enable_slow_task_profiling)) {
        option = FDB_NET_OPTION_ENABLE_SLOW_TASK_PROFILING;
    }
#if FDB_API_VERSION >= 630
    else if (IS_ATOM(argv[0], enable_run_loop_profiling)) {
        option = FDB_NET_OPTION_ENABLE_RUN_LOOP_PROFILING;
    }
#endif
    else {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[1], &value)) {
        return enif_make_badarg(env);
    }

    err = fdb_network_set_option(option, (uint8_t *)value.data, value.size);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_setup_network(ErlNifEnv *env, int argc,
                                         const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    fdb_error_t err;

    if (st->lib_state != ErlFDB_API_SELECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 0) {
        return enif_make_badarg(env);
    }

    err = fdb_setup_network();
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    if (enif_thread_create("fdb:network_thread", &(st->network_tid),
                           erlfdb_network_thread, (void *)st, NULL) != 0) {
        return enif_make_badarg(env);
    }

    enif_mutex_lock(st->lock);

    while (st->lib_state != ErlFDB_NETWORK_STARTING) {
        enif_cond_wait(st->cond, st->lock);
    }

    enif_mutex_unlock(st->lock);

    st->lib_state = ErlFDB_CONNECTED;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_future_cancel(ErlNifEnv *env, int argc,
                                         const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBFutureRes, &res)) {
        return enif_make_badarg(env);
    }
    future = (ErlFDBFuture *)res;

    enif_mutex_lock(future->lock);

    future->cancelled = true;
    fdb_future_cancel(future->future);

    enif_mutex_unlock(future->lock);

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_future_silence(ErlNifEnv *env, int argc,
                                          const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBFutureRes, &res)) {
        return enif_make_badarg(env);
    }
    future = (ErlFDBFuture *)res;

    enif_mutex_lock(future->lock);

    future->cancelled = true;

    enif_mutex_unlock(future->lock);

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_future_is_ready(ErlNifEnv *env, int argc,
                                           const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBFutureRes, &res)) {
        return enif_make_badarg(env);
    }
    future = (ErlFDBFuture *)res;

    if (fdb_future_is_ready(future->future)) {
        return ATOM_true;
    } else {
        return ATOM_false;
    }
}

static ERL_NIF_TERM erlfdb_future_get_error(ErlNifEnv *env, int argc,
                                            const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBFuture *future;
    fdb_error_t err;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBFutureRes, &res)) {
        return enif_make_badarg(env);
    }
    future = (ErlFDBFuture *)res;

    err = fdb_future_get_error(future->future);

    return T2(env, ATOM_erlfdb_error, enif_make_int(env, err));
}

static ERL_NIF_TERM erlfdb_future_get(ErlNifEnv *env, int argc,
                                      const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBFuture *f;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBFutureRes, &res)) {
        return enif_make_badarg(env);
    }
    f = (ErlFDBFuture *)res;

    if (f->ftype == ErlFDB_FT_VOID) {
        return erlfdb_future_get_void(env, f);
    } else if (f->ftype == ErlFDB_FT_INT64) {
        return erlfdb_future_get_int64(env, f);
    } else if (f->ftype == ErlFDB_FT_KEY) {
        return erlfdb_future_get_key(env, f);
    } else if (f->ftype == ErlFDB_FT_VALUE) {
        return erlfdb_future_get_value(env, f);
    } else if (f->ftype == ErlFDB_FT_STRING_ARRAY) {
        return erlfdb_future_get_string_array(env, f);
    } else if (f->ftype == ErlFDB_FT_KEYVALUE_ARRAY) {
        return erlfdb_future_get_keyvalue_array(env, f);
    } else if (f->ftype == ErlFDB_FT_MAPPEDKEYVALUE_ARRAY) {
        return erlfdb_future_get_mappedkeyvalue_array(env, f);
    }

    return enif_raise_exception(env, ATOM_invalid_future_type);
}

static ERL_NIF_TERM erlfdb_create_database(ErlNifEnv *env, int argc,
                                           const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlNifBinary bin;
    FDBDatabase *database;
    fdb_error_t err;
    ErlFDBDatabase *d;
    ERL_NIF_TERM ret;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[0], &bin)) {
        return enif_make_badarg(env);
    }

    if (bin.size < 1 || bin.data[bin.size - 1] != 0) {
        return enif_make_badarg(env);
    }

    err = fdb_create_database((const char *)bin.data, &database);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    d = enif_alloc_resource(ErlFDBDatabaseRes, sizeof(ErlFDBDatabase));
    d->database = database;

    ret = enif_make_resource(env, d);
    enif_release_resource(d);

    return T2(env, ATOM_erlfdb_database, ret);
}

static ERL_NIF_TERM erlfdb_database_open_tenant(ErlNifEnv *env, int argc,
                                                const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlNifBinary bin;
    ErlFDBDatabase *d;
    ErlFDBTenant *ten;
    FDBTenant *tenant;
    fdb_error_t err;
    ERL_NIF_TERM ret;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBDatabaseRes, &res)) {
        return enif_make_badarg(env);
    }
    d = (ErlFDBDatabase *)res;

    if (!enif_inspect_binary(env, argv[1], &bin)) {
        return enif_make_badarg(env);
    }

    if (bin.size < 1 || bin.data[bin.size - 1] != 0) {
        return enif_make_badarg(env);
    }

    err = fdb_database_open_tenant(d->database, (const unsigned char *)bin.data,
                                   bin.size - 1, &tenant);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    ten = enif_alloc_resource(ErlFDBTenantRes, sizeof(ErlFDBTenant));
    ten->tenant = tenant;

    ret = enif_make_resource(env, ten);
    enif_release_resource(ten);

    return T2(env, ATOM_erlfdb_tenant, ret);
}

static ERL_NIF_TERM erlfdb_database_set_option(ErlNifEnv *env, int argc,
                                               const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBDatabase *d;
    FDBDatabaseOption option;
    ErlNifBinary value;
    fdb_error_t err;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBDatabaseRes, &res)) {
        return enif_make_badarg(env);
    }
    d = (ErlFDBDatabase *)res;

    if (IS_ATOM(argv[1], location_cache_size)) {
        option = FDB_DB_OPTION_LOCATION_CACHE_SIZE;
    } else if (IS_ATOM(argv[1], max_watches)) {
        option = FDB_DB_OPTION_MAX_WATCHES;
    } else if (IS_ATOM(argv[1], machine_id)) {
        option = FDB_DB_OPTION_MACHINE_ID;
    } else if (IS_ATOM(argv[1], datacenter_id)) {
        option = FDB_DB_OPTION_DATACENTER_ID;
    } else if (IS_ATOM(argv[1], read_your_writes_enable)) {
        option = FDB_DB_OPTION_SNAPSHOT_RYW_ENABLE;
    } else if (IS_ATOM(argv[1], read_your_writes_disable)) {
        option = FDB_DB_OPTION_SNAPSHOT_RYW_DISABLE;
    } else if (IS_ATOM(argv[1], transaction_logging_max_field_length)) {
        option = FDB_DB_OPTION_TRANSACTION_LOGGING_MAX_FIELD_LENGTH;
    } else if (IS_ATOM(argv[1], timeout)) {
        option = FDB_DB_OPTION_TRANSACTION_TIMEOUT;
    } else if (IS_ATOM(argv[1], retry_limit)) {
        option = FDB_DB_OPTION_TRANSACTION_RETRY_LIMIT;
    } else if (IS_ATOM(argv[1], max_retry_delay)) {
        option = FDB_DB_OPTION_TRANSACTION_MAX_RETRY_DELAY;
    } else if (IS_ATOM(argv[1], size_limit)) {
        option = FDB_DB_OPTION_TRANSACTION_SIZE_LIMIT;
    } else if (IS_ATOM(argv[1], causal_read_risky)) {
        option = FDB_DB_OPTION_TRANSACTION_CAUSAL_READ_RISKY;
    } else if (IS_ATOM(argv[1], include_port_in_address)) {
        option = FDB_DB_OPTION_TRANSACTION_INCLUDE_PORT_IN_ADDRESS;
    } else {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &value)) {
        return enif_make_badarg(env);
    }

    err = fdb_database_set_option(d->database, option, (uint8_t *)value.data,
                                  value.size);

    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    return ATOM_ok;
}

static ERL_NIF_TERM
erlfdb_database_create_transaction(ErlNifEnv *env, int argc,
                                   const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBDatabase *d;
    ErlFDBTransaction *t;
    FDBTransaction *transaction;
    ErlNifPid pid;
    ERL_NIF_TERM ret;
    void *res;
    fdb_error_t err;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBDatabaseRes, &res)) {
        return enif_make_badarg(env);
    }
    d = (ErlFDBDatabase *)res;

    err = fdb_database_create_transaction(d->database, &transaction);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    t = enif_alloc_resource(ErlFDBTransactionRes, sizeof(ErlFDBTransaction));
    t->transaction = transaction;

    enif_self(env, &pid);
    t->owner = enif_make_pid(env, &pid);

    t->txid = 0;
    t->read_only = true;
    t->writes_allowed = true;
    t->has_watches = false;

    ret = enif_make_resource(env, t);
    enif_release_resource(t);
    return T2(env, ATOM_erlfdb_transaction, ret);
}

static ERL_NIF_TERM
erlfdb_tenant_create_transaction(ErlNifEnv *env, int argc,
                                 const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTenant *ten;
    ErlFDBTransaction *t;
    FDBTransaction *transaction;
    ErlNifPid pid;
    ERL_NIF_TERM ret;
    void *res;
    fdb_error_t err;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTenantRes, &res)) {
        return enif_make_badarg(env);
    }
    ten = (ErlFDBTenant *)res;

    err = fdb_tenant_create_transaction(ten->tenant, &transaction);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    t = enif_alloc_resource(ErlFDBTransactionRes, sizeof(ErlFDBTransaction));
    t->transaction = transaction;

    enif_self(env, &pid);
    t->owner = enif_make_pid(env, &pid);

    t->txid = 0;
    t->read_only = true;
    t->writes_allowed = true;
    t->has_watches = false;

    ret = enif_make_resource(env, t);
    enif_release_resource(t);
    return T2(env, ATOM_erlfdb_transaction, ret);
}

static ERL_NIF_TERM erlfdb_transaction_set_option(ErlNifEnv *env, int argc,
                                                  const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    FDBTransactionOption option;
    ErlNifBinary value;
    fdb_error_t err;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[1], allow_writes)) {
        t->writes_allowed = true;
        return ATOM_ok;
    } else if (IS_ATOM(argv[1], disallow_writes)) {
        if (!t->read_only) {
            return enif_make_badarg(env);
        }
        t->writes_allowed = false;
        return ATOM_ok;
    }

    if (IS_ATOM(argv[1], causal_write_risky)) {
        option = FDB_TR_OPTION_CAUSAL_WRITE_RISKY;
    } else if (IS_ATOM(argv[1], causal_read_risky)) {
        option = FDB_TR_OPTION_CAUSAL_READ_RISKY;
    } else if (IS_ATOM(argv[1], causal_read_disable)) {
        option = FDB_TR_OPTION_CAUSAL_READ_DISABLE;
    } else if (IS_ATOM(argv[1], include_port_in_address)) {
        option = FDB_TR_OPTION_INCLUDE_PORT_IN_ADDRESS;
    } else if (IS_ATOM(argv[1], next_write_no_write_conflict_range)) {
        option = FDB_TR_OPTION_NEXT_WRITE_NO_WRITE_CONFLICT_RANGE;
    } else if (IS_ATOM(argv[1], read_your_writes_disable)) {
        option = FDB_TR_OPTION_READ_YOUR_WRITES_DISABLE;
    } else if (IS_ATOM(argv[1], read_ahead_disable)) {
        option = FDB_TR_OPTION_READ_AHEAD_DISABLE;
    } else if (IS_ATOM(argv[1], durability_datacenter)) {
        option = FDB_TR_OPTION_DURABILITY_DATACENTER;
    } else if (IS_ATOM(argv[1], durability_risky)) {
        option = FDB_TR_OPTION_DURABILITY_RISKY;
    } else if (IS_ATOM(argv[1], durability_dev_null_is_web_scale)) {
        option = FDB_TR_OPTION_DURABILITY_DEV_NULL_IS_WEB_SCALE;
    } else if (IS_ATOM(argv[1], priority_system_immediate)) {
        option = FDB_TR_OPTION_PRIORITY_SYSTEM_IMMEDIATE;
    } else if (IS_ATOM(argv[1], priority_batch)) {
        option = FDB_TR_OPTION_PRIORITY_BATCH;
    } else if (IS_ATOM(argv[1], initialize_new_database)) {
        option = FDB_TR_OPTION_INITIALIZE_NEW_DATABASE;
    } else if (IS_ATOM(argv[1], access_system_keys)) {
        option = FDB_TR_OPTION_ACCESS_SYSTEM_KEYS;
    } else if (IS_ATOM(argv[1], read_system_keys)) {
        option = FDB_TR_OPTION_READ_SYSTEM_KEYS;
    } else if (IS_ATOM(argv[1], debug_retry_logging)) {
        option = FDB_TR_OPTION_DEBUG_RETRY_LOGGING;
    } else if (IS_ATOM(argv[1], transaction_logging_enable)) {
        option = FDB_TR_OPTION_TRANSACTION_LOGGING_ENABLE;
    } else if (IS_ATOM(argv[1], debug_transaction_identifier)) {
        option = FDB_TR_OPTION_DEBUG_TRANSACTION_IDENTIFIER;
    } else if (IS_ATOM(argv[1], log_transaction)) {
        option = FDB_TR_OPTION_LOG_TRANSACTION;
    } else if (IS_ATOM(argv[1], transaction_logging_max_field_length)) {
        option = FDB_TR_OPTION_TRANSACTION_LOGGING_MAX_FIELD_LENGTH;
    } else if (IS_ATOM(argv[1], timeout)) {
        option = FDB_TR_OPTION_TIMEOUT;
    } else if (IS_ATOM(argv[1], retry_limit)) {
        option = FDB_TR_OPTION_RETRY_LIMIT;
    } else if (IS_ATOM(argv[1], max_retry_delay)) {
        option = FDB_TR_OPTION_MAX_RETRY_DELAY;
    } else if (IS_ATOM(argv[1], snapshot_ryw_enable)) {
        option = FDB_TR_OPTION_SNAPSHOT_RYW_ENABLE;
    } else if (IS_ATOM(argv[1], snapshot_ryw_disable)) {
        option = FDB_TR_OPTION_SNAPSHOT_RYW_DISABLE;
    } else if (IS_ATOM(argv[1], lock_aware)) {
        option = FDB_TR_OPTION_LOCK_AWARE;
    } else if (IS_ATOM(argv[1], used_during_commit_protection_disable)) {
        option = FDB_TR_OPTION_USED_DURING_COMMIT_PROTECTION_DISABLE;
    } else if (IS_ATOM(argv[1], read_lock_aware)) {
        option = FDB_TR_OPTION_READ_LOCK_AWARE;
    } else if (IS_ATOM(argv[1], size_limit)) {
        option = FDB_TR_OPTION_SIZE_LIMIT;
    } else if (IS_ATOM(argv[1], use_provisional_proxies)) {
        option = FDB_TR_OPTION_USE_PROVISIONAL_PROXIES;
#if FDB_API_VERSION > 620
    } else if (IS_ATOM(argv[1], report_conflicting_keys)) {
        option = FDB_TR_OPTION_REPORT_CONFLICTING_KEYS;
#endif
#if FDB_API_VERSION > 630
    } else if (IS_ATOM(argv[1], special_key_space_enable_writes)) {
        option = FDB_TR_OPTION_SPECIAL_KEY_SPACE_ENABLE_WRITES;
#endif
    } else {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &value)) {
        return enif_make_badarg(env);
    }

    err = fdb_transaction_set_option(t->transaction, option,
                                     (uint8_t *)value.data, value.size);

    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    return ATOM_ok;
}

static ERL_NIF_TERM
erlfdb_transaction_set_read_version(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifSInt64 erl_vsn;
    int64_t fdb_vsn;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int64(env, argv[1], &erl_vsn)) {
        return enif_make_badarg(env);
    }

    fdb_vsn = erl_vsn;

    fdb_transaction_set_read_version(t->transaction, fdb_vsn);

    return ATOM_ok;
}

static ERL_NIF_TERM
erlfdb_transaction_get_read_version(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_read_version(t->transaction);

    return erlfdb_create_future(env, future, ErlFDB_FT_INT64);
}

static ERL_NIF_TERM erlfdb_transaction_get(ErlNifEnv *env, int argc,
                                           const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    fdb_bool_t snapshot;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_boolean(argv[2], &snapshot)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get(t->transaction, (uint8_t *)key.data, key.size,
                                 snapshot);

    return erlfdb_create_future(env, future, ErlFDB_FT_VALUE);
}

#if FDB_API_VERSION >= 630
static ERL_NIF_TERM
erlfdb_transaction_get_estimated_range_size(ErlNifEnv *env, int argc,
                                            const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary skey;
    ErlNifBinary ekey;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[1], &skey)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &ekey)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_estimated_range_size_bytes(
        t->transaction, (uint8_t *)skey.data, skey.size, (uint8_t *)ekey.data,
        ekey.size);

    return erlfdb_create_future(env, future, ErlFDB_FT_INT64);
}
#endif

static ERL_NIF_TERM erlfdb_transaction_get_key(ErlNifEnv *env, int argc,
                                               const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    fdb_bool_t or_equal;
    int offset;
    fdb_bool_t snapshot;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_key_selector(env, argv[1], &key, &or_equal, &offset)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_boolean(argv[2], &snapshot)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_key(t->transaction, (uint8_t *)key.data,
                                     key.size, or_equal, offset, snapshot);

    return erlfdb_create_future(env, future, ErlFDB_FT_KEY);
}

static ERL_NIF_TERM
erlfdb_transaction_get_addresses_for_key(ErlNifEnv *env, int argc,
                                         const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_addresses_for_key(
        t->transaction, (uint8_t *)key.data, key.size);

    return erlfdb_create_future(env, future, ErlFDB_FT_STRING_ARRAY);
}

static ERL_NIF_TERM erlfdb_transaction_get_range(ErlNifEnv *env, int argc,
                                                 const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;

    ErlNifBinary skey;
    fdb_bool_t sor_equal;
    int soffset;

    ErlNifBinary ekey;
    fdb_bool_t eor_equal;
    int eoffset;

    int limit;
    int target_bytes;
    FDBStreamingMode mode;
    int iteration;
    fdb_bool_t snapshot;
    int reverse;

    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 9) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_key_selector(env, argv[1], &skey, &sor_equal, &soffset)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_key_selector(env, argv[2], &ekey, &eor_equal, &eoffset)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[3], &limit)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[4], &target_bytes)) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[5], want_all)) {
        mode = FDB_STREAMING_MODE_WANT_ALL;
    } else if (IS_ATOM(argv[5], iterator)) {
        mode = FDB_STREAMING_MODE_ITERATOR;
    } else if (IS_ATOM(argv[5], exact)) {
        mode = FDB_STREAMING_MODE_EXACT;
    } else if (IS_ATOM(argv[5], small)) {
        mode = FDB_STREAMING_MODE_SMALL;
    } else if (IS_ATOM(argv[5], medium)) {
        mode = FDB_STREAMING_MODE_MEDIUM;
    } else if (IS_ATOM(argv[5], large)) {
        mode = FDB_STREAMING_MODE_LARGE;
    } else if (IS_ATOM(argv[5], serial)) {
        mode = FDB_STREAMING_MODE_SERIAL;
    } else if (!enif_get_int(env, argv[5], &mode)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[6], &iteration)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_boolean(argv[7], &snapshot)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[8], &reverse)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_range(
        t->transaction, (uint8_t *)skey.data, skey.size, sor_equal, soffset,
        (uint8_t *)ekey.data, ekey.size, eor_equal, eoffset, limit,
        target_bytes, mode, iteration, snapshot, reverse);

    return erlfdb_create_future(env, future, ErlFDB_FT_KEYVALUE_ARRAY);
}

#if FDB_API_VERSION >= 730
static ERL_NIF_TERM
erlfdb_transaction_get_mapped_range(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;

    ErlNifBinary skey;
    fdb_bool_t sor_equal;
    int soffset;

    ErlNifBinary ekey;
    fdb_bool_t eor_equal;
    int eoffset;

    ErlNifBinary mapper;

    int limit;
    int target_bytes;
    FDBStreamingMode mode;
    int iteration;
    fdb_bool_t snapshot;
    int reverse;

    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 10) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_key_selector(env, argv[1], &skey, &sor_equal, &soffset)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_key_selector(env, argv[2], &ekey, &eor_equal, &eoffset)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[3], &mapper)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[4], &limit)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[5], &target_bytes)) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[6], want_all)) {
        mode = FDB_STREAMING_MODE_WANT_ALL;
    } else if (IS_ATOM(argv[6], iterator)) {
        mode = FDB_STREAMING_MODE_ITERATOR;
    } else if (IS_ATOM(argv[6], exact)) {
        mode = FDB_STREAMING_MODE_EXACT;
    } else if (IS_ATOM(argv[6], small)) {
        mode = FDB_STREAMING_MODE_SMALL;
    } else if (IS_ATOM(argv[6], medium)) {
        mode = FDB_STREAMING_MODE_MEDIUM;
    } else if (IS_ATOM(argv[6], large)) {
        mode = FDB_STREAMING_MODE_LARGE;
    } else if (IS_ATOM(argv[6], serial)) {
        mode = FDB_STREAMING_MODE_SERIAL;
    } else if (!enif_get_int(env, argv[6], &mode)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[7], &iteration)) {
        return enif_make_badarg(env);
    }

    if (!erlfdb_get_boolean(argv[8], &snapshot)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[9], &reverse)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_mapped_range(
        t->transaction, (uint8_t *)skey.data, skey.size, sor_equal, soffset,
        (uint8_t *)ekey.data, ekey.size, eor_equal, eoffset,
        (uint8_t *)mapper.data, mapper.size, limit, target_bytes, mode,
        iteration, snapshot, reverse);

    return erlfdb_create_future(env, future, ErlFDB_FT_MAPPEDKEYVALUE_ARRAY);
}
#else
static ERL_NIF_TERM
erlfdb_transaction_get_mapped_range(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
    return enif_make_badarg(env);
}
#endif

static ERL_NIF_TERM erlfdb_transaction_set(ErlNifEnv *env, int argc,
                                           const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    ErlNifBinary val;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!t->writes_allowed) {
        return enif_raise_exception(env, ATOM_writes_not_allowed);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &val)) {
        return enif_make_badarg(env);
    }

    fdb_transaction_set(t->transaction, (uint8_t *)key.data, key.size,
                        (uint8_t *)val.data, val.size);

    t->read_only = false;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_transaction_clear(ErlNifEnv *env, int argc,
                                             const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!t->writes_allowed) {
        return enif_raise_exception(env, ATOM_writes_not_allowed);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    fdb_transaction_clear(t->transaction, (uint8_t *)key.data, key.size);

    t->read_only = false;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_transaction_clear_range(ErlNifEnv *env, int argc,
                                                   const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary skey;
    ErlNifBinary ekey;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 3) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!t->writes_allowed) {
        return enif_raise_exception(env, ATOM_writes_not_allowed);
    }

    if (!enif_inspect_binary(env, argv[1], &skey)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &ekey)) {
        return enif_make_badarg(env);
    }

    fdb_transaction_clear_range(t->transaction, (uint8_t *)skey.data, skey.size,
                                (uint8_t *)ekey.data, ekey.size);

    t->read_only = false;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_transaction_atomic_op(ErlNifEnv *env, int argc,
                                                 const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    FDBMutationType mtype;
    ErlNifBinary param;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 4) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!t->writes_allowed) {
        return enif_raise_exception(env, ATOM_writes_not_allowed);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &param)) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[3], add)) {
        mtype = FDB_MUTATION_TYPE_ADD;
    } else if (IS_ATOM(argv[3], bit_and)) {
        mtype = FDB_MUTATION_TYPE_BIT_AND;
    } else if (IS_ATOM(argv[3], bit_or)) {
        mtype = FDB_MUTATION_TYPE_BIT_OR;
    } else if (IS_ATOM(argv[3], bit_xor)) {
        mtype = FDB_MUTATION_TYPE_BIT_XOR;
    } else if (IS_ATOM(argv[3], append_if_fits)) {
        mtype = FDB_MUTATION_TYPE_APPEND_IF_FITS;
    } else if (IS_ATOM(argv[3], max)) {
        mtype = FDB_MUTATION_TYPE_MAX;
    } else if (IS_ATOM(argv[3], min)) {
        mtype = FDB_MUTATION_TYPE_MIN;
    } else if (IS_ATOM(argv[3], byte_min)) {
        mtype = FDB_MUTATION_TYPE_BYTE_MIN;
    } else if (IS_ATOM(argv[3], byte_max)) {
        mtype = FDB_MUTATION_TYPE_BYTE_MAX;
    } else if (IS_ATOM(argv[3], set_versionstamped_key)) {
        mtype = FDB_MUTATION_TYPE_SET_VERSIONSTAMPED_KEY;
    } else if (IS_ATOM(argv[3], set_versionstamped_value)) {
        mtype = FDB_MUTATION_TYPE_SET_VERSIONSTAMPED_VALUE;
    } else {
        return enif_make_badarg(env);
    }

    fdb_transaction_atomic_op(t->transaction, (uint8_t *)key.data, key.size,
                              (uint8_t *)param.data, param.size, mtype);

    t->read_only = false;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_transaction_commit(ErlNifEnv *env, int argc,
                                              const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_commit(t->transaction);

    return erlfdb_create_future(env, future, ErlFDB_FT_VOID);
}

static ERL_NIF_TERM
erlfdb_transaction_get_committed_version(ErlNifEnv *env, int argc,
                                         const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    int64_t fdb_vsn;
    ErlNifSInt64 erl_vsn;
    fdb_error_t err;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    err = fdb_transaction_get_committed_version(t->transaction, &fdb_vsn);
    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    erl_vsn = fdb_vsn;
    return enif_make_int64(env, erl_vsn);
}

static ERL_NIF_TERM
erlfdb_transaction_get_versionstamp(ErlNifEnv *env, int argc,
                                    const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_versionstamp(t->transaction);

    return erlfdb_create_future(env, future, ErlFDB_FT_KEY);
}

static ERL_NIF_TERM erlfdb_transaction_watch(ErlNifEnv *env, int argc,
                                             const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary key;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    // In order for the watches to fire the transaction must commit, even if it
    // is a read-only transaction. So if writes are explicitly disallowed, also
    // do not allow setting any watches.
    if (!t->writes_allowed) {
        return enif_raise_exception(env, ATOM_writes_not_allowed);
    }

    if (!enif_inspect_binary(env, argv[1], &key)) {
        return enif_make_badarg(env);
    }

    future =
        fdb_transaction_watch(t->transaction, (uint8_t *)key.data, key.size);

    t->has_watches = true;
    return erlfdb_create_future(env, future, ErlFDB_FT_VOID);
}

static ERL_NIF_TERM erlfdb_transaction_on_error(ErlNifEnv *env, int argc,
                                                const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    int erl_err;
    fdb_error_t fdb_err;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[1], &erl_err)) {
        return enif_make_badarg(env);
    }

    fdb_err = erl_err;

    future = fdb_transaction_on_error(t->transaction, fdb_err);

    return erlfdb_create_future(env, future, ErlFDB_FT_VOID);
}

static ERL_NIF_TERM erlfdb_transaction_reset(ErlNifEnv *env, int argc,
                                             const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    fdb_transaction_reset(t->transaction);

    t->txid = 0;
    t->read_only = true;
    t->has_watches = false;

    return ATOM_ok;
}

static ERL_NIF_TERM erlfdb_transaction_cancel(ErlNifEnv *env, int argc,
                                              const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    fdb_transaction_cancel(t->transaction);

    return ATOM_ok;
}

static ERL_NIF_TERM
erlfdb_transaction_add_conflict_range(ErlNifEnv *env, int argc,
                                      const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    ErlNifBinary skey;
    ErlNifBinary ekey;
    FDBConflictRangeType rtype;
    fdb_error_t err;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 4) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[1], &skey)) {
        return enif_make_badarg(env);
    }

    if (!enif_inspect_binary(env, argv[2], &ekey)) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[3], read)) {
        rtype = FDB_CONFLICT_RANGE_TYPE_READ;
    } else if (IS_ATOM(argv[3], write)) {
        if (!t->writes_allowed) {
            return enif_raise_exception(env, ATOM_writes_not_allowed);
        }
        rtype = FDB_CONFLICT_RANGE_TYPE_WRITE;
    } else {
        return enif_make_badarg(env);
    }

    err = fdb_transaction_add_conflict_range(
        t->transaction, (uint8_t *)skey.data, skey.size, (uint8_t *)ekey.data,
        ekey.size, rtype);

    if (err != 0) {
        return erlfdb_erlang_error(env, err);
    }

    if (rtype == FDB_CONFLICT_RANGE_TYPE_WRITE) {
        t->read_only = false;
    }

    return ATOM_ok;
}

static ERL_NIF_TERM
erlfdb_transaction_get_approximate_size(ErlNifEnv *env, int argc,
                                        const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    FDBFuture *future;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    future = fdb_transaction_get_approximate_size(t->transaction);

    return erlfdb_create_future(env, future, ErlFDB_FT_INT64);
}

static ERL_NIF_TERM
erlfdb_transaction_get_next_tx_id(ErlNifEnv *env, int argc,
                                  const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (t->txid > 65535) {
        return enif_make_badarg(env);
    }

    return enif_make_uint(env, t->txid++);
}

static ERL_NIF_TERM erlfdb_transaction_is_read_only(ErlNifEnv *env, int argc,
                                                    const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (t->read_only) {
        return ATOM_true;
    } else {
        return ATOM_false;
    }
}

static ERL_NIF_TERM erlfdb_transaction_has_watches(ErlNifEnv *env, int argc,
                                                   const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (t->has_watches) {
        return ATOM_true;
    } else {
        return ATOM_false;
    }
}

static ERL_NIF_TERM
erlfdb_transaction_get_writes_allowed(ErlNifEnv *env, int argc,
                                      const ERL_NIF_TERM argv[]) {
    ErlFDBSt *st = (ErlFDBSt *)enif_priv_data(env);
    ErlFDBTransaction *t;
    void *res;

    if (st->lib_state != ErlFDB_CONNECTED) {
        return enif_make_badarg(env);
    }

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_resource(env, argv[0], ErlFDBTransactionRes, &res)) {
        return enif_make_badarg(env);
    }
    t = (ErlFDBTransaction *)res;

    if (!erlfdb_transaction_is_owner(env, t)) {
        return enif_make_badarg(env);
    }

    if (t->writes_allowed) {
        return ATOM_true;
    } else {
        return ATOM_false;
    }
}

static ERL_NIF_TERM erlfdb_get_error(ErlNifEnv *env, int argc,
                                     const ERL_NIF_TERM argv[]) {
    int erl_err;
    fdb_error_t fdb_err;
    const char *str_err;
    ERL_NIF_TERM ret;
    unsigned char *buf;

    if (argc != 1) {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[0], &erl_err)) {
        return enif_make_badarg(env);
    }

    fdb_err = erl_err;
    str_err = fdb_get_error(fdb_err);

    // No clue if this happens ever
    if (!str_err) {
        return enif_make_badarg(env);
    }

    buf = enif_make_new_binary(env, strlen(str_err), &ret);
    memcpy(buf, str_err, strlen(str_err));

    return ret;
}

static ERL_NIF_TERM erlfdb_error_predicate(ErlNifEnv *env, int argc,
                                           const ERL_NIF_TERM argv[]) {
    int erl_err;
    fdb_error_t fdb_err;
    FDBErrorPredicate pred;

    if (argc != 2) {
        return enif_make_badarg(env);
    }

    if (IS_ATOM(argv[0], retryable)) {
        pred = FDB_ERROR_PREDICATE_RETRYABLE;
    } else if (IS_ATOM(argv[0], maybe_committed)) {
        pred = FDB_ERROR_PREDICATE_MAYBE_COMMITTED;
    } else if (IS_ATOM(argv[0], retryable_not_committed)) {
        pred = FDB_ERROR_PREDICATE_RETRYABLE_NOT_COMMITTED;
    } else {
        return enif_make_badarg(env);
    }

    if (!enif_get_int(env, argv[1], &erl_err)) {
        return enif_make_badarg(env);
    }

    fdb_err = erl_err;

    if (fdb_error_predicate(pred, fdb_err)) {
        return ATOM_true;
    } else {
        return ATOM_false;
    }
}

#define NIF_FUNC(name, arity)                                                  \
    { #name, arity, name }
static ErlNifFunc funcs[] = {
    NIF_FUNC(erlfdb_can_initialize, 0),

    NIF_FUNC(erlfdb_get_max_api_version, 0),
    NIF_FUNC(erlfdb_select_api_version, 1),

    NIF_FUNC(erlfdb_network_set_option, 2),
    NIF_FUNC(erlfdb_setup_network, 0),

    NIF_FUNC(erlfdb_future_cancel, 1),
    NIF_FUNC(erlfdb_future_silence, 1),
    NIF_FUNC(erlfdb_future_is_ready, 1),
    NIF_FUNC(erlfdb_future_get_error, 1),
    NIF_FUNC(erlfdb_future_get, 1),

    NIF_FUNC(erlfdb_create_database, 1),
    NIF_FUNC(erlfdb_database_set_option, 3),
    NIF_FUNC(erlfdb_database_open_tenant, 2),
    NIF_FUNC(erlfdb_database_create_transaction, 1),
    NIF_FUNC(erlfdb_tenant_create_transaction, 1),

    NIF_FUNC(erlfdb_transaction_set_option, 3),
    NIF_FUNC(erlfdb_transaction_set_read_version, 2),
    NIF_FUNC(erlfdb_transaction_get_read_version, 1),
    NIF_FUNC(erlfdb_transaction_get, 3),
    NIF_FUNC(erlfdb_transaction_get_key, 3),
    NIF_FUNC(erlfdb_transaction_get_addresses_for_key, 2),
    NIF_FUNC(erlfdb_transaction_get_range, 9),
    NIF_FUNC(erlfdb_transaction_get_mapped_range, 10),
    NIF_FUNC(erlfdb_transaction_set, 3),
    NIF_FUNC(erlfdb_transaction_clear, 2),
    NIF_FUNC(erlfdb_transaction_clear_range, 3),
    NIF_FUNC(erlfdb_transaction_atomic_op, 4),
    NIF_FUNC(erlfdb_transaction_commit, 1),
    NIF_FUNC(erlfdb_transaction_get_committed_version, 1),
    NIF_FUNC(erlfdb_transaction_get_versionstamp, 1),
    NIF_FUNC(erlfdb_transaction_watch, 2),
    NIF_FUNC(erlfdb_transaction_on_error, 2),
    NIF_FUNC(erlfdb_transaction_reset, 1),
    NIF_FUNC(erlfdb_transaction_cancel, 1),
    NIF_FUNC(erlfdb_transaction_add_conflict_range, 4),
    NIF_FUNC(erlfdb_transaction_get_approximate_size, 1),
    NIF_FUNC(erlfdb_transaction_get_next_tx_id, 1),
    NIF_FUNC(erlfdb_transaction_is_read_only, 1),
    NIF_FUNC(erlfdb_transaction_has_watches, 1),
    NIF_FUNC(erlfdb_transaction_get_writes_allowed, 1),

#if FDB_API_VERSION >= 630
    NIF_FUNC(erlfdb_transaction_get_estimated_range_size, 3),
#endif

    NIF_FUNC(erlfdb_get_error, 1),
    NIF_FUNC(erlfdb_error_predicate, 2)};
#undef NIF_FUNC

ERL_NIF_INIT(erlfdb_nif, funcs, &erlfdb_load, NULL, NULL, &erlfdb_unload);
