/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of ForteMedia FM801 chips
 *
 *  BUGS:
 *    --
 *
 *  TODO:
 *    --
 *
 *   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.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "control.h"
#include "info.h"
#include "fm801.h"

/*
 *  constants
 */

/*
 *  common I/O routines
 */

static void snd_fm801_codec_write(void *private_data,
				   unsigned short reg,
				   unsigned short val)
{
	fm801_t *codec = (fm801_t *) private_data;
	int idx;

	/*
	 *  Wait until the codec interface is not ready..
	 */
	for (idx = 0; idx < 100; idx++) {
		if (!(inw(FM801_REG(codec, AC97_CMD)) & (1<<9)))
			break;
		snd_delay(1);
	}
	if (inw(FM801_REG(codec, AC97_CMD)) & (1<<9)) {
		snd_printk("fm801: AC'97 interface is busy (1)\n");
		return;
	}
	/* write data and address */
	outw(val, FM801_REG(codec, AC97_DATA));
	outw(reg | FM801_AC97_ADDR, FM801_REG(codec, AC97_CMD));
	/*
	 *  Wait until the write command is not completed..
         */
	for (idx = 0; idx < 100; idx++) {
		if (!(inw(FM801_REG(codec, AC97_CMD)) & (1<<9)))
			break;
		snd_delay(1);
	}
	if (inw(FM801_REG(codec, AC97_CMD)) & (1<<9))
		snd_printk("fm801: AC'97 interface is busy (2)\n");
}

static unsigned short snd_fm801_codec_read(void *private_data,
					    unsigned short reg)
{
	fm801_t *codec = (fm801_t *) private_data;
	int idx;

	/*
	 *  Wait until the codec interface is not ready..
	 */
	for (idx = 0; idx < 100; idx++) {
		if (!(inw(FM801_REG(codec, AC97_CMD)) & (1<<9)))
			break;
		snd_delay(1);
	}
	if (inw(FM801_REG(codec, AC97_CMD)) & (1<<9)) {
		snd_printk("fm801: AC'97 interface is busy (1)\n");
		return 0;
	}
	/* read command */
	outw(reg | FM801_AC97_ADDR | (1<<7), FM801_REG(codec, AC97_CMD));
	for (idx = 0; idx < 100; idx++) {
		if (!(inw(FM801_REG(codec, AC97_CMD)) & (1<<9)))
			break;
		snd_delay(1);
	}
	/*
	 *  Wait until the codec interface is not ready..
	 */
	if (inw(FM801_REG(codec, AC97_CMD)) & (1<<9)) {
		snd_printk("fm801: AC'97 interface is busy (2)\n");
		return 0;
	}
	for (idx = 0; idx < 100; idx++) {
		if (inw(FM801_REG(codec, AC97_CMD)) & (1<<8))
			break;
		snd_delay(1);
	}
	if (!(inw(FM801_REG(codec, AC97_CMD)) & (1<<8))) {
		snd_printk("fm801: AC'97 interface is not valid (2)\n");
		return 0;
	}
	return inw(FM801_REG(codec, AC97_DATA));
}

/*
 *  Sample rate routines
 */

static unsigned int snd_fm801_real_rate(int rate)
{
	static unsigned int rates[] = {
		 5500,  8000,  9600, 11025,
		16000, 19200, 22050, 32000,
		38400, 44100, 48000 };
	int idx;

	for (idx = 0; idx < 11; idx++)
		if (rates[idx] >= rate)
			return rates[idx];
	return rates[10];
}

static unsigned short snd_fm801_rate_bits(int rate)
{
	static unsigned int rates[] = {
		 5500,  8000,  9600, 11025,
		16000, 19200, 22050, 32000,
		38400, 44100, 48000 };
	int idx;

	for (idx = 0; idx < 11; idx++)
		if (rates[idx] >= rate)
			return idx;
	return 10;
}

/*
 *  PCM part
 */

static int snd_fm801_playback_ioctl(snd_pcm1_t * pcm1,
				     unsigned int cmd,
				     unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		pcm1->playback.real_rate = snd_fm801_real_rate(pcm1->playback.rate);
		return 0;
	}
	return -ENXIO;
}

static int snd_fm801_record_ioctl(snd_pcm1_t * pcm1,
				    unsigned int cmd, unsigned long *arg)
{
	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		pcm1->record.real_rate = snd_fm801_real_rate(pcm1->record.rate);
		return 0;
	}
	return -ENXIO;
}

static void snd_fm801_playback_trigger(snd_pcm1_t * pcm1, int up)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	if (up) {
		codec->ply_ctrl &= ~(FM801_BUF1_LAST |
				     FM801_BUF2_LAST |
				     FM801_PAUSE);
		codec->ply_ctrl |= FM801_START |
				   FM801_IMMED_STOP;
		outw(codec->ply_ctrl, FM801_REG(codec, PLY_CTRL));
	} else {
		codec->ply_ctrl &= ~FM801_START;
		outw(codec->ply_ctrl, FM801_REG(codec, PLY_CTRL));
	}
	snd_spin_unlock(codec, reg, &flags);
}

static void snd_fm801_record_trigger(snd_pcm1_t * pcm1, int up)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	if (up) {
		codec->cap_ctrl &= ~(FM801_BUF1_LAST |
				     FM801_BUF2_LAST |
				     FM801_PAUSE);
		codec->cap_ctrl |= FM801_START |
				   FM801_IMMED_STOP;
		outw(codec->cap_ctrl, FM801_REG(codec, CAP_CTRL));
	} else {
		codec->cap_ctrl &= ~FM801_START;
		outw(codec->cap_ctrl, FM801_REG(codec, CAP_CTRL));
	}
	snd_spin_unlock(codec, reg, &flags);
}

static void snd_fm801_playback_prepare(snd_pcm1_t * pcm1,
					unsigned char *buffer,
					unsigned int size,
					unsigned int offset,
					unsigned int count)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	codec->ply_ctrl &= ~(FM801_START | FM801_16BIT |
			     FM801_STEREO | FM801_RATE_MASK);
	if (pcm1->playback.mode & SND_PCM1_MODE_16)
		codec->ply_ctrl |= FM801_16BIT;
	if (pcm1->playback.voices > 1)
		codec->ply_ctrl |= FM801_STEREO;
	codec->ply_ctrl |= snd_fm801_rate_bits(pcm1->playback.real_rate) << FM801_RATE_SHIFT;
	codec->ply_buf = 0;
	outw(codec->ply_ctrl, FM801_REG(codec, PLY_CTRL));
	outw((codec->ply_count = count) - 1, FM801_REG(codec, PLY_COUNT));
	codec->ply_buffer = virt_to_bus(buffer);
	codec->ply_size = size;
	outl(codec->ply_buffer, FM801_REG(codec, PLY_BUF1));
	outl(codec->ply_pos = codec->ply_buffer + count, FM801_REG(codec, PLY_BUF2));
	snd_spin_unlock(codec, reg, &flags);
}

static void snd_fm801_record_prepare(snd_pcm1_t * pcm1,
				      unsigned char *buffer,
				      unsigned int size,
				      unsigned int offset,
				      unsigned int count)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	codec->cap_ctrl &= ~(FM801_START | FM801_16BIT |
			     FM801_STEREO | FM801_RATE_MASK);
	if (pcm1->record.mode & SND_PCM1_MODE_16)
		codec->cap_ctrl |= FM801_16BIT;
	if (pcm1->record.voices > 1)
		codec->cap_ctrl |= FM801_STEREO;
	codec->cap_ctrl |= snd_fm801_rate_bits(pcm1->record.real_rate) << FM801_RATE_SHIFT;
	codec->cap_buf = 0;
	outw(codec->cap_ctrl, FM801_REG(codec, CAP_CTRL));
	outw((codec->cap_count = count) - 1, FM801_REG(codec, CAP_COUNT));
	codec->cap_buffer = virt_to_bus(buffer);
	codec->cap_size = size;
	outl(codec->cap_buffer, FM801_REG(codec, CAP_BUF1));
	outl(codec->cap_pos = codec->cap_buffer + count, FM801_REG(codec, CAP_BUF2));
	snd_spin_unlock(codec, reg, &flags);
}

static int snd_fm801_playback_open(snd_pcm1_t * pcm1)
{
	fm801_t *codec = (fm801_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma1ptr, "CS461x - DAC")) < 0)
		return err;
	return 0;
}

static int snd_fm801_record_open(snd_pcm1_t * pcm1)
{
	fm801_t *codec = (fm801_t *) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD, codec->dma2ptr, "CS461x - ADC")) < 0)
		return err;
	return 0;
}

static void snd_fm801_playback_close(snd_pcm1_t * pcm1)
{
	fm801_t *codec = (fm801_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, codec->dma1ptr);
}

static void snd_fm801_record_close(snd_pcm1_t * pcm1)
{
	fm801_t *codec = (fm801_t *) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_RECORD, codec->dma2ptr);
}

static unsigned int snd_fm801_playback_pointer(snd_pcm1_t * pcm1,
					       unsigned int used_size)
{
	unsigned long flags;
	unsigned int result;
	fm801_t *codec = (fm801_t *) pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	if (codec->ply_ctrl & FM801_START) {
		result = used_size - (inl(FM801_REG(codec, PLY_COUNT)) + 1);
	} else {
		result = 0;
	}
	snd_spin_unlock(codec, reg, &flags);
	return result;
}

static unsigned int snd_fm801_record_pointer(snd_pcm1_t * pcm1,
					     unsigned int used_size)
{
	unsigned long flags;
	unsigned int result;
	fm801_t *codec = (fm801_t *) pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	if (codec->cap_ctrl & FM801_START) {
		result = used_size - (inl(FM801_REG(codec, CAP_COUNT)) + 1);
	} else {
		result = 0;
	}
	snd_spin_unlock(codec, reg, &flags);
	return result;
}

void snd_fm801_interrupt(fm801_t *codec, unsigned short status)
{
	if ((status & (FM801_IRQ_PLAYBACK | FM801_IRQ_CAPTURE)) == 0)
		return;
	if (codec->pcm && (status & FM801_IRQ_PLAYBACK)) {
		unsigned long flags;
		snd_pcm1_t *pcm1 = (snd_pcm1_t *)codec->pcm->private_data;

		snd_spin_lock(codec, reg, &flags);
		codec->ply_buf++;
		codec->ply_pos += codec->ply_count;
		codec->ply_pos %= codec->ply_size;
		outl(codec->ply_buffer + codec->ply_pos,
				(codec->ply_buf & 1) ?
					FM801_REG(codec, PLY_BUF1) :
					FM801_REG(codec, PLY_BUF2));
		snd_spin_unlock(codec, reg, &flags);
		if (pcm1 && pcm1->playback.ack)
			pcm1->playback.ack(pcm1);
	}
	if (codec->pcm && (status & FM801_IRQ_CAPTURE)) {
		unsigned long flags;
		snd_pcm1_t *pcm1 = (snd_pcm1_t *)codec->pcm->private_data;

		snd_spin_lock(codec, reg, &flags);
		codec->cap_buf++;
		codec->cap_pos += codec->cap_count;
		codec->cap_pos %= codec->cap_size;
		outl(codec->cap_buffer + codec->cap_pos,
				(codec->cap_buf & 1) ?
					FM801_REG(codec, PLY_BUF1) :
					FM801_REG(codec, PLY_BUF2));
		snd_spin_unlock(codec, reg, &flags);
		if (pcm1 && pcm1->record.ack)
			pcm1->record.ack(pcm1);
	}
	/* clear interrupt */
	outw((status & (FM801_IRQ_PLAYBACK|FM801_IRQ_CAPTURE)), FM801_REG(codec, IRQ_STATUS));
}

static struct snd_stru_pcm1_hardware snd_fm801_playback =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	SND_PCM1_HW_AUTODMA | SND_PCM1_HW_BLOCKPTR,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* hardware formats */
	3,			/* align value */
	6,			/* minimal fragment */
	5500,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_fm801_playback_open,
	snd_fm801_playback_close,
	snd_fm801_playback_ioctl,
	snd_fm801_playback_prepare,
	snd_fm801_playback_trigger,
	snd_fm801_playback_pointer,
	snd_pcm1_playback_dma_ulaw_loud,
	snd_pcm1_dma_move,
	snd_pcm1_playback_dma_neutral
};

static struct snd_stru_pcm1_hardware snd_fm801_record =
{
	NULL,			/* private data */
	NULL,			/* private_free */
	SND_PCM1_HW_AUTODMA | SND_PCM1_HW_BLOCKPTR,	/* flags */
	SND_PCM_FMT_MU_LAW | SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
	SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* hardware formats */
	3,			/* align value */
	6,			/* minimal fragment */
	5500,			/* min. rate */
	48000,			/* max. rate */
	2,			/* max. voices */
	snd_fm801_record_open,
	snd_fm801_record_close,
	snd_fm801_record_ioctl,
	snd_fm801_record_prepare,
	snd_fm801_record_trigger,
	snd_fm801_record_pointer,
	snd_pcm1_record_dma_ulaw,
	snd_pcm1_dma_move,
	NULL
};

static void snd_fm801_pcm_free(void *private_data)
{
	fm801_t *codec = (fm801_t *) private_data;
	codec->pcm = NULL;
}

snd_pcm_t *snd_fm801_pcm(fm801_t * codec)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;

	pcm = snd_pcm1_new_device(codec->card, "FM801");
	if (!pcm)
		return NULL;
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	memcpy(&pcm1->playback.hw, &snd_fm801_playback, sizeof(snd_fm801_playback));
	memcpy(&pcm1->record.hw, &snd_fm801_record, sizeof(snd_fm801_record));
	pcm1->private_data = codec;
	pcm1->private_free = snd_fm801_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
	    SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD | SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "CS461x");
	return codec->pcm = pcm;
}

/*
 *  Mixer routines
 */

static void snd_fm801_mixer_free_ac97(ac97_t * ac97)
{
	((fm801_t *) ac97->private_data)->mixer = NULL;
	snd_free(ac97, sizeof(ac97_t));
}

snd_kmixer_t *snd_fm801_mixer(fm801_t * codec, int pcm_dev)
{
	ac97_t *ac97;
	snd_kmixer_t *mixer;

	ac97 = snd_calloc(sizeof(ac97_t));
	if (!ac97)
		return NULL;
	ac97->write = snd_fm801_codec_write;
	ac97->read = snd_fm801_codec_read;
	ac97->private_data = codec;
	ac97->private_free = snd_fm801_mixer_free_ac97;
	mixer = snd_ac97_mixer(codec->card, ac97, 1, &pcm_dev);
	if (!mixer) {
		snd_free(ac97, sizeof(ac97));
		return NULL;
	}
	return codec->mixer = mixer;
}

/*
 *  initialization routines
 */

fm801_t *snd_fm801_create(snd_card_t * card,
			    struct snd_pci_dev * pci,
			    snd_dma_t * dma1ptr,
			    snd_dma_t * dma2ptr,
			    snd_irq_t * irqptr)
{
	fm801_t *codec;
	unsigned short cmdw;
	unsigned char cmdb;
	
	codec = (fm801_t *) snd_calloc(sizeof(*codec));
	if (!codec)
		return NULL;
	snd_spin_prepare(codec, reg);
	snd_sleep_prepare(codec, codec);
	codec->card = card;
	codec->pci = pci;
	codec->dma1ptr = dma1ptr;
	codec->dma2ptr = dma2ptr;
	codec->irqptr = irqptr;
	codec->port = pci->base_address[0] & ~PCI_BASE_ADDRESS_SPACE;
	snd_pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if ((cmdw & (PCI_COMMAND_IO|PCI_COMMAND_MASTER)) != (PCI_COMMAND_IO|PCI_COMMAND_MASTER)) {
		cmdw |= PCI_COMMAND_IO|PCI_COMMAND_MASTER;
		snd_pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
	snd_pci_read_config_byte(pci, PCI_LATENCY_TIMER, &cmdb);
	if (cmdb < 32)
		cmdb = 32;
	snd_pci_write_config_byte(pci, PCI_LATENCY_TIMER, cmdb);

	/* codec cold reset + AC'97 warm reset */
	outw((1<<5)|(1<<6), FM801_REG(codec, CODEC_CTRL));
	snd_delay(10);
	outw(0, FM801_REG(codec, CODEC_CTRL));
	snd_delay(100);

	/* init volume */
	outw(0x0808, FM801_REG(codec, PCM_VOL));
	outw(0x0808, FM801_REG(codec, FM_VOL));
	outw(0x0808, FM801_REG(codec, I2S_VOL));

	/* interrupt setup - unmask MPU, PLAYBACK & CAPTURE */
	cmdw = inw(FM801_REG(codec, IRQ_MASK));
	cmdw &= ~0x0083;
	outw(cmdw, FM801_REG(codec, IRQ_MASK));

	/* interrupt clear */
	outw(FM801_IRQ_PLAYBACK|FM801_IRQ_CAPTURE|FM801_IRQ_MPU, FM801_REG(codec, IRQ_STATUS));

	return codec;
}

void snd_fm801_free(fm801_t * codec)
{
	unsigned short cmdw;

	/* interrupt setup - mask everything */
	cmdw = inw(FM801_REG(codec, IRQ_MASK));
	cmdw |= 0x00c3;
	outw(cmdw, FM801_REG(codec, IRQ_MASK));

	snd_free(codec, sizeof(*codec));
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_fm801_export;
#endif

int init_module(void)
{
#ifndef LINUX_2_1
	if (register_symtab(&snd_symbol_table_fm801_export) < 0)
		return -ENOMEM;
#endif
	return 0;
}

void cleanup_module(void)
{
}
