#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>

#include <array>
#include <string>

static std::size_t liveAllocations = 0;

static void *Mem_Alloc( int size ) {
	void *value = std::malloc( static_cast<std::size_t>( size ) );
	if ( value != nullptr ) {
		liveAllocations++;
	}
	return value;
}

static void *Mem_ClearedAlloc( int size ) {
	void *value = std::calloc( 1, static_cast<std::size_t>( size ) );
	if ( value != nullptr ) {
		liveAllocations++;
	}
	return value;
}

static void Mem_Free( void *value ) {
	if ( value != nullptr ) {
		if ( liveAllocations == 0 ) {
			std::fputs( "free without a matching allocation\n", stderr );
			std::abort();
		}
		liveAllocations--;
	}
	std::free( value );
}

// Match wasm32's unsigned widening when this harness runs on an LP64 host.
#define LittleShort(value) static_cast<unsigned long>( static_cast<std::uint16_t>( value ) )
#define LittleLong(value) static_cast<unsigned long>( static_cast<std::uint32_t>( value ) )
#define __EMSCRIPTEN__ 1

#ifndef Q4_UNZIP_CPP
#error "Q4_UNZIP_CPP must name the patched OpenQ4 Unzip.cpp"
#endif

#include Q4_UNZIP_CPP

namespace {

struct expected_entry_t {
	const char *name;
	std::string payload;
	unsigned long position = 0;
};

[[noreturn]] void Fail( const std::string &message ) {
	std::fprintf( stderr, "unzip cache harness: %s\n", message.c_str() );
	std::exit( 1 );
}

void Require( bool condition, const std::string &message ) {
	if ( !condition ) {
		Fail( message );
	}
}

std::string MakeMiddlePayload() {
	std::string payload;
	char line[96];
	for ( int i = 0; i < 128; i++ ) {
		const int length = std::snprintf(
			line,
			sizeof( line ),
			"middle-payload-%03d:0123456789abcdef0123456789abcdef\n",
			i
		);
		Require( length > 0 && static_cast<std::size_t>( length ) < sizeof( line ),
			"could not construct the expected middle payload" );
		payload.append( line, static_cast<std::size_t>( length ) );
	}
	return payload;
}

std::array<expected_entry_t, 7> ExpectedEntries() {
	return {{
		{ "00-first.txt", "first payload\n" },
		{ "01-alpha.txt", "alpha payload\n" },
		{ "02-beta.txt", "beta payload\n" },
		{ "03-middle.txt", MakeMiddlePayload() },
		{ "04-gamma.txt", "gamma payload\n" },
		{ "05-delta.txt", "delta payload\n" },
		{ "06-last.txt", "last payload\n" },
	}};
}

void EnumerateEntries( unzFile archive, std::array<expected_entry_t, 7> &entries ) {
	unz_global_info globalInfo = {};
	Require( unzGetGlobalInfo( archive, &globalInfo ) == UNZ_OK,
		"unzGetGlobalInfo failed" );
	Require( globalInfo.number_entry == entries.size(),
		"fixture entry count does not match the expected count" );
	Require( unzGoToFirstFile( archive ) == UNZ_OK,
		"unzGoToFirstFile failed" );

	for ( std::size_t index = 0; index < entries.size(); index++ ) {
		char name[UNZ_MAXFILENAMEINZIP + 1] = {};
		unz_file_info info = {};
		Require( unzGetCurrentFileInfoPosition( archive, &entries[index].position ) == UNZ_OK,
			"unzGetCurrentFileInfoPosition failed" );
		Require( unzGetCurrentFileInfo(
			archive, &info, name, sizeof( name ), nullptr, 0, nullptr, 0 ) == UNZ_OK,
			"unzGetCurrentFileInfo failed" );
		Require( entries[index].name == std::string( name ),
			"fixture entries are not in their expected order" );
		Require( info.uncompressed_size == entries[index].payload.size(),
			"fixture payload length does not match the expected length" );
		if ( index + 1 < entries.size() ) {
			Require( unzGoToNextFile( archive ) == UNZ_OK,
				"unzGoToNextFile failed before the last fixture entry" );
		}
	}
	Require( unzGoToNextFile( archive ) == UNZ_END_OF_LIST_OF_FILE,
		"unzGoToNextFile did not stop at the end of the fixture" );
}

void SelectAndOpen( unzFile archive, const expected_entry_t &entry ) {
	char name[UNZ_MAXFILENAMEINZIP + 1] = {};
	unz_file_info info = {};
	Require( unzSetCurrentFileInfoPosition( archive, entry.position ) == UNZ_OK,
		std::string( "could not restore central-directory position for " ) + entry.name );
	Require( unzGetCurrentFileInfo(
		archive, &info, name, sizeof( name ), nullptr, 0, nullptr, 0 ) == UNZ_OK,
		std::string( "could not read central-directory metadata for " ) + entry.name );
	Require( entry.name == std::string( name ),
		std::string( "restored the wrong central-directory entry for " ) + entry.name );
	Require( info.uncompressed_size == entry.payload.size(),
		std::string( "restored the wrong payload length for " ) + entry.name );
	Require( unzOpenCurrentFile( archive ) == UNZ_OK,
		std::string( "could not open payload for " ) + entry.name );
}

void ReadAndClose( unzFile archive, const expected_entry_t &entry ) {
	std::string actual;
	std::array<char, 17> buffer = {};
	for ( ;; ) {
		const int count = unzReadCurrentFile( archive, buffer.data(), buffer.size() );
		Require( count >= 0,
			std::string( "payload read failed for " ) + entry.name );
		if ( count == 0 ) {
			break;
		}
		actual.append( buffer.data(), static_cast<std::size_t>( count ) );
	}
	Require( unzeof( archive ) == 1,
		std::string( "payload did not reach EOF for " ) + entry.name );
	Require( actual == entry.payload,
		std::string( "payload bytes do not match for " ) + entry.name );
	auto *state = static_cast<unz_s *>( archive );
	const unsigned long actualCrc = state->pfile_in_zip_read->crc32;
	const unsigned long expectedCrc = state->pfile_in_zip_read->crc32_wait;
	const int closeResult = unzCloseCurrentFile( archive );
	if ( closeResult != UNZ_OK ) {
		char detail[192];
		std::snprintf(
			detail,
			sizeof( detail ),
			"payload CRC/close failed for %s (actual=%08lx expected=%08lx result=%d)",
			entry.name,
			actualCrc,
			expectedCrc,
			closeResult
		);
		Fail( detail );
	}
}

void CheckIndependentFiles( const unz_s *first, const unz_s *second ) {
	Require( first->file != second->file,
		"reopened archives unexpectedly share the payload FILE" );
	Require( first->central_dir_file != second->central_dir_file,
		"reopened archives unexpectedly share the central-directory FILE" );
}

void RunFixture( const char *path ) {
	auto entries = ExpectedEntries();
	unzFile original = unzOpen( path );
	Require( original != nullptr, "unzOpen failed" );
	EnumerateEntries( original, entries );

	auto *originalState = static_cast<unz_s *>( original );
	Require( originalState->central_dir_cache != nullptr,
		"the web central-directory cache was not created" );
	Require( originalState->central_dir_file != nullptr,
		"the web central-directory memory stream was not created" );
	unz_central_dir_cache_s *cache = originalState->central_dir_cache;
	Require( cache->references == 1, "new cache does not have one reference" );

	unzFile first = unzReOpen( path, original );
	unzFile middle = unzReOpen( path, original );
	Require( first != nullptr && middle != nullptr,
		"unzReOpen failed while the original was live" );
	auto *firstState = static_cast<unz_s *>( first );
	auto *middleState = static_cast<unz_s *>( middle );
	Require( firstState->central_dir_cache == cache && middleState->central_dir_cache == cache,
		"reopened archives did not retain the original cache" );
	Require( cache->references == 3, "cache reference count is wrong after two reopens" );
	CheckIndependentFiles( originalState, firstState );
	CheckIndependentFiles( originalState, middleState );
	CheckIndependentFiles( firstState, middleState );

	Require( unzClose( original ) == UNZ_OK,
		"closing the original archive failed" );
	Require( cache->references == 2,
		"closing the original archive released too many cache references" );

	unzFile last = unzReOpen( path, first );
	Require( last != nullptr,
		"unzReOpen from a surviving clone failed after the original closed" );
	auto *lastState = static_cast<unz_s *>( last );
	Require( lastState->central_dir_cache == cache,
		"clone-of-clone did not retain the shared cache" );
	Require( cache->references == 3,
		"cache reference count is wrong after reopening a surviving clone" );
	CheckIndependentFiles( firstState, lastState );
	CheckIndependentFiles( middleState, lastState );

	SelectAndOpen( first, entries.front() );
	SelectAndOpen( middle, entries[entries.size() / 2] );
	SelectAndOpen( last, entries.back() );

	ReadAndClose( middle, entries[entries.size() / 2] );
	Require( unzClose( middle ) == UNZ_OK, "closing the middle clone failed" );
	Require( cache->references == 2,
		"closing the middle clone released the wrong cache reference" );

	ReadAndClose( first, entries.front() );
	Require( unzClose( first ) == UNZ_OK, "closing the first clone failed" );
	Require( cache->references == 1,
		"closing the first clone released the wrong cache reference" );

	ReadAndClose( last, entries.back() );
	Require( unzClose( last ) == UNZ_OK, "closing the last clone failed" );
	Require( liveAllocations == 0,
		"OpenQ4 unzip allocations remain after the last clone closed" );
}

} // namespace

int main( int argc, char **argv ) {
	if ( argc != 2 ) {
		std::fputs( "usage: unzip-cache-harness <fixture.zip>\n", stderr );
		return 2;
	}
	RunFixture( argv[1] );
	std::printf( "zip cache lifecycle PASS: %s\n", argv[1] );
	return 0;
}
