#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <zlib.h>

#if ZLIB_VERNUM < 0x1260
#warning "Linked zlib version does not support transparent writing. This means that .VGM files will always be written compressed, even when the input is uncompressed."
#endif

bool errorOccurred = false;

uint32_t loopStart, loopEnd;
enum { beforeLoopStart =0, afterLoopStart =1, afterLoopEnd =2 } phase;
int sampleCount =0, lastSampleCount =-1;

static inline uint32_t get_lsb32(uint8_t* buf) { return buf[0]      | (buf[1]<<8) | (buf[2]<<16) | (buf[3]<<24); }

struct chip {
	uint8_t commandLow, commandHigh;
	uint8_t reg[512];
	int noteCount;
	int nthNoteAfterLoopStart[18];
	int nthNoteAfterLoopEnd  [18];
};

struct chip YM3812[2] = { { 0x5A }, { 0xAA } };
struct chip YM3526[2] = { { 0x5B }, { 0xAB } };
struct chip Y8950 [2] = { { 0x5C }, { 0xAC } };
struct chip YMF262[2] = { { 0x5E, 0x5F }, { 0xAE, 0xAF } };
struct chip *allChips[] = { &YM3526[0], &YM3526[1], &YM3812[0], &YM3812[1], &Y8950[0], &Y8950[1], &YMF262[0], &YMF262[1], NULL };

struct VGMHeader {
        char id[4];
        uint32_t rofsEOF;
        uint32_t version;
        uint32_t clockSN76489;
        uint32_t clockYM2413;
        uint32_t rofsGD3;
        uint32_t samplesInFile;
        uint32_t rofsLoop;
        uint32_t samplesInLoop;
        uint32_t videoRefreshRate;
        uint16_t SNfeedback;
        uint8_t SNshiftRegisterWidth;
        uint8_t SNflags;
        uint32_t clockYM2612;
        uint32_t clockYM2151;
        uint32_t rofsData;
        uint32_t clockSegaPCM;
        uint32_t interfaceRegisterSegaPCM;
        uint32_t clockRF5C68;
        uint32_t clockYM2203;
        uint32_t clockYM2608;
        uint32_t clockYM2610;
        uint32_t clockYM3812;
        uint32_t clockYM3526;
        uint32_t clockY8950;
        uint32_t clockYMF262;
        uint32_t clockYMF278B;
        uint32_t clockYMF271;
        uint32_t clockYMZ280B;
        uint32_t clockRF5C164;
        uint32_t clockPWM;
        uint32_t clockAY8910;
        uint8_t typeAY8910;
        uint8_t flagsAY8910;
        uint8_t flagsAY8910_YM2203;
        uint8_t flagsAY8910_YM2608;
        uint8_t volumeModifier;
        uint8_t reserved1;
        uint8_t loopBase;
        uint8_t loopModifier;
        uint32_t clockGBdmg;
        uint32_t clockNESapu;
        uint32_t clockMultiPCM;
        uint32_t clockUPD7759;
        uint32_t clockOKIM6258;
        uint8_t flagsOKIM6258;
        uint8_t flagsK054539;
        uint8_t typeC140;
        uint8_t reserved2;
        uint32_t clockOKIM6295;
        uint32_t clockK051649;
        uint32_t clockK054539;
        uint32_t clockHuC6280;
        uint32_t clockC140;
        uint32_t clockK053260;
        uint32_t clockPokey;
        uint32_t clockQSound;
        uint32_t clockSCSP;
        uint32_t rofsExtraHeader;
        uint32_t clockWonderSwan;
        uint32_t clockVBvsu;
        uint32_t clockSAA1099;
        uint32_t clockES5503;
        uint32_t clockES5505;
        uint8_t channelsES5503;
        uint8_t channelsES5506;
        uint8_t clockDividerC352;
        uint8_t reserved3;
        uint32_t clockX1_010;
        uint32_t clockC352;
        uint32_t clockGA20;
        uint32_t reserved4[7];
} __attribute__((packed));

void checkOPLWrite(struct chip *chip, uint8_t command, uint8_t index, uint8_t value) {
	uint16_t fullIndex;
	if (command ==chip->commandHigh)
		fullIndex =index |0x100;
	else if (command ==chip->commandLow)
		fullIndex =index;
	else {	if (command==0x66 && chip->noteCount >0) {
			// Determine result
			for (int note= 0; note <18; note++) {
				int result1 =-1;
				int result2 =-1;
				for (int ch =0; ch <18; ch++) if (chip->nthNoteAfterLoopStart[ch] ==note) { result1 =ch; break; }
				for (int ch =0; ch <18; ch++) if (chip->nthNoteAfterLoopEnd  [ch] ==note) { result2 =ch; break; }
				if (result1 ==-1 || result2 ==-1) break;
				printf("%02X -> %02X\n", result1, result2);
			}
		}
		return;
	}
	
	bool noteTurnedOn = index >=0xB0 && index <=0xB8 && !(chip->reg[fullIndex] &0x20) && value &0x20;
	if (noteTurnedOn) {
		uint16_t ch = (fullIndex &0xF) +9*(fullIndex >>8);
		if (phase ==afterLoopStart && chip->nthNoteAfterLoopStart[ch] ==-1) chip->nthNoteAfterLoopStart[ch] = chip->noteCount++; else
		if (phase ==afterLoopEnd   && chip->nthNoteAfterLoopEnd  [ch] ==-1) chip->nthNoteAfterLoopEnd  [ch] = chip->noteCount++;		
	}	
	chip->reg[fullIndex] =value;
}

#define UNK 0	// Illegal command
#define VAR 255	// Variable length (not implemented yet)
static const uint8_t commandLengths[256] = {
//	  0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
	UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,	// 0x00-0x0F
	UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,	// 0x10-0x1F
	UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,	// 0x20-0x2F
	  2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,   2,	// 0x30-0x3F
	UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,   2,	// 0x40-0x4F
	  2,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,	// 0x50-0x5F
	UNK,   3,   1,   1, UNK, UNK,   1, VAR, VAR, UNK, UNK, UNK, UNK, UNK, UNK, UNK,	// 0x60-0x6F
	  1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,	// 0x70-0x7F
	  1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,	// 0x80-0x8F
	VAR, VAR, VAR, VAR, VAR, VAR, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,	// 0x90-0x9F
	  3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,	// 0xA0-0xAF
	  3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,	// 0xB0-0xBF
	  4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,	// 0xC0-0xCF
	  4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,   4,	// 0xD0-0xDF
	  5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,	// 0xE0-0xEF
	  5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5	// 0xF0-0xFF	
};

void processFile(char* fileName, uint8_t** _fileData, size_t* _fileSize) {
	phase = beforeLoopStart;
	
	uint8_t* fileData =*_fileData;
	struct VGMHeader* header =(struct VGMHeader*) fileData;
	size_t headerSize = header->rofsData +offsetof(struct VGMHeader, rofsData);
	uint8_t* s =fileData +headerSize;
	
	for (struct chip** chip =allChips; *chip; chip++)
		for (int ch =0; ch <18; ch++) (*chip)->nthNoteAfterLoopStart[ch] = (*chip)->nthNoteAfterLoopEnd[ch] = -1;

	uint8_t command;
	do {	if (lastSampleCount !=sampleCount) {
			if (sampleCount ==loopStart || sampleCount==loopEnd) {
				phase++;
				for (struct chip** chip =allChips; *chip; chip++) (*chip)->noteCount =0;
			}
		}
		lastSampleCount =sampleCount;
		command =*s;
		for (struct chip** chip =allChips; *chip; chip++) {
			checkOPLWrite((*chip), command, s[1], s[2]);
		}
		if (command >=0x70 && command <=0x7F)
			sampleCount += command -0x6F; else
		if (command ==0x61)
			sampleCount += s[1] + (s[2]<<8); else
		if (command ==0x62)
			sampleCount += 735; else
		if (command ==0x63)
			sampleCount += 882;

		size_t commandLength =commandLengths[command];
		s +=commandLength;
	} while (command !=0x66);
}

void showUsage(void) {
	printf("Usage: vgm_remap file loopstart loopend\n");
}

void loadAndProcessFile(char* fileName) {
	struct gzFile_s *fileHandle = gzopen(fileName, "rb");
	if (fileHandle) {
		/* Read the first eight bytes to check for the .VGM signature, and get the total file size */
		uint8_t firstEightBytes[8];
		gzrewind(fileHandle);
		int result =gzread(fileHandle, firstEightBytes, 8);
		if (result ==8 && memcmp(firstEightBytes, "Vgm ", 4)==0) {
			bool isUncompressed =gzdirect(fileHandle);
			size_t fileSize = get_lsb32(&firstEightBytes[4]) +4;
			uint8_t* fileData = malloc(fileSize);
			if (fileData) {
				gzrewind(fileHandle);
				size_t readSize = gzread(fileHandle, fileData, fileSize);
				if (readSize == fileSize) {
					processFile(fileName, &fileData, &fileSize);
				} else {
					fprintf(stderr, "%s: Unexpected end of file; received only %d bytes, wanted %d bytes\n", fileName, readSize, fileSize);
					errorOccurred = true;
				}
				free(fileData);
			} else {
				fprintf(stderr, "%s: Cannot allocate %d bytes of memory\n", fileName, fileSize);
				errorOccurred = true;
			}
		} else {
			fprintf(stderr, "%s: Not a valid .VGM file\n", fileName);
			errorOccurred = true;
		}
		gzclose(fileHandle);
	} else {
		perror(fileName);
		errorOccurred = true;
	}
}

int main(int argc, char** argv) {
	if (argc <= 3) {
		showUsage();
		return EXIT_FAILURE;
	}
	loopStart =strtol(argv[2], NULL, 10);
	loopEnd   =strtol(argv[3], NULL, 10);
	loadAndProcessFile(argv[1]);
	return errorOccurred? EXIT_FAILURE: EXIT_SUCCESS;
}