#include "tn_ym.h"
#include "TN_YMAmp.bin_rev.h"
#include <stdlib.h>
#include <string.h>
#include <zlib.h>
#include "in_ym.h"
#include "endian.h"

const char verstag[] = VERSTAG;

struct ExecIFace *IExec;
struct Interface *INewlib;
struct DOSIFace *IDOS;

static int32 ym_open (struct TNDecHandle *handle, const char *filename);
static void ym_close (struct TNDecHandle *handle);
static int32 ym_process (struct TNDecHandle *handle, void *buf, uint32 len);
void ym_seek (struct TNDecHandle *handle, uint32 seconds);

ymiface_t iym = {
	ym_open,
	ym_close,
	ym_process,
	ym_seek
};

ymiface_t *_start (char *argstr, int32 arglen, struct ExecBase *SysBase) {
	IExec = (struct ExecIFace *)SysBase->MainInterface;

	samplerate = 44100;
	ym2151_clock = 100;
	ym2612_clock = 100;
	sn76496_clock = 100;
	channels = 0x3ffff;

	return &iym;
}

int isbitset(void *buffer, int bit) { /* returns nonzero if bit is set, else zero */
    return (*(unsigned int *)buffer) & (0x1 << bit);
}

void AddGYMTag () {
	char *data = (char *)&gymtag;
	memset(data, 0, 428);
	*(int32 *)&data[0] = GYM_MAGIC;
	strncpy(data + 4, "Unknown Song", 32);
	strncpy(data + 36, "Unknown Game", 32);
	strncpy(data + 68, "Unknown Publisher", 32);
	strncpy(data + 100, "Unknown Emulator", 32);
	strncpy(data + 132, "Unknown Person", 32);
	strncpy(data + 164, "Header added by TN_YM.tnplug", 256);
	*(int32 *)&data[420] = 0;
	*(int32 *)&data[424] = 0;
}

/*
** Determines the length in
** milliseconds of a CYM file.
*/
static int calc_cym_time_length(unsigned char *data, int size) {
    int loop, num_zeros = 0;

    for(loop = 0; loop < size; loop++) {
        if(!data[loop]) num_zeros++;
        else loop++;
    }
    return (int)((num_zeros / 60.0) * 1000.0);
}

/*
** Determines the length in
** milliseconds of a GYM file.
*/
static int calc_gym_time_length(unsigned char *data, int size) {
    int loop, num_zeros = 0;

    for(loop = 0; loop < size; loop++) {
        switch(data[loop]) {
            case(0x00):
                num_zeros++;
                continue;
            case(0x01):
                loop += 2;
                continue;
            case(0x02):
                loop += 2;
                continue;
            case(0x03):
                loop += 1;
                continue;
        }
    }
    return (int)((num_zeros / 60.0) * 1000.0);
}

uint8 *dac_data;
int16 *ym_buf[2], *snbuf;
int srate, srate152, srate15, srate30;
int32 data_pos, adjtime;
int update;
int dac_max;

static int32 ym_open (struct TNDecHandle *handle, const char *filename) {
	BPTR ym_file = ZERO;
	BOOL res = FALSE;
	uint8 *tmpbuf = NULL;
	const char *fileext;
	int ym_type = 0;

	INewlib = handle->newlib;
	IDOS = handle->dos;
	ym_data = NULL;

	ym_file = IDOS->Open(filename, MODE_OLDFILE);
	if (!ym_file) goto out;

	IDOS->Read(ym_file, &gymtag, 428);
	file_size = IDOS->GetFileSize(ym_file);
	if (gymtag.id == GYM_MAGIC) {
		file_size -= 428;
		ym_type = 2;
	} else {
		IDOS->ChangeFilePosition(ym_file, 0, OFFSET_BEGINNING);
	}
	if (file_size <= 0) goto out;

	tmpbuf = IExec->AllocVec(file_size, MEMF_SHARED);
	if (!tmpbuf) goto out;

	if (IDOS->Read(ym_file, tmpbuf, file_size) != file_size) {
		goto out;
	}

	fileext = strrchr(filename, '.');
	if (!ym_type && fileext++ && !stricmp(fileext, "cym")) {
		ym_type = 1;
		ym_data = tmpbuf;
		time_length = calc_cym_time_length(ym_data, file_size)*2;
	} else {
		ym_type = 2;
		if (gymtag.id != GYM_MAGIC) {
			AddGYMTag();
		}
		if (gymtag.compressed) {
			int zerr;
			uint32 destsize, srcsize;
			destsize = read_le32(&gymtag.compressed);
			srcsize = file_size;

			ym_data = IExec->AllocVec(destsize, MEMF_SHARED);
			if (!ym_data) goto out;

			zerr = uncompress(ym_data, &destsize, tmpbuf, srcsize);
			IExec->FreeVec(tmpbuf);
			tmpbuf = NULL;
			if (zerr != Z_OK) goto out;

			file_size = destsize;
			time_length = calc_gym_time_length(ym_data, file_size);
		} else {
			ym_data = tmpbuf;
			time_length = calc_gym_time_length(ym_data, file_size);
		}
	}

	srate = samplerate;
	srate15 = srate / 15;
	srate152 = (srate / 15) << 1;
	srate30 = srate / 30;

	if (ym_type == 1) {
		int loop;

		ym_buf[0] = IExec->AllocVec(srate15, MEMF_PRIVATE);
		ym_buf[1] = IExec->AllocVec(srate15, MEMF_PRIVATE);
		if (!ym_buf[0] || !ym_buf[1]) goto out;

		do {
			loop = YM2151Init(1, ym2151_clock * 3579545 / 100, srate);
			if (loop == DUP_INIT) YM2151Shutdown();
			else if (loop) goto out;
		} while (loop);
	} else {
		int loop;

		dac_data = IExec->AllocVec(srate30, MEMF_PRIVATE);
		if (!dac_data) goto out;

		ym_buf[0] = IExec->AllocVec(srate15, MEMF_PRIVATE);
		ym_buf[1] = IExec->AllocVec(srate15, MEMF_PRIVATE);
		if (!ym_buf[0] || !ym_buf[1]) goto out;

		snbuf = IExec->AllocVec(srate15, MEMF_PRIVATE);
		if (!snbuf) goto out;

		do {
			loop = YM2612Init(1, ym2612_clock * 7670442 / 100, srate, NULL, NULL);
			if (loop == DUP_INIT) YM2612Shutdown();
			else if (loop) goto out;
		} while (loop);

		if (SN76496_init(0, sn76496_clock * 3579580 / 100, srate)) {
			goto out;
		}
	}

	handle->ym_type = ym_type;
	handle->length = time_length;
	res = TRUE;
out:
	IDOS->Close(ym_file);
	if (!res) IExec->FreeVec(tmpbuf);

	return res;
}

static void ym_close (struct TNDecHandle *handle) {
	if (handle->ym_type == 1) {
		YM2151Shutdown();
		IExec->FreeVec(ym_buf[0]);
		IExec->FreeVec(ym_buf[1]);
	} else {
		YM2612Shutdown();
		IExec->FreeVec(snbuf);
		IExec->FreeVec(ym_buf[0]);
		IExec->FreeVec(ym_buf[1]);
		IExec->FreeVec(dac_data);
	}
	IExec->FreeVec(ym_data);

	IDOS = NULL;
	INewlib = NULL;
}

static int get_cym_data_pos(int time_position) {
    unsigned loop, num_zeros = 0, num_zeros_target;

    num_zeros_target = (int)((time_position / 1000.0) * 60.0);
    for(loop = 0; loop < file_size; loop++) {
        if(num_zeros == num_zeros_target)
            break;
        if(!ym_data[loop])
            num_zeros++;
        else
            loop++;
    }
    return loop;
}

static int get_gym_data_pos(int time_position) {
    unsigned loop, num_zeros = 0, num_zeros_target;

    num_zeros_target = (int)((time_position / 1000.0) * 60.0);
    for(loop = 0; loop < file_size; loop++) {
        if(num_zeros == num_zeros_target)
            break;
        switch(ym_data[loop]) {
            case(0x00):
                num_zeros++;
                continue;
            case(0x01):
                loop += 2;
                continue;
            case(0x02):
                loop += 2;
                continue;
            case(0x03):
                loop += 1;
                continue;
        }
    }
    return loop;
}

static inline short mix(int a, int b) {
    int c = (a + b) * 2;
    if(c > 32767)
        c = 32767;
    else if( c < -32768)
        c = -32768;
    return (short)c;
}

static int32 ym_process (struct TNDecHandle *handle, void *buf, uint32 len) {
	int16 *samp_buf = buf;
	int loop, loop2;
	if (handle->ym_type == 1) {
        if(seek_needed != -1) {
            time_pos = seek_needed - (seek_needed % 60);
            data_pos = get_cym_data_pos(time_pos / 2);
            adjtime = 0;
            seek_needed = -1;
        }
		while (data_pos < file_size) {
			if (!ym_data[data_pos]) {
				data_pos++;
				if (update) {
					YM2151UpdateOne(0, (void **)ym_buf, srate30);
					loop2 = 0;
					for (loop = 0; loop < srate30; loop++) {
						samp_buf[loop2++] = (int16)(ym_buf[0][loop]);
						samp_buf[loop2++] = (int16)(ym_buf[1][loop]);
					}
					if (adjtime == 2) {
						time_pos += 34;
						adjtime = 0;
					} else {
						time_pos += 33;
						adjtime++;
					}
					return srate30;
					update = 0;
				} else
					update = 1;
			} else {
				YM2151WriteReg(0, ym_data[data_pos], ym_data[data_pos + 1]);
				data_pos += 2;
			}
		}
		return 0;
	} else {
        if(seek_needed != -1) {
            time_pos = seek_needed - (seek_needed % 60);
            data_pos = get_gym_data_pos(time_pos);
            adjtime = 0;
            dac_max = 0;
            seek_needed = -1;
        }
		while (data_pos < file_size) {
			switch (ym_data[data_pos]) {
				case 0x00:
					data_pos++;
					if(update) {
						update = 0;
						if(dac_max && isbitset(&channels, 13)) {
							short *dac_buf[2];
							double update_cycle = (double)srate30 / dac_max; /* dac port write cycle */
							double stream_cnt = 0;
							for(loop = 0; loop < dac_max; loop++) {
								int old_cnt = (int)stream_cnt;
								int step;
								/* update DAC port */
								YM2612Write(0, 0, 0x2a);
								YM2612Write(0, 1, dac_data[loop]);
								if(loop == dac_max - 1)
									step = srate30 - old_cnt;
								else {
									stream_cnt += update_cycle;
									step = (int)stream_cnt - old_cnt;
								}
								/* update stream */
								dac_buf[0] = &ym_buf[0][old_cnt];
								dac_buf[1] = &ym_buf[1][old_cnt];
								YM2612UpdateOne(0, (void **)dac_buf, step);
							}
							dac_max = 0;
						} else
							YM2612UpdateOne(0, (void **)ym_buf, srate30);
						SN76496Update_16(0, snbuf, srate30);
						loop2 = 0;
						for(loop = 0; loop < srate30; loop++) {
							samp_buf[loop2++] = mix(ym_buf[0][loop], snbuf[loop] >> 1);
							samp_buf[loop2++] = mix(ym_buf[1][loop], snbuf[loop] >> 1);
						}
						if(adjtime == 2) {
							time_pos += 34;
							adjtime = 0;
						} else {
							time_pos += 33;
							adjtime++;
						}
						return srate30;
					} else
						update = 1;
					break;
				case(0x01):
					data_pos++;
					if(ym_data[data_pos] == 0x2a) {
						data_pos++;
						if(dac_max < srate30)
							dac_data[dac_max++] = ym_data[data_pos];
						data_pos++;
					} else {
						YM2612Write(0, 0, ym_data[data_pos++]);
						YM2612Write(0, 1, ym_data[data_pos++]);
					}
					break;
				case(0x02):
					data_pos++;
					YM2612Write(0, 2, ym_data[data_pos++]);
					YM2612Write(0, 3, ym_data[data_pos++]);
					break;
				case(0x03):
					data_pos++;
					SN76496Write(0, ym_data[data_pos++]);
					break;
				default:
					data_pos++;
					break;
			}
		}
		return 0;
	}
}

void ym_seek (struct TNDecHandle *handle, uint32 seconds) {
	seek_needed = seconds * 1000;
}
