/*
  The mediastreamer library aims at providing modular media processing and I/O
	for linphone, but also for any telephony application.
  Copyright (C) 2001  Simon MORLAT simon.morlat@linphone.org
  										
  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "alsacard.h"

#ifdef HAVE_ALSA_ASOUNDLIB_H

static gchar *over_pcmdev=NULL;

#include "msossread.h"
#include "msosswrite.h"

#include <signal.h>

int __alsa_card_write(AlsaCard *obj,char *buf,int size);

int alsa_set_params(AlsaCard *obj, int rw, int bits, int stereo, int rate)
{
	snd_pcm_hw_params_t *hwparams=NULL;
	snd_pcm_sw_params_t *swparams=NULL;
	snd_pcm_t *pcm_handle;
	gint dir,exact_value;
	gint channels;
	gint fsize=0;
	gint periods=8;
	gint periodsize=256;
	gint err;
	int format;
	
	if (rw) {
		pcm_handle=obj->write_handle;
	}
	else pcm_handle=obj->read_handle;
	
	/* Allocate the snd_pcm_hw_params_t structure on the stack. */
    snd_pcm_hw_params_alloca(&hwparams);
	
	/* Init hwparams with full configuration space */
    if (snd_pcm_hw_params_any(pcm_handle, hwparams) < 0) {
		g_warning("alsa_set_params: Cannot configure this PCM device.\n");
		return(-1);
    }
	
	if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
      g_warning("alsa_set_params: Error setting access.\n");
      return(-1);
    }
	/* Set sample format */
#ifdef WORDS_BIGENDIAN
	format=SND_PCM_FORMAT_S16_BE;
#else
	format=SND_PCM_FORMAT_S16_LE;
#endif
    if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, format) < 0) {
      g_warning("alsa_set_params: Error setting format.\n");
      return(-1);
    }
	/* Set number of channels */
	if (stereo) channels=2;
	else channels=1;
    if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, channels) < 0) {
      g_warning("alsa_set_params: Error setting channels.\n");
      return(-1);
    }
	/* Set sample rate. If the exact rate is not supported */
    /* by the hardware, use nearest possible rate.         */ 
	exact_value=rate;
	dir=0;
    if ((err=snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, &exact_value, &dir))<0){
		g_warning("alsa_set_params: Error setting rate to %i:%s",rate,snd_strerror(err));
 		return -1;
	}
    if (dir != 0) {
      g_warning("alsa_set_params: The rate %d Hz is not supported by your hardware.\n "
		"==> Using %d Hz instead.\n", rate, exact_value);
    }
	/* choose greater period size when rate is high */
	periodsize=periodsize*(rate/8000);	
	
	/* Set buffer size (in frames). The resulting latency is given by */
    /* latency = periodsize * periods / (rate * bytes_per_frame)     */
	/*
	fsize=periodsize * periods;
	exact_value=fsize;
    if ((err=snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hwparams,&exact_value)) < 0) {
      g_warning("alsa_set_params: Error setting buffer size:%s",snd_strerror(err));
      return(-1);
    }
	if (fsize!= exact_value) {
      g_warning("alsa_set_params: The buffer size %d is not supported by your hardware.\n "
		"==> Using %d instead.\n", fsize, exact_value);
    }
	*/
	/* set period size */
	exact_value=periodsize;
	dir=0;
    if (snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &exact_value, &dir) < 0) {
      g_warning("alsa_set_params: Error setting period size.\n");
      return(-1);
    }
	if (dir != 0) {
      g_warning("alsa_set_params: The period size %d is not supported by your hardware.\n "
		"==> Using %d instead.\n", periodsize, exact_value);
    }
	periodsize=exact_value;
	/* Set number of periods. Periods used to be called fragments. */ 
	exact_value=periods;
	dir=0;
    if (snd_pcm_hw_params_set_periods_near(pcm_handle, hwparams, &exact_value, &dir) < 0) {
      g_warning("alsa_set_params: Error setting periods.\n");
      return(-1);
    }
	if (dir != 0) {
      g_warning("alsa_set_params: The number of periods %d is not supported by your hardware.\n "
		"==> Using %d instead.\n", periods, exact_value);
    }
	/* Apply HW parameter settings to */
    /* PCM device and prepare device  */
    if ((err=snd_pcm_hw_params(pcm_handle, hwparams)) < 0) {
      g_warning("alsa_set_params: Error setting HW params:%s",snd_strerror(err));
      return(-1);
    }
	/*prepare sw params */
	if (rw){
		snd_pcm_sw_params_alloca(&swparams);
		snd_pcm_sw_params_current(pcm_handle, swparams);
		if ((err=snd_pcm_sw_params_set_start_threshold(pcm_handle, swparams,periodsize*2 ))<0){
			g_warning("alsa_set_params: Error setting start threshold:%s",snd_strerror(err));
			return -1;
		}
		if ((err=snd_pcm_sw_params(pcm_handle, swparams))<0){
			g_warning("alsa_set_params: Error setting SW params:%s",snd_strerror(err));
			return(-1);
		}
	}
	obj->frame_size=channels*(bits/8);
	SND_CARD(obj)->bsize=periodsize*obj->frame_size;
	/* //SND_CARD(obj)->bsize=4096; */
	obj->frames=periodsize;
	g_message("alsa_set_params:  blocksize=%i.",SND_CARD(obj)->bsize);
	return SND_CARD(obj)->bsize;	
}

int alsa_card_open_r(AlsaCard *obj,int bits,int stereo,int rate)
{
	int bsize;
	int err;
	snd_pcm_t *pcm_handle;
	gchar *pcmdev;
	if (over_pcmdev!=NULL) pcmdev=over_pcmdev;
	else pcmdev=obj->pcmdev;
	
	if (snd_pcm_open(&pcm_handle, pcmdev,SND_PCM_STREAM_CAPTURE,SND_PCM_NONBLOCK) < 0) {
		g_warning("alsa_card_open_r: Error opening PCM device %s\n",obj->pcmdev );
		return -1;
	}
	g_return_val_if_fail(pcm_handle!=NULL,-1);
	obj->read_handle=pcm_handle;
	if ((bsize=alsa_set_params(obj,0,bits,stereo,rate))<0){
		snd_pcm_close(pcm_handle);
		obj->read_handle=NULL;
		return -1;
	}
	obj->readbuf=g_malloc0(bsize);
	
	err=snd_pcm_start(obj->read_handle);
	if (err<0){
		g_warning("Cannot start read pcm: %s", snd_strerror(err));
	}
	obj->readpos=0;
	SND_CARD(obj)->bsize=bsize;
	SND_CARD(obj)->flags|=SND_CARD_FLAGS_OPENED;
	return 0;
}

int alsa_card_open_w(AlsaCard *obj,int bits,int stereo,int rate)
{
	int err,bsize;
	snd_pcm_t *pcm_handle;
	gchar *pcmdev;
	if (over_pcmdev!=NULL) pcmdev=over_pcmdev;
	else pcmdev=obj->pcmdev;
	
	if (snd_pcm_open(&pcm_handle, pcmdev,SND_PCM_STREAM_PLAYBACK,SND_PCM_NONBLOCK) < 0) {
      g_warning("alsa_card_open_w: Error opening PCM device %s\n", obj->pcmdev);
      return -1;
    }
	obj->write_handle=pcm_handle;
	if ((bsize=alsa_set_params(obj,1,bits,stereo,rate))<0){
		snd_pcm_close(pcm_handle);
		obj->write_handle=NULL;
		return -1;
	}
	obj->writebuf=g_malloc0(bsize);
	
	obj->writepos=0;
	SND_CARD(obj)->bsize=bsize;
	SND_CARD(obj)->flags|=SND_CARD_FLAGS_OPENED;
	return 0;
}


void alsa_card_set_blocking_mode(AlsaCard *obj, gboolean yesno){
	if (obj->read_handle!=NULL) snd_pcm_nonblock(obj->read_handle,!yesno);
	if (obj->write_handle!=NULL) snd_pcm_nonblock(obj->write_handle,!yesno);
}

void alsa_card_close_r(AlsaCard *obj)
{
	if (obj->read_handle!=NULL){
		snd_pcm_close(obj->read_handle);
		obj->read_handle=NULL;
		g_free(obj->readbuf);
		obj->readbuf=NULL;
	}
}

void alsa_card_close_w(AlsaCard *obj)
{
	if (obj->write_handle!=NULL){
		snd_pcm_close(obj->write_handle);
		obj->write_handle=NULL;
		g_free(obj->writebuf);
		obj->writebuf=NULL;
	}
}

int alsa_card_probe(AlsaCard *obj,int bits,int stereo,int rate)
{
	int ret;
	ret=alsa_card_open_w(obj,bits,stereo,rate);
	if (ret<0) return -1;
	ret=SND_CARD(obj)->bsize;
	alsa_card_close_w(obj);
	return ret;
}


void alsa_card_destroy(AlsaCard *obj)
{
	snd_card_uninit(SND_CARD(obj));
	g_free(obj->pcmdev);
	if (obj->readbuf!=0) g_free(obj->readbuf);
	if (obj->writebuf!=0) g_free(obj->writebuf);	
}

gboolean alsa_card_can_read(AlsaCard *obj)
{
	int frames;
	g_return_val_if_fail(obj->read_handle!=NULL,0);
	if (obj->readpos!=0) return TRUE;
	if ( frames=snd_pcm_avail_update(obj->read_handle)>=obj->frames) return 1;
	/* //g_message("frames=%i",frames); */
	return 0;
}



int __alsa_card_read(AlsaCard *obj,char *buf,int bsize)
{
	int err;
	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set,SIGALRM);
	sigprocmask(SIG_BLOCK,&set,NULL);
	err=snd_pcm_readi(obj->read_handle,buf,bsize/obj->frame_size);
	if (err<0) {
		if (err!=-EPIPE){
			g_warning("alsa_card_read: snd_pcm_readi() failed:%s.",snd_strerror(err));
		}
		snd_pcm_prepare(obj->read_handle);
		err=snd_pcm_readi(obj->read_handle,buf,bsize/obj->frame_size);
		if (err<0) g_warning("alsa_card_read: snd_pcm_readi() failed:%s.",snd_strerror(err));
	}
	sigprocmask(SIG_UNBLOCK,&set,NULL);
	return err*obj->frame_size;
}

int alsa_card_read(AlsaCard *obj,char *buf,int size)
{
	int err;
	gint bsize=SND_CARD(obj)->bsize;
	g_return_val_if_fail(obj->read_handle!=NULL,-1);
	if (size<bsize){
		gint canread=MIN(bsize-obj->readpos,size);
		
		if (obj->readpos==0){
			err=__alsa_card_read(obj,obj->readbuf,bsize);
		}
			
		memcpy(buf,&obj->readbuf[obj->readpos],canread);
		obj->readpos+=canread;
		if (obj->readpos>=bsize) obj->readpos=0;
		return canread;
	}else{
		err=__alsa_card_read(obj,buf,size);
		return err;
	}
	
}

int __alsa_card_write(AlsaCard *obj,char *buf,int size)
{
	int err;
	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set,SIGALRM);
	sigprocmask(SIG_BLOCK,&set,NULL);
	if ((err=snd_pcm_writei(obj->write_handle,buf,size/obj->frame_size))<0){
		if (err!=-EPIPE){
			g_warning("alsa_card_write: snd_pcm_writei() failed:%s.",snd_strerror(err));
		}
		snd_pcm_prepare(obj->write_handle);
		err=snd_pcm_writei(obj->write_handle,buf,size/obj->frame_size);
		if (err<0) g_warning("alsa_card_write: Error writing sound buffer (size=%i):%s",size,snd_strerror(err));
		
	}
	sigprocmask(SIG_UNBLOCK,&set,NULL);
	return err;
}

int alsa_card_write(AlsaCard *obj,char *buf,int size)
{
	int err;
	gint bsize=SND_CARD(obj)->bsize;
	g_return_val_if_fail(obj->write_handle!=NULL,-1);
	if (size<bsize){
		gint canwrite;
		
		canwrite=MIN(bsize-obj->writepos,size);
		memcpy(&obj->writebuf[obj->writepos],buf,canwrite);
		obj->writepos+=canwrite;
		if (obj->writepos>=bsize){
			err=__alsa_card_write(obj,obj->writebuf,bsize);
			obj->writepos=0;
		}
		return canwrite;
	}else{
		return __alsa_card_write(obj,buf,bsize);
	}
}

snd_mixer_t *alsa_mixer_open(AlsaCard *obj){
	snd_mixer_t *mixer=NULL;
	int err;
	err=snd_mixer_open(&mixer,0);
	if (err<0){
		g_warning("Could not open alsa mixer: %s",snd_strerror(err));
		return NULL;
	}
	if ((err = snd_mixer_attach (mixer, obj->mixdev)) < 0){
		g_warning("Could not attach mixer to card: %s",snd_strerror(err));
		snd_mixer_close(mixer);
		return NULL;
	}
	if ((err = snd_mixer_selem_register (mixer, NULL, NULL)) < 0){
		g_warning("snd_mixer_selem_register: %s",snd_strerror(err));
		snd_mixer_close(mixer);
		return NULL;
	}
	if ((err = snd_mixer_load (mixer)) < 0){
		g_warning("snd_mixer_load: %s",snd_strerror(err));
		snd_mixer_close(mixer);
		return NULL;
	}
	obj->mixer=mixer;
	return mixer;
}

void alsa_mixer_close(AlsaCard *obj){
	snd_mixer_close(obj->mixer);
	obj->mixer=NULL;
}

typedef enum {CAPTURE, PLAYBACK, CAPTURE_SWITCH, PLAYBACK_SWITCH} MixerAction;

static gint get_mixer_element(snd_mixer_t *mixer,const char *name, MixerAction action){
	long value=0;
	const char *elemname;
	snd_mixer_elem_t *elem;
	int err;
	long sndMixerPMin;
	long sndMixerPMax;
	long newvol;
	elem=snd_mixer_first_elem(mixer);
	while (elem!=NULL){
		elemname=snd_mixer_selem_get_name(elem);
		/* //g_message("Found alsa mixer element %s.",elemname); */
		if (strcmp(elemname,name)==0){
			switch (action){
				case CAPTURE:
				if (snd_mixer_selem_has_capture_volume(elem)){
					snd_mixer_selem_get_playback_volume_range(elem, &sndMixerPMin, &sndMixerPMax);
					err=snd_mixer_selem_get_capture_volume(elem,SND_MIXER_SCHN_UNKNOWN,&newvol);
					newvol-=sndMixerPMin;
					value=(100*newvol)/(sndMixerPMax-sndMixerPMin);
					if (err<0) g_warning("Could not get capture volume for %s:%s",name,snd_strerror(err));
					/* //else g_message("Succesfully get capture level for %s.",elemname); */
					break;
				}
				break;
				case PLAYBACK:
				if (snd_mixer_selem_has_playback_volume(elem)){
					snd_mixer_selem_get_playback_volume_range(elem, &sndMixerPMin, &sndMixerPMax);
					err=snd_mixer_selem_get_playback_volume(elem,SND_MIXER_SCHN_FRONT_LEFT,&newvol);
					newvol-=sndMixerPMin;
					value=(100*newvol)/(sndMixerPMax-sndMixerPMin);
					if (err<0) g_warning("Could not get playback volume for %s:%s",name,snd_strerror(err));
					/* //else g_message("Succesfully get playback level for %s.",elemname); */
					break;
				}
				break;
				case CAPTURE_SWITCH:
				
				break;
			}
		}
		elem=snd_mixer_elem_next(elem);
	}
	
	return value;
}


static void set_mixer_element(snd_mixer_t *mixer,const char *name, gint level,MixerAction action){
	const char *elemname;
	snd_mixer_elem_t *elem;
	int tmp;
	long sndMixerPMin;
	long sndMixerPMax;
	long newvol;
	
	elem=snd_mixer_first_elem(mixer);
	
	while (elem!=NULL){
		elemname=snd_mixer_selem_get_name(elem);
		/* //g_message("Found alsa mixer element %s.",elemname); */
		if (strcmp(elemname,name)==0){
			switch(action){
				case CAPTURE:
				if (snd_mixer_selem_has_capture_volume(elem)){
					snd_mixer_selem_get_playback_volume_range(elem, &sndMixerPMin, &sndMixerPMax);
					newvol=(((sndMixerPMax-sndMixerPMin)*level)/100)+sndMixerPMin;
					snd_mixer_selem_set_capture_volume_all(elem,newvol);
					/* //g_message("Succesfully set capture level for %s.",elemname); */
					return;
				}
				break;
				case PLAYBACK:
				if (snd_mixer_selem_has_playback_volume(elem)){
					snd_mixer_selem_get_playback_volume_range(elem, &sndMixerPMin, &sndMixerPMax);
					newvol=(((sndMixerPMax-sndMixerPMin)*level)/100)+sndMixerPMin;
					snd_mixer_selem_set_playback_volume_all(elem,newvol);
					/* //g_message("Succesfully set playback level for %s.",elemname); */
					return;
				}
				break;
				case CAPTURE_SWITCH:
				if (snd_mixer_selem_has_capture_switch(elem)){
					snd_mixer_selem_set_capture_switch_all(elem,level);
					/* //g_message("Succesfully set capture switch for %s.",elemname); */
				}
				break;
				case PLAYBACK_SWITCH:
				if (snd_mixer_selem_has_playback_switch(elem)){
					snd_mixer_selem_set_playback_switch_all(elem,level);
					/* //g_message("Succesfully set capture switch for %s.",elemname); */
				}
				break;

			}
		}
		elem=snd_mixer_elem_next(elem);
	}

	return ;
}


void alsa_card_set_level(AlsaCard *obj,gint way,gint a)
{	
	snd_mixer_t *mixer;
	mixer=alsa_mixer_open(obj);
	if (mixer==NULL) return ;
	switch(way){
		case SND_CARD_LEVEL_GENERAL:
			set_mixer_element(mixer,"Master",a,PLAYBACK);
		break;
		case SND_CARD_LEVEL_INPUT:
			set_mixer_element(mixer,"Capture",a,CAPTURE);
		break;
		case SND_CARD_LEVEL_OUTPUT:
			set_mixer_element(mixer,"PCM",a,PLAYBACK);
		break;
		default:
			g_warning("oss_card_set_level: unsupported command.");
	}
	alsa_mixer_close(obj);
}

gint alsa_card_get_level(AlsaCard *obj,gint way)
{
	snd_mixer_t *mixer;
	gint value;
	mixer=alsa_mixer_open(obj);
	if (mixer==NULL) return 0;
	switch(way){
		case SND_CARD_LEVEL_GENERAL:
			value=get_mixer_element(mixer,"Master",PLAYBACK);
		break;
		case SND_CARD_LEVEL_INPUT:
			value=get_mixer_element(mixer,"Capture",CAPTURE);
		break;
		case SND_CARD_LEVEL_OUTPUT:
			value=get_mixer_element(mixer,"PCM",PLAYBACK);
		break;
		default:
			g_warning("oss_card_set_level: unsupported command.");
	}
	alsa_mixer_close(obj);
	return value;
}

void alsa_card_set_source(AlsaCard *obj,int source)
{
	snd_mixer_t *mixer;
	mixer=alsa_mixer_open(obj);
	if (mixer==NULL) return;
	switch (source){
		case 'm':
			set_mixer_element(mixer,"Mic",1,CAPTURE_SWITCH);
			set_mixer_element(mixer,"Capture",1,CAPTURE_SWITCH);
			break;
		case 'l':
			set_mixer_element(mixer,"Line",1,CAPTURE_SWITCH);
			set_mixer_element(mixer,"Capture",1,CAPTURE_SWITCH);
			break;
	}
}

MSFilter *alsa_card_create_read_filter(AlsaCard *card)
{
	MSFilter *f=ms_oss_read_new();
	ms_oss_read_set_device(MS_OSS_READ(f),SND_CARD(card)->index);
	return f;
}

MSFilter *alsa_card_create_write_filter(AlsaCard *card)
{
	MSFilter *f=ms_oss_write_new();
	ms_oss_write_set_device(MS_OSS_WRITE(f),SND_CARD(card)->index);
	return f;
}


SndCard * alsa_card_new(gint devid)
{
	AlsaCard * obj;
	SndCard *base;
	int err;
	gchar *name=NULL;
	
	/* carefull: this is an alsalib call despite its name! */
	err=snd_card_get_name(devid,&name);
	if (err<0) {
		return NULL;
	}
	obj= g_new0(AlsaCard,1);
	base= SND_CARD(obj);
	snd_card_init(base);
	
	base->card_name=g_strdup_printf("%s (Advanced Linux Sound Architecture)",name);
	base->_probe=(SndCardOpenFunc)alsa_card_probe;
	base->_open_r=(SndCardOpenFunc)alsa_card_open_r;
	base->_open_w=(SndCardOpenFunc)alsa_card_open_w;
	base->_can_read=(SndCardPollFunc)alsa_card_can_read;
	base->_set_blocking_mode=(SndCardSetBlockingModeFunc)alsa_card_set_blocking_mode;
	base->_read=(SndCardIOFunc)alsa_card_read;
	base->_write=(SndCardIOFunc)alsa_card_write;
	base->_close_r=(SndCardCloseFunc)alsa_card_close_r;
	base->_close_w=(SndCardCloseFunc)alsa_card_close_w;
	base->_set_rec_source=(SndCardMixerSetRecSourceFunc)alsa_card_set_source;
	base->_set_level=(SndCardMixerSetLevelFunc)alsa_card_set_level;
	base->_get_level=(SndCardMixerGetLevelFunc)alsa_card_get_level;
	base->_destroy=(SndCardDestroyFunc)alsa_card_destroy;
	base->_create_read_filter=(SndCardCreateFilterFunc)alsa_card_create_read_filter;
	base->_create_write_filter=(SndCardCreateFilterFunc)alsa_card_create_write_filter;
	
	
	obj->pcmdev=g_strdup_printf("plughw:%i,0",devid);
	obj->mixdev=g_strdup_printf("hw:%i",devid);
	obj->readbuf=NULL;
	obj->writebuf=NULL;
	return base;
}


gint alsa_card_manager_init(SndCardManager *m, gint index)
{
	gint devindex;
	gint i;
	gint found=0;
	gchar *name=NULL;
	for(devindex=0;index<MAX_SND_CARDS && devindex<MAX_SND_CARDS ;devindex++){
		if (snd_card_get_name(devindex,&name)==0){
			g_message("Found ALSA device: %s",name);
			m->cards[index]=alsa_card_new(devindex);
			m->cards[index]->index=index;
			found++;
			index++;
		}
	}
	return found;
}

void alsa_card_manager_set_default_pcm_device(const gchar *pcmdev){
	if (over_pcmdev!=NULL){
		g_free(over_pcmdev);	
	}
	over_pcmdev=g_strdup(pcmdev);
}

#endif
