#include "pak.h"
#include "aliases.h"
#include "darr.h"
#include "io.h"
#include <linux/limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PAK_MAGIC 0x4B41502054535341
#define PAK_MAJOR 1
#define PAK_MINOR 0
#define PAK_PATCH 0
#define PAK_EXT ".pak"

struct asset_pack {
  pak_t header;
  FILE *fp;
  u8 *toc;
  toc_entry_t **toc_entries;
  u64 assets_length;
  u64 assets_offset;
};

toc_entry_t *create_toc_entry(const char *name, u64 offset);
void write_toc_entry(toc_entry_t *entry, FILE *fp);
void write_toc_entries(const toc_t *toc, toc_entry_t **entries, FILE *fp);
pak_entry_t *create_pak_entry(u64 data_length);
pak_entry_t *create_pak_entry_from_file(FILE *fp);
void write_pak_entry(const pak_entry_t *entry, FILE *fp);
void write_pak(const pak_t *pak, toc_entry_t **toc_entries,
               pak_entry_t **pak_entries, FILE *fp);

bool create_asset_pack(const char *dirpath, const char *output) {
  bool created = false;

  char dst[PATH_MAX] = {0};

  u64 output_length = strlen(output);
  strncpy(dst, output, output_length);

  u64 ext_length = strlen(PAK_EXT);
  strncat(dst, PAK_EXT, ext_length);

  FILE *fp = fopen(dst, "wb");
  if (!fp) {
    goto RETURN_CREATE_ASSET_PAK;
  }

  darr_t *toc_entries_darr = NULL;
  darr_t *pak_entries_darr = NULL;

  bool initialiased =
      darr_init(&toc_entries_darr) && darr_init(&pak_entries_darr);

  if (!initialiased) {
    goto FREE_ARRAYS_CREATE_ASSET_PAK;
  }

  pak_t pak = {
      .magic = PAK_MAGIC,
      .version =
          (pak_ver_t){
              .major = PAK_MAJOR,
              .minor = PAK_MINOR,
              .patch = PAK_PATCH,
          },
      .toc =
          (toc_t){
              .count = 0,
              .size = 0,
          },
  };

  dirwalk_t *dw = NULL;
  dirwalk_result_t result = {0};

  result = walk_dir(&dw, dirpath);
  if (!(result.fp)) {
    goto FREE_ARRAYS_CREATE_ASSET_PAK;
  }

  u64 offset = 0;

  while (result.reading) {
    if (result.fp) {
      ++(pak.toc.count);

      toc_entry_t *toc_entry = create_toc_entry(result.name, offset);
      pak_entry_t *pak_entry = create_pak_entry_from_file(result.fp);

      if (!toc_entry || !pak_entry) {
        goto FREE_ARRAYS_CREATE_ASSET_PAK;
      }

      pak.toc.size += sizeof(toc_entry_t) + toc_entry->length;
      offset += sizeof(pak_entry_t) + pak_entry->size;

      darr_add(&toc_entries_darr, (void *)toc_entry);
      darr_add(&pak_entries_darr, (void *)pak_entry);
    }

    result = walk_dir(&dw, NULL);
  }

  toc_entry_t **toc_entries = (toc_entry_t **)darr_get_items(toc_entries_darr);
  pak_entry_t **pak_entries = (pak_entry_t **)darr_get_items(pak_entries_darr);

  write_pak(&pak, toc_entries, pak_entries, fp);

  created = true;

FREE_ARRAYS_CREATE_ASSET_PAK:
  darr_free(&pak_entries_darr);
  darr_free(&toc_entries_darr);

  fclose(fp);

RETURN_CREATE_ASSET_PAK:
  return created;
}

asset_pack_t *load_asset_pack(const char *filepath) {
  char full_path[PATH_MAX] = {0};

  realpath(filepath, full_path);

  asset_pack_t *pack = (asset_pack_t *)malloc(sizeof(asset_pack_t));
  if (!pack) {
    return NULL;
  }

  pack->fp = fopen(full_path, "rb");
  if (!(pack->fp)) {
    close_asset_pack(&pack);

    return NULL;
  }

  u64 file_size = get_file_length(pack->fp);

  fread((void *)(&(pack->header)), sizeof(pak_t), 1, pack->fp);

  u64 toc_size = pack->header.toc.size;

  pack->toc = (u8 *)malloc(toc_size);
  if (!(pack->toc)) {
    close_asset_pack(&pack);

    return NULL;
  }

  fread((void *)(pack->toc), toc_size, 1, pack->fp);

  u64 entries_size = sizeof(toc_entry_t *) * pack->header.toc.count;

  pack->toc_entries = (toc_entry_t **)malloc(entries_size);
  if (!(pack->toc_entries)) {
    close_asset_pack(&pack);

    return NULL;
  }

  toc_entry_t *next = (toc_entry_t *)pack->toc;

  for (u64 i = 0; i < pack->header.toc.count; ++i) {
    pack->toc_entries[i] = next;

    next = (toc_entry_t *)((u8 *)next + sizeof(toc_entry_t) + next->length);
  }

  pack->assets_offset = ftell(pack->fp);

  pack->assets_length = file_size - pack->assets_offset;

  return pack;
}

buf_t *read_file_from_pack(asset_pack_t *pack, const char *filename) {
  toc_entry_t *entry = NULL;
  buf_t *output = NULL;

  for (u64 i = 0; i < pack->header.toc.count; ++i) {
    entry = pack->toc_entries[i];

    if (STRNEQ(filename, entry->name, entry->length)) {
      fseek(pack->fp, pack->assets_offset + entry->offset, SEEK_SET);

      u64 size = 0;
      fread(&size, sizeof(u64), 1, pack->fp);

      output = (buf_t *)malloc(sizeof(buf_t) + size);
      if (!output) {
        break;
      }

      output->size = size;

      fread(&(output->data), size, 1, pack->fp);

      break;
    }
  }

  return output;
}

void close_pack_file(buf_t **buf) {
  buf_t *b = *buf;

  if (!b || b->size == 0) {
    return;
  }

  free(*buf);
  *buf = NULL;
}

void close_asset_pack(asset_pack_t **pack) {
  if (!(*pack)) {
    return;
  }

  if ((*pack)->toc_entries) {
    free((*pack)->toc_entries);
  }

  if ((*pack)->toc) {
    free((*pack)->toc);
  }

  fclose((*pack)->fp);

  free(*pack);

  *pack = NULL;
}

toc_entry_t *create_toc_entry(const char *name, u64 offset) {
  u64 length = strlen(name);
  u64 size = sizeof(toc_entry_t) + length + 1;

  toc_entry_t *entry = (toc_entry_t *)malloc(size);
  if (!entry) {
    return NULL;
  }

  memset((void *)entry, 0, size);

  entry->offset = offset;
  entry->length = length + 1;

  strncpy(entry->name, name, length);

  return entry;
}

void write_toc_entry(toc_entry_t *entry, FILE *fp) {
  fwrite((void *)entry, sizeof(toc_entry_t) + entry->length, 1, fp);
}

void write_toc_entries(const toc_t *toc, toc_entry_t **entries, FILE *fp) {
  if (!fp || !entries) {
    return;
  }

  for (u64 i = 0; i < toc->count; ++i) {
    write_toc_entry(entries[i], fp);

    if (!entries[i]) {
      continue;
    }

    free(entries[i]);

    entries[i] = NULL;
  }
}

pak_entry_t *create_pak_entry(u64 data_length) {
  pak_entry_t *entry = (pak_entry_t *)malloc(sizeof(pak_entry_t) + data_length);
  if (!entry) {
    return NULL;
  }

  memset(entry, 0, data_length);

  entry->size = data_length;

  return entry;
}

pak_entry_t *create_pak_entry_from_file(FILE *fp) {
  pak_entry_t *entry = NULL;

  if (!fp) {
    goto RETURN_PAK_ENTRY;
  }

  u64 length = get_file_length(fp) + 1;

  entry = create_pak_entry(length);
  if (!entry) {
    goto RETURN_PAK_ENTRY;
  }

  read_entire_file((void *)(entry->data), fp);

RETURN_PAK_ENTRY:
  return entry;
}

void write_pak_entry(const pak_entry_t *entry, FILE *fp) {
  fwrite((void *)entry, sizeof(pak_entry_t) + entry->size, 1, fp);
}

void write_pak(const pak_t *pak, toc_entry_t **toc_entries,
               pak_entry_t **pak_entries, FILE *fp) {
  fwrite(pak, sizeof(pak_t), 1, fp);

  write_toc_entries(&(pak->toc), toc_entries, fp);

  for (u64 i = 0; i < pak->toc.count; ++i) {
    write_pak_entry(pak_entries[i], fp);

    free(pak_entries[i]);
  }
}