/*

    Copyright (C) 2000,2001 Jozef Kosoru
                            jozef.kosoru@pobox.sk
			  (C) 2000,2001 Stefan Westerfeld
			                stefan@space.twc.de

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.
  
    This library 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
    Library General Public License for more details.
   
    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.

    */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/**
 * only compile 'alsa' AudioIO class if configure thinks it is a good idea
 */
#ifdef HAVE_LIBASOUND

#ifdef HAVE_ALSA_ASOUNDLIB_H
#include <alsa/asoundlib.h>
#elif defined(HAVE_SYS_ASOUNDLIB_H)
#include <sys/asoundlib.h>
#endif

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/stat.h>

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>		// Needed on some systems.
#endif

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <algorithm>

#include "debug.h"
#include "audioio.h"

namespace Arts {

class AudioIOALSA : public AudioIO {
protected:
	int audio_read_fd;
	int audio_write_fd;
	int requestedFragmentSize;
	int requestedFragmentCount;

	enum BufferMode{block, stream};
	int m_card;
	int m_device;
	int m_format;
	BufferMode m_bufferMode;

	snd_pcm_t *m_pcm_handle;
	snd_pcm_channel_info_t m_cinfo;
	snd_pcm_format_t m_cformat;
	snd_pcm_channel_params_t m_params;
	snd_pcm_channel_setup_t m_setup;

	int setPcmParams(const int channel);
	void checkCapabilities();

public:
	AudioIOALSA();

	void setParam(AudioParam param, int& value);
	int getParam(AudioParam param);

	bool open();
	void close();
	int read(void *buffer, int size);
	int write(void *buffer, int size);
};

REGISTER_AUDIO_IO(AudioIOALSA,"alsa","Advanced Linux Sound Architecture");
};

using namespace std;
using namespace Arts;

AudioIOALSA::AudioIOALSA()
{
 	param(samplingRate) = 44100;
	paramStr(deviceName) = "/dev/dsp"; //!! alsa doesn't need this
	requestedFragmentSize = param(fragmentSize) = 1024;
	requestedFragmentCount = param(fragmentCount) = 7;
	param(channels) = 2;
	param(direction) = directionWrite;

	/*
	 * default parameters
	 */
    m_card = snd_defaults_pcm_card();  //!! need interface !!
    m_device = snd_defaults_pcm_device(); //!!
#ifdef WORDS_BIGENDIAN
    m_format = SND_PCM_SFMT_S16_BE;
#else
    m_format = SND_PCM_SFMT_S16_LE;
#endif
    m_bufferMode = block;  //block/stream (stream mode doesn't work yet)

	if(m_card >= 0) {
    	char* cardname = 0;

    	if(snd_card_get_name(m_card, &cardname) == 0 && cardname != 0)
		{
			//!! thats not what devicename is intended to do
			//!! devicename is an input information into
			//!! the "driver", to select which card to use
			//!! not an output information
    		paramStr(deviceName) = cardname;
    		free(cardname);
		}
	}
}

bool AudioIOALSA::open()
{
	string& _error = paramStr(lastError);
	string& _deviceName = paramStr(deviceName);
	int& _channels = param(channels);
	int& _fragmentSize = param(fragmentSize);
	int& _fragmentCount = param(fragmentCount);
	int& _samplingRate = param(samplingRate);
	int& _direction = param(direction);
	int& _format = param(format);

	/*
	 * initialize format - TODO: implement fallback (i.e. if no format given,
	 * it should try 16bit first, then fall back to 8bit)
	 */
	switch(_format)
	{
		default:	_format = 16;

		case 16:	// 16bit, signed little endian
				m_format = SND_PCM_SFMT_S16_LE;
			break;

		case 17:	// 16bit, signed big endian
				m_format = SND_PCM_SFMT_S16_BE;
			break;

		case 8:		// 8bit, unsigned
				m_format = SND_PCM_SFMT_U8;
			break;
	}

	/* open pcm device */
	int mode = SND_PCM_OPEN_NONBLOCK;

	if(_direction == directionReadWrite)
    	mode |= SND_PCM_OPEN_DUPLEX;
	else if(_direction == directionWrite)
    	mode |= SND_PCM_OPEN_PLAYBACK;
	else
	{
		_error = "invalid direction";
		return false;
	}

	int err;
	if((err = snd_pcm_open(&m_pcm_handle, m_card, m_device, mode)) < 0) {
		_error = "device: ";
		_error += _deviceName.c_str();
		_error += " can't be opened (";
		_error += snd_strerror(err);
		_error += ")";
		return false;
	}
	else {
		artsdebug("ALSA driver: %s", _deviceName.c_str());
	}

	snd_pcm_nonblock_mode(m_pcm_handle, 0);

	/* flush buffers */
	(void)snd_pcm_capture_flush(m_pcm_handle);
	if(_direction & directionRead)
		(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE);
	if(_direction & directionWrite)
		(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK);

	/* check device capabilities */
	checkCapabilities();

	/* set the fragment settings to what the user requested */
	_fragmentSize = requestedFragmentSize;
	_fragmentCount = requestedFragmentCount;

  	/* set PCM communication parameters */
  	if((_direction & directionRead) && setPcmParams(SND_PCM_CHANNEL_CAPTURE))
		return false;
  	if((_direction & directionWrite) && setPcmParams(SND_PCM_CHANNEL_PLAYBACK))
		return false;

  	/* prepare channel */
    if((_direction & directionRead) &&
		snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE) < 0)
	{
      	_error = "Unable to prepare capture channel!";
		return false;
    }
    if((_direction & directionWrite) &&
    	snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK) < 0)
	{
    	_error = "Unable to prepare playback channel!";
	  	return false;
    }

	/* obtain current PCM setup (may differ from requested one) */
	(void)memset(&m_setup, 0, sizeof(m_setup));

	m_setup.channel = SND_PCM_CHANNEL_PLAYBACK;
	if(snd_pcm_channel_setup(m_pcm_handle, &m_setup) < 0) {
    	_error = "Unable to obtain channel setup!";
		return false;
	}

	/* check samplerate */
	const int tolerance = _samplingRate/10+1000;
	if(abs(m_setup.format.rate-_samplingRate) > tolerance)
	{
		_error = "Can't set requested sampling rate!";
		char details[80];
		sprintf(details," (requested rate %d, got rate %d)",
            _samplingRate, m_setup.format.rate);
    	_error += details;
		return false;
  	}
	_samplingRate = m_setup.format.rate;

	/* check format */
	if(m_setup.format.format != m_format) {
    	_error = "Can't set requested format:";
    	_error += snd_pcm_get_format_name(m_format);
		return false;
	}

	/* check voices */
	if(m_setup.format.voices != _channels) {
		_error = "Audio device doesn't support number of requested channels!";
		return false;
	}

	/* update fragment settings with what we got */
	switch(m_bufferMode) {
		case block:
      			_fragmentSize = m_setup.buf.block.frag_size;
      			_fragmentCount = m_setup.buf.block.frags_max-1;
      		break;
    	case stream:
      			_fragmentSize = m_setup.buf.stream.queue_size;
      			_fragmentCount = 1;
      		break;
  	}

  	artsdebug("buffering: %d fragments with %d bytes "
            "(audio latency is %1.1f ms)", _fragmentCount, _fragmentSize,
            (float)(_fragmentSize*_fragmentCount) /
            (float)(2.0 * _samplingRate * _channels)*1000.0);

  	/* obtain PCM file descriptor(s) */
	audio_read_fd = audio_write_fd = -1;

	if(_direction & directionRead)
  		audio_read_fd = snd_pcm_file_descriptor(m_pcm_handle,
												SND_PCM_CHANNEL_CAPTURE);
	if(_direction & directionWrite)
  		audio_write_fd = snd_pcm_file_descriptor(m_pcm_handle,
												SND_PCM_CHANNEL_PLAYBACK);

  	/* start recording */
  	if((_direction & directionRead) && snd_pcm_capture_go(m_pcm_handle)) {
    	_error = "Can't start recording!";
		return false;
  	}

  	return true;
}

void AudioIOALSA::close()
{
	int& _direction = param(direction);
	if(_direction & directionRead)
		(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE);
	if(_direction & directionWrite)
		(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK);
	(void)snd_pcm_close(m_pcm_handle);
}

void AudioIOALSA::setParam(AudioParam p, int& value)
{
	switch(p)
	{
		case fragmentSize:
				param(p) = requestedFragmentSize = value;
			break;
		case fragmentCount:
				param(p) = requestedFragmentCount = value;
			break;
		default:
				param(p) = value;
			break;
	}
}

int AudioIOALSA::getParam(AudioParam p)
{
    snd_pcm_channel_status_t status;
    (void)memset(&status, 0, sizeof(status));

	switch(p)
	{
		case canRead:
      			status.channel = SND_PCM_CHANNEL_CAPTURE;
      			if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
        			arts_warning("Capture channel status error!");
        			return -1;
      			}
				return status.free;
			break;

		case canWrite:
      			status.channel = SND_PCM_CHANNEL_PLAYBACK;
      			if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
        			arts_warning("Playback channel status error!");
        			return -1;
      			}
				return status.free;
			break;

		case selectReadFD:
				return audio_read_fd;
			break;

		case selectWriteFD:
				return audio_write_fd;
			break;

		case autoDetect:
				/*
				 * that the ALSA driver could be compiled doesn't say anything
				 * about whether it will work (the user might be using an OSS
				 * kernel driver) so we'll use a value less than the OSS one
				 * here, because OSS will most certainly work (ALSA's OSS emu)
				 */
				return 5;
			break;

		default:
				return param(p);
			break;
	}
}

int AudioIOALSA::read(void *buffer, int size)
{
	int length;
	do {
		length = snd_pcm_read(m_pcm_handle, buffer, size);
	} while (length == -EINTR);
	if(length == -EPIPE) {
		snd_pcm_channel_status_t status;
		(void)memset(&status, 0, sizeof(status));
		status.channel = SND_PCM_CHANNEL_CAPTURE;
		if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
			arts_info("Capture channel status error!");
			return -1;
		}
		else if(status.status == SND_PCM_STATUS_RUNNING) {
			length = 0;
		}
		else if(status.status == SND_PCM_STATUS_OVERRUN) {
			artsdebug("Overrun at position: %d" ,status.scount);
			if(snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE)<0)
			{
				arts_info("Overrun: capture prepare error!");
				return -1;
			}
			length = 0;
		}
		else {
			arts_info("Unknown capture error!");
			return -1;
		}
	}
    else if(length < 0) {
		arts_info("Capture error: %s", snd_strerror(length));
		return -1;
	}
	return length;
}

int AudioIOALSA::write(void *buffer, int size)
{
	int length;
	while((length = snd_pcm_write(m_pcm_handle, buffer, size)) != size) {
	if (length == -EINTR)
		continue; // Try again
        snd_pcm_channel_status_t status;
        (void)memset(&status, 0, sizeof(status));
        status.channel = SND_PCM_CHANNEL_PLAYBACK;

        if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
        	arts_warning("Playback channel status error!");
        	return -1;
        }
		else if(status.status == SND_PCM_STATUS_UNDERRUN) {
        	artsdebug("Underrun at position: %d", status.scount);
          	if(snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK)
														< 0) {
            	arts_warning("Underrun: playback prepare error!");
            	return -1;
          	}
        }
        else {
        	arts_warning("Unknown playback error!");
        	return -1;
        }
    }
	return size;
}

int AudioIOALSA::setPcmParams(const int channel)
{
	int &_samplingRate = param(samplingRate);
	int &_channels = param(channels);
	int &_fragmentSize = param(fragmentSize);
	int &_fragmentCount = param(fragmentCount);

	(void)memset(&m_cformat, 0, sizeof(m_cformat));
	m_cformat.interleave = 1;
	m_cformat.format = m_format;
	m_cformat.rate = _samplingRate;
	m_cformat.voices = _channels;

	(void)memset(&m_params, 0, sizeof(m_params));
	switch(m_bufferMode){
		case stream:
				m_params.mode=SND_PCM_MODE_STREAM;
			break;
		case block:
				m_params.mode=SND_PCM_MODE_BLOCK;
			break;
	}
	m_params.channel=channel;
	(void)memcpy(&m_params.format, &m_cformat, sizeof(m_cformat));
	if(channel==SND_PCM_CHANNEL_CAPTURE){
		m_params.start_mode=SND_PCM_START_GO;
		m_params.stop_mode=SND_PCM_STOP_ROLLOVER;
	}
	else{	//SND_PCM_CHANNEL_PLAYBACK
		m_params.start_mode= (m_bufferMode==block) ? SND_PCM_START_FULL : SND_PCM_START_DATA;
		m_params.stop_mode=SND_PCM_STOP_ROLLOVER;	// SND_PCM_STOP_STOP
		//use this ^^^ if you want to track underruns
	}

	switch(m_bufferMode){
		case stream:
			m_params.buf.stream.queue_size=1024*1024; //_fragmentSize*_fragmentCount;
			m_params.buf.stream.fill=SND_PCM_FILL_SILENCE_WHOLE;
			m_params.buf.stream.max_fill=1024;
		break;
		case block:
			m_params.buf.block.frag_size=_fragmentSize;
			if(channel==SND_PCM_CHANNEL_CAPTURE){
				m_params.buf.block.frags_max=1;
				m_params.buf.block.frags_min=1;
			}
			else{	//SND_PCM_CHANNEL_PLAYBACK
				m_params.buf.block.frags_max=_fragmentCount+1;
				m_params.buf.block.frags_min=1;
			}
	}
	if(snd_pcm_channel_params(m_pcm_handle, &m_params)<0){
		paramStr(lastError) = "Unable to set channel params!";
		return 1;
	}
	else {
		return 0;
	}
}

void AudioIOALSA::checkCapabilities()
{
	snd_pcm_info_t info;
	(void)memset(&info, 0, sizeof(info));
	if(!snd_pcm_info(m_pcm_handle, &info)) {
		string flags = "";
		if(info.flags & SND_PCM_INFO_PLAYBACK) flags += "playback ";
		if(info.flags & SND_PCM_INFO_CAPTURE) flags += "capture ";
		if(info.flags & SND_PCM_INFO_DUPLEX) flags += "duplex ";
		if(info.flags & SND_PCM_INFO_DUPLEX_RATE) flags += "duplex_rate ";
		artsdebug(" type:%d id:%s\n"
							" flags:%s\n"
							" playback_subdevices:%d capture_subdevices:%d",
							info.type, info.id,
							flags.c_str(),
							info.playback+1, info.capture+1);
	}
	else {
		arts_warning("Can't get device info!"); //not fatal error
	}
	
	(void)memset(&m_cinfo, 0, sizeof(m_cinfo));
	m_cinfo.channel = SND_PCM_CHANNEL_PLAYBACK;
	if(!snd_pcm_channel_info(m_pcm_handle, &m_cinfo)) {
		string flags = "";
		if(m_cinfo.flags & SND_PCM_CHNINFO_MMAP) flags += "mmap ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_STREAM) flags += "stream ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_BLOCK) flags += "block ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_BATCH) flags += "batch ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_INTERLEAVE) flags += "interleave ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_NONINTERLEAVE) flags += "noninterleave ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_BLOCK_TRANSFER) flags += "block_transfer ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_OVERRANGE) flags += "overrange ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_MMAP_VALID) flags += "mmap_valid ";
		if(m_cinfo.flags & SND_PCM_CHNINFO_PAUSE) flags += "pause ";
	
		artsdebug(" subdevice:%d\n"
				  "  flags:%s\n"
				  "  min_rate:%d max_rate:%d\n"
				  "  buffer_size:%d min_fragment_size:%d max_fragment_size:%d\n"
				  "  fragment_align:%d fifo_size:%d transfer_block_size:%d\n"
				  "  mmap_size:%d",
				  m_cinfo.subdevice,
				  flags.c_str(),
				  m_cinfo.min_rate, m_cinfo.max_rate,
				  m_cinfo.buffer_size, m_cinfo.min_fragment_size, m_cinfo.max_fragment_size,
				  m_cinfo.fragment_align, m_cinfo.fifo_size, m_cinfo.transfer_block_size,
				  m_cinfo.mmap_size);
	}
	else {
		arts_warning("Can't get channel info!"); //not fatal error
	}
}

#endif /* HAVE_LIBASOUND */