#include "audiosubsys.h"
#include <assert.h>
#include <sys/soundcard.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

#define DEVICE_NAME "/dev/dsp"

const char *AudioSubSystem::error()
{
	return _error.c_str();
}

AudioSubSystem *AudioSubSystem::the()
{
	static AudioSubSystem *as = 0;

	if(as == 0) as = new AudioSubSystem();
	return as;
}

AudioSubSystem::AudioSubSystem()
{
	_running = false;
	usageCount = 0;
	consumer = 0;
	producer = 0;
}

bool AudioSubSystem::attachProducer(ASProducer *producer)
{
	assert(producer);
	if(this->producer) return false;

	this->producer = producer;
	return true;
}

bool AudioSubSystem::attachConsumer(ASConsumer *consumer)
{
	assert(consumer);
	if(this->consumer) return false;

	this->consumer = consumer;
	return true;
}

void AudioSubSystem::detachProducer()
{
	assert(producer);
	producer = 0;

	if(_running) close();
}

void AudioSubSystem::detachConsumer()
{
	assert(consumer);
	consumer = 0;

	if(_running) close();
}

int AudioSubSystem::open(int fragments,int size, int samplingrate, int channels,
                         bool wantfullduplex)
{
	int mode;

	if(wantfullduplex)
		mode = O_RDWR|O_NDELAY;
	else
		mode = O_WRONLY|O_NDELAY;

	audio_fd = ::open(DEVICE_NAME, mode, 0);

	if(audio_fd == -1)
	{
		_error = "device ";
		_error += DEVICE_NAME;
		_error += " can't be opened (";
		_error += strerror(errno);
		_error += ")";
		return -1;
	}
	// this is required here since we'll use close to close down the
	// audio subsystem again if anything else goes wrong
	_running = true;

	int format = AFMT_S16_LE;  
	if (ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format)==-1)  
	{
		_error = "SNDCTL_DSP_SETFMT failed - ";
		_error += strerror(errno);

		close();
		return -1;
	}  

	if (format != AFMT_S16_LE)  
	{  
		_error = "Can't set 16bit (AFMT_S16_LE) playback";

		close();
		return -1;
	} 

	int stereo=-1;     /* 0=mono, 1=stereo */

	if(channels == 1)
	{
		stereo = 0;
	}
	if(channels == 2)
	{
		stereo = 1;
	}

	_channels = channels;

	if(stereo == -1)
	{
		_error = "internal error; set channels to 1 (mono) or 2 (stereo)";

		close();
		return -1;
	}

	int requeststereo = stereo;

	if (ioctl(audio_fd, SNDCTL_DSP_STEREO, &stereo)==-1)
	{
		_error = "SNDCTL_DSP_STEREO failed - ";
		_error += strerror(errno);

		close();
		return -1;
	}

	if (requeststereo != stereo)
	{
		_error = "audio device doesn't support number of requested channels";

		close();
		return -1;
	}

	int speed = samplingrate;

	if (ioctl(audio_fd, SNDCTL_DSP_SPEED, &speed)==-1)  
	{
		_error = "SNDCTL_DSP_SPEED failed - ";
		_error += strerror(errno);

		close();
		return -1;
	}  

    /*
     * Some soundcards seem to be able to only supply "nearly" the requested
     * sampling rate - so allow a tolerance of 2%. This will result in slight
     * detuning, but at least it will work.
     */
    int tolerance = samplingrate/50+1;
 
    if (abs(speed-samplingrate) > tolerance)
	{  
		_error = "can't set requested samplingrate";

		close();
		return -1;
	} 

	// lower 16 bits are the fragment size (as 2^S)
	// higher 16 bits are the number of fragments
	int frag_arg = 0;

	fragment_size = size;

	while(size > 0) { size /= 2; frag_arg++; }
	frag_arg += (fragments << 16);
	if(ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, &frag_arg) == -1)
	{
		char buffer[1024];
		_error = "can't set requested fragments settings";
		sprintf(buffer,"size%d:count%d\n",fragment_size,fragments);
		close();
		return -1;
	}

	// FIXME: check here if frag_arg changed

	if(wantfullduplex)
	{
		int enable_bits = ~(PCM_ENABLE_OUTPUT|PCM_ENABLE_INPUT);

		if(ioctl(audio_fd,SNDCTL_DSP_SETTRIGGER, &enable_bits) == -1)
		{
			_error = "can't request synchronous start of fullduplex operation";

			close();
			return -1;
		}

		char *zbuffer = (char *)calloc(sizeof(char), size);

		while(::write(audio_fd,zbuffer,size) > 0)
			/* fills up the audio buffer */;

		/*
	 	 * Go now, and hope! that the application does the select trick
	 	 * correctly, otherwise we'll get buffer over/underruns soon
	 	 */
		enable_bits = PCM_ENABLE_OUTPUT|PCM_ENABLE_INPUT;
		if(ioctl(audio_fd,SNDCTL_DSP_SETTRIGGER, &enable_bits) == -1)
		{
			_error = "can't start of fullduplex operation";

			close();
			return -1;
		}
	}

	fullDuplex = wantfullduplex;
	return audio_fd;
}

void AudioSubSystem::close()
{
	assert(_running);

	::close(audio_fd);

	wBuffer.clear();
	rBuffer.clear();

	_running = false;
	audio_fd = 0;
}

bool AudioSubSystem::running()
{
	return _running;
}

void AudioSubSystem::handleIO(int type)
{
	if(type & ioRead)
	{
		char buffer[fragment_size];
		int len = ::read(audio_fd,buffer,fragment_size);

		if(len > 0)
		{
			rBuffer.write(len,buffer);
		}
	}

	if(type & ioWrite)
	{
		char buffer[fragment_size];
		while(wBuffer.size() < fragment_size)
		{
			long wbsz = wBuffer.size();
			producer->needMore();

			if(wbsz == wBuffer.size())
			{
				/*
				 * Even though we asked the client to supply more
				 * data, he didn't give us more. So we can't supply
				 * output data as well. Bad luck. Might produce a
				 * buffer underrun - but we can't help here.
				 */
				printf("FULL DUPLEX WARNING: client->needMore() failed; no more data available\n");
				return;
			}
		}
		/*
		 * look how much we really can write without blocking
		 */
		audio_buf_info info;
		ioctl(audio_fd, SNDCTL_DSP_GETOSPACE, &info);

		int can_write = min(info.bytes, fragment_size);

		/*
		 * ok, so write it (as we checked that our buffer has enough data
		 * to do so and the soundcardbuffer has enough data to handle this
		 * write, nothing can go wrong here)
		 */
		int rSize = wBuffer.read(can_write,buffer);
		assert(rSize == can_write);

		int len = ::write(audio_fd,buffer,can_write);
		assert(len == can_write);
	}

	assert((type & ioExcept) == 0);
}

void AudioSubSystem::read(void *buffer, int size)
{
	while(rBuffer.size() < size)
	{
		fd_set readfds;

		FD_ZERO(&readfds);
		FD_SET(audio_fd,&readfds);

		//printf("must use select\n");
		int rc = select(audio_fd+1,&readfds,NULL,NULL,NULL);
		assert(rc > 0);

		handleIO(ioRead);
	}
	int rSize = rBuffer.read(size,buffer);
	assert(rSize == size);
}

void AudioSubSystem::write(void *buffer, int size)
{
	wBuffer.write(size,buffer);
}

int AudioSubSystem::channels()
{
	return _channels;
}

