/*
 * Copyright 2019-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 "mongocrypt-cache-private.h"

#include "mongocrypt-private.h"

/* Did the cache pair expire? Caller must hold lock. */
static bool _pair_expired(_mongocrypt_cache_t *cache, _mongocrypt_cache_pair_t *pair) {
    int64_t current;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(pair);

    current = bson_get_monotonic_time() / 1000;
    BSON_ASSERT(current >= INT64_MIN + pair->last_updated);
    BSON_ASSERT(cache->expiration <= INT64_MAX);
    return cache->expiration > 0 && (current - pair->last_updated) > (int64_t)cache->expiration;
}

/* Return the pair after the one being destroyed. */
static _mongocrypt_cache_pair_t *
_destroy_pair(_mongocrypt_cache_t *cache, _mongocrypt_cache_pair_t *prev, _mongocrypt_cache_pair_t *pair) {
    _mongocrypt_cache_pair_t *tmp;

    BSON_ASSERT_PARAM(cache);
    /* prev is checked before being used, so it can be NULL */
    BSON_ASSERT_PARAM(pair);

    tmp = pair->next;

    /* Unlink */
    if (!prev) {
        cache->pair = cache->pair->next;
    } else {
        prev->next = pair->next;
    }

    /* Destroy pair */
    cache->destroy_attr(pair->attr);
    cache->destroy_value(pair->value);
    bson_free(pair);

    return tmp;
}

/* Caller must hold mutex. */
static void _mongocrypt_cache_evict(_mongocrypt_cache_t *cache) {
    _mongocrypt_cache_pair_t *pair, *prev;

    BSON_ASSERT_PARAM(cache);

    prev = NULL;
    pair = cache->pair;
    while (pair) {
        if (_pair_expired(cache, pair)) {
            pair = _destroy_pair(cache, prev, pair);
            continue;
        }
        prev = pair;
        pair = pair->next;
    }
}

/* Caller must hold mutex. */
static bool _mongocrypt_remove_matches(_mongocrypt_cache_t *cache, void *attr) {
    _mongocrypt_cache_pair_t *pair, *prev;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);

    prev = NULL;
    pair = cache->pair;
    while (pair) {
        int res;

        if (!cache->cmp_attr(pair->attr, attr, &res)) {
            return false;
        }

        if (0 == res) {
            pair = _destroy_pair(cache, prev, pair);
            continue;
        }
        prev = pair;
        pair = pair->next;
    }

    return true;
}

void _mongocrypt_cache_set_expiration(_mongocrypt_cache_t *cache, uint64_t milli) {
    BSON_ASSERT_PARAM(cache);

    cache->expiration = milli;
}

/* caller must hold lock. */
static bool _find_pair(_mongocrypt_cache_t *cache, void *attr, _mongocrypt_cache_pair_t **out) {
    _mongocrypt_cache_pair_t *pair;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);
    BSON_ASSERT_PARAM(out);

    *out = NULL;

    pair = cache->pair;
    while (pair) {
        int res;
        /* TODO: this is a naive O(n) lookup. Consider optimizing
           with a hash map (possibly vendor one). */
        if (!cache->cmp_attr(pair->attr, attr, &res)) {
            return false;
        }

        if (res == 0) {
            *out = pair;
            return true;
        }
        pair = pair->next;
    }
    return true;
}

/* Create a new pair on linked list. Caller must hold lock. */
static _mongocrypt_cache_pair_t *_pair_new(_mongocrypt_cache_t *cache, void *attr) {
    _mongocrypt_cache_pair_t *pair;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);

    pair = bson_malloc0(sizeof(_mongocrypt_cache_pair_t));
    BSON_ASSERT(pair);

    pair->attr = cache->copy_attr(attr);
    /* add rest of values. */
    pair->next = cache->pair;
    pair->last_updated = bson_get_monotonic_time() / 1000;
    cache->pair = pair;
    return pair;
}

/* Caller must hold lock. */
static void _cache_pair_destroy(_mongocrypt_cache_t *cache, _mongocrypt_cache_pair_t *pair) {
    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(pair);

    cache->destroy_attr(pair->attr);
    cache->destroy_value(pair->value);
    bson_free(pair);
}

bool _mongocrypt_cache_get(_mongocrypt_cache_t *cache,
                           void *attr, /* attr of cache item */
                           void **value /* copied to. */) {
    _mongocrypt_cache_pair_t *match;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);
    BSON_ASSERT_PARAM(value);

    *value = NULL;

    _mongocrypt_mutex_lock(&cache->mutex);
    /* TODO CDRIVER-3120: optimize the eviction algorithm to avoid unnecessary
     * O(n) traversal */
    _mongocrypt_cache_evict(cache);
    if (!_find_pair(cache, attr, &match)) {
        _mongocrypt_mutex_unlock(&cache->mutex);
        return false;
    }

    if (match) {
        *value = cache->copy_value(match->value);
    }
    _mongocrypt_mutex_unlock(&cache->mutex);
    return true;
}

static bool
_cache_add(_mongocrypt_cache_t *cache, void *attr, void *value, mongocrypt_status_t *status, bool steal_value) {
    _mongocrypt_cache_pair_t *pair;

    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);
    BSON_ASSERT_PARAM(value);

    _mongocrypt_mutex_lock(&cache->mutex);
    _mongocrypt_cache_evict(cache);
    if (!_mongocrypt_remove_matches(cache, attr)) {
        CLIENT_ERR("error removing from cache");
        _mongocrypt_mutex_unlock(&cache->mutex);
        return false;
    }

    pair = _pair_new(cache, attr);

    if (steal_value) {
        pair->value = value;
    } else {
        pair->value = cache->copy_value(value);
    }
    _mongocrypt_mutex_unlock(&cache->mutex);
    return true;
}

bool _mongocrypt_cache_add_copy(_mongocrypt_cache_t *cache, void *attr, void *value, mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);
    BSON_ASSERT_PARAM(value);

    return _cache_add(cache, attr, value, status, false);
}

bool _mongocrypt_cache_add_stolen(_mongocrypt_cache_t *cache, void *attr, void *value, mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(cache);
    BSON_ASSERT_PARAM(attr);
    BSON_ASSERT_PARAM(value);

    return _cache_add(cache, attr, value, status, true);
}

void _mongocrypt_cache_cleanup(_mongocrypt_cache_t *cache) {
    _mongocrypt_cache_pair_t *pair, *tmp;

    if (!cache) {
        return;
    }

    pair = cache->pair;
    while (pair) {
        tmp = pair->next;
        _cache_pair_destroy(cache, pair);
        pair = tmp;
    }
}

/* Print the contents of the cache (for debugging purposes) */
void _mongocrypt_cache_dump(_mongocrypt_cache_t *cache) {
    _mongocrypt_cache_pair_t *pair;
    int count;

    BSON_ASSERT_PARAM(cache);

    _mongocrypt_mutex_lock(&cache->mutex);
    count = 0;
    for (pair = cache->pair; pair != NULL; pair = pair->next) {
        /* don't check that int64_t fits in int, since this is only diagnostic */
        printf("entry:%d last_updated:%d\n", count, (int)pair->last_updated);
        if (cache->dump_attr) {
            printf("- attr:");
            cache->dump_attr(pair->attr);
        }
        count++;
    }

    _mongocrypt_mutex_unlock(&cache->mutex);
}

uint32_t _mongocrypt_cache_num_entries(_mongocrypt_cache_t *cache) {
    _mongocrypt_cache_pair_t *pair;
    uint32_t count;

    BSON_ASSERT_PARAM(cache);

    _mongocrypt_mutex_lock(&cache->mutex);
    count = 0;
    for (pair = cache->pair; pair != NULL; pair = pair->next) {
        count++;
    }

    _mongocrypt_mutex_unlock(&cache->mutex);
    return count;
}
