#include "zip.h"
#include <string.h>

static uint16_t readu16(const uint8_t* p) { uint16_t x; memcpy(&x, p, sizeof(x)); return x; }
static uint32_t readu32(const uint8_t* p) { uint32_t x; memcpy(&x, p, sizeof(x)); return x; }

bool zip_open(zip_state* zip) {
  const uint8_t* p = zip->data + zip->size - 22;

  if (zip->size < 22 || readu32(p) != 0x06054b50) {
    return false;
  }

  zip->count = readu16(p + 10);
  zip->cursor = readu32(p + 16);
  zip->base = 0;

  if (zip->cursor + 4 > zip->size) {
    return false;
  }

  // See if the central directory starts where the endOfCentralDirectory said it would.
  // If it doesn't, then it might be a self-extracting archive with broken offsets (common).
  // In this case, assume the central directory is directly adjacent to the endOfCentralDirectory,
  // located at (offsetOfEndOfCentralDirectory (aka size - 22) - sizeOfCentralDirectory).
  // If we find a central directory there, then compute a "base" offset that equals the difference
  // between where it is and where it was supposed to be, and apply this offset to everything else.
  if (readu32(zip->data + zip->cursor) != 0x02014b50) {
    size_t offsetOfEndOfCentralDirectory = zip->size - 22;
    size_t sizeOfCentralDirectory = readu32(p + 12);
    size_t centralDirectoryOffset = offsetOfEndOfCentralDirectory - sizeOfCentralDirectory;

    if (sizeOfCentralDirectory > offsetOfEndOfCentralDirectory || centralDirectoryOffset + 4 > zip->size) {
      return false;
    }

    if (readu32(zip->data + centralDirectoryOffset) != 0x02014b50) {
      return false;
    }

    zip->base = centralDirectoryOffset - zip->cursor;
    zip->cursor = centralDirectoryOffset;
  }

  return true;
}

bool zip_next(zip_state* zip, zip_file* file) {
  const uint8_t* p = zip->data + zip->cursor;

  if (zip->cursor + 46 > zip->size || readu32(p) != 0x02014b50) {
    return false;
  }

  file->mtime = readu16(p + 12);
  file->mdate = readu16(p + 14);
  file->csize = readu32(p + 20);
  file->size = readu32(p + 24);
  file->length = readu16(p + 28);
  file->offset = readu32(p + 42) + zip->base;
  file->name = (const char*) (p + 46);
  zip->cursor += 46 + file->length + readu16(p + 30) + readu16(p + 32);
  return zip->cursor < zip->size;
}

void* zip_load(zip_state* zip, size_t offset, bool* compressed) {
  if (zip->size < 30 || offset > zip->size - 30) {
    return NULL;
  }

  uint8_t* p = zip->data + offset;
  if (readu32(p) != 0x04034b50) {
    return NULL;
  }

  uint16_t compression = readu16(p + 8);
  if (compression != 0 && compression != 8) {
    return false;
  }

  *compressed = (compression == 8);
  uint32_t skip = readu16(p + 26) + readu16(p + 28);
  return p + 30 + skip;
}
