#include "./store.h"

#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include <msgpack.h>
#include <msgpack/fbuffer.h>

#include "util/memory/memory.h"
#include "util/mpkutil/file.h"

#include "./chunk.h"
#include "./generator.h"
#include "./poolset.h"

#define LOWORLD_STORE_PATH_MAX_LENGTH      256
#define LOWORLD_STORE_UNPACKER_BUFFER_SIZE (1024*1024)

typedef struct {
  loworld_chunk_t data;

  bool loaded;
  bool used;

  uint64_t last_tick;
} loworld_store_chunk_t;

struct loworld_store_t {
  char   path[LOWORLD_STORE_PATH_MAX_LENGTH+LOWORLD_CHUNK_FILENAME_MAX];
  size_t basepath_length;

  const loworld_poolset_t*   pools;
  const loworld_generator_t* generator;

  msgpack_unpacker unpacker;
  msgpack_unpacked unpacked;

  uint64_t tick;
  bool     error;

  size_t                chunks_length;
  loworld_store_chunk_t chunks[1];
};

static bool loworld_store_find_chunk_index_(
    const loworld_store_t* store, size_t* index, int32_t x, int32_t y) {
  assert(store != NULL);
  assert(index != NULL);

  for (size_t i = 0; i < store->chunks_length; ++i) {
    if (!store->chunks[i].loaded) continue;

    const loworld_chunk_t* chunk = &store->chunks[i].data;
    if (chunk->pos.x == x && chunk->pos.y == y) {
      *index = i;
      return true;
    }
  }
  return false;
}

static bool loworld_store_find_unused_chunk_index_(
    const loworld_store_t* store, size_t* index) {
  assert(store != NULL);
  assert(index != NULL);

  for (size_t i = 0; i < store->chunks_length; ++i) {
    if (!store->chunks[i].loaded) {
      *index = i;
      return true;
    }
  }

  bool     found  = false;
  uint64_t oldest = UINT64_MAX;
  for (size_t i = 0; i < store->chunks_length; ++i) {
    const loworld_store_chunk_t* chunk = &store->chunks[i];
    if (!chunk->used && chunk->last_tick <= oldest) {
      *index = i;
      oldest = chunk->last_tick;
      found  = true;
    }
  }
  return found;
}

/* Builds filename on the store->path as zero-terminated string. */
static void loworld_store_build_chunk_filename_(
    loworld_store_t* store, const loworld_chunk_t* chunk) {
  assert(store != NULL);
  assert(chunk != NULL);

  loworld_chunk_build_filename(chunk,
      &store->path[store->basepath_length], LOWORLD_CHUNK_FILENAME_MAX);
}

static bool loworld_store_load_chunk_from_file_(
    loworld_store_t* store, loworld_chunk_t* chunk) {
  assert(store != NULL);
  assert(chunk != NULL);

  loworld_store_build_chunk_filename_(store, chunk);

  FILE* fp = fopen(store->path, "rb");
  if (fp == NULL) return false;

  bool success = false;

  msgpack_unpacker_reset(&store->unpacker);
  if (mpkutil_file_unpack_with_unpacker(
        &store->unpacked, fp, &store->unpacker)) {
    loworld_chunk_clear(chunk);
    success = loworld_chunk_unpack(chunk, &store->unpacked.data, store->pools);
  }
  fclose(fp);

  if (!success) {
    fprintf(stderr,
        "failed to load chunk (%"PRId32", %"PRId32")\n",
        chunk->pos.x, chunk->pos.y);
  }
  return success;
}

static bool loworld_store_save_chunk_to_file_(
    loworld_store_t* store, const loworld_chunk_t* chunk) {
  assert(store != NULL);
  assert(chunk != NULL);

  loworld_store_build_chunk_filename_(store, chunk);

  FILE* fp = fopen(store->path, "wb");
  if (fp == NULL) return false;

  msgpack_packer packer;
  msgpack_packer_init(&packer, fp, msgpack_fbuffer_write);

  loworld_chunk_pack(chunk, &packer);

  const bool success = (ferror(fp) == 0);
  fclose(fp);
  return success;
}

loworld_store_t* loworld_store_new(
    const loworld_poolset_t*   pools,
    const loworld_generator_t* generator,
    size_t                     chunks_length,
    const char*                basepath,
    size_t                     basepath_length) {
  assert(pools     != NULL);
  assert(generator != NULL);
  assert(chunks_length > 0);

  if (basepath_length >= LOWORLD_STORE_PATH_MAX_LENGTH) {
    fprintf(stderr, "too long path name\n");
    abort();
  }

  loworld_store_t* store = memory_new(
      sizeof(*store) + (chunks_length-1)*sizeof(store->chunks[0]));
  *store = (typeof(*store)) {
    .basepath_length = basepath_length,
    .pools           = pools,
    .generator       = generator,
    .chunks_length   = chunks_length,
  };
  strncpy(store->path, basepath, LOWORLD_STORE_PATH_MAX_LENGTH);

  if (!msgpack_unpacker_init(
        &store->unpacker, LOWORLD_STORE_UNPACKER_BUFFER_SIZE)) {
    fprintf(stderr, "failed to initialize unpacker\n");
    abort();
  }
  msgpack_unpacked_init(&store->unpacked);

  for (size_t i = 0; i < store->chunks_length; ++i) {
    loworld_store_chunk_t* chunk = &store->chunks[i];
    *chunk = (typeof(*chunk)) {0};
    loworld_chunk_initialize(&chunk->data);
  }
  return store;
}

void loworld_store_delete(loworld_store_t* store) {
  if (store == NULL) return;

  msgpack_unpacker_destroy(&store->unpacker);
  msgpack_unpacked_destroy(&store->unpacked);

  for (size_t i = 0; i < store->chunks_length; ++i) {
    loworld_chunk_deinitialize(&store->chunks[i].data);
  }
  memory_delete(store);
}

loworld_chunk_t* loworld_store_load_chunk(
    loworld_store_t* store, int32_t chunk_x, int32_t chunk_y) {
  assert(store != NULL);

  size_t index;
  if (!loworld_store_find_chunk_index_(store, &index, chunk_x, chunk_y)) {
    if (!loworld_store_find_unused_chunk_index_(store, &index)) {
      fprintf(stderr, "world store chunk overflow\n");
      abort();
    }
  }

  loworld_store_chunk_t* chunk = &store->chunks[index];

  if (chunk->loaded &&
      (chunk->data.pos.x != chunk_x || chunk->data.pos.y != chunk_y)) {
    assert(!chunk->used);
    store->error = !loworld_store_save_chunk_to_file_(store, &chunk->data);
    chunk->loaded = false;
  }
  if (!chunk->loaded) {
    chunk->data.pos.x = chunk_x;
    chunk->data.pos.y = chunk_y;

    if (!loworld_store_load_chunk_from_file_(store, &chunk->data)) {
      loworld_chunk_clear(&chunk->data);
      loworld_generator_generate(store->generator, &chunk->data);
    }
    chunk->loaded = true;
  }

  chunk->used       = true;
  chunk->last_tick  = store->tick++;
  return &chunk->data;
}

void loworld_store_unload_chunk(
    loworld_store_t* store, loworld_chunk_t* chunk) {
  assert(store != NULL);
  assert(chunk != NULL);

  loworld_store_chunk_t* c = (typeof(c)) chunk;
  assert(store->chunks <= c && c < store->chunks+store->chunks_length);

  c->used = false;
}

void loworld_store_flush(loworld_store_t* store) {
  assert(store != NULL);

  for (size_t i = 0; i < store->chunks_length; ++i) {
    loworld_store_chunk_t* chunk = &store->chunks[i];
    if (!chunk->loaded) continue;

    store->error = !loworld_store_save_chunk_to_file_(store, &chunk->data);
  }
}

bool loworld_store_is_error_happened(const loworld_store_t* store) {
  assert(store != NULL);

  return store->error;
}
