/*
 * Gnophone: A client for the Asterisk PBX
 *
 * Copyright (C) 2000, Linux Support Services, Inc.
 *
 * Written by Mark Spencer
 *
 * Linux/UNIX version distributed under the terms of
 * the GNU General Public License
 *
 * audio-alsa.c: Native ALSA Audio Driver
 *
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/asoundlib.h>
#include <string.h>
#include <errno.h>
#include "gnophone.h"
#include "audio.h"
#include "frame.h"
 
static int alsa_open(struct audio_channel *c);
static int alsa_close(struct audio_channel *c);
static int alsa_activate(struct audio_channel *c);
static int alsa_deactivate(struct audio_channel *c);
static int alsa_configure(struct audio_channel *c);
static int alsa_setspeed(struct audio_channel *c, int speed);
static int alsa_simduplex(struct audio_channel *c, int write);
static int alsa_sendaudio(struct audio_channel *c, int format, void *data, int len);
static int alsa_readaudio(struct audio_channel *c, int format, void *buffer, int *len);

static char alsa_driver[] = "Gnophone/ALSA";

static int alsa_direct = 0;

#define ALSA_MAX_FRAGS 4

struct alsa_pvt {
	/* Desired fragment size */
	int frags;
	int card;
	snd_pcm_t *pcm;
};

char *alsa_card_is_good(int card, int silent, int speed, int *duplex, snd_pcm_t **pcm)
{
	int lowquality = 0;
	snd_pcm_channel_params_t params;
	snd_pcm_channel_setup_t setup;
	
	bzero(&params, sizeof(params));
	bzero(&setup, sizeof(setup));
	
	if (snd_pcm_open(pcm, card, 0 /* XXX Don't hard code */,
					SND_PCM_OPEN_DUPLEX | SND_PCM_OPEN_NONBLOCK) < 0) {
		if (snd_pcm_open(pcm, card, 0 /* XXX Don't hard code */,
					SND_PCM_OPEN_NONBLOCK) < 0) {
			return "Unable to open device";
		}
		if (duplex)
			*duplex = 0;
		if (!silent)
			fprintf(stderr, "Note: card %d is NOT full duplex\n", card);
	} else if (duplex)
		*duplex = 1;	
	/* XXX Maybe we want to choose the output device at some point XXX */
	
	params.channel = SND_PCM_CHANNEL_PLAYBACK;
	params.mode = SND_PCM_MODE_BLOCK;
	params.format.interleave = 0;
	params.format.format = SND_PCM_SFMT_S16_LE;
	params.format.rate = speed;
	params.format.voices = 1;
	/* params.digital ignored */
	/* params.start_mode = SND_PCM_START_FULL; */
	params.start_mode = SND_PCM_START_DATA;
	params.stop_mode = SND_PCM_STOP_STOP;
	/* Use GSM optimised fragment size */
	params.buf.block.frag_size = 320;
	params.buf.block.frags_min = 1;
	params.buf.block.frags_max = ALSA_MAX_FRAGS;
	if (alsa_direct) {
		if (snd_pcm_channel_params(*pcm, &params) < 0) {
			return "Unable to set native playback channel parameters";
		}
	} else {
		if (snd_pcm_plugin_params(*pcm, &params) < 0) {
			return "Unable to set plugin playback channel paramters";
		}
	}
	params.channel = SND_PCM_CHANNEL_CAPTURE;
	params.start_mode = SND_PCM_START_DATA;
	params.stop_mode = SND_PCM_STOP_ROLLOVER;	
	if (alsa_direct) {
		if (snd_pcm_channel_params(*pcm, &params) < 0) {
			return "Unable to set native record channel parameters";
		}
	} else {
		if (snd_pcm_plugin_params(*pcm, &params) < 0) {
			return "Unable to set plugin record channel paramters";
		}
	}
	setup.channel = SND_PCM_CHANNEL_PLAYBACK;
	if (alsa_direct) {
		if (snd_pcm_channel_setup(*pcm, &setup) < 0) {
			return "Unable to retrieve native playback settings";
		}
	} else {
		if (snd_pcm_plugin_setup(*pcm, &setup) < 0) {
			return "Unable to retrieve plugin playback settings";
		}
	}
	if (setup.format.format != SND_PCM_SFMT_S16_LE) {
		return "Unable to set to 16-bit signed linear";
	}
	if (!silent)
		fprintf(stderr, " -- Playback: %d %d-byte fragments of 16-bit signed linear at %dhz\n",
			setup.buf.block.frags_max, setup.buf.block.frag_size, setup.format.rate);
	if (setup.format.rate != 8000)
		lowquality++;
	if (setup.buf.block.frag_size != 320)
		lowquality++;
	if (lowquality && !silent) {
		fprintf(stderr, " -- Warning: Quality may be poor for this sound card\n");
	}
	return NULL;
}

struct audio_channel *alsa_channel_new(int card, int duplex, char *name, int speed)
{
	struct audio_channel *ac;
	struct alsa_pvt *pvt = (struct alsa_pvt *)malloc(sizeof(struct alsa_pvt));
	ac = audio_new();
	pvt = (struct alsa_pvt *)malloc(sizeof(struct alsa_pvt));
	bzero(pvt, sizeof(struct alsa_pvt));
	if (ac && pvt) {
		pvt->card = card;
		pvt->frags = ALSA_MAX_FRAGS;
		strncpy(ac->name, name, sizeof(ac->name));
		ac->priority = 50;
		ac->driver = alsa_driver;
		ac->open = alsa_open;
		ac->close = alsa_close;
		ac->play_digit = std_play_digit;
		ac->activate = alsa_activate;
		ac->ring = std_ring;
		ac->busy = std_busy;
		ac->hz = speed;
		ac->fastbusy = std_fastbusy;
		ac->ringing = std_ringing;
		ac->deactivate = alsa_deactivate;
		ac->configure = alsa_configure;
		ac->sendaudio = alsa_sendaudio;
		ac->readaudio = alsa_readaudio;
		/* ac->flush = NULL; */
		ac->setspeed = alsa_setspeed;
		ac->sformats = AST_FORMAT_SLINEAR;
		/* ac->cananswer = 0; */
		ac->duplex = duplex;
		ac->simduplex = alsa_simduplex;
		ac->fd = -1;
		ac->pvt = pvt;
		ac->echocancelled = 0;
		
	} else if (!pvt) { free(ac); ac = NULL; }
	return ac;
}

static int alsa_simduplex(struct audio_channel *ac, int write)
{
	if (ac->writemode != write) {
		alsa_close(ac);
		ac->writemode = write;
		alsa_open(ac);
	}
	return 0;
}

static int alsa_open(struct audio_channel *ac)
{
	struct alsa_pvt *pvt = ac->pvt;
	snd_pcm_t *pcm=NULL;
	int duplex;
	
	if ((ac->fd > -1) || (pvt->pcm)) {
		fprintf(stderr, "Channel %s already open?\n", ac->name);
		return 0;
	}
	if (alsa_card_is_good(pvt->card, 1, ac->hz, &duplex, &pcm)) {
		fprintf(stderr, "Unable to open card: %s\n", strerror(errno));
		if (pcm) {
			snd_pcm_close(pcm);
			pcm = NULL;
		}
		return -1;
	}
	if (duplex != ac->duplex) {
		fprintf(stderr, "Warning: Device %s changed duplexibility from %d to %d\n", ac->name, ac->duplex, duplex);
		ac->duplex = duplex;
	}
	if (ac->duplex || ac->writemode) {
		if (alsa_direct) {
			if (snd_pcm_channel_prepare(pcm, SND_PCM_CHANNEL_PLAYBACK) < 0) {
				fprintf(stderr, "Unable to prepare channel for native playback\n");
				snd_pcm_close(pcm);
				return -1;
			}
		} else {
			if (snd_pcm_plugin_prepare(pcm, SND_PCM_CHANNEL_PLAYBACK) < 0) {
				fprintf(stderr, "Unable to prepare channel for plugin playback\n");
				snd_pcm_close(pcm);
				return -1;
			}
		}
	}
	if (ac->duplex || !ac->writemode) {
		if (alsa_direct) {
			if (snd_pcm_channel_prepare(pcm, SND_PCM_CHANNEL_CAPTURE) < 0) {
				fprintf(stderr, "Unable to prepare channel for native capture\n");
				snd_pcm_close(pcm);
				return -1;
			}
			if (snd_pcm_channel_go(pcm, SND_PCM_CHANNEL_CAPTURE) < 0) {
				fprintf(stderr, "Unable to 'go' channel for native capture\n");
				snd_pcm_close(pcm);
				return -1;
			}
		} else {
			if (snd_pcm_plugin_prepare(pcm, SND_PCM_CHANNEL_CAPTURE) < 0) {
				fprintf(stderr, "Unable to prepare channel for plugin capture\n");
				snd_pcm_close(pcm);
				return -1;
			}
		}
	}
	pvt->pcm = pcm;
	ac->fd = snd_pcm_file_descriptor(pcm, SND_PCM_CHANNEL_CAPTURE);
	return 0;
}

static int alsa_sendaudio(struct audio_channel *ac, int format, void *data, int len)
{
	struct alsa_pvt *pvt = ac->pvt;
	struct snd_pcm_channel_status status;
	
	
	if (!ac->duplex && !ac->writemode) {
		fprintf(stderr, "Unable to write on %s, in read mode and not full duplex\n", ac->name);
		return -1;
	}
	
	if (format != AST_FORMAT_SLINEAR) {
		fprintf(stderr, "Can only handle signed linear (little endian) data on %s\n", ac->name);
		return -1;
	}

	bzero(&status, sizeof(status));
	status.channel = SND_PCM_CHANNEL_PLAYBACK;

	if (!snd_pcm_channel_status(pvt->pcm, &status)) {
		switch(status.status) {
		case SND_PCM_STATUS_RUNNING:
			/* If we get to look at the output space, check before we write */
			if ((status.free < len) && (status.status != SND_PCM_STATUS_PREPARED)) {
				fprintf(stderr, "Only have %d bytes of buffer, and %d bytes to write -- dropping frame\n", status.free, len);
				return 0;
			}
			break;
		case SND_PCM_STATUS_UNDERRUN:
			/* We've had an underrun, not surprising...  Prepare the channel
			   again */
			if (snd_pcm_channel_prepare(pvt->pcm, SND_PCM_CHANNEL_PLAYBACK) < 0) {
				fprintf(stderr, "Unable to re-prepare channel after underrun\n");
				return -1;
			}
			break;
		case SND_PCM_STATUS_PREPARED:
			break;
		default:
			fprintf(stderr, "Status is %d!\n", status.status);
			return -1;
		}
	}
	if (alsa_direct) {
		if (snd_pcm_write(pvt->pcm, data, len) < len) {
			fprintf(stderr, "Error writing native frame: %s\n", strerror(errno));
			return -1;
		}
	} else {
		if (snd_pcm_plugin_write(pvt->pcm, data, len) < len) {
			fprintf(stderr, "Error writing plugin frame: %s\n", strerror(errno));
			return -1;
		}
	}
	return 0;
}

static int alsa_readaudio(struct audio_channel *ac, int format, void *data, int *datalen)
{
	int res;
	struct alsa_pvt *pvt = ac->pvt;
	struct snd_pcm_channel_status status;
	if (!ac->duplex && ac->writemode) {
		fprintf(stderr, "Unable to read on %s, in write mode and not full duplex\n", ac->name);
		return -1;
	}
	if (format != AST_FORMAT_SLINEAR) {
		fprintf(stderr, "Can only handle signed linear (little endian) data on %s\n", ac->name);
		return -1;
	}

	bzero(&status, sizeof(status));
	status.channel = SND_PCM_CHANNEL_CAPTURE;

#if 0		
    /* It seems sometimes there really is nothing to read */
	if (!snd_pcm_channel_status(pvt->pcm, &status)) {
		/* If we get to look at the output space, check before we write */
		if (!status.count) {
			fprintf(stderr, "Nothing to read (status=%d)!\n", status.status);
			return 0;
		}
	}
#endif

	if (alsa_direct) {
		if ((res = snd_pcm_read(pvt->pcm, data, *datalen)) < 0) {
			fprintf(stderr, "Error reading native frame: %s\n", strerror(errno));
			return -1;
		}
	} else {
		if ((res = snd_pcm_plugin_read(pvt->pcm, data, *datalen)) < 0) {
			/* XXX Alsa bug?  Sometimes we read 0 bytes...  but
			       there is no error returned XXX */
			if (!errno) {
				*datalen = 0;
				return 0;
			}
			fprintf(stderr, "Error reading plugin frame: %s\n", strerror(errno));
			return -1;
		}
	}
	*datalen = res;
	return 0;
}

static int alsa_close(struct audio_channel *ac)
{
	struct alsa_pvt *pvt = ac->pvt;
	if ((ac->fd < 0) || (!pvt->pcm)) {
		fprintf(stderr, "Device is already closed?\n");
		return -1;
	}
	ac->fd = -1;
	snd_pcm_close(pvt->pcm);
	pvt->pcm = NULL;
	return 0;
}

static int alsa_setspeed(struct audio_channel *ac, int speed)
{
	if (ac->hz != speed) {
		ac->hz = speed;
		alsa_close(ac);
		return alsa_open(ac);
	}
	return 0;
}

static int alsa_activate(struct audio_channel *ac)
{
	/* Nothing really necessary to activate us */
	return 0;		
}

static int alsa_deactivate(struct audio_channel *ac)
{
	/* Nothing really necessary to deactivate us either */
	return 0;
}

static int alsa_configure(struct audio_channel *ac)
{
	/* XXX Bug: I ought to bring a configuration window so you can
	       set the frags and see info, etc XXX */
	return 0;
}

char *key() 
{
	return KEY;
}

char *name()
{
	return "alsa";
}

int init()
{
	int count;
	char *n;
	char *ln;
	int x;
	int duplex;
	int found = 0;
	char *c;
	snd_pcm_t *pcm;
	struct audio_channel *ac;
	
	count = snd_cards();
	fprintf(stderr, "Detected ALSA sound cards: %d\n", count);
	for (x=0;x<count;x++) {
		if (snd_card_get_name(x, &n)) {
			fprintf(stderr, "Unable to get name for card %d\n", x);
			continue;
		}
		if (snd_card_get_longname(x, &ln)) {
			fprintf(stderr, "Unable to get long name for card %d\n", x);
			continue;
		}
		fprintf(stderr, "Card %d: %s (%s)\n", x, n, ln);
		if (!(c = alsa_card_is_good(x, 0, AUDIO_DEFAULT_SPEED, &duplex, &pcm))) {
			found++;
			ac = alsa_channel_new(x, duplex, ln, AUDIO_DEFAULT_SPEED);
			if (pcm) {
				snd_pcm_close(pcm);
				pcm = NULL;
			}
			if (ac)
				audio_register_channel(ac);
		} else {
			fprintf(stderr, "Note: Card '%s' rejected because %s\n", n, c);
		}
		if (pcm) {
			snd_pcm_close(pcm);
			pcm = NULL;
		}
	}
	return 0;
}
