/*
 * Copyright (c) 2010, Wei Mingzhi <whistler_wmz@users.sf.net>.
 * All Rights Reserved.
 *
 * Based on: Cdrom for Psemu Pro like Emulators
 * By: linuzappz <linuzappz@hotmail.com>
 *
 * 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, see <http://www.gnu.org/licenses>.
 */

#include "cdr.h"

#ifndef USE_NULL
static char *LibName = N_("CD-ROM Drive Reader");
#else
static char *LibName = N_("CDR NULL Plugin");
#endif

int initial_time = 0;

pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

volatile CacheData *cdcache;
volatile int cacheaddr;

volatile unsigned char *cdbuffer;

crdata cr;

unsigned char lastTime[3];
pthread_t thread;
int subqread;
volatile int stopth, found, locked, playing;

long (*ReadTrackT[])() = {
	ReadNormal,
	ReadThreaded,
};

unsigned char* (*GetBufferT[])() = {
	GetBNormal,
	GetBThreaded,
};

long (*fReadTrack)();
unsigned char* (*fGetBuffer)();

void *CdrThread(void *arg);

long CDRinit(void) {
	thread = (pthread_t)-1;
	return 0;
}

long CDRshutdown(void) {
	return 0;
}

long CDRopen(void) {
	LoadConf();

#ifndef _MACOSX
	if (IsCdHandleOpen())
		return 0; // it's already open
#endif

	if (OpenCdHandle(CdromDev) == -1) { // if we can't open the cdrom we'll works as a null plugin
		fprintf(stderr, "CDR: Could not open %s\n", CdromDev);
	}

	fReadTrack = ReadTrackT[ReadMode];
	fGetBuffer = GetBufferT[ReadMode];

	if (ReadMode == THREADED) {
		cdcache = (CacheData *)malloc(CacheSize * sizeof(CacheData));
		if (cdcache == NULL) return -1;
		memset((void *)cdcache, 0, CacheSize * sizeof(CacheData));

		found = 0;
	} else {
		cdbuffer = cr.buf + 12; /* skip sync data */
	}

	if (ReadMode == THREADED) {
		pthread_attr_t attr;

		pthread_mutex_init(&mut, NULL);
		pthread_cond_init(&cond, NULL);
		locked = 0;

		pthread_attr_init(&attr);
		pthread_create(&thread, &attr, CdrThread, NULL);

		cacheaddr = -1;
	} else thread = (pthread_t)-1;

	playing = 0;
	stopth = 0;
	initial_time = 0;

	return 0;
}

long CDRclose(void) {
	if (!IsCdHandleOpen()) return 0;

	if (playing) CDRstop();

	CloseCdHandle();

	if (thread != (pthread_t)-1) {
		if (locked == 0) {
			stopth = 1;
			while (locked == 0) usleep(5000);
		}

		stopth = 2;
		pthread_mutex_lock(&mut);
		pthread_cond_signal(&cond);
		pthread_mutex_unlock(&mut);

		pthread_join(thread, NULL);
		pthread_mutex_destroy(&mut);
		pthread_cond_destroy(&cond);
	}

	if (ReadMode == THREADED) {
		free((void *)cdcache);
	}

	return 0;
}

// return Starting and Ending Track
// buffer:
//  byte 0 - start track
//  byte 1 - end track
long CDRgetTN(unsigned char *buffer) {
	long ret;

	if (!IsCdHandleOpen()) {
		buffer[0] = 1;
		buffer[1] = 1;
		return 0;
	}

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = GetTN(buffer);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	return ret;
}

// return Track Time
// buffer:
//  byte 0 - frame
//  byte 1 - second
//  byte 2 - minute
long CDRgetTD(unsigned char track, unsigned char *buffer) {
	long ret;

	if (!IsCdHandleOpen()) {
		memset(buffer + 1, 0, 3);
		return 0;
	}

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = GetTD(track, buffer);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	return ret;
}

// normal reading
long ReadNormal() {
	if (ReadSector(&cr) == -1)
		return -1;

	return 0;
}

unsigned char *GetBNormal() {
	return (unsigned char *)cdbuffer;
}

// threaded reading (with cache)
long ReadThreaded() {
	int addr = msf_to_lba(cr.msf.cdmsf_min0, cr.msf.cdmsf_sec0, cr.msf.cdmsf_frame0);
	int i;

	if (addr >= cacheaddr && addr < (cacheaddr + CacheSize) && cacheaddr != -1) {
		i = addr - cacheaddr;
		PRINTF("found %d\n", (addr - cacheaddr));
		cdbuffer = cdcache[i].cr.buf + 12;
		while (cdcache[i].msf[0] != cr.msf.cdmsf_min0 ||
			   cdcache[i].msf[1] != cr.msf.cdmsf_sec0 ||
			   cdcache[i].msf[2] != cr.msf.cdmsf_frame0) {
			if (locked == 1) {
				if (cdcache[i].ret == 0) break;
				return -1;
			}
			usleep(5000);
		}
		PRINTF("%d:%d:%d, %p, %p\n", cdcache[i].msf[0], cdcache[i].msf[1], cdcache[i].msf[2], cdbuffer, cdcache);
		found = 1;

		return 0;
	} else found = 0;

	if (locked == 0) {
		stopth = 1;
		while (locked == 0) { usleep(5000); }
		stopth = 0;
	}

	// not found in cache
	locked = 0;

	pthread_mutex_lock(&mut);
	pthread_cond_signal(&cond);
	pthread_mutex_unlock(&mut);

	return 0;
}

unsigned char *GetBThreaded() {
	PRINTF("threadc %d\n", found);

	if (found == 1) return (unsigned char *)cdbuffer;

	cdbuffer = cdcache[0].cr.buf + 12;

	while (cdcache[0].msf[0] != cr.msf.cdmsf_min0 ||
		   cdcache[0].msf[1] != cr.msf.cdmsf_sec0 ||
		   cdcache[0].msf[2] != cr.msf.cdmsf_frame0) {
		if (locked == 1) return NULL;
		usleep(5000);
	}
	if (cdcache[0].ret == -1) return NULL;

	return (unsigned char *)cdbuffer;
}

void *CdrThread(void *arg) {
	unsigned char curTime[3];
	int i;

	for (;;) {
		pthread_mutex_lock(&mut);
		locked = 1;

		pthread_cond_wait(&cond, &mut);

		if (stopth == 2) pthread_exit(NULL);
		// refill the buffer
		cacheaddr = msf_to_lba(cr.msf.cdmsf_min0, cr.msf.cdmsf_sec0, cr.msf.cdmsf_frame0);

		memcpy(curTime, &cr.msf, 3);

		PRINTF("start thc %d:%d:%d\n", curTime[0], curTime[1], curTime[2]);

		for (i = 0; i < CacheSize; i++) {
			cdcache[i].cr.msf.cdmsf_min0 = curTime[0];
			cdcache[i].cr.msf.cdmsf_sec0 = curTime[1];
			cdcache[i].cr.msf.cdmsf_frame0 = curTime[2];

			PRINTF("reading %d:%d:%d\n", curTime[0], curTime[1], curTime[2]);

			cdcache[i].ret = ReadSector((crdata *)&cdcache[i].cr);
			if (cdcache[i].ret == -1) break;

			PRINTF("readed %x:%x:%x\n", cdcache[i].cr.buf[12], cdcache[i].cr.buf[13], cdcache[i].cr.buf[14]);

			cdcache[i].msf[0] = curTime[0];
			cdcache[i].msf[1] = curTime[1];
			cdcache[i].msf[2] = curTime[2];

			curTime[2]++;
			if (curTime[2] == 75) {
				curTime[2] = 0;
				curTime[1]++;
				if (curTime[1] == 60) {
					curTime[1] = 0;
					curTime[0]++;
				}
			}

			if (stopth) break;
		}

		pthread_mutex_unlock(&mut);
	}

	return NULL;
}

// read track
// time:
//  byte 0 - minute
//  byte 1 - second
//  byte 2 - frame
// uses bcd format
long CDRreadTrack(unsigned char *time) {
	if (!IsCdHandleOpen()) {
		memset(cr.buf, 0, DATA_SIZE);
		return 0;
	}

	PRINTF("CDRreadTrack %d:%d:%d\n", btoi(time[0]), btoi(time[1]), btoi(time[2]));

	if (UseSubQ) memcpy(lastTime, time, 3);
	subqread = 0;

	cr.msf.cdmsf_min0 = btoi(time[0]);
	cr.msf.cdmsf_sec0 = btoi(time[1]);
	cr.msf.cdmsf_frame0 = btoi(time[2]);

	return fReadTrack();
}

// return readed track
unsigned char *CDRgetBuffer(void) {
	return fGetBuffer();
}

// plays cdda audio
// sector:
//  byte 0 - minute
//  byte 1 - second
//  byte 2 - frame
// does NOT uses bcd format
long CDRplay(unsigned char *sector) {
	long ret;

	if (!IsCdHandleOpen())
		return 0;

	// If play was called with the same time as the previous call,
	// don't restart it. Of course, if play is called with a different
	// track, stop playing the current stream.
	if (playing) {
		if (msf_to_lba(sector[0], sector[1], sector[2]) == initial_time)
			return 0;
		else
			CDRstop();
	}

	initial_time = msf_to_lba(sector[0], sector[1], sector[2]);

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = PlayCDDA(sector);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	if (ret == 0) {
		playing = 1;
		return 0;
	}

	return -1;
}

// stops cdda audio
long CDRstop(void) {
	long ret;

	if (!IsCdHandleOpen())
		return 0;

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = StopCDDA();
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	if (ret == 0) {
		playing = 0;
		initial_time = 0;

		return 0;
	}

	return -1;
}

// reads cdr status
// type:
//  0x00 - unknown
//  0x01 - data
//  0x02 - audio
//  0xff - no cdrom
// status: (only shell open supported)
//  0x00 - unknown
//  0x01 - error
//  0x04 - seek error
//  0x10 - shell open
//  0x20 - reading
//  0x40 - seeking
//  0x80 - playing
// time:
//  byte 0 - minute
//  byte 1 - second
//  byte 2 - frame

long CDRgetStatus(struct CdrStat *stat) {
	long ret;

	if (!IsCdHandleOpen())
		return -1;

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = GetStatus(playing, stat);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	return ret;
}

unsigned char *CDRgetBufferSub(void) {
	static unsigned char *p = NULL;

	if (!UseSubQ) return NULL;
	if (subqread) return p;

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	p = ReadSub(lastTime);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	if (p != NULL) subqread = 1;

	return p;
}

// read CDDA sector into buffer
long CDRreadCDDA(unsigned char m, unsigned char s, unsigned char f, unsigned char *buffer) {
	unsigned char msf[3] = {itob(m), itob(s), itob(f)};
	unsigned char *p;

	if (CDRreadTrack(msf) != 0) return -1;

	p = CDRgetBuffer();
	if (p == NULL) return -1;

	memcpy(buffer, p - 12, CD_FRAMESIZE_RAW); // copy from the beginning of the sector
	return 0;
}

// get Track End Time
long CDRgetTE(unsigned char track, unsigned char *m, unsigned char *s, unsigned char *f) {
	long ret;

	if (!IsCdHandleOpen()) return -1;

	if (ReadMode == THREADED) pthread_mutex_lock(&mut);
	ret = GetTE(track, m, s, f);
	if (ReadMode == THREADED) pthread_mutex_unlock(&mut);

	return ret;
}

#ifndef _MACOSX

void ExecCfg(char *arg) {
	char cfg[256];
	struct stat buf;

	strcpy(cfg, "./cfgDFCdrom");
	if (stat(cfg, &buf) != -1) {
		int pid = fork();
		if (pid == 0) {
			if (fork() == 0) {
				execl(cfg, "cfgDFCdrom", arg, NULL);
			}
			exit(0);
		} else if (pid > 0) {
			waitpid(pid, NULL, 0);
		}
		return;
	}

	strcpy(cfg, "./cfg/cfgDFCdrom");
	if (stat(cfg, &buf) != -1) {
		int pid = fork();
		if (pid == 0) {
			if (fork() == 0) {
				execl(cfg, "cfgDFCdrom", arg, NULL);
			}
			exit(0);
		} else if (pid > 0) {
			waitpid(pid, NULL, 0);
		}
		return;
	}

	fprintf(stderr, "cfgDFCdrom file not found!\n");
}

long CDRconfigure() {
#ifndef USE_NULL
	ExecCfg("configure");
#endif
	return 0;
}

void CDRabout() {
	ExecCfg("about");
}

#endif

long CDRtest(void) {
#ifndef USE_NULL
	if (OpenCdHandle(CdromDev) == -1)
		return -1;
	CloseCdHandle();
#endif
	return 0;
}

char *PSEgetLibName(void) {
	return _(LibName);
}

unsigned long PSEgetLibType(void) {
	return PSE_LT_CDR;
}

unsigned long PSEgetLibVersion(void) {
	return 1 << 16;
}
