/*
 * 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-oss.c: OSS Audio Driver
 *
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include "audio.h"
#include "phonecore.h"
#include <iax/iax-client.h>

#define MAX_DEVS 4

/* 
 * Fragment size is of the form 2^x.  With 160 byte GSM frames, we can 
 * only use 64-byte fragments, which are very small and create fairly
 * substantial overhead.  at some point we might want to use larger 
 * fragments
 * 
 */

#define FRAG_SIZE  6

/*
 * It takes 5 64-byte buffers per 20 millisecond sample.  80 milliseconds
 * is probably enough protection against underruns in most cases.
 */

#define BUFFERS    40

static int oss_open(struct audio_channel *c);
static int oss_close(struct audio_channel *c);
static int oss_activate(struct audio_channel *c);
static int oss_deactivate(struct audio_channel *c);
static int oss_configure(struct audio_channel *c);
static int oss_setspeed(struct audio_channel *c, int speed);
static int oss_simduplex(struct audio_channel *c, int write);
static int oss_sendaudio(struct audio_channel *c, int format, void *data, int len);
static int oss_readaudio(struct audio_channel *c, int format, void *buffer, int *len);
static int oss_flush(struct audio_channel *c);

static char oss_driver[] = "Gnophone/OSS";

struct oss_pvt {
	/* Desired fragment size */
	int frags;
	char fn[80];
};

static int empty_size = 65536;

static char *oss_card_is_good(int fd, int silent, int speed)
{
	/* Perform checks on the sound card to be sure it will work for us */

	int format = AFMT_S16_LE;
	int channels = 1;
	int origspeed = speed;
#if 1
	int fragsize = (BUFFERS << 16) | (FRAG_SIZE);
#else
	int fragsize = 0x000A0006;
#endif
	struct audio_buf_info ispace, ospace;

	if (ioctl(fd, SNDCTL_DSP_SETFMT, &format) ||
	    (format != AFMT_S16_LE) &&
	    (format != AFMT_S16_BE)) {
		return "Device does not support 16-bit signed linear samples";
	}

	if (ioctl(fd, SNDCTL_DSP_CHANNELS, &channels) ||
	    (channels != 1)) {
		return "Device does not support mono PCM data";
	}
	
	if (ioctl(fd, SNDCTL_DSP_SPEED, &speed)) {
		return "Unable to set speed";
	}
	if ((speed > ((float)origspeed * 1.05)) || (speed < ((float)origspeed * 0.95))) {
		return "Could not set requested Hz";
	}
	if (speed != origspeed) {
		/* If we're close to the requested (+/- 5%), it's probably good enough, but at least
		   let them know. */
		if (!silent)
			fprintf(stderr, "Speed isn't exactly 8000, but %d instead...\n", speed);
	}

	if (ioctl(fd, SNDCTL_DSP_SETFRAGMENT, &fragsize)) {
		printf("Fragsize: %d\n", fragsize);
		if (!silent) {
			fprintf(stderr, "Sound card won't let me set fragment size to 10 64-byte buffers (%x)\n"
							"so sound may be choppy: %s.\n", fragsize, strerror(errno));
		}
		return NULL;
	}	

	bzero(&ispace, sizeof(ispace));
	bzero(&ospace, sizeof(ospace));

	if (ioctl(fd, SNDCTL_DSP_GETISPACE, &ispace)) {
		/* They don't support block size stuff, so just return but notify the user */
		if (!silent)
			fprintf(stderr, "Sound card won't let me know the input buffering...\n");
		return NULL;
	}
	if (ioctl(fd, SNDCTL_DSP_GETOSPACE, &ospace)) {
		/* They don't support block size stuff, so just return but notify the user */
		if (!silent)
			fprintf(stderr, "Sound card won't let me know the output buffering...\n");
		return NULL;
	}
	empty_size = ospace.bytes;
	if (!silent) {
		fprintf(stderr, "New input space:  %d of %d %d byte fragments (%d bytes left)\n", 
			ispace.fragments, ispace.fragstotal, ispace.fragsize, ispace.bytes);
		fprintf(stderr, "New output space:  %d of %d %d byte fragments (%d bytes left)\n", 
			ospace.fragments, ospace.fragstotal, ospace.fragsize, ospace.bytes);
	}
	
	return NULL;
	
}

static int oss_close(struct audio_channel *c)
{
	close(c->fd);
	c->fd = -1;
	return 0;
}

static int card_is_full_duplex(int fd)
{
	if (ioctl(fd, SNDCTL_DSP_SETDUPLEX, 0)) {
		/* This is a half duplex device only */
		return 0;
	}
	return 1;
}

static char *oss_get_device_name(char *type, int num)
{
	/* We're supposed to ignore the format of /dev/sndstat, but i'm hoping
	   it will remain the same for this purpose ;-) */
	static char buf[80];
	char *var, *value;
	int rightsection = 0;
	FILE *f;
	f = fopen("/dev/sndstat", "r");
	while(f && !feof(f)) {
		fgets(buf, sizeof(buf), f);
		if (!feof(f)) {
			/* Trim off trailing carriage return */
			buf[strlen(buf) - 1] = '\0';
		}
		var = strtok(buf, ":");
		value = strtok(NULL,  ":");
		if (var) {
			if (value && strlen(value)) {
				if (rightsection &&
				     atoi(var) == num) {
					 	fclose(f);
					 	return value;
				}
			} else if (!strcmp(var, type)) 
				rightsection = 1;
			else
				rightsection = 0;
		}
	}
	snprintf(buf, sizeof(buf), "Unknown %s", type);
	/* Take off the "s" */
	buf[strlen(buf)-1] = '\0';
	return buf;
}

static struct audio_channel *oss_channel_new(int dev, char *filename, int duplex, int speed)
{
	struct audio_channel *ac;
	struct oss_pvt *pvt = (struct oss_pvt *)malloc(sizeof(struct oss_pvt));
	ac = audio_new();
	pvt = (struct oss_pvt *)malloc(sizeof(struct oss_pvt));
	bzero(pvt, sizeof(struct oss_pvt));
	if (ac && pvt) {
		strncpy(pvt->fn, filename, sizeof(pvt->fn));
		pvt->frags = BUFFERS;
		snprintf(ac->name, sizeof(ac->name), "%s on %s", oss_get_device_name("Audio devices", dev), filename);
		ac->priority = 25;
		ac->driver = oss_driver;
		ac->open = oss_open;
		ac->close = oss_close;
		ac->play_digit = std_play_digit;
		ac->activate = oss_activate;
		ac->ring = std_ring;
		ac->busy = std_busy;
		ac->hz = speed;
		ac->fastbusy = std_fastbusy;
		ac->ringing = std_ringing;
		ac->deactivate = oss_deactivate;
		ac->configure = oss_configure;
		ac->sendaudio = oss_sendaudio;
		ac->readaudio = oss_readaudio;
		ac->flush = oss_flush;
		ac->setspeed = oss_setspeed;
		ac->sformats = AST_FORMAT_SLINEAR;
		/* ac->cananswer = 0; */
		ac->duplex = duplex;
		ac->simduplex = oss_simduplex;
		ac->fd = -1;
		ac->pvt = pvt;
		ac->echocancelled = 0;
		
	} else if (!pvt) { free(ac); ac = NULL; }
	return ac;
}

static int oss_open(struct audio_channel *ac)
{
	struct oss_pvt *pvt = ac->pvt;
	char *c;
	char buf[2];
	if (ac->fd > -1) {
		fprintf(stderr, "Channel %s already open?\n", ac->name);
		return 0;
	}
	if (ac->duplex){
		ac->fd = open(pvt->fn, O_RDWR | O_NONBLOCK, 0);
		fprintf(stdout,"Opened %s with fd %d\n",pvt->fn,ac->fd);
	}
	else {
		if (ac->writemode)
			ac->fd = open(pvt->fn, O_WRONLY | O_NONBLOCK, 0);
		else
			ac->fd = open(pvt->fn, O_RDONLY | O_NONBLOCK, 0);
	}
	if (ac->fd < 0) {
		fprintf(stderr, "Error opening %s: %s\n", pvt->fn, strerror(errno));
		return -1;
	}
	if (ac->duplex) {
		if (!card_is_full_duplex(ac->fd)) {
			fprintf(stderr, "Card isn't full duplex anymore??\n");
			return -1;
		}
	}
	if ((c=oss_card_is_good(ac->fd, 0, ac->hz))) {
		fprintf(stderr, "Unable to set mode: %s\n", c);
		return -1;
	}
	read(ac->fd, buf, 2);
	return 0;
}

static int oss_simduplex(struct audio_channel *ac, int write)
{
	if (ac->writemode != write) {
		oss_close(ac);
		ac->writemode = write;
		oss_open(ac);
	}
	return 0;
}

static int oss_setspeed(struct audio_channel *ac, int speed)
{
	if (ac->hz != speed) {
		ac->hz = speed;
		oss_close(ac);
		return oss_open(ac);
	}
	return 0;
}

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

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

static int oss_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;
}

static int oss_readaudio(struct audio_channel *ac, int format, void *data, int *len)
{
	int res;
	struct audio_buf_info ispace;
	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;
	}
	if (!ioctl(ac->fd, SNDCTL_DSP_GETISPACE, &ispace)) {
		/* If we get to look at the output space, check before we write */
		if (!ispace.bytes) {
			fprintf(stderr, "No bytes to read\n");
			*len = 0;
			return -1;
		}
	}
	res = read(ac->fd, data, *len);
	if (res < 0) {
		fprintf(stderr, "Error reading from %s: %s\n", ac->name, strerror(errno));
		*len = 0;
		return -1;
	}
	*len = res;
	return 0;
}

static int oss_sendaudio(struct audio_channel *ac, int format, void *data, int len)
{
	struct audio_buf_info ospace;
	static char bytes[8000];
	static int numbytes = 0;
	int written=0;
	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;
	}
	
	if (!ioctl(ac->fd, SNDCTL_DSP_GETOSPACE, &ospace)) {
		/* If we get to look at the output space, check before we write */
		if (ospace.bytes < len) {
			fprintf(stderr, "Only have %d bytes of buffer, and %d bytes to write -- dropping frame\n", ospace.bytes, len);
			return 0;
		}
		if (ospace.bytes + numbytes < len) {
			fprintf(stderr, "Only have %d bytes of buffer, and %d bytes to write -- dropping backlog\n", ospace.bytes, len + numbytes);
			numbytes = 0;
		}
	}
	if (numbytes) {
		if ((written=write(ac->fd, bytes, numbytes)) < numbytes) {
			numbytes = 0;
			fprintf(stderr, "%s Error writing frame of %d bytes to fd %d: %s\n",__PRETTY_FUNCTION__,written,ac->fd, strerror(errno));
			return -1;
		}
		numbytes = 0;
	} else {
		/* There's nothing in the buffer.  Send this and the next at the same time so
		   that we keep it topped off */
		if (ospace.bytes >= empty_size) {
#if 0
			printf("Delaying... (%d >= %d)\n", ospace.bytes, empty_size);
#endif			
			memcpy(bytes, data, len);
			numbytes = len;
			return 0;
		}
	}
	if (write(ac->fd, data, len) < len) {
		fprintf(stderr, "%s:%d Error writing frame: %s\n", __PRETTY_FUNCTION__,__LINE__,strerror(errno));
		return -1;
	}
	return 0;
	
}

int init()
{
	char fn[80];
	int fd;
	int x;
	int duplex;
	struct audio_channel *ac;
	struct stat statbuf;
	char *c;
	for (x=0;x<MAX_DEVS; x++) {
		if (!x && stat("/dev/dsp0", &statbuf)) {
			/* Some systems have only /dev/dsp and not /dev/dsp0 */
			strcpy(fn, "/dev/dsp");
		} else
			sprintf(fn, "/dev/dsp%d", x);
		fd = open(fn, O_RDWR | O_NONBLOCK, 0);
		if (fd >= 0) {
			duplex = card_is_full_duplex(fd);
			if (!(c = oss_card_is_good(fd, 0, AUDIO_DEFAULT_SPEED))) {
				/* Candidate sound card */
				ac = oss_channel_new(x, fn, duplex, AUDIO_DEFAULT_SPEED);
				close(fd);
				fd = -1;
				if (ac)
					audio_register_channel(ac);
			} else {
				fprintf(stderr, "Card %s is no good because %s\n", fn, c);
			}
			if (fd >= 0)
				close(fd);
		}
	}
	return 0;
}

char *key() 
{
	return KEY;
}

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

static int oss_flush(struct audio_channel *c)
{
	ioctl(c->fd, SNDCTL_DSP_POST, 0);
	return 0;
}
