/*
 *  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 "../../include/driver.h"
#include "../../include/control.h"
#include "../../include/info.h"
#include "../../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 < 1000; 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 < 1000; 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(void *private_data,
				    snd_pcm_subchn_t * subchn,
				    unsigned int cmd,
				    unsigned long *arg)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = snd_fm801_real_rate(subchn1->rate);
		return 0;
	}
	return -ENXIO;
}

static int snd_fm801_capture_ioctl(void *private_data,
				   snd_pcm_subchn_t * subchn,
				   unsigned int cmd,
				   unsigned long *arg)
{
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	switch (cmd) {
	case SND_PCM1_IOCTL_RATE:
		subchn1->real_rate = snd_fm801_real_rate(subchn1->rate);
		return 0;
	}
	return -ENXIO;
}

static void snd_fm801_playback_trigger(void *private_data,
				       snd_pcm_subchn_t * subchn,
				       int up)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)private_data;

	spin_lock_irqsave(&codec->reg_lock, 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));
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static void snd_fm801_capture_trigger(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      int up)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)private_data;

	spin_lock_irqsave(&codec->reg_lock, 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));
	}
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

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

	spin_lock_irqsave(&codec->reg_lock, flags);
	codec->ply_ctrl &= ~(FM801_START | FM801_16BIT |
			     FM801_STEREO | FM801_RATE_MASK);
	if (subchn1->mode & SND_PCM1_MODE_16)
		codec->ply_ctrl |= FM801_16BIT;
	if (subchn1->voices > 1)
		codec->ply_ctrl |= FM801_STEREO;
	codec->ply_ctrl |= snd_fm801_rate_bits(subchn1->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));
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static void snd_fm801_capture_prepare(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned char *buffer,
				      unsigned int size,
				      unsigned int offset,
				      unsigned int count)
{
	unsigned long flags;
	fm801_t *codec = (fm801_t *)private_data;
	snd_pcm1_subchn_t *subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;

	spin_lock_irqsave(&codec->reg_lock, flags);
	codec->cap_ctrl &= ~(FM801_START | FM801_16BIT |
			     FM801_STEREO | FM801_RATE_MASK);
	if (subchn1->mode & SND_PCM1_MODE_16)
		codec->cap_ctrl |= FM801_16BIT;
	if (subchn1->voices > 1)
		codec->cap_ctrl |= FM801_STEREO;
	codec->cap_ctrl |= snd_fm801_rate_bits(subchn1->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));
	spin_unlock_irqrestore(&codec->reg_lock, flags);
}

static int snd_fm801_playback_open(void *private_data,
				   snd_pcm_subchn_t * subchn)
{
	fm801_t *codec = (fm801_t *) private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(subchn, codec->dma1ptr, "FM801 - DAC")) < 0)
		return err;
	codec->playback_subchn = subchn;
	codec->playback_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	return 0;
}

static int snd_fm801_capture_open(void *private_data,
				  snd_pcm_subchn_t * subchn)
{
	fm801_t *codec = (fm801_t *) private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(subchn, codec->dma2ptr, "FM801 - ADC")) < 0)
		return err;
	codec->capture_subchn = subchn;
	codec->capture_subchn1 = (snd_pcm1_subchn_t *)subchn->private_data;
	return 0;
}

static void snd_fm801_playback_close(void *private_data,
				     snd_pcm_subchn_t * subchn)
{
	snd_pcm1_dma_free(subchn);
}

static void snd_fm801_capture_close(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	snd_pcm1_dma_free(subchn);
}

static unsigned int snd_fm801_playback_pointer(void *private_data,
					       snd_pcm_subchn_t * subchn,
					       unsigned int used_size)
{
	unsigned long flags;
	unsigned int result;
	fm801_t *codec = (fm801_t *) private_data;

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

static unsigned int snd_fm801_capture_pointer(void *private_data,
					      snd_pcm_subchn_t * subchn,
					      unsigned int used_size)
{
	unsigned long flags;
	unsigned int result;
	fm801_t *codec = (fm801_t *) private_data;

	spin_lock_irqsave(&codec->reg_lock, flags);
	if (codec->cap_ctrl & FM801_START) {
		result = used_size - (inl(FM801_REG(codec, CAP_COUNT)) + 1);
	} else {
		result = 0;
	}
	spin_unlock_irqrestore(&codec->reg_lock, 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;

		spin_lock_irqsave(&codec->reg_lock, 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));
		spin_unlock_irqrestore(&codec->reg_lock, flags);
		codec->playback_subchn1->ack(codec->playback_subchn);
	}
	if (codec->pcm && (status & FM801_IRQ_CAPTURE)) {
		unsigned long flags;

		spin_lock_irqsave(&codec->reg_lock, 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));
		spin_unlock_irqrestore(&codec->reg_lock, flags);
		codec->capture_subchn1->ack(codec->capture_subchn);
	}
	/* clear interrupt */
	outw((status & (FM801_IRQ_PLAYBACK|FM801_IRQ_CAPTURE)), FM801_REG(codec, IRQ_STATUS));
}

static struct snd_stru_pcm1_hardware snd_fm801_playback =
{
	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_capture =
{
	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_capture_open,
	snd_fm801_capture_close,
	snd_fm801_capture_ioctl,
	snd_fm801_capture_prepare,
	snd_fm801_capture_trigger,
	snd_fm801_capture_pointer,
	snd_pcm1_capture_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_channel_t *pchn1;

	pcm = snd_pcm1_new_device(codec->card, "FM801", 1, 1);
	if (!pcm)
		return NULL;
	pchn1 = (snd_pcm1_channel_t *)pcm->playback.private_data;
	memcpy(&pchn1->hw, &snd_fm801_playback, sizeof(snd_fm801_playback));
	pchn1->private_data = codec;
	pchn1 = (snd_pcm1_channel_t *)pcm->capture.private_data;
	memcpy(&pchn1->hw, &snd_fm801_capture, sizeof(snd_fm801_capture));
	pchn1->private_data = codec;
	pcm->private_data = codec;
	pcm->private_free = snd_fm801_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
			  SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
			  SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "FM801");
	return codec->pcm = pcm;
}

/*
 *  Mixer routines
 */

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

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

	ac97 = snd_kcalloc(sizeof(ac97_t), GFP_KERNEL);
	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_kfree(ac97);
		return NULL;
	}
	return codec->mixer = mixer;
}

/*
 *  initialization routines
 */

fm801_t *snd_fm801_create(snd_card_t * card,
			    struct 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_kcalloc(sizeof(*codec), GFP_KERNEL);
	if (!codec)
		return NULL;
	codec->reg_lock = SPIN_LOCK_UNLOCKED;
	codec->card = card;
	codec->pci = pci;
	codec->dma1ptr = dma1ptr;
	codec->dma2ptr = dma2ptr;
	codec->irqptr = irqptr;
#ifdef NEW_PCI
	codec->port = pci->resource[0].start;
#else
	codec->port = pci->base_address[0] & ~PCI_BASE_ADDRESS_SPACE;
#endif
	pci_set_master(pci);
	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;
		pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
	pci_read_config_byte(pci, PCI_LATENCY_TIMER, &cmdb);
	if (cmdb < 32)
		cmdb = 32;
	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_kfree(codec);
}

EXPORT_SYMBOL(snd_fm801_create);
EXPORT_SYMBOL(snd_fm801_free);
EXPORT_SYMBOL(snd_fm801_interrupt);
EXPORT_SYMBOL(snd_fm801_pcm);
EXPORT_SYMBOL(snd_fm801_mixer);

/*
 *  INIT part
 */

int init_module(void)
{
	return 0;
}

void cleanup_module(void)
{
}
