ERL_NIF_TERM _hh_to_binary_uncompressed(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;
    ErlNifBinary target;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 1 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL)
    {
        return enif_make_badarg(env);
    }
    int size = 0;
    uint8_t* data = NULL;
    int success = hdr_encode_uncompressed(ctx->data, &data, &size);

    if (!enif_alloc_binary(size, &target))
    {
        return make_error(env, "bad_hdr_binary_alloc");
    }
    target.size = size;
    memcpy(target.data,data,size);
    free(data);

    if (success != 0)
    {
        return make_error(env, "bad_hdr_binary");
    }

    return enif_make_binary(env, &target);
}
ERL_NIF_TERM _hh_lowest_at(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int64_t value = 0;
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 2 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_int64(env, argv[1], &value))
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        return enif_make_int64(
            env,
            hdr_lowest_equivalent_value(ctx->data, value)
        );
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_from_binary(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    ErlNifBinary source;

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

    hdr_histogram_t* target = NULL;
    int success = hdr_decode(source.data, source.size, &target);

    if (success != 0)
    {
        return make_error(env, "bad_hdr_binary");
    }

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    hh_ctx_t* ctx = (hh_ctx_t*)enif_alloc_resource(ctx_type, sizeof(hh_ctx_t));

    ctx->data = (hdr_histogram_t*)target;
    ctx->highest_trackable_value = target->highest_trackable_value;
    ctx->significant_figures = target->significant_figures;

    ERL_NIF_TERM result = enif_make_resource(env, ctx);
    enif_release_resource(ctx);

    return enif_make_tuple2(env, ATOM_OK, result);
}
ERL_NIF_TERM _hh_log_csv(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    char fname[64];
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 2 || ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_string(env, argv[1], fname, 64, ERL_NIF_LATIN1))
    {
        return enif_make_badarg(env);
    }

    FILE* stream = fopen(fname,"w+");
    if (stream == NULL)
    {
        return make_error(env, "cannot_create_or_write_to_file");
    }

    if (ctx != NULL)
    {
        hdr_percentiles_print(ctx->data, stream, 5, 1.0, CSV);
        if (fclose(stream) != 0)
        {
            return make_error(env, "bad_file");
        }
        return ATOM_OK;
    }

    (void)fclose(stream);

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_same(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int64_t a = 0;
    int64_t b = 0;
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 3 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_int64(env, argv[1], &a) ||
        !enif_get_int64(env, argv[2], &b))
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        return hdr_values_are_equivalent(ctx->data,a,b)
            ? ATOM_TRUE : ATOM_FALSE;
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_percentile(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    double percentile;
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 2 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_double(env, argv[1], &percentile))
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        if (ctx->data->total_count == 0)
        {
            return enif_make_double(env, 0.);
        }
        else
        {
            return enif_make_double(
                env,
                round_to_significant_figures(
                    hdr_value_at_percentile(ctx->data,percentile),
                    ctx->significant_figures
                )
            );
        }
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_max(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 1 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL)
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        if (ctx->data->total_count == 0)
        {
            return enif_make_int64(env, 0);
        }
        else
        {
            return enif_make_int64(env, hdr_max(ctx->data));
        }
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_record_many(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    int64_t value = 0;
    int64_t count = 0;
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if(argc != 3 ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_int64(env, argv[1], &value) ||
        !enif_get_int64(env, argv[2], &count))
    {
        return enif_make_badarg(env);
    }

    if (value < 0 || value > ctx->highest_trackable_value)
    {
	    return make_error(env, "value_out_of_range");
    }

    if (ctx != NULL && ctx->data != NULL)
    {
        hdr_record_values(ctx->data, value, count);
    }

    return ATOM_OK;
}
ERL_NIF_TERM _hh_rotate(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;
    hh_ctx_t* to = NULL;
    ErlNifBinary target;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 2 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_resource(env, argv[1], ctx_type, (void **)&to) ||
        to->data == NULL)
    {
        return enif_make_badarg(env);
    }

    hdr_histogram_t* diff_histogram;
    int rc = 0;
    rc = hdr_alloc(ctx->highest_trackable_value, ctx->significant_figures, &diff_histogram);

    if (ENOMEM == rc)
    {
        return make_error(env, "not_enough_memory");
    }

    hdr_rotate(ctx->data, to->data, diff_histogram);

    int size = 0;
    uint8_t* data = NULL;
    int success = hdr_encode_uncompressed(diff_histogram, &data, &size);

    if (!enif_alloc_binary(size, &target))
    {
        return make_error(env, "bad_hdr_binary_alloc");
    }
    target.size = size;
    memcpy(target.data, data, size);
    free(data);
    free(diff_histogram);

    if (success != 0)
    {
        return make_error(env, "bad_hdr_binary");
    }

    return enif_make_binary(env, &target);
}
ERL_NIF_TERM _hh_get_total_count(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (ctx_type != NULL &&
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx))
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        return enif_make_ulong(env,ctx->data->total_count);
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (ctx_type != NULL &&
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx))
    {
        return enif_make_badarg(env);
    }

    if (ctx != NULL && ctx->data != NULL)
    {
        free(ctx->data);
        ctx->data = NULL;
    }

    return ATOM_OK;
}
ERL_NIF_TERM _hh_print_csv(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (ctx_type != NULL &&
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx))
    {
        return enif_make_badarg(env);
    }

    if (ctx != NULL)
    {
        hdr_percentiles_print(ctx->data, stdout, 5, 1.0, CSV);
        return ATOM_OK;
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_min(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 1 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx))
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        return enif_make_long(env, hdr_min(ctx->data));
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hh_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    long highest_trackable_value = 0;
    int significant_figures = 0;
    if (argc != 2 ||
        !enif_get_int64(env, argv[0], &highest_trackable_value) ||
        !enif_get_int(env, argv[1], &significant_figures))
    {
        return enif_make_badarg(env);
    }

    hdr_histogram_t* raw_histogram;

    int rc = 0;
    rc = hdr_alloc(highest_trackable_value, significant_figures, &raw_histogram);

    if (EINVAL == rc)
    {
        return make_error(env, "bad_significant_factor");
    }

    if (ENOMEM == rc)
    {
        return make_error(env, "not_enough_memory");
    }

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    hh_ctx_t* ctx = (hh_ctx_t*)enif_alloc_resource(ctx_type, sizeof(hh_ctx_t));

    ctx->data = raw_histogram; 
    ctx->highest_trackable_value = highest_trackable_value;
    ctx->significant_figures = significant_figures;

    ERL_NIF_TERM result = enif_make_resource(env, ctx);
    enif_release_resource(ctx);

    return enif_make_tuple2(env, ATOM_OK, result);
}
ERL_NIF_TERM _hh_add(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hh_ctx_t* ctx = NULL;
    hh_ctx_t* from = NULL;

    ErlNifResourceType* ctx_type = get_hh_ctx_type(env);
    if (argc != 2 ||
        ctx_type == NULL ||
        !enif_get_resource(env, argv[0], ctx_type, (void **)&ctx) ||
        ctx->data == NULL ||
        !enif_get_resource(env, argv[1], ctx_type, (void **)&from) ||
        from->data == NULL)
    {
        return enif_make_badarg(env);
    }
   
    if (ctx != NULL)
    {
        return enif_make_int64(env, hdr_add(ctx->data, from->data));
    }

    return make_error(env, "bad_hdr_histogram_nif_impl");
}
ERL_NIF_TERM _hi_init(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    hi_ctx_t* ctx = NULL;
    hh_ctx_t* hdr = NULL;
    hi_opts_t* opts = NULL;

    ErlNifResourceType* hh_ctx_type = get_hh_ctx_type(env);
    ErlNifResourceType* hi_ctx_type = get_hi_ctx_type(env);
    if (argc != 3 ||
        hh_ctx_type == NULL ||
        hi_ctx_type == NULL ||
        !enif_get_resource(env, argv[0], hi_ctx_type, (void **)&ctx) ||
        !enif_get_resource(env, argv[1], hh_ctx_type, (void **)&hdr) ||
        !enif_is_list(env, argv[2]))
    {
        return enif_make_badarg(env);
    }

    opts = (hi_opts_t *)enif_alloc(sizeof(hi_opts_t));
    parse_opts(env, argv[2], opts, (void *)parse_opt);
    uint32_t iterator_type = ctx->type;

    void* it = NULL;

    if (iterator_type == HDR_ITER_REC)
    {
        struct hdr_recorded_iter * iter =
            enif_alloc(sizeof(struct hdr_recorded_iter));
        hdr_recorded_iter_init(iter, hdr->data);
        it = iter;
    }

    if (iterator_type == HDR_ITER_LIN)
    {
        if (opts->linear_value_units_per_bucket <= 0)
        {
            return make_error(env, "bad_linear_value_unit");
        }
        struct hdr_linear_iter * iter =
            enif_alloc(sizeof(struct hdr_linear_iter));
        hdr_linear_iter_init(
            iter,
            hdr->data,
            opts->linear_value_units_per_bucket);
        it = iter;
    }

    if (iterator_type == HDR_ITER_LOG)
    {
        if (opts->log_value_units_first_bucket <= 0)
        {
            return make_error(env, "bad_log_value_unit");
        }
        if (opts->log_base <= 0)
        {
            return make_error(env, "bad_log_base");
        }
        struct hdr_log_iter * iter =
            enif_alloc(sizeof(struct hdr_log_iter));
        hdr_log_iter_init(
            iter,
            hdr->data,
            opts->log_value_units_first_bucket,
            opts->log_base);
        it = iter;
    }

    if (iterator_type == HDR_ITER_PCT)
    {
        if (opts->percentile_ticks_per_half_distance <= 0)
        {
            return make_error(env, "bad_percentile_half_ticks");
        }
        struct hdr_percentile_iter * iter =
            enif_alloc(sizeof(struct hdr_percentile_iter));
        hdr_percentile_iter_init(
            iter,
            hdr->data,
            opts->percentile_ticks_per_half_distance);
        it = iter;
    }

    ctx->type = iterator_type;
    ctx->opts = opts;
    ctx->iter = it;

    return ATOM_OK;
}