/*
    Driver for PsionW Sound output

    (c) 2002 Simon Howard

    Microphone support by Simon Kagstrom <ska@bth.se>

    Changes:
              
        Alexandre Orlov: fixes and overhaul, A-Law and 16-bit PCM support


    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/* TODO:
 *
 *  - /dev/mixer support
 */

#include <linux/fs.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/version.h>
#include <linux/module.h>
#include <linux/sound.h>
#include <linux/soundcard.h>

#include <asm/irq.h>
#include <asm/mach/irq.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <asm/hardware/psionw.h>
#include <asm/arch/irqs.h>
#include <asm/arch/psionw-power.h>


/* utilities declarations */

struct fifo_t;
static inline void fifo_init(struct fifo_t* pbuf);
static inline void fifo_clear(struct fifo_t *pbuf);
static inline void fifo_lock(struct fifo_t* pbuf);
static inline void fifo_unlock(struct fifo_t* pbuf);
static inline size_t fifo_size(struct fifo_t *pbuf);
static inline size_t fifo_empty(struct fifo_t* pbuf);
static inline void fifo_put(struct fifo_t* pbuf, char ch);
static inline char fifo_get(struct fifo_t* pbuf);
static inline size_t fifo_write(struct fifo_t* pbuf, const char* from, size_t count);
static inline size_t fifo_read(struct fifo_t* pbuf, char* to, size_t count);

int alaw2linear(unsigned char ulawbyte);
unsigned char linear2alaw(int sample);

/* these ought to be in the hardware headers: */

#define CODOUTEN (1 << 0)
#define CODINEN  (1 << 1)

#define CTXFF    (1 << 1)
#define CRXFF    (1 << 0)

#define SAMPLE_RATE 8000 /* this cant be changed */
#define DSP_BLKSIZE 8

#define SAMPLE_BUF_SIZE (DSP_BLKSIZE * 4) /* buffers for transcoding samples */


extern void enable_irq(unsigned int irq);
extern void disable_irq(unsigned int irq);

/* driver context */

static int audio_dev;
static int users;
static struct semaphore users_lock;
static int is_read; /* flag - recording mode */
static int is_reset; /* flag - need to stop now */
static int is_sync; /* flag - last byte was written to the isr fifo */
static struct fifo_t writebuf;
static struct fifo_t readbuf;
static int current_format;


/* interrupt routine */
void psionw_sound_isr(unsigned int irq){
	size_t i = 0, count = 0;
	
	/* write data to the speaker */
	count = fifo_size(&writebuf);
	if(DSP_BLKSIZE < count) count = DSP_BLKSIZE;
	for(i = 0; i < count; i++){
		psionw_writeb(fifo_get(&writebuf), CODR);
	}
	for(; i < DSP_BLKSIZE; i++){
		psionw_writeb(0, CODR);
	}

	if(is_read){
		/* read data from microphone */
		count = fifo_empty(&readbuf);
		for(i = 0; i < count && !(psionw_readb(COLFG) & CRXFF); i++){
			fifo_put(&readbuf, psionw_readb(CODR));
		}
	}

	/* clear interrupt */
	psionw_writel(1, COEOI);
}

static ssize_t psionw_sound_read(struct file *file, char *buffer, 
				 size_t count, loff_t *ppos)
{
	ssize_t read_cnt = 0, cnt = 0;
	size_t i = 0;
	char samplebuf[SAMPLE_BUF_SIZE];
	char* targetbuf = NULL;

	is_reset = 0;
	while (0 < count && !is_reset) {
		/* check if there is anything in the fifo */
		cnt = fifo_size(&readbuf);
		if(0 == cnt){
			if(file->f_flags & O_NONBLOCK) break;
			set_current_state(TASK_INTERRUPTIBLE);
			schedule_timeout(1);
			continue;
		}

		/* read samples */
		switch(current_format){
		case AFMT_A_LAW:
			targetbuf = buffer + read_cnt;
			if(cnt > count) cnt = count;
			break;
		case AFMT_S16_LE:
			targetbuf = samplebuf;
			if(cnt > SAMPLE_BUF_SIZE) cnt = SAMPLE_BUF_SIZE;
			if(cnt > count / 2) cnt = count / 2;
			break;
		}

		if(NULL == targetbuf){
			printk("psion_sound_read(), unknown format: 0x%x", current_format);
			break;
		}

		fifo_lock(&readbuf);
		disable_irq(IRQ_CSINT);
		cnt = fifo_read(&readbuf, targetbuf, cnt);
		enable_irq(IRQ_CSINT);
		fifo_unlock(&readbuf);
			
		/* transcode samples */
		switch(current_format){
		case AFMT_A_LAW:
			read_cnt += cnt;
			count -= cnt;
			break;
		case AFMT_S16_LE:
			for(i = 0; i < cnt; i++){
				int sample = alaw2linear(samplebuf[i]);
				buffer[read_cnt] = (char)(sample & 0xff);
				buffer[read_cnt + 1] = (char)((sample >> 8) & 0xff);
				read_cnt += 2;
				count -= 2;
			}
			/* don't bother with the last byte */
			if(1 == count) count = 0;
			break;
		}
	}
	if (file->f_flags & O_NONBLOCK && 0 == read_cnt)
		return -EAGAIN;

	return read_cnt;
}

static ssize_t psionw_sound_write(struct file *file, const char *buffer, 
				  size_t count, loff_t *ppos)
{
	ssize_t write_cnt = 0, cnt = 0;
	size_t i = 0;
	char samplebuf[SAMPLE_BUF_SIZE];
	char* sourcebuf = NULL;

	is_sync = 0;
	is_reset = 0;
	while (0 < count && !is_reset) {
		/* check if we can fit anything into the fifo */
		cnt = fifo_empty(&writebuf);
		if(0 == cnt){
			if(file->f_flags & O_NONBLOCK) break;
			set_current_state(TASK_INTERRUPTIBLE);
			schedule_timeout(1);
			continue;
		}

		/* transcode samples */
		switch(current_format){
		case AFMT_A_LAW:
			sourcebuf = (char*)buffer + write_cnt;
			if(cnt > count) cnt = count;
			write_cnt += cnt;
			count -= cnt;
			break;
		case AFMT_S16_LE:
			sourcebuf = samplebuf;
			if(cnt > SAMPLE_BUF_SIZE) cnt = SAMPLE_BUF_SIZE;
			if(cnt > count / 2) cnt = count / 2;
			for(i = 0; i < cnt; i++){
				int sample = (int)buffer[write_cnt] & 0xff;
				sample |= (int)buffer[write_cnt + 1] << 8; 
				samplebuf[i] = linear2alaw(sample);
				count -= 2;
				write_cnt += 2;
			}
			/* don't bother with the last byte */
			if(1 == count) {
				count = 0;
				write_cnt++;
			} 
			break;
		}

		if(NULL == sourcebuf){
			printk("psion_sound_write(), unknown format: 0x%x", current_format);
			break;
		}

		/* copy to the isr fifo */
		fifo_lock(&writebuf);
		disable_irq(IRQ_CSINT);
		fifo_write(&writebuf, sourcebuf, cnt);
		enable_irq(IRQ_CSINT);
		fifo_unlock(&writebuf);
	}

	if (file->f_flags & O_NONBLOCK && 0 == write_cnt)
		return -EAGAIN;

	is_sync = 1;
	return write_cnt;
}

static int psionw_sound_ioctl(struct inode *inode, struct file *file, 
	 		      unsigned int cmd, unsigned long arg)
{
	int val;

	switch (cmd) {
	case OSS_GETVERSION:
		return put_user(SOUND_VERSION, (int *)arg);

	case SNDCTL_DSP_RESET:
		is_reset = 1;
		fifo_lock(&writebuf);
		fifo_clear(&writebuf);
		fifo_unlock(&writebuf);
		return 0;

	case SNDCTL_DSP_SYNC:
		while(!is_sync){
			set_current_state(TASK_INTERRUPTIBLE);
			schedule_timeout(1);
			continue;
		}
		return 0;

	case SNDCTL_DSP_SPEED:  /* set sample rate */
		/* we cant change it */

		if (get_user(val, (int *)arg))
			return -EFAULT;

		if (val >= 0)
			return put_user(SAMPLE_RATE, (int *)arg);
		else
			return -EFAULT;

	case SNDCTL_DSP_STEREO:
		return put_user(0, (int *)arg); /* we are stuck in mono mode */

	case SNDCTL_DSP_GETBLKSIZE:
		return put_user(DSP_BLKSIZE, (int *)arg);  /* 8 byte block size (?) */

	case SNDCTL_DSP_GETFMTS:
		return put_user(AFMT_A_LAW | AFMT_S16_LE, (int *)arg);

	case SNDCTL_DSP_SETFMT:
		if (get_user(val, (int *)arg))
			return -EFAULT;

		if(AFMT_A_LAW == val || AFMT_S16_LE == val)
			current_format = val;

		return put_user(current_format, (int *)arg);

	case SNDCTL_DSP_CHANNELS: 	/* set channels */
		return put_user(1, (int *) arg);

	case SNDCTL_DSP_NONBLOCK:
		file->f_flags |= O_NONBLOCK;
		return 0;
	}

	return -EINVAL;
}

static int psionw_sound_open(struct inode *inode, struct file *file)
{
	down(&users_lock);

	if (users > 0) {
		up(&users_lock);
		return -EBUSY;
	}
	++users;

	/* default sound format */
	switch(0x0f & inode->i_cdev->dev){
	case SND_DEV_DSP:
		current_format = AFMT_S16_LE;
		break;
	case SND_DEV_AUDIO:
		current_format = AFMT_A_LAW;
		break;
	default:
		current_format = AFMT_A_LAW;
	}

	/* Check state flags */
	is_read = file->f_mode & FMODE_READ;
	is_sync = 1;
	is_reset = 0;

	/* empty list */
	fifo_clear(&writebuf);
	fifo_clear(&readbuf);

	/* enable codec */
	psionw_writeb(psionw_readb(PDDR) | PDDR_AMPEN | PDDR_CDE, PDDR);
	psionw_writel(psionw_readl(CONFG) | CODINEN | CODOUTEN, CONFG);

	/* enable sound irq */
	irq_desc[IRQ_CSINT].mask_ack = psionw_sound_isr;
	enable_irq(IRQ_CSINT);

	up(&users_lock);
	return 0; 
}

static int psionw_sound_release(struct inode *inode, struct file *file)
{
	down(&users_lock);

	--users;

	/* disable codec */
	psionw_writeb(psionw_readb(PDDR) & ~(PDDR_AMPEN|PDDR_CDE), PDDR);
	psionw_writel(psionw_readl(CONFG) & ~(CODINEN|CODOUTEN), CONFG);

	/* disable sound irq */
	disable_irq(IRQ_CSINT);

	/* empty list */
	fifo_clear(&writebuf);
	fifo_clear(&readbuf);

	up(&users_lock);
	return 0;
}

static int psionw_sound_mmap(struct file *file, struct vm_area_struct *vma)
{
	return -ENODEV;
}

static struct file_operations sound_ops = {
        owner:          THIS_MODULE,
	llseek: 	no_llseek,
	read: 		psionw_sound_read,
        write:          psionw_sound_write,
        ioctl:          psionw_sound_ioctl,
	mmap:		psionw_sound_mmap,
        open:           psionw_sound_open,
        release:        psionw_sound_release,
};

int __init init_psionw_sound(void)
{
	audio_dev = register_sound_dsp(&sound_ops, -1);

	if (audio_dev < 0) {
		printk(KERN_ERR "psionw_sound: cannot register sound device\n");
		return -ENODEV;
	}

	init_MUTEX(&users_lock);
	fifo_init(&writebuf);
	fifo_init(&readbuf);

	printk("psionw_sound: initialised sound output\n");

	return 0;
}

void __exit cleanup_psionw_sound(void)
{
	unregister_sound_dsp(audio_dev);
}


MODULE_DESCRIPTION("psionw sound output driver");
MODULE_AUTHOR("Simon Howard");
MODULE_LICENSE("GPL");

module_init(init_psionw_sound);
module_exit(cleanup_psionw_sound);


/*
 * Utilities
 */

/* fifo for buffering dsp data */

#define BUFSIZE 128

struct fifo_t {
	char buffer[BUFSIZE];
	size_t head;
	size_t tail;
	size_t size;
	struct semaphore lock;
};

static inline void fifo_clear(struct fifo_t *pbuf){
	pbuf->head = pbuf->tail = pbuf->size = 0;
}

static inline void fifo_init(struct fifo_t* pbuf){
	init_MUTEX(&pbuf->lock);
	fifo_clear(pbuf);
}

static inline void fifo_lock(struct fifo_t* pbuf){
	down(&pbuf->lock);
}

static inline void fifo_unlock(struct fifo_t* pbuf){
	up(&pbuf->lock);
}

static inline size_t fifo_size(struct fifo_t *pbuf){
	return pbuf->size;
}

static inline size_t fifo_empty(struct fifo_t* pbuf){
	return BUFSIZE - pbuf->size;
}

/* 
 * puts a byte into fifo
 * will overrun fifo if it is full 
*/
static inline void fifo_put(struct fifo_t* pbuf, char ch){
	pbuf->buffer[pbuf->tail] = ch;
	pbuf->tail++;
	if(BUFSIZE == pbuf->tail) pbuf->tail = 0;
	pbuf->size++;
	if(BUFSIZE < pbuf->size) pbuf->size = 1;
}

/*
 * reads a byte from fifo
 * if fifo is empty returns 0, call fifo_size() to detrmine if fifo is empty
 */
static inline char fifo_get(struct fifo_t* pbuf){
	char ch = 0;
	if(pbuf->size){
		ch = pbuf->buffer[pbuf->head];
		pbuf->head++;
		if(BUFSIZE == pbuf->head) pbuf->head = 0;
		pbuf->size--;
	}
	return ch;
}

/*
 * writes to fifo, will not overrun fifo
 * returns number of bytes written
 */
static inline size_t fifo_write(struct fifo_t* pbuf, const char* from, size_t count){
	size_t write_cnt = 0;
	if(BUFSIZE > pbuf->size){
		if(pbuf->tail >= pbuf->head){
			/*
			 *  |   xxxxx   |
			 *      |    |
			 *     head tail
			 */
			size_t cnt = BUFSIZE - pbuf->tail;
			if(cnt > count) cnt = count;
			memcpy(pbuf->buffer + pbuf->tail, from, cnt);
			count -= cnt;
			write_cnt = cnt;
			cnt = pbuf->head;
			if(cnt > count) cnt = count;
			memcpy(pbuf->buffer, from + write_cnt, cnt);
			write_cnt += cnt;
			pbuf->tail = (pbuf->tail + write_cnt) % BUFSIZE;
			pbuf->size += write_cnt;
		}
		else{
			/*
			 *  |xxx     xxx|
			 *      |    |
			 *     tail head
			 */
			write_cnt = pbuf->head - pbuf->tail;
			if(write_cnt > count) write_cnt = count;
			memcpy(pbuf->buffer + pbuf->tail, from, write_cnt);
			pbuf->tail += write_cnt;
			pbuf->size += write_cnt;
		}
	}
	return write_cnt;
}

/*
 * reads from fifo
 * returns number of bytes read
 */
static inline size_t fifo_read(struct fifo_t* pbuf, char* to, size_t count){
	size_t read_cnt = 0;
	if(pbuf->size){
		if(pbuf->tail > pbuf->head){
			/*
			 *  |   xxxxx   |
			 *      |    |
			 *     head tail
			 */
			read_cnt = pbuf->tail - pbuf->head;
			if(read_cnt > count) read_cnt = count;
			memcpy(to, pbuf->buffer + pbuf->head, read_cnt);
			pbuf->head += read_cnt;
			pbuf->size -= read_cnt;
		}
		else{
			/*
			 *  |xxx     xxx|
			 *      |    |
			 *     tail head
			 */
			size_t cnt = BUFSIZE - pbuf->head;
			if(cnt > count) cnt = count;
			memcpy(to, pbuf->buffer + pbuf->head, cnt);
			count -= cnt;
			read_cnt = cnt;
			cnt = pbuf->tail;
			if(cnt > count) cnt = count;
			memcpy(to + read_cnt, pbuf->buffer, cnt);
			read_cnt += cnt;
			pbuf->head = (pbuf->head + read_cnt) % BUFSIZE;
			pbuf->size -= read_cnt;
		}
	}
	return read_cnt;
}

/*
 * A-Law - linear conversion
 * based on Sun's implementation
 */

#define SIGN_BIT_ALAW   0x80   /* Sign bit for a A-law byte. */
#define SIGN_BIT_LINEAR 0x1000 /* Sign bit for linear sample */
#define QUANT_MASK      0xf    /* Quantization field mask. */
#define NSEGS           7      /* Max number of A-law segment. */
#define SEG_SHIFT       4      /* Left shift for segment number. */
#define SEG_MASK        0x70   /* Segment field mask. */

static int seg_aend[NSEGS] = {0x1F, 0x3F, 0x7F, 0xFF, 
				       0x1FF, 0x3FF, 0x7FF};

static int seg_search(int val){
	int i;
	val &= 0xfff;
	for (i = 0; i < NSEGS; i++) {
		if (val <= seg_aend[i]) break;
	}
	return i;
}

/*
 * linear2alaw() accepts an 16-bit integer, shifts it to the right by 3 bits
 * and encodes resulting 13-bit signed linear PCM to a A-law data
 *
 *Linear Input Code         Compressed Code
 *------------------------  ---------------
 *0000000wxyza              000wxyz
 *0000001wxyza              001wxyz
 *000001wxyzab              010wxyz
 *00001wxyzabc              011wxyz
 *0001wxyzabcd              100wxyz
 *001wxyzabcde              101wxyz
 *01wxyzabcdef              110wxyz
 *1wxyzabcdefg              111wxyz
 *
 * For further information see John C. Bellamy's Digital Telephony, 1982,
 * John Wiley & Sons, pps 98-111 and 472-476.
 */
unsigned char linear2alaw(int pcm_val){
	int seg;
	unsigned char mask;
	unsigned char aval;

	/* A-law using even bit inversion */
	pcm_val = pcm_val >> 3;
	if (pcm_val & SIGN_BIT_LINEAR) {
		mask = 0x55; /* sign bit = 1 */
		pcm_val = ~pcm_val; /* 2's complement */
	}
	else {
		mask = 0xD5; /* sign bit = 0 */
	}

	/* Convert the scaled magnitude to segment number. */
	seg = seg_search(pcm_val);

	/* Combine the sign, segment, and quantization bits. */
	aval = (unsigned char) seg << SEG_SHIFT;
	if (seg < 2)
		aval |= (pcm_val >> 1) & QUANT_MASK;
	else
		aval |= (pcm_val >> seg) & QUANT_MASK;
	return (aval ^ mask);
}

/*
 * alaw2linear() - Convert an A-law value to 16-bit signed linear PCM
 */
int alaw2linear(unsigned char a_val){
	int t;
	int seg;

	a_val ^= 0x55;

	t = (a_val & QUANT_MASK) << 4;
	seg = ((unsigned int)a_val & SEG_MASK) >> SEG_SHIFT;
	switch (seg) {
	case 0:
		t += 8;
		break;
	case 1:
		t += 0x108;
		break;
	default:
		t += 0x108;
		t <<= seg - 1;
	}
	return ((a_val & SIGN_BIT_ALAW) ? t : -t);
}
