/*
 * 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;

   current = bson_get_monotonic_time () / 1000;
   return (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;
   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. */
void
_mongocrypt_cache_evict (_mongocrypt_cache_t *cache)
{
   _mongocrypt_cache_pair_t *pair, *prev;

   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;

   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)
{
   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;

   *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;

   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)
{
   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;

   *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;

   _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)
{
   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)
{
   return _cache_add (cache, attr, value, status, true);
}

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

   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;

   _mongocrypt_mutex_lock (&cache->mutex);
   count = 0;
   for (pair = cache->pair; pair != NULL; pair = pair->next) {
      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;

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

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