/* OpenCP Module Player
 * copyright (c) 2020-'26 Stian Sebastian Skjelstad <stian.skjelstad@gmail.com>
 *
 * Archive Meta DataBase (replaces previous adb.c Archive DataBase)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "config.h"
#include <assert.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "types.h"
#include "adbmeta.h"
#include "boot/psetting.h"
#include "stuff/file.h"
#include "stuff/utf-16.h"

#ifndef ADBMETA_SILENCE_OPEN_ERRORS
 #define ADBMETA_SILENCE_OPEN_ERRORS 0
#endif

#ifdef CFDATAHOMEDIR_OVERRIDE
# define CFDATAHOMEDIR CFDATAHOMEDIR_OVERRIDE
#else
# define CFDATAHOMEDIR configAPI->DataHomePath
#endif

const char adbMetaTag[16] = "OCPArchiveMeta\x1b\x00";
/*
 16 bytes header
  4 bytes entries#N

  X bytes FILENAME\0
  X bytes SIG\0
  8 bytes filesize
  4 bytes datasize
  X bytes data

 */

struct adbMetaHeader
{
	char Signature[16];
	uint32_t entries;
};

struct adbMetaEntry_t
{
	char          *filename;
	uint64_t       filesize;
	char          *SIG;
	uint32_t       datasize;
	unsigned char *data;
};

static osfile                 *adbMetaFile;
static struct adbMetaEntry_t **adbMetaEntries;
static uint_fast32_t           adbMetaCount;
static uint_fast32_t           adbMetaSize; /* slots allocated */
static uint8_t                 adbMetaDirty;

static struct adbMetaEntry_t *adbMetaInit_CreateBlob (const char          *filename,
                                                      uint64_t             filesize,
                                                      const char          *signature,
                                                      const unsigned char *data,
                                                      uint32_t             datasize)
{
	struct adbMetaEntry_t *retval;

	long filename_length_sizeof = strlen (filename) + 1;
	long signature_length_sizeof = strlen (signature) + 1;
#if 0
/* the famous standard power-of-two rounding up */
#define _LONGSIZE(n) ((n + sizeof(long) - 1) & ~(sizeof(long) - 1))
	filename_length_sizeof = _LONGSIZE (filename_length_sizeof + 1);
	signature_length_sizeof = _LONGSIZE (signature_length_sizeof + 1);
#endif
	retval = calloc (sizeof (*retval) +
                         filename_length_sizeof +
                         signature_length_sizeof +
                         datasize, 1); /* last one is rounded up up malloc() if needed */

	if (!retval)
	{
		return 0;
	}

#ifdef ADBMETA_DEBUG
	fprintf (stderr, "adbMetaInit_CreateBlob (\"%s\", %"PRIu64", \"%s\", %p, %"PRId32")\n", filename, filesize, signature, data, datasize);
#endif

	retval->filename = (char *)&(retval[1]);
	retval->filesize = filesize;
	retval->SIG      = retval->filename + filename_length_sizeof;
	retval->data     = (unsigned char *)(retval->SIG) + signature_length_sizeof;
	retval->datasize = datasize;

	strcpy (retval->filename, filename);
	strcpy (retval->SIG, signature);
	memcpy (retval->data, data, datasize);

	return retval;
}

static int adbMetaInit_ParseFd (osfile *f)
{
	/* record counter */
	uint_fast32_t counter;
	/* read buffer */
	uint_fast32_t fill;
	uint_fast32_t size;
	uint8_t *data;

	fill = 0;
	size = 65536;
	data = malloc (size);
	if (!data)
	{
		fprintf (stderr, "adbMetaInit: malloc() readbuffer failed\n");
		return -1;
	}

	for (counter = 0; counter < adbMetaSize; counter++)
	{
		uint_fast32_t offset;
		uint_fast64_t filesize;
		uint_fast32_t datasize;

		/* char *filename == data */
		char *signature;

#ifdef ADBMETA_DEBUG
		fprintf (stderr, "adbMeta: attempt to parse entry %d/%d\n", (int)counter + 1, (int)adbMetaSize);
#endif
		if (fill < 16)
		{
			int result;
fillmore:
			if (fill == size)
			{
				uint8_t *temp;
				size += 65536;
				temp = realloc (data, size);
				if (!temp)
				{
					fprintf (stderr, "realloc() readbuffer failed\n");
					adbMetaCount = counter;
					free (data);
					return 1;
				}
#ifdef ADBMETA_DEBUG
				fprintf (stderr, "adbMeta: increased buffer size up to %d bytes\n", (int)size);
#endif
				data = temp;
			}
			result = size - fill;
			if (result > 65536) result = 65536;
			result = osfile_read (f, data + fill, result);
			if (result < 0)
			{
				adbMetaCount = counter;
				free (data);
				return 1;
			}
			if (result == 0)
			{
				fprintf (stderr, "ran out of data\n");
				adbMetaCount = counter;
				free (data);
				return 1;
			}
			fill += result;
#ifdef ADBMETA_DEBUG
			fprintf (stderr, "adbMeta: input buffer is filled with %d (of %d possible)\n", (int)fill, (int)size);
#endif
		}

/* filename always start at data + 0 */
		for (offset = 0; ; offset++)
		{
			if (offset >= fill)
			{
				goto fillmore;
			}
			if (data[offset])
			{
				continue;
			}
			//filename_length = offset;
			offset++;
			break;
		}
		signature = (char *)data + offset;

		for (; ; offset++)
		{
			if (offset >= fill)
			{
				goto fillmore;
			}
			if (data[offset])
			{
				continue;
			}
			//signature_length = (data + offset) - signature;
			offset++;
			break;
		}

		if (offset + 12 > fill)
		{
			goto fillmore;
		}
		filesize = ((uint_fast64_t)data[offset+0] << 56) |
		           ((uint_fast64_t)data[offset+1] << 48) |
		           ((uint_fast64_t)data[offset+2] << 40) |
		           ((uint_fast64_t)data[offset+3] << 32) |
		           ((uint_fast64_t)data[offset+4] << 24) |
		           ((uint_fast64_t)data[offset+5] << 16) |
		           ((uint_fast64_t)data[offset+6] << 8) |
		           ((uint_fast64_t)data[offset+7]);
		offset += 8;
		datasize = ((uint_fast32_t)data[offset+0] << 24) |
		           ((uint_fast32_t)data[offset+1] << 16) |
		           ((uint_fast32_t)data[offset+2] << 8) |
		           ((uint_fast32_t)data[offset+3]);
		offset += 4;

		if (offset + datasize > fill)
		{
			goto fillmore;
		}

		adbMetaEntries[counter] = adbMetaInit_CreateBlob ((char *)data,
		                                                  filesize,
		                                                  signature,
		                                                  data + offset,
		                                                  datasize);
		if (!adbMetaEntries[counter])
		{
			fprintf (stderr, "adbMetaInit: failed to allocate memory for entry #%ld\n", (long)counter);
			adbMetaCount = counter;
			free (data);
			return -1;
		}

		offset += datasize;

		memmove (data, data + offset, fill - offset);
		fill -= offset;
	}
	adbMetaCount = counter;
	free (data);

	return 0;
}

static int adbMeta_cmp (const void *_a, const void *_b)
{
	const struct adbMetaEntry_t * const *a = _a;
	const struct adbMetaEntry_t * const *b = _b;

	if (a[0]->filesize > b[0]->filesize) return 1;
	if (a[0]->filesize < b[0]->filesize) return -1;
	return 0;
}

int adbMetaInit (const struct configAPI_t *configAPI)
{
	int retval;
	struct adbMetaHeader header;
	char *adbMetaPath;

	if (adbMetaFile)
	{
		fprintf (stderr, "adbMetaInit: Already loaded\n");
		return 1;
	}

	adbMetaPath = malloc ( strlen(CFDATAHOMEDIR) + 13 + 1);
	if (!adbMetaPath)
	{
		fprintf (stderr, "adbMetaInit: malloc() failed\n");
		return 1;
	}
	sprintf (adbMetaPath, "%sCPARCMETA.DAT", CFDATAHOMEDIR);

#ifdef _WIN32
	uint16_t *wadbMetaPath = utf8_to_utf16 (adbMetaPath);
	fwprintf(stderr, L"Loading %ls .. ", wadbMetaPath);
	free (wadbMetaPath);
#else
	fprintf(stderr, "Loading %s .. ", adbMetaPath);
#endif

	adbMetaFile = osfile_open_readwrite (adbMetaPath, 1, 0);
	free (adbMetaPath);
	adbMetaPath = 0;
	if (!adbMetaFile)
	{
		if (!ADBMETA_SILENCE_OPEN_ERRORS)
		{
			fprintf(stderr, "adbMetaInit: open(DataHomeDir/CPARCMETA.DAT) failed\n");
		}
		return 1;
	}

	if (osfile_read (adbMetaFile, &header, sizeof (header)) != sizeof (header))
	{
		fprintf (stderr, "No header - empty file\n");
		return 1;
	}

	if (memcmp (header.Signature, adbMetaTag, 16))
	{
		fprintf (stderr, "Invalid header\n");
		return 1;
	}

	adbMetaSize = uint32_big (header.entries);
	if (!adbMetaSize)
	{
		fprintf (stderr, "Empty - no entries\n");
		return 0;
	}
	adbMetaEntries = malloc (sizeof (adbMetaEntries[0]) * adbMetaSize);
	if (!adbMetaEntries)
	{
		fprintf (stderr, "malloc() failed\n");
		return 1;
	}

	retval = adbMetaInit_ParseFd (adbMetaFile);

	if (adbMetaCount > 1)
	{
		qsort (adbMetaEntries, adbMetaCount, sizeof (adbMetaEntries[0]), adbMeta_cmp);
	}

	osfile_purge_readahead_cache (adbMetaFile);

	fprintf (stderr, "Done\n");

	return retval;
}

void adbMetaCommit (void)
{
	struct adbMetaHeader header;
	/* record counter */
	uint_fast32_t counter;

	memcpy (header.Signature, adbMetaTag, sizeof (adbMetaTag));
	header.entries = uint32_big (adbMetaCount);

	if ((!adbMetaDirty) || (!adbMetaFile))
	{
		return;
	}

	osfile_setpos (adbMetaFile, 0);
	if (osfile_write (adbMetaFile, &header, sizeof (header)) < 0) { fprintf (stderr, "adbMetaCommit write failed #1\n"); return; };

	for (counter = 0; counter < adbMetaCount; counter++)
	{
		uint8_t buffer[12];
		if (osfile_write (adbMetaFile, adbMetaEntries[counter]->filename, strlen (adbMetaEntries[counter]->filename) + 1) < 0) { fprintf (stderr, "adbMetaCommit write failed #2\n"); return; };
		if (osfile_write (adbMetaFile, adbMetaEntries[counter]->SIG,      strlen (adbMetaEntries[counter]->SIG)      + 1) < 0) { fprintf (stderr, "adbMetaCommit write failed #3\n"); return; };
		buffer[ 0] = adbMetaEntries[counter]->filesize >> 56;
		buffer[ 1] = adbMetaEntries[counter]->filesize >> 48;
		buffer[ 2] = adbMetaEntries[counter]->filesize >> 40;
		buffer[ 3] = adbMetaEntries[counter]->filesize >> 32;
		buffer[ 4] = adbMetaEntries[counter]->filesize >> 24;
		buffer[ 5] = adbMetaEntries[counter]->filesize >> 16;
		buffer[ 6] = adbMetaEntries[counter]->filesize >> 8;
		buffer[ 7] = adbMetaEntries[counter]->filesize;
		buffer[ 8] = adbMetaEntries[counter]->datasize >> 24;
		buffer[ 9] = adbMetaEntries[counter]->datasize >> 16;
		buffer[10] = adbMetaEntries[counter]->datasize >> 8;
		buffer[11] = adbMetaEntries[counter]->datasize;
		if (osfile_write (adbMetaFile, buffer, 12) < 0) { fprintf (stderr, "adbMetaCommit write failed #4\n"); return; };
		if (osfile_write (adbMetaFile, adbMetaEntries[counter]->data, adbMetaEntries[counter]->datasize) < 0) { fprintf (stderr, "adbMetaCommit write failed #5\n"); return; };
	}

	adbMetaDirty = 0;
}

void adbMetaClose (void)
{
	int i;
	adbMetaCommit();
	for (i=0; i < adbMetaCount; i++)
	{
		free (adbMetaEntries[i]);
		adbMetaEntries[i]=0;
	}
	free (adbMetaEntries);
	adbMetaEntries = 0;
	adbMetaCount = adbMetaSize = 0;
	adbMetaDirty = 0;
	if (adbMetaFile)
	{
		osfile_close (adbMetaFile);
		adbMetaFile = 0;
	}
}

static uint32_t adbMetaBinarySearchFilesize (const uint64_t filesize)
{
	uint_fast32_t searchbase = 0, searchlen = adbMetaCount;
//	int i;
//	fprintf (stderr, "\n SEARCH %"PRIu64"..\n", filesize);

	if (adbMetaCount == 0)
	{
		return 0;
	}

	while (searchlen > 1)
	{
		uint_fast32_t halfmark;

		halfmark = searchlen >> 1;

//		for (i=0; i < searchlen; i++)
//		{
//			fprintf (stderr, " %"PRIu64"(%d)%s ", adbMetaEntries[searchbase + i]->filesize, (int)(searchbase + i), (i == halfmark)?"*":"");
//		}
//		fprintf (stderr, "\n");

		if (filesize > adbMetaEntries[searchbase + halfmark]->filesize)
		{
			searchbase += halfmark;
			searchlen -= halfmark;
		} else {
			searchlen = halfmark;
		}
	}

//	for (i=0; i < searchlen; i++)
//	{
//		fprintf (stderr, " %"PRIu64"(%d) ", adbMetaEntries[searchbase + i]->filesize, (int)(searchbase + i));
//	}
//	fprintf (stderr, "\n");

	/* fine-tune the position */
	if (searchbase < adbMetaCount)
	{
/*
		while (searchbase && (adbMetaEntries[searchbase]->filesize == filesize))
		{
			searchbase--;
		}
*/
		if (adbMetaEntries[searchbase]->filesize < filesize)
		{
//			fprintf (stderr, "Finetune += 1\n");
			searchbase++;
		}
	}
//	fprintf (stderr, "\n");

	return searchbase;
}

int adbMetaAdd (const char *filename, const uint64_t filesize, const char *SIG, const unsigned char *data, const uint32_t datasize)
{
	uint_fast32_t searchindex = adbMetaBinarySearchFilesize (filesize);
	int_fast32_t search;
	struct adbMetaEntry_t *temp;

#ifdef ADBMETA_DEBUG
	fprintf (stderr, "adbMetaAdd (\"%s\", %"PRIu64", \"%s\", 0x%p, %"PRIu32")\n", filename, filesize, SIG, data, datasize);
#endif

	if (searchindex == adbMetaCount)
	{
		goto DoInsert;
	}

	assert (adbMetaEntries[searchindex]->filesize >= filesize);

	assert (datasize);

	if (adbMetaEntries[searchindex]->filesize > filesize)
	{
		goto DoInsert;
	}

	for (search = searchindex; search < adbMetaCount; search++)
	{
		if (adbMetaEntries[search]->filesize != filesize)
		{
			break;
		}
		if (strcmp (adbMetaEntries[search]->filename, filename))
		{
			continue;
		}
		if (strcmp (adbMetaEntries[search]->SIG, SIG))
		{
			continue;
		}
		if ((adbMetaEntries[search]->datasize == datasize) && (!memcmp (adbMetaEntries[search]->data, data, datasize)))
		{
			return 0;
		}

		/* replace the entry */
		temp = adbMetaInit_CreateBlob (filename, filesize, SIG, data, datasize);
		if (!temp)
		{
			fprintf (stderr, "adbMetaAdd: error allocating memory for an entry\n");
			return -1;
		}
		free (adbMetaEntries[search]);
		adbMetaEntries[search] = temp;

		adbMetaDirty = 1;
		return 0;
	}

DoInsert:
	if (adbMetaCount >= adbMetaSize)
	{
		struct adbMetaEntry_t **r;
		r = realloc (adbMetaEntries, (adbMetaSize + 8) * sizeof (adbMetaEntries[0]));
		if (!r)
		{
			fprintf (stderr, "adbMetaAdd: error allocating memory for index\n");
			return -1;
		}
		adbMetaEntries = r;
		adbMetaSize += 8;
	}

	temp = adbMetaInit_CreateBlob (filename, filesize, SIG, data, datasize);
	if (!temp)
	{
		fprintf (stderr, "adbMetaAdd: error allocating memory for an entry\n");
		return -1;
	}
	memmove (adbMetaEntries + searchindex + 1, adbMetaEntries + searchindex, (adbMetaCount - searchindex) * sizeof (adbMetaEntries[0]));
	adbMetaEntries[searchindex] = temp;
	adbMetaCount++;

	adbMetaDirty = 1;

	return 0;
}

int adbMetaRemove (const char *filename, const uint64_t filesize, const char *SIG)
{
	uint_fast32_t searchindex = adbMetaBinarySearchFilesize (filesize);
	int_fast32_t search;

#ifdef ADBMETA_DEBUG
	fprintf (stderr, "adbMetaRemove (\"%s\", %"PRIu64", \"%s\")\n", filename, filesize, SIG);
#endif

	if (searchindex == adbMetaCount)
	{
		return 1; /* not found */
	}

	assert (adbMetaEntries[searchindex]->filesize >= filesize);

	if (adbMetaEntries[searchindex]->filesize > filesize)
	{
		return 1; /* not found */
	}

	for (search = searchindex; search < adbMetaCount; search++)
	{
		if (adbMetaEntries[search]->filesize != filesize)
		{
			return 1; /* not found */
		}
		if (strcmp (adbMetaEntries[search]->filename, filename))
		{
			continue;
		}
		if (strcmp (adbMetaEntries[search]->SIG, SIG))
		{
			continue;
		}

		free (adbMetaEntries[search]);
		memmove (adbMetaEntries + search, adbMetaEntries + search + 1, (adbMetaCount - search - 1) * sizeof (adbMetaEntries[0]));
		adbMetaCount--;
		adbMetaDirty = 1;
		return 0;
	}

	return 1; /* not found */
}

int adbMetaGet (const char *filename, const uint64_t filesize, const char *SIG, unsigned char **data, uint32_t *datasize)
{
	uint_fast32_t searchindex = adbMetaBinarySearchFilesize (filesize);
	int_fast32_t search;

#ifdef ADBMETA_DEBUG
	fprintf (stderr, "adbMetaGet (\"%s\", %"PRIu64", \"%s\") ", filename, filesize, SIG);
#endif

	*data = 0;
	*datasize = 0;

	if (searchindex == adbMetaCount)
	{
#ifdef ADBMETA_DEBUG
		fprintf (stderr, " => NULL #1\n");
#endif
		return 1; /* not found */
	}

	assert (adbMetaEntries[searchindex]->filesize >= filesize);

	if (adbMetaEntries[searchindex]->filesize > filesize)
	{
#ifdef ADBMETA_DEBUG
		fprintf (stderr, " => NULL #2\n");
#endif
		return 1; /* not found */
	}

	for (search = searchindex; search < adbMetaCount; search++)
	{
		if (adbMetaEntries[search]->filesize != filesize)
		{
#ifdef ADBMETA_DEBUG
		fprintf (stderr, " => NULL #3\n");
#endif
			return 1; /* not found */
		}
		if (strcmp (adbMetaEntries[search]->filename, filename))
		{
			continue;
		}
		if (strcmp (adbMetaEntries[search]->SIG, SIG))
		{
			continue;
		}

		*data = malloc (adbMetaEntries[search]->datasize);
		if (!*data)
		{
			fprintf (stderr, "adbMetaGet: failed to allocate memory for BLOB\n");
			return -1;
		}
		memcpy (*data, adbMetaEntries[search]->data, adbMetaEntries[search]->datasize);
		*datasize = adbMetaEntries[search]->datasize;

#ifdef ADBMETA_DEBUG
		fprintf (stderr, " => %p %"PRIu32"\n", *data, *datasize);
#endif
		return 0;
	}


#ifdef ADBMETA_DEBUG
	fprintf (stderr, " => NULL #4\n");
#endif
	return 1; /* not found */
}
