#include <stdlib.h> // malloc
#include <string.h> // memcmp

#include "zip.h"

static struct zip_footer *zip_find_footer(char *const file, size_t const size, int *const error);
static int index_zip(struct zip_index *const restrict headers, char *const restrict file, struct zip_footer const *const restrict footer);

enum {
	ZIP_OK,
	ZIP_BIG,
	ZIP_SMALL,
	ZIP_SPLIT,
	ZIP_NO_FOOTER,
	ZIP_BAD_SIGNATURE,
	ZIP_UNSUPPORTED,
	ZIP_SIZE_MISMATCH,
	ZIP_VALUE_MISMATCH,
	ZIP_64,
};

char const *const zip_error(int const code) {
	return (char const *const []) {
		"ok",
		"probably not a zip file (file too big)",
		"not a zip file (file too smol)",
		"split archives arent supported",
		"found no zip footer",
		"bad signature",
		"unsupported configuration",
		"size mismatch",
		"central and local headers dont match",
		"zip 64 is not supported",
	} [code];
}

struct zip_file *zip_index(char *const restrict file, size_t const size, int *const restrict error) {
	int err;
	// find and verify the footer
	struct zip_footer *footer = zip_find_footer(file, size, &err);
	if (footer == NULL) {
		if (error) {
			*error = err;
		}
		return NULL;
	}
	
	// The Rest &trade;
	struct zip_file *headers = malloc(sizeof (*headers) + footer->headers_no * sizeof (*headers->files));
	err = index_zip(headers->files, file, footer);
	if (err) {
		if (error) {
			*error = err;
		}
		free(headers);
		return NULL;
	}
	headers->file_count = footer->headers_no;
	return headers;
}

static struct zip_footer *zip_find_footer(char *const file, size_t const size, int *const error) {
	// check if zip file is too big, and if size_t can store values bigger than a max* size zip file
	if ((size_t) 1 << 31 << 1 && size >= (size_t) 1 << 31 << 1) {
		*error = ZIP_BIG;
		return NULL;
	}

	// the other way around, smallest zip file is 22 bytes
	if (size < sizeof (struct zip_footer)) {
		*error = ZIP_SMALL;
		return NULL;
	}

	// scan for the footer
	for (size_t i = 0, bounds = size - sizeof (struct zip_footer) + 1; i < 65536 /* hacky limit */ && /* obligatory check */ bounds; i++, bounds--) {
		struct zip_footer *footer = (struct zip_footer *) (file + (size - sizeof (struct zip_footer) - i));
		if (
			footer->magic != ZIP_FOOTER_MAGIC               || // incorrect signature
			footer->comment_length != i                     || // incorrect comment length
			footer->headers_no_total < footer->headers_no   || // logical error
			footer->headers_addr > size                     || // metadata allegedly located after EOF
			footer->headers_addr + footer->headers_size > size // metadata ends after EOF
		) {
			continue; // not the footer / unusable, try again
		}
		if (footer->disk_no != 0 || footer->central_disk_no != 0) { // split archive
			*error = ZIP_SPLIT;
			continue;
		}
		*error = ZIP_OK;
		return footer; // 100% valid footer, safe to return
	}
	if (*error != ZIP_SPLIT) {
		*error = ZIP_NO_FOOTER;
	}
	return NULL;
}

static int index_zip(struct zip_index *const restrict headers, char *const restrict file, struct zip_footer const *const footer) {
	// needs more error checking
	char *current_header = file + footer->headers_addr;
	for (uint16_t i = 0; i < footer->headers_no; i++) {
		struct zip_central *const central = (void *) current_header;
		struct zip_header *const local = (void *) (file + central->file_addr);
		
		// magic number check
		if (central->magic != ZIP_CENTRAL_MAGIC) {
			return ZIP_BAD_SIGNATURE;
		}
		
		// zip64 detection
		if (
			central->compressed_size == 0xffffffff &&
			central->original_size == 0xffffffff &&
			central->file_disk_no == 0xffff &&
			central->file_addr == 0xffffffff
		) {
			return ZIP_64;
		}
		
		// magic check 2
		if (local->magic != ZIP_HEADER_MAGIC) {
			return ZIP_BAD_SIGNATURE;
		}

		// more zip64 detection
		if (
			local->compressed_size == 0xffffffff &&
			local->original_size == 0xffffffff
		) {
			return ZIP_64;
		}

		// various checks
		if (
			local->ver_needed != central->ver_needed                        ||
			local->flags != central->flags                                  ||
			local->method != central->method                                ||
			memcmp(&local->mtime, &central->mtime, sizeof (struct dos_date))||
			local->checksum != central->checksum                            ||
			local->compressed_size != central->compressed_size              ||
			local->original_size != central->original_size                  ||
			local->filename_length != central->filename_length              ||
			memcmp(local->filename, central->filename, local->filename_length)
		) {
			return ZIP_VALUE_MISMATCH;
		}
		
		// feature check
		if (central->flags & 0x0079) { // 0b0000'0000'0111'1001, that is encryption, data trailer, "enhanced deflating", or patches
			return ZIP_UNSUPPORTED;
		}

		headers[i].central = central;
		headers[i].header = local;
		
		headers[i].data = headers[i].header->filename + local->filename_length + local->extra_length;
		headers[i].filename = local->filename;
		headers[i].filename_length = local->filename_length;
		headers[i].crc32 = local->checksum;
		headers[i].compressed_size = local->compressed_size;
		headers[i].original_size = local->original_size;
		headers[i].method = local->method;
		headers[i].flags = local->flags;
		current_header += sizeof (struct zip_central) + central->filename_length + central->extra_length + central->comment_length;
	}
	if (current_header - footer->headers_size != file + footer->headers_addr) {
		return ZIP_SIZE_MISMATCH;
	}
	return 0;
}