#include "i_musicinterns.h"
#include "c_cvars.h"
#include "cmdlib.h"

#if !defined(_WIN32) && 0
// Under Linux, buffer output from Timidity to try to avoid "bubbles"
// in the sound output.
class FPipeBuffer
{
public:
	FPipeBuffer::FPipeBuffer (int fragSize, int nFrags, int pipe);
	~FPipeBuffer ();

	int ReadFrag (BYTE *Buf);

private:
	int PipeHandle;
	int FragSize;
	int BuffSize;
	int WritePos, ReadPos;
	BYTE *Buffer;
	bool GotFull;
	SDL_mutex *BufferMutex;
	SDL_thread *Reader;

	static int ThreadProc (void *data);
};
#endif

#ifndef _WIN32
#include <sys/types.h>
#include <sys/wait.h>
#include <wordexp.h>

int ChildQuit;

void ChildSigHandler (int signum)
{
	ChildQuit = waitpid (-1, NULL, WNOHANG);
}
#endif

#ifdef _WIN32
const char TimiditySong::EventName[] = "TiMidity Killer";

CVAR (String, timidity_exe, "timidity.exe", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
#else
CVAR (String, timidity_exe, "timidity", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
#endif
CVAR (String, timidity_extargs, "", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)	// extra args to pass to Timidity
CVAR (String, timidity_chorus, "0", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (String, timidity_reverb, "0", CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_stereo, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_8bit, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Bool, timidity_byteswap, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)

CUSTOM_CVAR (Int, timidity_pipe, 60, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
{ // pipe size in ms
	if (timidity_pipe < 0)
	{ // a negative size makes no sense
		timidity_pipe = 0;
	}
}

CUSTOM_CVAR (Int, timidity_frequency, 22050, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
{ // Clamp frequency to Timidity's limits
	if (self < 4000)
		self = 4000;
	else if (self > 65000)
		self = 65000;
}

void TimiditySong::Play (bool looping)
{
	m_Status = STATE_Stopped;
	m_Looping = looping;

	if (LaunchTimidity ())
	{
		if (m_Stream != NULL)
		{
			if (m_Stream->Play (snd_musicvolume))
			{
				m_Status = STATE_Playing;
			}
		}
		else
		{ // Assume success if not mixing with FMOD
			m_Status = STATE_Playing;
		}
	}
}

void TimiditySong::Stop ()
{
	if (m_Status != STATE_Stopped)
	{
		if (m_Stream != NULL)
		{
			m_Stream->Stop ();
		}
#ifdef _WIN32
		if (ChildProcess != INVALID_HANDLE_VALUE)
		{
			SetEvent (KillerEvent);
			if (WaitForSingleObject (ChildProcess, 500) != WAIT_OBJECT_0)
			{
				TerminateProcess (ChildProcess, 666);
			}
			CloseHandle (ChildProcess);
			ChildProcess = INVALID_HANDLE_VALUE;
		}
#else
		if (ChildProcess != -1)
		{
			if (kill (ChildProcess, SIGTERM) != 0)
			{
				kill (ChildProcess, SIGKILL);
			}
			waitpid (ChildProcess, NULL, 0);
			ChildProcess = -1;
		}
#endif
	}
	m_Status = STATE_Stopped;
}

TimiditySong::~TimiditySong ()
{
	Stop ();
#if _WIN32
	if (WriteWavePipe != INVALID_HANDLE_VALUE)
	{
		CloseHandle (WriteWavePipe);
		WriteWavePipe = INVALID_HANDLE_VALUE;
	}
	if (ReadWavePipe != INVALID_HANDLE_VALUE)
	{
		CloseHandle (ReadWavePipe);
		ReadWavePipe = INVALID_HANDLE_VALUE;
	}
	if (KillerEvent != INVALID_HANDLE_VALUE)
	{
		CloseHandle (KillerEvent);
		KillerEvent = INVALID_HANDLE_VALUE;
	}
#else
	if (WavePipe[1] != -1)
	{
		close (WavePipe[1]);
		WavePipe[1] = -1;
	}
	if (WavePipe[0] != -1)
	{
		close (WavePipe[0]);
		WavePipe[0] = -1;
	}
#endif
	if (CommandLine != NULL)
	{
		delete[] CommandLine;
	}
}

TimiditySong::TimiditySong (FILE *file, int len)
	: DiskName ("zmid"),
#ifdef _WIN32
	  ReadWavePipe (INVALID_HANDLE_VALUE), WriteWavePipe (INVALID_HANDLE_VALUE),
	  KillerEvent (INVALID_HANDLE_VALUE),
	  ChildProcess (INVALID_HANDLE_VALUE),
	  Validated (false),
#else
	  ChildProcess (-1),
#endif
	  CommandLine (NULL)
{
	bool success;
	FILE *f;

#ifndef _WIN32
	WavePipe[0] = WavePipe[1] = -1;
#endif
	
	if (DiskName == NULL)
	{
		Printf (PRINT_BOLD, "Could not create temp music file\n");
		return;
	}

	f = fopen (DiskName, "wb");
	if (f == NULL)
	{
		Printf (PRINT_BOLD, "Could not open temp music file\n");
		return;
	}

	BYTE *buf = new BYTE[len];
	fread (buf, 1, len, file);

	// The file type has already been checked before this class instance was
	// created, so we only need to check one character to determine if this
	// is a MUS or MIDI file and write it to disk as appropriate.
	if (buf[1] == 'T')
	{
		success = (fwrite (buf, 1, len, f) == (size_t)len);
	}
	else
	{
		success = ProduceMIDI (buf, f);
	}
	fclose (f);
	delete[] buf;

	if (success)
	{
		PrepTimidity ();
	}
	else
	{
		Printf (PRINT_BOLD, "Could not write temp music file\n");
	}
}

void TimiditySong::PrepTimidity ()
{
	char cmdline[512];
	int cmdsize;
	int pipeSize;
#ifdef _WIN32
	static SECURITY_ATTRIBUTES inheritable = { sizeof(inheritable), NULL, TRUE };
	
	if (!Validated && !ValidateTimidity ())
	{
		return;
	}

	Validated = true;
	KillerEvent = CreateEvent (NULL, FALSE, FALSE, EventName);
	if (KillerEvent == INVALID_HANDLE_VALUE)
	{
		Printf (PRINT_BOLD, "Could not create TiMidity++ kill event.\n");
		return;
	}
#endif // WIN32

	cmdsize = sprintf (cmdline, "%s %s -EFchorus=%s -EFreverb=%s -s%d ",
		*timidity_exe, *timidity_extargs,
		*timidity_chorus, *timidity_reverb, *timidity_frequency);

	pipeSize = (timidity_pipe * timidity_frequency / 1000)
		<< (timidity_stereo + !timidity_8bit);

	if (pipeSize != 0)
	{
#ifdef _WIN32
		// Round pipe size up to nearest power of 2 to try and avoid partial
		// buffer reads in FillStream() under NT. This does not seem to be an
		// issue under 9x.
		int bitmask = pipeSize & -pipeSize;
		
		while (bitmask < pipeSize)
			bitmask <<= 1;
		pipeSize = bitmask;

		if (!CreatePipe (&ReadWavePipe, &WriteWavePipe, &inheritable, pipeSize))
#else // WIN32
		if (pipe (WavePipe) == -1)
#endif
		{
			Printf (PRINT_BOLD, "Could not create a data pipe for TiMidity++.\n");
			pipeSize = 0;
		}
		else
		{
			m_Stream = GSnd->CreateStream (FillStream, pipeSize,
				(timidity_stereo ? 0 : SoundStream::Mono) |
				(timidity_8bit ? SoundStream::Bits8 : 0),
				timidity_frequency, this);
			if (m_Stream == NULL)
			{
				Printf (PRINT_BOLD, "Could not create music stream.\n");
				pipeSize = 0;
#ifdef _WIN32
				CloseHandle (WriteWavePipe);
				CloseHandle (ReadWavePipe);
				ReadWavePipe = WriteWavePipe = INVALID_HANDLE_VALUE;
#else
				close (WavePipe[1]);
				close (WavePipe[0]);
				WavePipe[0] = WavePipe[1] = -1;
#endif
			}
		}
		
		if (pipeSize == 0)
		{
			Printf (PRINT_BOLD, "If your soundcard cannot play more than one\n"
								"wave at a time, you will hear no music.\n");
		}
		else
		{
			cmdsize += sprintf (cmdline + cmdsize, "-o - -Ors");
		}
	}

	if (pipeSize == 0)
	{
		cmdsize += sprintf (cmdline + cmdsize, "-Od");
	}

	cmdline[cmdsize++] = timidity_stereo ? 'S' : 'M';
	cmdline[cmdsize++] = timidity_8bit ? '8' : '1';
	if (timidity_byteswap)
	{
		cmdline[cmdsize++] = 'x';
	}

	LoopPos = cmdsize + 4;

	sprintf (cmdline + cmdsize, " -idl %s", DiskName.GetName ());

	CommandLine = copystring (cmdline);
}

#ifdef _WIN32
// Check that this TiMidity++ knows about the TiMidity Killer event.
// If not, then we can't use it, because Win32 provides no other way
// to conveniently signal it to quit. The check is done by simply
// searching for the event's name somewhere in the executable.
bool TimiditySong::ValidateTimidity ()
{
	char foundPath[MAX_PATH];
	char *filePart;
	DWORD pathLen;
	DWORD fileLen;
	HANDLE diskFile;
	HANDLE mapping;
	const BYTE *exeBase;
	const BYTE *exeEnd;
	const BYTE *exe;
	bool good;

	pathLen = SearchPath (NULL, timidity_exe, NULL, MAX_PATH, foundPath, &filePart);
	if (pathLen == 0)
	{
		Printf (PRINT_BOLD, "Please set the timidity_exe cvar to the location of TiMidity++\n");
		return false;
	}
	if (pathLen > MAX_PATH)
	{
		Printf (PRINT_BOLD, "The path to TiMidity++ is too long\n");
		return false;
	}

	diskFile = CreateFile (foundPath, GENERIC_READ, FILE_SHARE_READ, NULL,
		OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
	if (diskFile == INVALID_HANDLE_VALUE)
	{
		Printf (PRINT_BOLD, "Could not access %s\n", foundPath);
		return false;
	}
	fileLen = GetFileSize (diskFile, NULL);
	mapping = CreateFileMapping (diskFile, NULL, PAGE_READONLY, 0, 0, NULL);
	if (mapping == NULL)
	{
		Printf (PRINT_BOLD, "Could not create mapping for %s\n", foundPath);
		CloseHandle (diskFile);
		return false;
	}
	exeBase = (const BYTE *)MapViewOfFile (mapping, FILE_MAP_READ, 0, 0, 0);
	if (exeBase == NULL)
	{
		Printf (PRINT_BOLD, "Could not map %s\n", foundPath);
		CloseHandle (mapping);
		CloseHandle (diskFile);
		return false;
	}

	good = false;
	try
	{
		for (exe = exeBase, exeEnd = exeBase+fileLen; exe < exeEnd; )
		{
			const char *tSpot = (const char *)memchr (exe, 'T', exeEnd - exe);
			if (tSpot == NULL)
			{
				break;
			}
			if (memcmp (tSpot+1, EventName+1, sizeof(EventName)-1) == 0)
			{
				good = true;
				break;
			}
			exe = (const BYTE *)tSpot + 1;
		}
	}
	catch (...)
	{
		Printf (PRINT_BOLD, "Error reading %s\n", foundPath);
	}
	if (!good)
	{
		Printf (PRINT_BOLD, "ZDoom requires a special version of TiMidity++\n");
	}

	UnmapViewOfFile ((LPVOID)exeBase);
	CloseHandle (mapping);
	CloseHandle (diskFile);

	return good;
}
#endif // _WIN32

bool TimiditySong::LaunchTimidity ()
{
	if (CommandLine == NULL)
	{
		return false;
	}

	// Tell Timidity whether it should loop or not
	CommandLine[LoopPos] = m_Looping ? 'l' : ' ';
	DPrintf ("cmd: \x1cG%s\n", CommandLine);

#ifdef _WIN32
	STARTUPINFO startup = { sizeof(startup), };
	PROCESS_INFORMATION procInfo;

	startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;

	startup.hStdInput = INVALID_HANDLE_VALUE;
	startup.hStdOutput = WriteWavePipe != INVALID_HANDLE_VALUE ?
						  WriteWavePipe : GetStdHandle (STD_OUTPUT_HANDLE);
	startup.hStdError = GetStdHandle (STD_ERROR_HANDLE);

	startup.lpTitle = "TiMidity (ZDoom Launched)";
	startup.wShowWindow = SW_SHOWMINNOACTIVE;

	if (CreateProcess (NULL, CommandLine, NULL, NULL, TRUE,
		/*HIGH_PRIORITY_CLASS|*/DETACHED_PROCESS, NULL, NULL, &startup, &procInfo))
	{
		ChildProcess = procInfo.hProcess;
		//SetThreadPriority (procInfo.hThread, THREAD_PRIORITY_HIGHEST);
		CloseHandle (procInfo.hThread);		// Don't care about the created thread
		return true;
	}

	char hres[9];
	LPTSTR msgBuf;
	HRESULT err = GetLastError ();

	if (!FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER |
						FORMAT_MESSAGE_FROM_SYSTEM |
						FORMAT_MESSAGE_IGNORE_INSERTS,
						NULL, err, 0, (LPTSTR)&msgBuf, 0, NULL))
	{
		sprintf (hres, "%08lx", err);
		msgBuf = hres;
	}

	Printf (PRINT_BOLD, "Could not run timidity with the command line:\n%s\n"
						"Reason: %s\n", CommandLine, msgBuf);
	if (msgBuf != hres)
	{
		LocalFree (msgBuf);
	}
	return false;
#else
	if (WavePipe[0] != -1 && WavePipe[1] == -1 && m_Stream != NULL)
	{
		// Timidity was previously launched, so the write end of the pipe
		// is closed, and the read end is still open. Close the pipe
		// completely and reopen it.
		
		close (WavePipe[0]);
		WavePipe[0] = -1;
		delete m_Stream;
		m_Stream = NULL;
		PrepTimidity ();
	}
	
	int forkres;
	wordexp_t words;

	switch (wordexp (CommandLine, &words, 0))
	{
	case 0: // all good
		break;
		
	case WRDE_NOSPACE:
		wordfree (&words);
	default:
		return false;
	}

	forkres = fork ();

	if (forkres == 0)
	{
		close (WavePipe[0]);
		dup2 (WavePipe[1], STDOUT_FILENO);
		freopen ("/dev/null", "r", stdin);
		freopen ("/dev/null", "w", stderr);
		close (WavePipe[1]);
		
		printf ("exec %s\n", words.we_wordv[0]);
		execvp (words.we_wordv[0], words.we_wordv);
		exit (0);	// if execvp succeeds, we never get here
	}
	else if (forkres < 0)
	{
		Printf (PRINT_BOLD, "Could not fork when trying to start timidity\n");
	}
	else
	{
//		printf ("child is %d\n", forkres);
		ChildProcess = forkres;
		close (WavePipe[1]);
		WavePipe[1] = -1;
	}
	
	wordfree (&words);
	return ChildProcess != -1;
#endif // _WIN32
}

bool TimiditySong::FillStream (SoundStream *stream, void *buff, int len, void *userdata)
{
	TimiditySong *song = (TimiditySong *)userdata;
	
#ifdef _WIN32
	DWORD avail, got, didget;

	if (!PeekNamedPipe (song->ReadWavePipe, NULL, 0, NULL, &avail, NULL) || avail == 0)
	{ // If nothing is available from the pipe, play silence.
		memset (buff, 0, len);
	}
	else
	{
		didget = 0;
		for (;;)
		{
			ReadFile (song->ReadWavePipe, (BYTE *)buff+didget, len-didget, &got, NULL);
			didget += got;
			if (didget >= (DWORD)len)
				break;

			// Give TiMidity a chance to output something more to the pipe
			Sleep (10);
			if (!PeekNamedPipe (song->ReadWavePipe, NULL, 0, NULL, &avail, NULL) || avail == 0)
			{
				memset ((BYTE *)buff+didget, 0, len-didget);
				break;
			}
		} 
	}
#else
	ssize_t got;

	got = read (song->WavePipe[0], (BYTE *)buff, len);
	if (got < len)
	{
		memset ((BYTE *)buff+got, 0, len-got);
	}

	if (ChildQuit == song->ChildProcess)
	{
		ChildQuit = 0;
//		printf ("child gone\n");
		song->ChildProcess = -1;
		return false;
	}
#endif
	return true;
}

bool TimiditySong::IsPlaying ()
{
#ifdef _WIN32
	if (ChildProcess != INVALID_HANDLE_VALUE)
	{
		if (WaitForSingleObject (ChildProcess, 0) != WAIT_TIMEOUT)
		{ // Timidity has quit
			CloseHandle (ChildProcess);
			ChildProcess = INVALID_HANDLE_VALUE;
#else
	if (ChildProcess != -1)
	{
		if (waitpid (ChildProcess, NULL, WNOHANG) == ChildProcess)
		{
			ChildProcess = -1;
#endif
			if (m_Looping)
			{
				if (!LaunchTimidity ())
				{
					Stop ();
					return false;
				}
				return true;
			}
			return false;
		}
		return true;
	}
	return false;
}

#if !defined(_WIN32) && 0
FPipeBuffer::FPipeBuffer (int fragSize, int nFrags, int pipe)
	: PipeHandle (pipe),
	  FragSize (fragSize),
	  BuffSize (fragSize * nFrags),
	  WritePos (0), ReadPos (0), GotFull (false),
	  Reader (0), PleaseExit (0)
{
	Buffer = new BYTE[BuffSize];
	if (Buffer != NULL)
	{
		BufferMutex = SDL_CreateMutex ();
		if (BufferMutex == NULL)
		{
			Reader = SDL_CreateThread (ThreadProc, (void *)this);
		}
	}
}

FPipeBuffer::~FPipeBuffer ()
{
	if (Reader != NULL)
	{ // I like the Win32 IPC facilities better than SDL's
	  // Fortunately, this is a simple thread, so I can cheat
	  // like this.
		SDL_KillThread (ThreadProc);
	}
	if (BufferMutex != NULL)
	{
		SDL_DestroyMutex (BufferMutex);
	}
	if (Buffer != NULL)
	{
		delete[] Buffer;
	}
}

int FPipeBuffer::ReadFrag (BYTE *buf)
{
	int startavvail;
	int avail;
	int pos;
	int opos;
	
	if (SDL_mutexP (BufferMutex) == -1)
		return 0;
	
	if (WritePos > ReadPos)
	{
		avail = WritePos - ReadPos;
	}
	else
	{
		avail = BuffSize - ReadPos + WritePos;
	}
	if (avail > FragSize)
		avail = FragSize;
	
	startavail = avali;
	pos = ReadPos;
	opos = 0;
	
	while (avail != 0)
	{
		int thistime;
		
		thistime = (pos + avail > BuffSize) ? BuffSize - pos : avail;
		memcpy (buf + opos, Buffer + pos, thistime);
		if (thistime != avail)
		{
			pos = 0;
			avail -= thistime;
		}
		opos += thistime;
	}

	ReadPos = pos;
	
	SDL_mutexV (BufferMutex);
	
	return startavail;
}
#endif	// !_WIN32
