/*
 * Copyright 2022-present MongoDB, Inc.
 *
 * 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 "mc-optional-private.h"
#include "mc-range-edge-generation-private.h"

#include "mc-array-private.h"
#include "mc-check-conversions-private.h"
#include "mc-cmp-private.h"
#include "mc-range-encoding-private.h"
#include "mongocrypt-private.h"

struct _mc_edges_t {
    size_t sparsity;
    /* edges is an array of `char*` edge strings. */
    mc_array_t edges;
    char *leaf;
    int32_t usedTrimFactor; // The `trimFactor` that was used to produce these edges.
};

int32_t mc_edges_get_used_trimFactor(const mc_edges_t *edges) {
    return edges->usedTrimFactor;
}

static mc_edges_t *mc_edges_new(const char *leaf,
                                size_t sparsity,
                                mc_optional_int32_t opt_trimFactor,
                                mongocrypt_status_t *status,
                                bool use_range_v2) {
    BSON_ASSERT_PARAM(leaf);
    if (sparsity < 1) {
        CLIENT_ERR("sparsity must be 1 or larger");
        return NULL;
    }

    const size_t leaf_len = strlen(leaf);
    const int32_t trimFactor = trimFactorDefault(leaf_len, opt_trimFactor, use_range_v2);
    if (trimFactor != 0 && mc_cmp_greater_equal_su(trimFactor, leaf_len)) {
        // We append a total of leaf_len + 1 (for the root) - trimFactor edges. When this number is equal to 1, we
        // degenerate into equality, which is not desired, so trimFactor must be less than leaf_len.
        CLIENT_ERR("trimFactor must be less than the number of bits (%ld) used to represent an element of the domain, "
                   "but got %" PRId32,
                   leaf_len,
                   trimFactor);
        return NULL;
    }
    if (trimFactor < 0) {
        CLIENT_ERR("trimFactor must be >= 0, but got %" PRId32, trimFactor);
        return NULL;
    }

    mc_edges_t *edges = bson_malloc0(sizeof(mc_edges_t));
    edges->usedTrimFactor = trimFactor;
    edges->sparsity = sparsity;
    _mc_array_init(&edges->edges, sizeof(char *));
    edges->leaf = bson_strdup(leaf);

    if (trimFactor == 0) {
        char *root = bson_strdup("root");
        _mc_array_append_val(&edges->edges, root);
    }

    char *leaf_copy = bson_strdup(leaf);
    _mc_array_append_val(&edges->edges, leaf_copy);

    // Start loop at max(trimFactor, 1). The full leaf is unconditionally appended after loop.
    BSON_ASSERT(mc_in_range_size_t_signed(trimFactor));
    size_t trimFactor_sz = (size_t)trimFactor;
    size_t startLevel = trimFactor > 0 ? trimFactor_sz : 1;
    for (size_t i = startLevel; i < leaf_len; i++) {
        if (i % sparsity == 0) {
            char *edge = bson_malloc(i + 1);
            bson_strncpy(edge, leaf, i + 1);
            _mc_array_append_val(&edges->edges, edge);
        }
    }

    return edges;
}

const char *mc_edges_get(mc_edges_t *edges, size_t index) {
    BSON_ASSERT_PARAM(edges);
    if (edges->edges.len == 0 || index > edges->edges.len - 1u) {
        return NULL;
    }
    return _mc_array_index(&edges->edges, char *, index);
}

size_t mc_edges_len(mc_edges_t *edges) {
    BSON_ASSERT_PARAM(edges);
    return edges->edges.len;
}

void mc_edges_destroy(mc_edges_t *edges) {
    if (NULL == edges) {
        return;
    }
    for (size_t i = 0; i < edges->edges.len; i++) {
        char *val = _mc_array_index(&edges->edges, char *, i);
        bson_free(val);
    }
    _mc_array_destroy(&edges->edges);
    bson_free(edges->leaf);
    bson_free(edges);
}

bool mc_edges_is_leaf(const mc_edges_t *edges, const char *edge) {
    BSON_ASSERT_PARAM(edges);
    BSON_ASSERT_PARAM(edge);

    return strcmp(edge, edges->leaf) == 0;
}

mc_bitstring mc_convert_to_bitstring_u64(uint64_t in) {
    mc_bitstring ret = {{0}};
    char *out = ret.str;
    uint64_t bit = UINT64_C(1) << 63;
    int loops = 0; // used to determine a bit shift
    while (bit > 0) {
        *out++ = (char)('0' + ((bit & in) >> (63 - loops)));
        bit >>= 1;
        loops++;
    }
    return ret;
}

mc_bitstring mc_convert_to_bitstring_u32(uint32_t in) {
    mc_bitstring ret = {{0}};
    char *out = ret.str;
    uint32_t bit = UINT32_C(1) << 31;
    while (bit > 0) {
        if (bit & in) {
            *out++ = '1';
        } else {
            *out++ = '0';
        }
        bit >>= 1;
    }
    return ret;
}

mc_bitstring mc_convert_to_bitstring_u128(mlib_int128 i) {
    const uint64_t lo = mlib_int128_to_u64(i);
    const uint64_t hi = mlib_int128_to_u64(mlib_int128_rshift(i, 64));
    mc_bitstring his = mc_convert_to_bitstring_u64(hi);
    mc_bitstring los = mc_convert_to_bitstring_u64(lo);
    mc_bitstring ret = {{0}};
    const size_t off = mlib_strnmcopy(ret.str, sizeof ret.str, his.str, sizeof his.str);
    mlib_strnmcopy(ret.str + off, (sizeof ret.str) - off, los.str, sizeof los.str);
    return ret;
}

mc_edges_t *mc_getEdgesInt32(mc_getEdgesInt32_args_t args, mongocrypt_status_t *status, bool use_range_v2) {
    mc_OSTType_Int32 got;
    if (!mc_getTypeInfo32((mc_getTypeInfo32_args_t){.value = args.value, .min = args.min, .max = args.max},
                          &got,
                          status)) {
        return NULL;
    }

    // `max` is the domain of values. `max` is used to determine the maximum bit
    // length. `min` is expected to be zero. The `min` and `max` terms are kept
    // for consistency with the server implementation.
    BSON_ASSERT(got.min == 0);

    mc_bitstring valueBin = mc_convert_to_bitstring_u32(got.value);
    size_t offset = mc_count_leading_zeros_u32(got.max);
    const char *leaf = valueBin.str + offset;
    mc_edges_t *ret = mc_edges_new(leaf, args.sparsity, args.trimFactor, status, use_range_v2);
    return ret;
}

mc_edges_t *mc_getEdgesInt64(mc_getEdgesInt64_args_t args, mongocrypt_status_t *status, bool use_range_v2) {
    mc_OSTType_Int64 got;
    if (!mc_getTypeInfo64((mc_getTypeInfo64_args_t){.value = args.value, .min = args.min, .max = args.max},
                          &got,
                          status)) {
        return NULL;
    }

    // `max` is the domain of values. `max` is used to determine the maximum bit
    // length. `min` is expected to be zero. The `min` and `max` terms are kept
    // for consistency with the server implementation.
    BSON_ASSERT(got.min == 0);

    mc_bitstring valueBin = mc_convert_to_bitstring_u64(got.value);
    size_t offset = mc_count_leading_zeros_u64(got.max);
    const char *leaf = valueBin.str + offset;
    mc_edges_t *ret = mc_edges_new(leaf, args.sparsity, args.trimFactor, status, use_range_v2);
    return ret;
}

mc_edges_t *mc_getEdgesDouble(mc_getEdgesDouble_args_t args, mongocrypt_status_t *status, bool use_range_v2) {
    mc_OSTType_Double got;
    if (!mc_getTypeInfoDouble((mc_getTypeInfoDouble_args_t){.value = args.value,
                                                            .min = args.min,
                                                            .max = args.max,
                                                            .precision = args.precision},
                              &got,
                              status,
                              use_range_v2)) {
        return NULL;
    }

    // `max` is the domain of values. `max` is used to determine the maximum bit
    // length. `min` is expected to be zero. The `min` and `max` terms are kept
    // for consistency with the server implementation.
    BSON_ASSERT(got.min == 0);

    mc_bitstring valueBin = mc_convert_to_bitstring_u64(got.value);
    size_t offset = mc_count_leading_zeros_u64(got.max);
    const char *leaf = valueBin.str + offset;
    mc_edges_t *ret = mc_edges_new(leaf, args.sparsity, args.trimFactor, status, use_range_v2);
    return ret;
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
mc_edges_t *mc_getEdgesDecimal128(mc_getEdgesDecimal128_args_t args, mongocrypt_status_t *status, bool use_range_v2) {
    mc_OSTType_Decimal128 got;
    if (!mc_getTypeInfoDecimal128(
            (mc_getTypeInfoDecimal128_args_t){
                .value = args.value,
                .min = args.min,
                .max = args.max,
                .precision = args.precision,
            },
            &got,
            status,
            use_range_v2)) {
        return NULL;
    }

    BSON_ASSERT(mlib_int128_eq(got.min, MLIB_INT128(0)));

    mc_bitstring bits = mc_convert_to_bitstring_u128(got.value);
    size_t offset = mc_count_leading_zeros_u128(got.max);
    const char *leaf = bits.str + offset;
    mc_edges_t *ret = mc_edges_new(leaf, args.sparsity, args.trimFactor, status, use_range_v2);
    return ret;
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
