/*
 * Copyright © 2022 Collabora, Ltd.
 *
 * Based on Fossilize DB:
 * Copyright © 2020 Valve Corporation
 *
 * SPDX-License-Identifier: MIT
 */

#include "detect_os.h"

#if DETECT_OS_WINDOWS == 0

#include <fcntl.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

#include "crc32.h"
#include "disk_cache.h"
#include "hash_table.h"
#include "mesa-sha1.h"
#include "mesa_cache_db.h"
#include "os_time.h"
#include "ralloc.h"
#include "u_qsort.h"

#define MESA_CACHE_DB_VERSION          1
#define MESA_CACHE_DB_MAGIC            "MESA_DB"

struct PACKED mesa_db_file_header {
   char magic[8];
   uint32_t version;
   uint64_t uuid;
};

struct PACKED mesa_cache_db_file_entry {
   cache_key key;
   uint32_t crc;
   uint32_t size;
};

struct PACKED mesa_index_db_file_entry {
   uint64_t hash;
   uint32_t size;
   uint64_t last_access_time;
   uint64_t cache_db_file_offset;
};

struct mesa_index_db_hash_entry {
   uint64_t cache_db_file_offset;
   uint64_t index_db_file_offset;
   uint64_t last_access_time;
   uint32_t size;
   bool evicted;
};

static inline bool mesa_db_seek_end(FILE *file)
{
   return !fseek(file, 0, SEEK_END);
}

static inline bool mesa_db_seek(FILE *file, long pos)
{
   return !fseek(file, pos, SEEK_SET);
}

static inline bool mesa_db_seek_cur(FILE *file, long pos)
{
   return !fseek(file, pos, SEEK_CUR);
}

static inline bool mesa_db_read_data(FILE *file, void *data, size_t size)
{
   return fread(data, 1, size, file) == size;
}
#define mesa_db_read(file, var) mesa_db_read_data(file, var, sizeof(*(var)))

static inline bool mesa_db_write_data(FILE *file, const void *data, size_t size)
{
   return fwrite(data, 1, size, file) == size;
}
#define mesa_db_write(file, var) mesa_db_write_data(file, var, sizeof(*(var)))

static inline bool mesa_db_truncate(FILE *file, long pos)
{
   return !ftruncate(fileno(file), pos);
}

static bool
mesa_db_lock(struct mesa_cache_db *db)
{
   simple_mtx_lock(&db->flock_mtx);

   if (flock(fileno(db->cache.file), LOCK_EX) == -1)
      goto unlock_mtx;

   if (flock(fileno(db->index.file), LOCK_EX) == -1)
      goto unlock_cache;

   return true;

unlock_cache:
   flock(fileno(db->cache.file), LOCK_UN);
unlock_mtx:
   simple_mtx_unlock(&db->flock_mtx);

   return false;
}

static void
mesa_db_unlock(struct mesa_cache_db *db)
{
   flock(fileno(db->index.file), LOCK_UN);
   flock(fileno(db->cache.file), LOCK_UN);
   simple_mtx_unlock(&db->flock_mtx);
}

static uint64_t to_mesa_cache_db_hash(const uint8_t *cache_key_160bit)
{
   uint64_t hash = 0;

   for (unsigned i = 0; i < 8; i++)
      hash |= ((uint64_t)cache_key_160bit[i]) << i * 8;

   return hash;
}

static uint64_t
mesa_db_generate_uuid(void)
{
   /* This simple UUID implementation is sufficient for our needs
    * because UUID is updated rarely. It's nice to make UUID meaningful
    * and incremental by adding the timestamp to it, which also prevents
    * the potential collisions. */
   return ((os_time_get() / 1000000) << 32) | rand();
}

static bool
mesa_db_read_header(FILE *file, struct mesa_db_file_header *header)
{
   rewind(file);
   fflush(file);

   if (!mesa_db_read(file, header))
      return false;

   if (strncmp(header->magic, MESA_CACHE_DB_MAGIC, sizeof(header->magic)) ||
       header->version != MESA_CACHE_DB_VERSION || !header->uuid)
      return false;

   return true;
}

static bool
mesa_db_load_header(struct mesa_cache_db_file *db_file)
{
   struct mesa_db_file_header header;

   if (!mesa_db_read_header(db_file->file, &header))
      return false;

   db_file->uuid = header.uuid;

   return true;
}

static bool mesa_db_uuid_changed(struct mesa_cache_db *db)
{
   struct mesa_db_file_header cache_header;
   struct mesa_db_file_header index_header;

   if (!mesa_db_read_header(db->cache.file, &cache_header) ||
       !mesa_db_read_header(db->index.file, &index_header) ||
       cache_header.uuid != index_header.uuid ||
       cache_header.uuid != db->uuid)
      return true;

   return false;
}

static bool
mesa_db_write_header(struct mesa_cache_db_file *db_file,
                     uint64_t uuid, bool reset)
{
   struct mesa_db_file_header header;

   rewind(db_file->file);

   sprintf(header.magic, "MESA_DB");
   header.version = MESA_CACHE_DB_VERSION;
   header.uuid = uuid;

   if (!mesa_db_write(db_file->file, &header))
      return false;

   if (reset) {
      if (!mesa_db_truncate(db_file->file, ftell(db_file->file)))
         return false;
   }

   fflush(db_file->file);

   return true;
}

/* Wipe out all database cache files.
 *
 * Whenever we get an unmanageable error on reading or writing to the
 * database file, wipe out the whole database and start over. All the
 * cached entries will be lost, but the broken cache will be auto-repaired
 * reliably. Normally cache shall never get corrupted and losing cache
 * entries is acceptable, hence it's more practical to repair DB using
 * the simplest method.
 */
static bool
mesa_db_zap(struct mesa_cache_db *db)
{
   /* Disable cache to prevent the recurring faults */
   db->alive = false;

   /* Zap corrupted database files to start over from a clean slate */
   if (!mesa_db_truncate(db->cache.file, 0) ||
       !mesa_db_truncate(db->index.file, 0))
      return false;

   fflush(db->cache.file);
   fflush(db->index.file);

   return true;
}

static bool
mesa_db_index_entry_valid(struct mesa_index_db_file_entry *entry)
{
   return entry->size && entry->hash &&
          (int64_t)entry->cache_db_file_offset >= sizeof(struct mesa_db_file_header);
}

static bool
mesa_db_cache_entry_valid(struct mesa_cache_db_file_entry *entry)
{
   return entry->size && entry->crc;
}

static bool
mesa_db_update_index(struct mesa_cache_db *db)
{
   struct mesa_index_db_hash_entry *hash_entry;
   struct mesa_index_db_file_entry index_entry;
   size_t file_length;

   if (!mesa_db_seek_end(db->index.file))
      return false;

   file_length = ftell(db->index.file);

   if (!mesa_db_seek(db->index.file, db->index.offset))
      return false;

   while (db->index.offset < file_length) {
      if (!mesa_db_read(db->index.file, &index_entry))
         break;

      /* Check whether the index entry looks valid or we have a corrupted DB */
      if (!mesa_db_index_entry_valid(&index_entry))
         break;

      hash_entry = ralloc(db->mem_ctx, struct mesa_index_db_hash_entry);
      if (!hash_entry)
         break;

      hash_entry->cache_db_file_offset = index_entry.cache_db_file_offset;
      hash_entry->index_db_file_offset = db->index.offset;
      hash_entry->last_access_time = index_entry.last_access_time;
      hash_entry->size = index_entry.size;

      _mesa_hash_table_u64_insert(db->index_db, index_entry.hash, hash_entry);

      db->index.offset += sizeof(index_entry);
   }

   if (!mesa_db_seek(db->index.file, db->index.offset))
      return false;

   return db->index.offset == file_length;
}

static void
mesa_db_hash_table_reset(struct mesa_cache_db *db)
{
   _mesa_hash_table_u64_clear(db->index_db);
   ralloc_free(db->mem_ctx);
   db->mem_ctx = ralloc_context(NULL);
}

static bool
mesa_db_recreate_files(struct mesa_cache_db *db)
{
   db->uuid = mesa_db_generate_uuid();

   if (!mesa_db_write_header(&db->cache, db->uuid, true) ||
       !mesa_db_write_header(&db->index, db->uuid, true))
         return false;

   return true;
}

static bool
mesa_db_load(struct mesa_cache_db *db, bool reload)
{
   /* reloading must be done under the held lock */
   if (!reload) {
      if (!mesa_db_lock(db))
         return false;
   }

   /* If file headers are invalid, then zap database files and start over */
   if (!mesa_db_load_header(&db->cache) ||
       !mesa_db_load_header(&db->index) ||
       db->cache.uuid != db->index.uuid) {

      /* This is unexpected to happen on reload, bail out */
      if (reload)
         goto fail;

      if (!mesa_db_recreate_files(db))
         goto fail;
   } else {
      db->uuid = db->cache.uuid;
   }

   db->index.offset = ftell(db->index.file);

   if (reload)
      mesa_db_hash_table_reset(db);

   if (!mesa_db_update_index(db))
      goto fail;

   if (!reload)
      mesa_db_unlock(db);

   db->alive = true;

   return true;

fail:
   if (!reload)
      mesa_db_unlock(db);

   return false;
}

static bool
mesa_db_reload(struct mesa_cache_db *db)
{
   fflush(db->cache.file);
   fflush(db->index.file);

   return mesa_db_load(db, true);
}

static void
touch_file(const char* path)
{
   close(open(path, O_CREAT | O_CLOEXEC, 0644));
}

static bool
mesa_db_open_file(struct mesa_cache_db_file *db_file,
                  const char *cache_path,
                  const char *filename)
{
   if (asprintf(&db_file->path, "%s/%s", cache_path, filename) == -1)
      return false;

   /* The fopen("r+b") mode doesn't auto-create new file, hence we need to
    * explicitly create the file first.
    */
   touch_file(db_file->path);

   db_file->file = fopen(db_file->path, "r+b");
   if (!db_file->file) {
      free(db_file->path);
      return false;
   }

   return true;
}

static void
mesa_db_close_file(struct mesa_cache_db_file *db_file)
{
   fclose(db_file->file);
   free(db_file->path);
}

static int
entry_sort_lru(const void *_a, const void *_b, void *arg)
{
   const struct mesa_index_db_hash_entry *a = *((const struct mesa_index_db_hash_entry **)_a);
   const struct mesa_index_db_hash_entry *b = *((const struct mesa_index_db_hash_entry **)_b);

   /* In practice it's unlikely that we will get two entries with the
    * same timestamp, but technically it's possible to happen if OS
    * timer's resolution is low. */
   if (a->last_access_time == b->last_access_time)
      return 0;

   return a->last_access_time > b->last_access_time ? 1 : -1;
}

static int
entry_sort_offset(const void *_a, const void *_b, void *arg)
{
   const struct mesa_index_db_hash_entry *a = *((const struct mesa_index_db_hash_entry **)_a);
   const struct mesa_index_db_hash_entry *b = *((const struct mesa_index_db_hash_entry **)_b);
   struct mesa_cache_db *db = arg;

   /* Two entries will never have the identical offset, otherwise DB is
    * corrupted. */
   if (a->cache_db_file_offset == b->cache_db_file_offset)
      mesa_db_zap(db);

   return a->cache_db_file_offset > b->cache_db_file_offset ? 1 : -1;
}

static uint32_t blob_file_size(uint32_t blob_size)
{
   return sizeof(struct mesa_cache_db_file_entry) + blob_size;
}

static bool
mesa_db_compact(struct mesa_cache_db *db, int64_t blob_size)
{
   uint32_t num_entries, buffer_size = sizeof(struct mesa_index_db_file_entry);
   struct mesa_db_file_header cache_header, index_header;
   FILE *compacted_cache = NULL, *compacted_index = NULL;
   struct mesa_index_db_file_entry index_entry;
   struct mesa_index_db_hash_entry **entries;
   bool success = false, compact = false;
   void *buffer = NULL;
   unsigned int i = 0;

   /* reload index to sync the last access times */
   if (!mesa_db_reload(db))
      return false;

   num_entries = _mesa_hash_table_num_entries(db->index_db->table);
   entries = calloc(num_entries, sizeof(*entries));
   if (!entries)
      return false;

   compacted_cache = fopen(db->cache.path, "r+b");
   compacted_index = fopen(db->index.path, "r+b");
   if (!compacted_cache || !compacted_index)
      goto cleanup;

   /* The database file has been replaced if UUID changed. We opened
    * some other cache, stop processing this database. */
   if (!mesa_db_read_header(compacted_cache, &cache_header) ||
       !mesa_db_read_header(compacted_index, &index_header) ||
       cache_header.uuid != db->uuid ||
       index_header.uuid != db->uuid)
      goto cleanup;

   hash_table_foreach(db->index_db->table, entry) {
      entries[i] = entry->data;
      entries[i]->evicted = false;
      buffer_size = MAX2(buffer_size, blob_file_size(entries[i]->size));
      i++;
   }

   util_qsort_r(entries, num_entries, sizeof(*entries),
                entry_sort_lru, db);

   for (i = 0; blob_size > 0 && i < num_entries; i++) {
      blob_size -= blob_file_size(entries[i]->size);
      entries[i]->evicted = true;
   }

   util_qsort_r(entries, num_entries, sizeof(*entries),
                entry_sort_offset, db);

   /* entry_sort_offset() may zap the database */
   if (!db->alive)
      goto cleanup;

   buffer = malloc(buffer_size);
   if (!buffer)
      goto cleanup;

   /* Mark cache file invalid by writing zero-UUID header. If compaction will
    * fail, then the file will remain to be invalid since we can't repair it. */
   if (!mesa_db_write_header(&db->cache, 0, false) ||
       !mesa_db_write_header(&db->index, 0, false))
      goto cleanup;

   /* Sync the file pointers */
   if (!mesa_db_seek(compacted_cache, ftell(db->cache.file)) ||
       !mesa_db_seek(compacted_index, ftell(db->index.file)))
      goto cleanup;

   /* Do the compaction */
   for (i = 0; i < num_entries; i++) {
      blob_size = blob_file_size(entries[i]->size);

      /* Sanity-check the cache-read offset */
      if (ftell(db->cache.file) != entries[i]->cache_db_file_offset)
         goto cleanup;

      if (entries[i]->evicted) {
         /* Jump over the evicted entry */
         if (!mesa_db_seek_cur(db->cache.file, blob_size) ||
             !mesa_db_seek_cur(db->index.file, sizeof(index_entry)))
            goto cleanup;

         compact = true;
         continue;
      }

      if (compact) {
         /* Compact the cache file */
         if (!mesa_db_read_data(db->cache.file,   buffer, blob_size) ||
             !mesa_db_cache_entry_valid(buffer) ||
             !mesa_db_write_data(compacted_cache, buffer, blob_size))
            goto cleanup;

         /* Compact the index file */
         if (!mesa_db_read(db->index.file, &index_entry) ||
             !mesa_db_index_entry_valid(&index_entry) ||
             index_entry.cache_db_file_offset != entries[i]->cache_db_file_offset ||
             index_entry.size != entries[i]->size)
            goto cleanup;

         index_entry.cache_db_file_offset = ftell(compacted_cache) - blob_size;

         if (!mesa_db_write(compacted_index, &index_entry))
            goto cleanup;
      } else {
         /* Sanity-check the cache-write offset */
         if (ftell(compacted_cache) != entries[i]->cache_db_file_offset)
            goto cleanup;

         /* Jump over the unchanged entry */
         if (!mesa_db_seek_cur(db->index.file,  sizeof(index_entry)) ||
             !mesa_db_seek_cur(compacted_index, sizeof(index_entry)) ||
             !mesa_db_seek_cur(db->cache.file,  blob_size) ||
             !mesa_db_seek_cur(compacted_cache, blob_size))
            goto cleanup;
      }
   }

   fflush(compacted_cache);
   fflush(compacted_index);

   /* Cut off the the freed space left after compaction */
   if (!mesa_db_truncate(db->cache.file, ftell(compacted_cache)) ||
       !mesa_db_truncate(db->index.file, ftell(compacted_index)))
      goto cleanup;

   /* Set the new UUID to let all cache readers know that the cache was changed */
   db->uuid = mesa_db_generate_uuid();

   if (!mesa_db_write_header(&db->cache, db->uuid, false) ||
       !mesa_db_write_header(&db->index, db->uuid, false))
      goto cleanup;

   success = true;

cleanup:
   free(buffer);
   if (compacted_index)
      fclose(compacted_index);
   if (compacted_cache)
      fclose(compacted_cache);
   free(entries);

   /* reload compacted index */
   if (success && !mesa_db_reload(db))
      success = false;

   return success;
}

bool
mesa_cache_db_open(struct mesa_cache_db *db, const char *cache_path)
{
   if (!mesa_db_open_file(&db->cache, cache_path, "mesa_cache.db"))
      return false;

   if (!mesa_db_open_file(&db->index, cache_path, "mesa_cache.idx"))
      goto close_cache;

   db->mem_ctx = ralloc_context(NULL);
   if (!db->mem_ctx)
      goto close_index;

   simple_mtx_init(&db->flock_mtx, mtx_plain);

   db->index_db = _mesa_hash_table_u64_create(NULL);
   if (!db->index_db)
      goto destroy_mtx;

   if (!mesa_db_load(db, false))
      goto destroy_hash;

   return true;

destroy_hash:
   _mesa_hash_table_u64_destroy(db->index_db);
destroy_mtx:
   simple_mtx_destroy(&db->flock_mtx);

   ralloc_free(db->mem_ctx);
close_index:
   mesa_db_close_file(&db->index);
close_cache:
   mesa_db_close_file(&db->cache);

   return false;
}

void
mesa_cache_db_close(struct mesa_cache_db *db)
{
   _mesa_hash_table_u64_destroy(db->index_db);
   simple_mtx_destroy(&db->flock_mtx);
   ralloc_free(db->mem_ctx);

   mesa_db_close_file(&db->index);
   mesa_db_close_file(&db->cache);
}

void
mesa_cache_db_set_size_limit(struct mesa_cache_db *db,
                             uint64_t max_cache_size)
{
   db->max_cache_size = max_cache_size;
}

unsigned int
mesa_cache_db_file_entry_size(void)
{
   return sizeof(struct mesa_cache_db_file_entry);
}

void *
mesa_cache_db_read_entry(struct mesa_cache_db *db,
                         const uint8_t *cache_key_160bit,
                         size_t *size)
{
   uint64_t hash = to_mesa_cache_db_hash(cache_key_160bit);
   struct mesa_cache_db_file_entry cache_entry;
   struct mesa_index_db_file_entry index_entry;
   struct mesa_index_db_hash_entry *hash_entry;
   void *data = NULL;

   if (!mesa_db_lock(db))
      return NULL;

   if (!db->alive)
      goto fail;

   if (mesa_db_uuid_changed(db) && !mesa_db_reload(db))
      goto fail_fatal;

   if (!mesa_db_update_index(db))
      goto fail_fatal;

   hash_entry = _mesa_hash_table_u64_search(db->index_db, hash);
   if (!hash_entry)
      goto fail;

   if (!mesa_db_seek(db->cache.file, hash_entry->cache_db_file_offset) ||
       !mesa_db_read(db->cache.file, &cache_entry) ||
       !mesa_db_cache_entry_valid(&cache_entry))
      goto fail_fatal;

   if (memcmp(cache_entry.key, cache_key_160bit, sizeof(cache_entry.key)))
      goto fail;

   data = malloc(cache_entry.size);
   if (!data)
      goto fail;

   if (!mesa_db_read_data(db->cache.file, data, cache_entry.size) ||
       util_hash_crc32(data, cache_entry.size) != cache_entry.crc)
      goto fail_fatal;

   if (!mesa_db_seek(db->index.file, hash_entry->index_db_file_offset) ||
       !mesa_db_read(db->index.file, &index_entry) ||
       !mesa_db_index_entry_valid(&index_entry) ||
       index_entry.cache_db_file_offset != hash_entry->cache_db_file_offset ||
       index_entry.size != hash_entry->size)
      goto fail_fatal;

   index_entry.last_access_time = os_time_get_nano();
   hash_entry->last_access_time = index_entry.last_access_time;

   if (!mesa_db_seek(db->index.file, hash_entry->index_db_file_offset) ||
       !mesa_db_write(db->index.file, &index_entry))
      goto fail_fatal;

   fflush(db->index.file);

   mesa_db_unlock(db);

   *size = cache_entry.size;

   return data;

fail_fatal:
   mesa_db_zap(db);
fail:
   free(data);

   mesa_db_unlock(db);

   return NULL;
}

bool
mesa_cache_db_entry_write(struct mesa_cache_db *db,
                          const uint8_t *cache_key_160bit,
                          const void *blob, size_t blob_size)
{
   uint64_t hash = to_mesa_cache_db_hash(cache_key_160bit);
   struct mesa_index_db_hash_entry *hash_entry = NULL;
   struct mesa_cache_db_file_entry cache_entry;
   struct mesa_index_db_file_entry index_entry;

   if (!mesa_db_lock(db))
      return false;

   if (!db->alive)
      goto fail;

   if (mesa_db_uuid_changed(db) && !mesa_db_reload(db))
      goto fail_fatal;

   if (!mesa_db_seek_end(db->cache.file))
      goto fail_fatal;

   if (ftell(db->cache.file) + blob_file_size(blob_size) > db->max_cache_size) {
      if (!mesa_db_compact(db, MAX2(blob_size, db->max_cache_size / 2)))
         goto fail_fatal;
   } else {
      if (!mesa_db_update_index(db))
         goto fail_fatal;
   }

   hash_entry = _mesa_hash_table_u64_search(db->index_db, hash);
   if (hash_entry) {
      hash_entry = NULL;
      goto fail;
   }

   if (!mesa_db_seek_end(db->cache.file) ||
       !mesa_db_seek_end(db->index.file))
      goto fail_fatal;

   memcpy(cache_entry.key, cache_key_160bit, sizeof(cache_entry.key));
   cache_entry.crc = util_hash_crc32(blob, blob_size);
   cache_entry.size = blob_size;

   index_entry.hash = hash;
   index_entry.size = blob_size;
   index_entry.last_access_time = os_time_get_nano();
   index_entry.cache_db_file_offset = ftell(db->cache.file);

   hash_entry = ralloc(db->mem_ctx, struct mesa_index_db_hash_entry);
   if (!hash_entry)
      goto fail;

   hash_entry->cache_db_file_offset = index_entry.cache_db_file_offset;
   hash_entry->index_db_file_offset = ftell(db->index.file);
   hash_entry->last_access_time = index_entry.last_access_time;
   hash_entry->size = index_entry.size;

   if (!mesa_db_write(db->cache.file, &cache_entry) ||
       !mesa_db_write_data(db->cache.file, blob, blob_size) ||
       !mesa_db_write(db->index.file, &index_entry))
      goto fail_fatal;

   fflush(db->cache.file);
   fflush(db->index.file);

   db->index.offset = ftell(db->index.file);

   _mesa_hash_table_u64_insert(db->index_db, hash, hash_entry);

   mesa_db_unlock(db);

   return true;

fail_fatal:
   mesa_db_zap(db);
fail:
   mesa_db_unlock(db);

   if (hash_entry)
      ralloc_free(hash_entry);

   return false;
}

#endif /* DETECT_OS_WINDOWS */
