#include "psf2fs.h"

#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#include <zlib.h>

/////////////////////////////////////////////////////////////////////////////

#define MYMAXPATH (1024)

struct SOURCE_FILE {
  uint8_t * reserved_data;
  int reserved_size;
  struct SOURCE_FILE *next;
};

struct DIR_ENTRY {
  char name[37];
  struct DIR_ENTRY *subdir;
  int length;
  int block_size;
  struct SOURCE_FILE *source;
  int *offset_table;
  struct DIR_ENTRY *next;
};

struct CACHEBLOCK {
  struct SOURCE_FILE *from_source;
  int   from_offset;
  char *uncompressed_data;
  int   uncompressed_size;
};

struct PSF2FS {
  struct SOURCE_FILE *sources;
  struct DIR_ENTRY *dir;
  struct CACHEBLOCK cacheblock;

  int adderror;
};

/////////////////////////////////////////////////////////////////////////////

static void source_cleanup_free(struct SOURCE_FILE *source) {
  while(source) {
    struct SOURCE_FILE *next = source->next;
    if(source->reserved_data) free( source->reserved_data );
    free( source );
    source = next;
  }
}

static void dir_cleanup_free(struct DIR_ENTRY *dir) {
  while(dir) {
    struct DIR_ENTRY *next = dir->next;
    if(dir->subdir) dir_cleanup_free(dir->subdir);
    free( dir );
    dir = next;
  }
}

static void cache_cleanup(struct CACHEBLOCK *cacheblock) {
  if(!cacheblock) return;
  if(cacheblock->uncompressed_data) free( cacheblock->uncompressed_data );
}

/////////////////////////////////////////////////////////////////////////////

void *psf2fs_create(void) {
  struct PSF2FS *fs;
  fs = ( struct PSF2FS * ) malloc( sizeof( struct PSF2FS ) );
  if(!fs) return NULL;
  memset(fs, 0, sizeof(struct PSF2FS));
  return fs;
}

/////////////////////////////////////////////////////////////////////////////

void psf2fs_delete(void *psf2fs) {
  struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
  if(fs->sources) source_cleanup_free(fs->sources);
  if(fs->dir) dir_cleanup_free(fs->dir);
  cache_cleanup(&(fs->cacheblock));
  free( fs );
}

/////////////////////////////////////////////////////////////////////////////

static int isdirsep(char c) { return (c == '/' || c == '\\' || c == '|' || c == ':'); }

/////////////////////////////////////////////////////////////////////////////

static unsigned read32lsb(const uint8_t * foo) {
  return (
    ((foo[0] & 0xFF) <<  0) |
    ((foo[1] & 0xFF) <<  8) |
    ((foo[2] & 0xFF) << 16) |
    ((foo[3] & 0xFF) << 24)
  );
}

/////////////////////////////////////////////////////////////////////////////

static int __memicmp(const char * a, const char * b, int length)
{
  int o, p;
  for (o = 0; o < length; o++) {
    p = tolower(a[o]) - tolower(b[o]);
    if (p) return p;
  }
  return 0;
}

/////////////////////////////////////////////////////////////////////////////

static struct DIR_ENTRY *finddirentry(
  struct DIR_ENTRY *dir,
  const char *name,
  int name_l
) {
  if(name_l > 36) return NULL;
  while(dir) {
    if(!__memicmp(dir->name, name, name_l) && dir->name[name_l] == 0) return dir;
    dir = dir->next;
  }
  return NULL;
}

/////////////////////////////////////////////////////////////////////////////
//
// Make a DIR_ENTRY list for a given file and Reserved offset.
// Recurses subdirectories also.
// All entries are set to point to the given SOURCE_FILE.
//
static struct DIR_ENTRY *makearchivedir(
  struct PSF2FS *fs,
  int offset,
  struct SOURCE_FILE *source
) {
  struct DIR_ENTRY *dir = NULL;
  const uint8_t *file = source->reserved_data;
  int n, num;
  if(offset < 0) goto corrupt;
  if(offset >= source->reserved_size) { goto corrupt; }
  if((offset + 4) > source->reserved_size) { goto corrupt; }
  num = read32lsb(file + offset);
  offset += 4;
  if(num < 0) goto corrupt;
  for(n = 0; n < num; n++) {
    int o, u, b;
    if((offset + 48) > source->reserved_size) { goto corrupt; }
    { struct DIR_ENTRY *entry = ( struct DIR_ENTRY * ) malloc( sizeof( struct DIR_ENTRY ) );
      if(!entry) goto outofmemory;
      memset(entry, 0, sizeof(struct DIR_ENTRY));
      entry->next = dir;
      dir = entry;
    }
    memcpy(dir->name, file + offset, 36);
    o = read32lsb(file + offset + 36);
    u = read32lsb(file + offset + 40);
    b = read32lsb(file + offset + 44);
    offset += 48;
    if(o < 0) goto corrupt;
    if(u < 0) goto corrupt;
    if(b < 0) goto corrupt;
    if(o && o < offset) {
//      char s[100];
//      sprintf(s,"q[o=%08X offset=%08X]",o,offset);
//      errormessageadd(fs, s);
      goto corrupt;
    }
    // if this new entry describes a subdirectory:
    if(u == 0 && b == 0 && o != 0) {
      dir->subdir = makearchivedir(fs, o, source);
      if(fs->adderror) goto error;
//      if(!dir->subdir) goto error;
    // if this new entry describes a zero-length file:
    } else if(u == 0 || b == 0 || o == 0) {
      // fields were zero anyway
    // if this new entry describes a real source file:
    } else {
      int i;
      int blocks = (u + (b-1)) / b;
      int dataofs = o + 4 * blocks;
      if(dataofs >= source->reserved_size) { goto corrupt; }
      // record the info
      dir->length = u;
      dir->block_size = b;
      dir->source = source;
      dir->offset_table = (int *) malloc( ( blocks + 1 ) * sizeof( int ) );
      if(!dir->offset_table) goto outofmemory;
      for(i = 0; i < blocks; i++) {
        int cbs;
        if((o + 4) > source->reserved_size) { goto corrupt; }
        cbs = read32lsb(file + o);
        o += 4;
        dir->offset_table[i] = dataofs;
        dataofs += cbs;
      }
      dir->offset_table[i] = dataofs;
    }
  }
//success:
  return dir;

corrupt:
  goto error;
outofmemory:
  goto error;
error:
  dir_cleanup_free(dir);
  fs->adderror = 1;
  return NULL;
}

/////////////////////////////////////////////////////////////////////////////
//
// Merge two SOURCE_FILE lists.
// Guaranteed to succeed and not to free anything.
//
static struct SOURCE_FILE *mergesource(
  struct SOURCE_FILE *to,
  struct SOURCE_FILE *from
) {
  struct SOURCE_FILE *to_tail;
  if(!to && !from) return NULL;
  if(!to) {
    struct SOURCE_FILE *t;
    t = to; to = from; from = t;
  }
  to_tail = to;
  while(to_tail->next) { to_tail = to_tail->next; }
  to_tail->next = from;
  return to;
}

/////////////////////////////////////////////////////////////////////////////
//
// Merge two DIR_ENTRY lists.
// Guaranteed to succeed. May free some structures.
//
static struct DIR_ENTRY *mergedir(
  struct DIR_ENTRY *to,
  struct DIR_ENTRY *from
) {
  // will traverse "from", and add to "to".
  while(from) {
    struct DIR_ENTRY *entry_to;
    struct DIR_ENTRY *entry_from;
    entry_from = from;
    from = from->next;
    // delink entry_from
    entry_from->next = NULL;
    // look for a duplicate entry in "to"
    entry_to = finddirentry(to, entry_from->name, (int)strlen(entry_from->name));
    // if there is one, do something fancy and then free entry_from.
    if(entry_to) {
      // if both are subdirs, merge the subdirs
      if((entry_to->subdir) && (entry_from->subdir)) {
        entry_to->subdir = mergedir(entry_to->subdir, entry_from->subdir);
        entry_from->subdir = NULL;
      // if both are files, copy over the info
      } else if((!(entry_to->subdir)) && (!(entry_from->subdir))) {
        entry_to->length = entry_from->length;
        entry_to->block_size = entry_from->block_size;
        entry_to->source = entry_from->source;
        if(entry_to->offset_table) free( entry_to->offset_table );
        entry_to->offset_table = entry_from->offset_table;
        entry_from->offset_table = NULL;
      // if one's a subdir but the other's not, we lose "from". this is fine.
      }
      dir_cleanup_free(entry_from);
      entry_from = NULL;
    // otherwise, just relink to the top of "to"
    } else {
      entry_from->next = to;
      to = entry_from;
    }
  }
  return to;
}

/////////////////////////////////////////////////////////////////////////////
//
// only modifies *psource and *pdir on success
//
static int addarchive(
  struct PSF2FS *fs,
  const uint8_t *reserved_data,
  int reserved_size,
  struct SOURCE_FILE **psource,
  struct DIR_ENTRY **pdir
) {
  struct SOURCE_FILE *source = *psource;
  struct DIR_ENTRY *dir = *pdir;
  // these relate to the current file
  struct SOURCE_FILE *this_source = NULL;
  struct DIR_ENTRY *this_dir = NULL;

  // default to no error
  fs->adderror = 0;

  // create a source entry for this psf2
  this_source = ( struct SOURCE_FILE * ) malloc( sizeof( struct SOURCE_FILE ) );
  if(!this_source) goto outofmemory;
  this_source->next = NULL;
  this_source->reserved_data = ( uint8_t * ) malloc( reserved_size );
  if(!this_source->reserved_data) goto outofmemory;
  memcpy(this_source->reserved_data, reserved_data, reserved_size);
  this_source->reserved_size = reserved_size;
  this_dir = makearchivedir(fs, 0, this_source);
  if(fs->adderror) goto error;

  // success
  // now merge everything
  *psource = mergesource(source, this_source);
  *pdir = mergedir(dir, this_dir);
//success:
  return 0;

outofmemory:
  goto error;
error:
  if(dir) dir_cleanup_free(dir);
  if(source) source_cleanup_free(source);
  if(this_dir) dir_cleanup_free(this_dir);
  if(this_source) source_cleanup_free(this_source);
  return -1;
}

/////////////////////////////////////////////////////////////////////////////
//
//
//
int psf2fs_load_callback(void * psf2fs, const uint8_t * exe, size_t exe_size,
                                  const uint8_t * reserved, size_t reserved_size) {
  struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
  (void)exe;
  (void)exe_size;
  return addarchive(fs, reserved, (int)reserved_size, &(fs->sources), &(fs->dir));
}

/////////////////////////////////////////////////////////////////////////////
//
//
//
static int virtual_read(struct PSF2FS *fs, struct DIR_ENTRY *entry, int offset, char *buffer, int length) {
  int length_read = 0;
  int r;
  unsigned long destlen;
  if(offset >= entry->length) return 0;
  if((offset + length) > entry->length) length = entry->length - offset;
  while(length_read < length) {
    // get info on the current block
    int blocknum = offset / entry->block_size;
    int ofs_within_block = offset % entry->block_size;
    int canread;
    int block_zofs  = entry->offset_table[blocknum];
    int block_zsize = entry->offset_table[blocknum+1] - block_zofs;
    int block_usize;
    if(block_zofs <= 0 || block_zofs >= entry->source->reserved_size) goto bounds;
    if((block_zofs+block_zsize) > entry->source->reserved_size) goto bounds;

    // get the actual uncompressed size of this block
    block_usize = entry->length - (blocknum * entry->block_size);
    if(block_usize > entry->block_size) block_usize = entry->block_size;

    // if it's not already in the cache block, read it
    if(
      (fs->cacheblock.from_offset != block_zofs) ||
      (fs->cacheblock.from_source != entry->source)
    ) {
      // invalidate cache without freeing buffer
      fs->cacheblock.from_source = NULL;

      // make sure there's a buffer allocated
      // but only reallocate if the size is different
      if(fs->cacheblock.uncompressed_size != block_usize) {
        fs->cacheblock.uncompressed_size = 0;
        if(fs->cacheblock.uncompressed_data) {
          free( fs->cacheblock.uncompressed_data );
          fs->cacheblock.uncompressed_data = NULL;
        }
        fs->cacheblock.uncompressed_data = ( char * ) malloc( block_usize );
        if(!fs->cacheblock.uncompressed_data) goto outofmemory;
        fs->cacheblock.uncompressed_size = block_usize;
      }
      destlen = block_usize;
      // attempt decompress
      r = uncompress((unsigned char *) fs->cacheblock.uncompressed_data, &destlen, (const unsigned char *) entry->source->reserved_data + block_zofs, block_zsize);
      if(r != Z_OK || destlen != block_usize) {
//        char s[999];
//        sprintf(s,"zdata=%02X %02X %02X blockz=%d blocku=%d destlenout=%d", zdata[0], zdata[1], zdata[2], block_zsize, block_usize, destlen);
//        errormessageadd(fs, s);
        goto error;
      }
    }

    // at this point, we can read whatever we want out of the cacheblock
    canread = fs->cacheblock.uncompressed_size - ofs_within_block;
    if(canread > (length - length_read)) canread = length - length_read;

    // copy
    memcpy(buffer, fs->cacheblock.uncompressed_data + ofs_within_block, canread);

    // advance pointers/counters
    offset += canread;
    length_read += canread;
    buffer += canread;
  }

//success:
  return length_read;

bounds:
  goto error;
outofmemory:
  goto error;
error:
  // if cacheblock was invalidated, we can free it
  if(!fs->cacheblock.from_source) {
    fs->cacheblock.uncompressed_size = 0;
    if(fs->cacheblock.uncompressed_data) {
      free( fs->cacheblock.uncompressed_data );
      fs->cacheblock.uncompressed_data = NULL;
    }
  }
  return -1;
}

/////////////////////////////////////////////////////////////////////////////
//
//
//
int psf2fs_virtual_readfile(void *psf2fs, const char *path, int offset, char *buffer, int length) {
  struct PSF2FS *fs = (struct PSF2FS*)psf2fs;
  struct DIR_ENTRY *entry = fs->dir;


  if(!path) goto invalidarg;
  if(offset < 0) goto invalidarg;
  if(!buffer) goto invalidarg;
  if(length < 0) goto invalidarg;

  for(;;) {
    int l;
    int need_dir;
    if(!entry) goto pathnotfound;
    while(isdirsep(*path)) path++;
    for(l = 0;; l++) {
      if(!path[l]) { need_dir = 0; break; }
      if(isdirsep(path[l])) { need_dir = 1; break; }
    }
    entry = finddirentry(entry, path, l);
    if(!entry) goto pathnotfound;
    if(!need_dir) break;
    entry = entry->subdir;
    path += l;
  }

  // if we "found" a file but it's a directory, then we didn't find it
  if(entry->subdir) goto pathnotfound;

  // special case: if requested length is 0, return the total file length
  if(!length) return entry->length;

  // otherwise, read from source
  return virtual_read(fs, entry, offset, buffer, length);

pathnotfound:
  goto error;
invalidarg:
  goto error;
error:
  return -1;
}

/////////////////////////////////////////////////////////////////////////////
