/*
 * Audio support for JFDuke3D using JFAud
 * by Jonathon Fowler (jonof@edgenetwork.org)
 *
 * Duke Nukem 3D 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * Original Source: 1996 - Todd Replogle
 * Prepared for public release: 03/21/2003 - Charlie Wiederhold, 3D Realms
 */

#include "types.h"
#include "duke3d.h"
extern "C" {
#ifdef RENDERTYPEWIN
# include "winlayer.h"
#endif
#include "osd.h"
long numenvsnds;
}

typedef uint64 uint64_t;
#ifdef __APPLE__
# include <jfaud/jfaud.hpp>
#else
# include "jfaud.hpp"
#endif

#define SOUNDM_LOOP   1
#define SOUNDM_MSFX   2
#define SOUNDM_DUKE   4
#define SOUNDM_PARENT 8
#define SOUNDM_GLOBAL 16
#define SOUNDM_NICE   64	// Added for JFDuke3D so JFAud doesn't use nearest filtering for the sound
#define SOUNDM_PLAYER 128

#define UNITSPERMETRE 1024.0
#define DEFAULTREFDIST (6720.0/1024.0)	// in the original code, ((255-150)<<6) == 6720
#define DEFAULTMAXDIST (31444.0/1024.0)	// in the original code, 31444
#define DEFAULTROLLOFF 1.0//0.75
#define OCCLUDEDFACTOR 0.8

static JFAudMixerChannel::Filter DefaultFilter = JFAudMixerChannel::FilterNearest;
static int osdcmd_setsoundfilter(const osdfuncparm_t *parm);

#ifdef SCREWED_UP_CPP
# include "watcomhax/cmath"
#else
# include <cmath>
#endif

#define MAXCACHE1DSIZE (16*1048576)

class KenFile : public JFAudFile {
private:
	int fh;
public:
	KenFile(const char *filename, const char *subfilename)
		: JFAudFile(filename, subfilename)
	{
		fh = kopen4load(const_cast<char*>(filename), 0);
	}

	virtual ~KenFile()
	{
		if (fh >= 0) kclose(fh);
	}

	virtual bool IsOpen(void) const { return fh >= 0; }

	virtual long Read(long nbytes, void *buf)
	{
		if (fh < 0) return -1;
		return kread(fh, buf, nbytes);
	}
	virtual long Seek(long pos, SeekFrom where)
	{
		int when;
		if (fh < 0) return -1;
		switch (where) {
			case JFAudFile::Set: when = SEEK_SET; break;
			case JFAudFile::Cur: when = SEEK_CUR; break;
			case JFAudFile::End: when = SEEK_END; break;
			default: return -1;
		}
		return klseek(fh, pos, when);
	}
	virtual long Tell(void) const
	{
		if (fh < 0) return -1;
		return klseek(fh, 0, SEEK_CUR);
	}
	virtual long Length(void) const
	{
		if (fh < 0) return -1;
		return kfilelength(fh);
	}
};

static JFAudFile *openfile(const char *fn, const char *subfn)
{
	return static_cast<JFAudFile*>(new KenFile(fn,subfn));
}

static void logfunc(const char *s) { initprintf("jfaud: %s", s); }

#define PITCHRANGE 2		// octave range in each direction
#define PITCHSTEPS 24		// pitch points per octave
static float pitchtable[PITCHRANGE*PITCHSTEPS*2+1];
static void buildpitchtable(void)
{
	int i,j;
	
	for (i=-PITCHRANGE*PITCHSTEPS; i<=PITCHRANGE*PITCHSTEPS; i++) {
		pitchtable[i+PITCHRANGE*PITCHSTEPS] = pow(1.0005777895, (1200.0/PITCHSTEPS)*(float)i);
	}
}
static float translatepitch(int p)
{
	float t;
	int x;
	x = (p * PITCHSTEPS / 1200) + PITCHRANGE*PITCHSTEPS;
	if (x < 0) x = 0;
	else if (x > (int)(sizeof(pitchtable)/sizeof(float))) x = sizeof(pitchtable)/sizeof(float);
	t = pitchtable[x];
	/*if (t > 2.0) {
		initprintf("translatepitch(%d) > 2.0\n", p);
		t = 2.0;
	}*/
	return t;
}

class SoundChannel {
public:
	JFAudMixerChannel *chan;
	int owner;	// sprite number
	int soundnum;	// sound number
	bool done;
	
	SoundChannel()
	{
		chan = NULL;
		owner = -1;
		soundnum = -1;
		done = false;
	}
};

static SoundChannel *chans = NULL;
static JFAud *jfaud = NULL;

static bool havemidi = false, havewave = false;


static void stopcallback(int r)
{
	chans[r].done = true;
}

void testcallback(unsigned long num)
{
}

static int keephandle(JFAudMixerChannel *handle, int soundnum, int owner)
{
	int i, freeh=-1;
	for (i=NumVoices-1;i>=0;i--) {
		if (!chans[i].chan && freeh<0) freeh=i;
		else if (chans[i].chan == handle) { freeh=i; break; }
	}
	if (freeh<0) {
		initprintf("Warning: keephandle() exhausted handle space!\n");
		return -1;
	}

	chans[freeh].chan = handle;
	chans[freeh].soundnum = soundnum;
	chans[freeh].owner = owner;
	chans[freeh].done = false;
	
	return freeh;
}

void SoundStartup(void)
{
	int i;

	if (FXDevice < 0) return;

	if (jfaud) return;
	buildpitchtable();

	JFAud_SetLogFunc(logfunc);

	jfaud = new JFAud();
	if (!jfaud) return;

	jfaud->SetUserOpenFunc(openfile);
#ifdef _WIN32
	jfaud->SetWindowHandle((void*)win_gethwnd());
#endif

	havewave = havemidi = false;
	if (!jfaud->InitWave("software", NumVoices, MixRate)) {
		delete jfaud;
		jfaud = NULL;
		return;
	}

	{
		// the engine will take 60% of the system memory size for cache1d if there
		// is less than the 16MB asked for in loadpics(), so we'll
		// take 30% of what's left for the sound cache if that happened, or
		// 50% of the system memory sans the 16MB maximum otherwise
		unsigned k;
		if (Bgetsysmemsize() <= MAXCACHE1DSIZE)
			k = Bgetsysmemsize()/100*30;
		else
			k = Bgetsysmemsize()/100*50 - MAXCACHE1DSIZE;
		jfaud->SetCacheSize(k,k/2);
		jfaud->SetCacheItemAge(24*120);	// 24 movements per second, 120 seconds max lifetime
	}
	
	chans = new SoundChannel[NumVoices];
	if (!chans) {
		delete jfaud;
		jfaud = NULL;
		return;
	}

	havewave = true;

	if (jfaud->InitMIDI(NULL)) havemidi = true;

	OSD_RegisterFunction("setsoundfilter","setsoundfilter: 0=nearest 1=linear 2=4point",osdcmd_setsoundfilter);
}

void SoundShutdown(void)
{
	if (jfaud) delete jfaud;
	if (chans) delete [] chans;
	jfaud = NULL;
	chans = NULL;
	havewave = havemidi = false;
}

void MusicStartup(void)
{
}

void MusicShutdown(void)
{
}

void AudioUpdate(void)
{
	int i;

	if (!jfaud) return;
	if (havewave)
	for (i=NumVoices-1; i>=0; i--) {
		if (!chans[i].done) continue;
		if (chans[i].chan) jfaud->FreeSound(chans[i].chan);
		chans[i].chan = NULL;
		chans[i].done = false;

		if (chans[i].owner >= 0 &&
		    sprite[chans[i].owner].picnum == MUSICANDSFX &&
		    sector[sprite[chans[i].owner].sectnum].lotag < 3 &&
		    sprite[chans[i].owner].lotag < 999)
			hittype[chans[i].owner].temp_data[0] = 0;
	}
	jfaud->Update(false);	// don't age the cache here
}


static char menunum = 0;
void intomenusounds(void)
{
	short i;
	short menusnds[] = {
		LASERTRIP_EXPLODE,	DUKE_GRUNT,			DUKE_LAND_HURT,
		CHAINGUN_FIRE,		SQUISHED,			KICK_HIT,
		PISTOL_RICOCHET,	PISTOL_BODYHIT,		PISTOL_FIRE,
		SHOTGUN_FIRE,		BOS1_WALK,			RPG_EXPLODE,
		PIPEBOMB_BOUNCE,	PIPEBOMB_EXPLODE,	NITEVISION_ONOFF,
		RPG_SHOOT,			SELECT_WEAPON
	};
	sound(menusnds[menunum++]);
	menunum %= sizeof(menusnds)/sizeof(menusnds[0]);
}

void playmusic(const char *fn)
{
	char dafn[BMAX_PATH], *dotpos;
	int i;
	const char *extns[] = { ".ogg",".mp3",".mid", NULL };

	if (!MusicToggle) return;
	if (!jfaud) return;

	dotpos = Bstrrchr((char *)fn,'.');
	if (dotpos && Bstrcasecmp(dotpos,".mid")) {
		// has extension but isn't midi
		jfaud->PlayMusic(fn, NULL);
	} else {
		Bstrcpy(dafn,fn);
		dotpos = Bstrrchr(dafn,'.');
		if (!dotpos) dotpos = dafn+strlen(dafn);

		for (i=0; extns[i]; i++) {
			Bstrcpy(dotpos, extns[i]);
			if (jfaud->PlayMusic(dafn, NULL)) return;
		}
	}
}

int loadsound(unsigned short num) { return 1; }

int isspritemakingsound(short i, int num)	// if num<0, check if making any sound at all
{
	int j,n=0;
	
	if (!jfaud || !havewave) return 0;
	for (j=NumVoices-1; j>=0; j--) {
		if (chans[j].done || !chans[j].chan) continue;
		if (chans[j].owner != i) continue;
		if (num < 0 || chans[j].soundnum == num) n++;
	}
	return n;
}

int issoundplaying(short i, int num)
{
	int j,n=0;
	
	if (!jfaud || !havewave) return 0;
	for (j=NumVoices-1; j>=0; j--) {
		if (chans[j].done || !chans[j].chan) continue;
		if (chans[j].soundnum == num) n++;
	}
	
	return n;
}

int xyzsound(short num, short i, long x, long y, long z)
{
	JFAudMixerChannel *chan;
	int r, global = 0;
	float gain = 1.0, pitch = 1.0, refdist = DEFAULTREFDIST, maxdist = DEFAULTMAXDIST, rolloff = DEFAULTROLLOFF;

	if (!jfaud || !havewave ||
	    num >= NUM_SOUNDS ||
	    ((soundm[num] & SOUNDM_PARENT) && ud.lockout) ||	// parental mode
	    SoundToggle == 0 ||
	    (ps[myconnectindex].timebeforeexit > 0 && ps[myconnectindex].timebeforeexit <= 26*3) ||
	    (ps[myconnectindex].gm & MODE_MENU)
	   ) return -1;

	if (soundm[num] & SOUNDM_PLAYER) {
		sound(num);
		return 0;
	}
	
	if (soundm[num] & SOUNDM_DUKE) {
		// Duke speech, one at a time only
		int j;
		
		if (VoiceToggle == 0 ||
		    (ud.multimode > 1 && PN == APLAYER && sprite[i].yvel != screenpeek && ud.coop != 1)
		   ) return -1;

		for (j=NumVoices-1; j>=0; j--) {
			if (chans[j].done || !chans[j].chan || chans[j].owner < 0) continue;
			if (soundm[ chans[j].soundnum ] & SOUNDM_DUKE) return -1;
		}
	}
	
	if( i >= 0 && !(soundm[num] & SOUNDM_GLOBAL) && PN == MUSICANDSFX && SLT < 999 && (sector[SECT].lotag&0xff) < 9) {
		float d = (float)SHT/UNITSPERMETRE;
	        refdist = d / 2.0;
		maxdist = d;
		rolloff = 1.0;
	}
	
	{
		int ps = soundps[num], pe = soundpe[num], cx;
		cx = labs(pe-ps);
		if (cx) {
			if (ps < pe) pitch = translatepitch(ps + rand()%cx);
			else pitch = translatepitch(pe + rand()%cx);
		} else pitch = translatepitch(ps);
	}
	
	{
		float d = 1.0-(float)soundvo[num]/(12.0*1024.0);
		maxdist *= d;
		refdist *= d;
	}
	if (PN != MUSICANDSFX &&
	    !cansee(ps[screenpeek].oposx,ps[screenpeek].oposy,ps[screenpeek].oposz-(24<<8),
	            ps[screenpeek].cursectnum,SX,SY,SZ-(24<<8),SECT) )
		gain *= OCCLUDEDFACTOR;
	
	switch(num)
	{
		case PIPEBOMB_EXPLODE:
		case LASERTRIP_EXPLODE:
		case RPG_EXPLODE:
			gain = 1.0;
			global = 1;
			if (sector[ps[screenpeek].cursectnum].lotag == 2) pitch -= translatepitch(1024);
			break;
		default:
			if(sector[ps[screenpeek].cursectnum].lotag == 2 && (soundm[num]&SOUNDM_DUKE) == 0)
				pitch = translatepitch(-768);
			//if( sndist > 31444 && PN != MUSICANDSFX)
			//	return -1;
			break;
	}

/*
	// XXX: this is shit
	if( Sound[num].num > 0 && PN != MUSICANDSFX )
	{
		if( SoundOwner[num][0].i == i ) stopsound(num);
		else if( Sound[num].num > 1 ) stopsound(num);
		else if( badguy(&sprite[i]) && sprite[i].extra <= 0 ) stopsound(num);
	}
*/
	
	chan = jfaud->PlaySound(sounds[num], NULL, soundpr[num]);
	if (!chan) return -1;
	
	chan->SetGain(gain);
	chan->SetPitch(pitch);
	chan->SetLoop((soundm[num] & SOUNDM_LOOP) == SOUNDM_LOOP);
	if (soundm[num] & SOUNDM_GLOBAL) global = 1;
	chan->SetRefDist(refdist);
	chan->SetMaxDist(maxdist);
	chan->SetFilter((soundm[num]&SOUNDM_NICE) ? JFAudMixerChannel::Filter4Point : DefaultFilter);
	chan->SetDistanceModel(JFAudMixerChannel::DistanceModelLinear);
	
	if (PN == APLAYER && sprite[i].yvel == screenpeek) {
		chan->SetRolloff(0.0);
		chan->SetFollowListener(true);
		chan->SetPosition(0.0, 0.0, 0.0);
	} else {
		chan->SetRolloff(global ? 0.0 : rolloff);
		chan->SetFollowListener(false);
		chan->SetPosition((float)x/UNITSPERMETRE, (float)(-z>>4)/UNITSPERMETRE, (float)y/UNITSPERMETRE);
	}
	r = keephandle(chan, num, i);
	if (r >= 0) chan->SetStopCallback(stopcallback, r);
	chan->Play();
	
	return 0;
}

void sound(short num)
{
	JFAudMixerChannel *chan;
	int r;
	float pitch = 1.0;

	if (!jfaud || !havewave ||
	    num >= NUM_SOUNDS ||
	    SoundToggle == 0 ||
	    ((soundm[num] & SOUNDM_DUKE) && VoiceToggle == 0) ||
	    ((soundm[num] & SOUNDM_PARENT) && ud.lockout)	// parental mode
	   ) return;

	{
		int ps = soundps[num], pe = soundpe[num], cx;
		cx = labs(pe-ps);
		if (cx) {
			if (ps < pe) pitch = translatepitch(ps + rand()%cx);
			else pitch = translatepitch(pe + rand()%cx);
		} else pitch = translatepitch(ps);
	}
	
	chan = jfaud->PlaySound(sounds[num], NULL, soundpr[num]);
	if (!chan) return;

	chan->SetGain(1.0);
	chan->SetPitch(pitch);
	chan->SetLoop((soundm[num] & SOUNDM_LOOP) == SOUNDM_LOOP);
	chan->SetRolloff(0.0);
	chan->SetRefDist(DEFAULTREFDIST);
	chan->SetFollowListener(true);
	chan->SetPosition(0.0, 0.0, 0.0);
	chan->SetFilter((soundm[num]&SOUNDM_NICE) ? JFAudMixerChannel::Filter4Point : DefaultFilter);
	chan->SetDistanceModel(JFAudMixerChannel::DistanceModelLinear);

	r = keephandle(chan, num, -1);
	if (r >= 0) chan->SetStopCallback(stopcallback, r);
	chan->Play();
}

int spritesound(unsigned short num, short i)
{
	if (num >= NUM_SOUNDS) return -1;
	return xyzsound(num,i,SX,SY,SZ);
}

void stopsound(short num)
{
	int j;
	
	if (!jfaud || !havewave) return;
	for (j=NumVoices-1;j>=0;j--) {
		if (chans[j].done || !chans[j].chan || chans[j].soundnum != num) continue;
		
		jfaud->FreeSound(chans[j].chan);
		chans[j].chan = NULL;
		chans[j].done = false;
	}
}

void stopspritesound(short num, short i)
{
	int j;
	
	if (!jfaud || !havewave) return;
	for (j=NumVoices-1;j>=0;j--) {
		if (chans[j].done || !chans[j].chan || chans[j].owner != i || chans[j].soundnum != num) continue;
		
		jfaud->FreeSound(chans[j].chan);
		chans[j].chan = NULL;
		chans[j].done = false;
		return;
	}
}

void stopenvsound(short num, short i)
{
	int j;
	
	if (!jfaud || !havewave) return;
	for (j=NumVoices-1;j>=0;j--) {
		if (chans[j].done || !chans[j].chan || chans[j].owner != i) continue;

		jfaud->FreeSound(chans[j].chan);
		chans[j].chan = NULL;
		chans[j].done = false;
	}
}

void pan3dsound(void)
{
	JFAudMixer *mix;
	int j, global;
	short i;
	long cx, cy, cz, sx,sy,sz;
	short ca,cs;
	float gain, rolloff;

	numenvsnds = 0;
	if (!jfaud || !havewave) return;
	mix = jfaud->GetWave();
	if (!mix) return;

	jfaud->AgeCache();
	
	if(ud.camerasprite == -1) {
		cx = ps[screenpeek].oposx;
		cy = ps[screenpeek].oposy;
		cz = ps[screenpeek].oposz;
		cs = ps[screenpeek].cursectnum;
		ca = ps[screenpeek].ang+ps[screenpeek].look_ang;
	} else {
		cx = sprite[ud.camerasprite].x;
		cy = sprite[ud.camerasprite].y;
		cz = sprite[ud.camerasprite].z;
		cs = sprite[ud.camerasprite].sectnum;
		ca = sprite[ud.camerasprite].ang;
	}

	mix->SetListenerPosition((float)cx/UNITSPERMETRE, (float)(-cz>>4)/UNITSPERMETRE, (float)cy/UNITSPERMETRE);
	mix->SetListenerOrientation((float)sintable[(ca+512)&2047]/16384.0, 0.0, (float)sintable[ca&2047]/16384.0,
		0.0, 1.0, 0.0);
	
	for (j=NumVoices-1; j>=0; j--) {
		if (chans[j].done || !chans[j].chan || chans[j].owner < 0) continue;

		global = 0;
		gain = 1.0;
		rolloff = DEFAULTROLLOFF;
		i = chans[j].owner;

		sx = sprite[i].x;
		sy = sprite[i].y;
		sz = sprite[i].z;

		if (PN != MUSICANDSFX && !cansee(cx,cy,cz-(24<<8),cs,sx,sy,sz-(24<<8),SECT) )
			gain *= OCCLUDEDFACTOR;
		
		if(PN == MUSICANDSFX && SLT < 999) rolloff = 1.0;
		if( soundm[ chans[j].soundnum ]&SOUNDM_GLOBAL ) global = 1;
		
		switch(chans[j].soundnum) {
			case PIPEBOMB_EXPLODE:
			case LASERTRIP_EXPLODE:
			case RPG_EXPLODE:
				gain = 1.0;
				global = 1;
				break;
			default:
				//if( sndist > 31444 && PN != MUSICANDSFX) {
				//	stopsound(j);
				//	continue;
				//}
				break;
		}
		
		// A sound may move from player-relative 3D if the viewpoint shifts from the player
		// through a viewscreen or viewpoint switching
		chans[j].chan->SetGain(gain);
		if (PN == APLAYER && sprite[i].yvel == screenpeek) {
			chans[j].chan->SetRolloff(0.0);
			chans[j].chan->SetFollowListener(true);
			chans[j].chan->SetPosition(0.0, 0.0, 0.0);
		} else {
			chans[j].chan->SetRolloff(global ? 0.0 : rolloff);
			chans[j].chan->SetFollowListener(false);
			chans[j].chan->SetPosition((float)sx/UNITSPERMETRE, (float)(-sz>>4)/UNITSPERMETRE, (float)sy/UNITSPERMETRE);
		}
	}
}

void clearsoundlocks(void)
{
}

void FX_SetVolume( int volume )
{
}

void FX_SetReverseStereo( int setting )
{
}

void FX_SetReverb( int reverb )
{
}

void FX_SetReverbDelay( int delay )
{
}

int FX_VoiceAvailable( int priority )
{
	if (!jfaud) return 0;
	return 1;
}

int FX_PlayVOC3D( char *ptr, int pitchoffset, int angle, int distance,
       int priority, unsigned long callbackval )
{
	printf("FX_PlayVOC3D()\n");
	return 0;
}

int FX_PlayWAV3D( char *ptr, int pitchoffset, int angle, int distance,
       int priority, unsigned long callbackval )
{
	printf("FX_PlayWAV3D()\n");
	return 0;
}

int FX_StopSound( int handle )
{
	printf("FX_StopSound()\n");
	return 0;
}

int FX_StopAllSounds( void )
{
	int j;
	
	if (!jfaud || !havewave) return 0;
	for (j=NumVoices-1; j>=0; j--) {
		if (!chans[j].chan || !jfaud->IsValidSound(chans[j].chan)) continue;

		jfaud->FreeSound(chans[j].chan);
		chans[j].chan = NULL;
		chans[j].owner = -1;
	}

	return 0;
}

void MUSIC_SetVolume( int volume )
{
}

void MUSIC_Pause( void )
{
	if (jfaud) jfaud->PauseMusic(true);
}

void MUSIC_Continue( void )
{
	if (jfaud) jfaud->PauseMusic(false);
}

int MUSIC_StopSong( void )
{
	if (jfaud) jfaud->StopMusic();
	return 0;
}

void MUSIC_RegisterTimbreBank( unsigned char *timbres )
{
}

static int osdcmd_setsoundfilter(const osdfuncparm_t *parm)
{
	int filt = 0;
	if (parm->numparms < 1) return OSDCMD_SHOWHELP;

	filt = Batol(parm->parms[0]);
	if (filt < JFAudMixerChannel::FilterNearest) filt = JFAudMixerChannel::FilterNearest;
	else if (filt > JFAudMixerChannel::Filter4Point) filt = JFAudMixerChannel::Filter4Point;
	DefaultFilter = (JFAudMixerChannel::Filter)filt;
	return OSDCMD_OK;
}

int EnumAudioDevs(struct audioenumdrv **wave, struct audioenumdev **midi, struct audioenumdev **cda)
{
	char **enumerdrv, *defdrv;
	int i;
	bool isdefdrv, isdefdev;
	struct audioenumdev **d;

	*wave = NULL;
	//*midi = *cda = NULL;

	enumerdrv = JFAud::EnumerateWaveDevices(NULL, &defdrv);
	if (enumerdrv) {
		*wave = (struct audioenumdrv *)calloc(1,sizeof(struct audioenumdrv));
		(*wave)->def = defdrv;
		(*wave)->drvs = enumerdrv;
		(*wave)->devs = NULL;
		d = &(*wave)->devs;
		for (i=0; enumerdrv[i]; i++) {
			*d = (struct audioenumdev *)calloc(1,sizeof(struct audioenumdev));
			(*d)->devs = JFAud::EnumerateWaveDevices(enumerdrv[i], &(*d)->def);
			(*d)->next = NULL;
			d = &(*d)->next;
		}
	}

	return 0;
}

