/*
 *   ALSA driver for VIA VT82C686A (South Bridge)
 *
 *	Copyright (c) 2000 Jaroslav Kysela <perex@suse.cz>
 *
 *   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.
 *
 */      

#include "driver.h"
#include "info.h"
#include "control.h"
#include "supersouth.h"
#include "initval.h"
#include "dma.h"

#ifdef __QNXNTO__
int snd_index[SND_CARDS] = SND_DEFAULT_IDX;	/* Index 0-MAX */
char *snd_id[SND_CARDS] = SND_DEFAULT_STR;	/* ID for this card */
int snd_mpu_port[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = -1};
int snd_joystick[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = -1};
int snd_pbk_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_cap_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_fm_frame_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
#else
int snd_index[SND_CARDS] = {-1,-1,-1,-1,-1,-1,-1,-1};	/* Index 0-MAX */
char *snd_id[SND_CARDS];	/* ID for this card */
int snd_mpu_port[SND_CARDS] = {-1,-1,-1,-1,-1,-1,-1,-1};
int snd_joystick[SND_CARDS] = {-1,-1,-1,-1,-1,-1,-1,-1};
int snd_pbk_frame_size[SND_CARDS] = {128,128,128,128,128,128,128,128};
int snd_cap_frame_size[SND_CARDS] = {128,128,128,128,128,128,128,128};
int snd_fm_frame_size[SND_CARDS] = {128,128,128,128,128,128,128,128};
#endif

typedef struct snd_via686a_card {
	struct snd_pci_dev pci;
	snd_irq_t *irqptr;
	snd_dma_t *dma_pbk;	/* playback */
	snd_dma_t *dma_rec;	/* record */
	snd_dma_t *dma_fm;	/* MIC record */
	snd_card_t *card;
	via686a_t *codec;
	snd_pcm_t *pcm;
	snd_pcm_t *pcm_fm;
	snd_kmixer_t *mixer;
	snd_rawmidi_t *midi_uart;
} via686a_card_t;

/* 
 *  Globals
 */

#ifdef __QNXNTO__
static via686a_card_t *snd_via686a_cards[SND_CARDS] = SND_DEFAULT_PTR;
#else
static via686a_card_t *snd_via686a_cards[SND_CARDS];
#endif

/*
 *  Basic I/O
 */

static inline unsigned int snd_via686a_codec_xread(via686a_t *codec)
{
	return (unsigned int)inw(VIAREG(codec, AC97)) |
	       ((unsigned int)inw(VIAREG(codec, AC97) + 2) << 16);
}
 
static inline void snd_via686a_codec_xwrite(via686a_t *codec, unsigned int val)
{
	outw((unsigned short)val, VIAREG(codec, AC97));
	outw((unsigned short)(val >> 16), VIAREG(codec, AC97) + 2);
}
 
static int snd_via686a_codec_ready(via686a_t *codec, int secondary, int sched)
{
	signed long end_time;
	unsigned int val;

	end_time = 500;
	do {
		if (!((val = snd_via686a_codec_xread(codec)) & VIA_REG_AC97_BUSY))
			return val & 0xffff;
		
		snd_delay(50);

	} while (end_time-- >= 0);
	snd_printk("snd_via686a_codec_ready: codec %i is not ready [0x%x]\n", secondary, snd_via686a_codec_xread(codec));
	return -EIO;
}
 
static int snd_via686a_codec_valid(via686a_t *codec, int secondary)
{
	signed long end_time;
	int val;
	int stat = !secondary ? VIA_REG_AC97_PRIMARY_VALID :
				VIA_REG_AC97_SECONDARY_VALID;
	
	end_time = 500;
	do {
		if ((val = snd_via686a_codec_xread(codec)) & stat)
			return 0;

		snd_delay(50);

	} while (end_time-- >= 0);
	snd_printk("snd_via686a_codec_valid: codec %i is not valid [0x%x]\n", secondary, snd_via686a_codec_xread(codec));
	return -EIO;
}
 
static void snd_via686a_codec_write(void *private_data,
				    unsigned short reg,
				    unsigned short val)
{
	via686a_t *codec = (via686a_t *)private_data;
	unsigned int xval;
	
	snd_via686a_codec_ready(codec, 0, 0);
	xval = 0 << VIA_REG_AC97_CODEC_ID_SHIFT;
	xval |= VIA_REG_AC97_PRIMARY_VALID;
	xval |= reg << VIA_REG_AC97_CMD_SHIFT;
	xval |= val << VIA_REG_AC97_DATA_SHIFT;
	snd_via686a_codec_xwrite(codec, xval);
}

static unsigned short snd_via686a_codec_read(void *private_data,
					      unsigned short reg)
{
	via686a_t *codec = (via686a_t *)private_data;
	unsigned int xval;

	if (snd_via686a_codec_ready(codec, 0, 0) < 0)
		return ~0;
	xval = 0 << VIA_REG_AC97_CODEC_ID_SHIFT;
	xval |= VIA_REG_AC97_PRIMARY_VALID;
	xval |= VIA_REG_AC97_READ;
	xval |= reg << VIA_REG_AC97_CMD_SHIFT;
	snd_via686a_codec_xwrite(codec, xval);
	if (snd_via686a_codec_ready(codec, 0, 0) < 0)
		return ~0;
	if (snd_via686a_codec_valid(codec, 0) < 0)
		return ~0;
	return snd_via686a_codec_xread(codec) & 0xffff;
}

#if 0
static void snd_via686a_channel_print(via686a_t *codec, viadev_t *viadev)
{
	unsigned int port = codec->port + viadev->reg_offset;

	printk("[0x%x] status = 0x%x, control = 0x%x, type = 0x%x, ptr = 0x%x, count = 0x%x\n",
			port,
			inb(port + VIA_REG_OFFSET_STATUS),
			inb(port + VIA_REG_OFFSET_CONTROL),
			inb(port + VIA_REG_OFFSET_TYPE),
			inl(port + VIA_REG_OFFSET_CURR_PTR),
			inl(port + VIA_REG_OFFSET_CURR_COUNT));
}
#endif

static void snd_via686a_channel_reset(via686a_t *codec, viadev_t *viadev)
{
	unsigned int port = codec->port + viadev->reg_offset;

	outb(VIA_REG_CTRL_PAUSE | VIA_REG_CTRL_TERMINATE | VIA_REG_CTRL_RESET, port + VIA_REG_OFFSET_CONTROL);
	snd_delay(50);
	outb(0x00, port + VIA_REG_OFFSET_CONTROL);
	outb(0xff, port + VIA_REG_OFFSET_STATUS);
	outb(0x00, port + VIA_REG_OFFSET_TYPE);
	outl(0, port + VIA_REG_OFFSET_CURR_PTR);
}

static void snd_via686a_trigger(via686a_t *codec, viadev_t *viadev, int up)
{
	unsigned char val = 0;
	unsigned int port = codec->port + viadev->reg_offset;
	
	if (up) {
		val = VIA_REG_CTRL_START;
	} else {
		val = VIA_REG_CTRL_TERMINATE | VIA_REG_CTRL_RESET;
	}
	snd_mutex_lock(codec, reg_lock);
	outb(val, port + VIA_REG_OFFSET_CONTROL);
	if (!up)
		snd_via686a_channel_reset(codec, viadev);
	snd_mutex_unlock(codec, reg_lock);
	return;
}

static void snd_via686a_setup_fragments(via686a_t *codec, viadev_t *viadev, snd_pcm1_channel_t *pchn)
{
	int idx, frags;
	unsigned int *table = viadev->table;
	unsigned int port = codec->port + viadev->reg_offset;

	snd_via686a_channel_reset(codec, viadev);
	outl(virt_to_bus(viadev->table), port + VIA_REG_OFFSET_TABLE_PTR);
	outb(VIA_REG_TYPE_AUTOSTART |
	     (pchn->mode & SND_PCM1_MODE_16 ? VIA_REG_TYPE_16BIT : 0) |
	     (pchn->voices > 1 ? VIA_REG_TYPE_STEREO : 0) |
	     VIA_REG_TYPE_INT_EOL |
	     VIA_REG_TYPE_INT_FLAG, port + VIA_REG_OFFSET_TYPE);
	if (viadev->size == viadev->fragsize) {
		table[0] = viadev->physbuf;
		table[1] = 0xc0000000 | /* EOL + flag */
			   viadev->fragsize;
	} else {
		frags = viadev->size / viadev->fragsize;
		for (idx = 0; idx < frags - 1; idx++) {
			table[(idx << 1) + 0] = (viadev->physbuf + (idx * viadev->fragsize));
			table[(idx << 1) + 1] = 0x40000000 |	/* flag */
						viadev->fragsize;
		}
		table[((frags-1) << 1) + 0] = (viadev->physbuf + ((frags-1) * viadev->fragsize));
		table[((frags-1) << 1) + 1] = 0x80000000 |	/* EOL */
					      viadev->fragsize;
	}
}

/*
 *  Interrupt handler
 */

static inline void snd_via686a_update(via686a_t *codec, int channel, viadev_t *viadev)
{
	snd_pcm1_t *pcm1;

	if ((!codec) || (!viadev))
		return;

	pcm1 = viadev->pcm1;

	snd_mutex_lock(codec, reg_lock);
	viadev->position += viadev->fragsize;
	viadev->position %= viadev->size;
	snd_mutex_unlock(codec, reg_lock);
	switch(channel) {
	case SND_PCM1_PLAYBACK:
		if( pcm1 && pcm1->playback.ack ) 
			pcm1->playback.ack(pcm1);
		break;
	case SND_PCM1_RECORD:
		if( pcm1 && pcm1->record.ack ) 
			pcm1->record.ack(pcm1);
		break;
	}
	outb(VIA_REG_STAT_FLAG | VIA_REG_STAT_EOL, VIAREG(codec, OFFSET_STATUS) + viadev->reg_offset);
}

static void snd_via686a_card_interrupt(void *dev_id)
{
	via686a_card_t *scard = (via686a_card_t *) dev_id;
	via686a_t *codec;
	unsigned int status;

	if (scard == NULL || (codec = scard->codec) == NULL)
		return;
	status = inl(VIAREG(codec, SGD_SHADOW));
	if ((status & 0x00000077) == 0)
		return;
	if (inb(VIAREG(codec, PLAYBACK_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, SND_PCM1_PLAYBACK, &codec->playback);
	if (inb(VIAREG(codec, FM_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, SND_PCM1_PLAYBACK, &codec->playback_fm);
	if (inb(VIAREG(codec, CAPTURE_STATUS)) & (VIA_REG_STAT_EOL|VIA_REG_STAT_FLAG))
		snd_via686a_update(codec, SND_PCM1_RECORD, &codec->record);
}

/*
 *  PCM part
 */
static int snd_via686a_playback_open(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma_pbk,
				     "VIA 82C686A - DAC")) < 0) {
		return err;
	}
	return 0;
}

static int snd_via686a_record_open(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_RECORD, codec->dma_rec,
				     "VIA 82C686A - ADC")) < 0) {
		return err;
	}
	return 0;
}

void snd_ac97_write_lock(ac97_t *ac97, unsigned short reg, unsigned short value)
{
	int tmp = 0;

	snd_mutex_lock(ac97, access);
        ac97->write(ac97->private_data, reg, value);

        tmp = ac97->read(ac97->private_data, reg);
	snd_mutex_unlock(ac97, access);
}

void snd_ac97_write_bitmask_lock(ac97_t *ac97, unsigned short reg,
                                 unsigned short mask, unsigned short value)
{
	int tmp = 0;
	int tmp2 = 0;
	int tmp3 = 0;

	snd_mutex_lock(ac97, access);
//	ac97->write(ac97->private_data, reg, (ac97->read(ac97->private_data, reg) & mask) | value);
        tmp2= ac97->read(ac97->private_data, 0x28);
        tmp3= ac97->read(ac97->private_data, 0x2A);
        ac97->write(ac97->private_data, 0x2A, 0x0001);
        tmp = ac97->read(ac97->private_data, reg) & mask;
        ac97->write(ac97->private_data, reg, tmp | value);
	snd_mutex_unlock(ac97, access);
}

static void snd_via686a_playback_close(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, codec->dma_pbk);
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	return;
}

static void snd_via686a_record_close(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, codec->dma_rec);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	return;
}

static int snd_via686a_playback_ioctl(snd_pcm1_t *pcm1,
				      unsigned int cmd,
				      unsigned long *arg)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	switch (cmd) {
#if 00
	case SND_PCM1_IOCTL_FRAGS:
		// max number of frags == 32
		if( *(unsigned int*)arg < 32 ) {
			*(unsigned int*)arg = 32;
		break;
#endif
	}

	/* to get rid of "unreferenced" compiler warning */
	codec = codec;

	return -ENXIO;
}

static int snd_via686a_record_ioctl(snd_pcm1_t *pcm1,
				      unsigned int cmd,
				      unsigned long *arg)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	switch (cmd) {
#if 00
	case SND_PCM1_IOCTL_FRAGS:
		// max number of frags == 32
		if( *(unsigned int*)arg < 32 ) {
			*(unsigned int*)arg = 32;
		break;
#endif
	}

	/* to get rid of "unreferenced" compiler warning */
	codec = codec;

	return -ENXIO;
}


static void snd_via686a_playback_trigger(snd_pcm1_t *pcm1, int up)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	snd_via686a_trigger(codec, &codec->playback, up);

	return;
}

static void snd_via686a_record_trigger(snd_pcm1_t *pcm1, int up)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	snd_via686a_trigger(codec, &codec->record, up);

	return;
}

static void snd_via686a_playback_prepare(snd_pcm1_t *pcm1,
					unsigned char *buffer,
					unsigned int size,
					unsigned int offset,
					unsigned int count)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	codec->playback.physbuf = virt_to_bus(buffer);
	codec->playback.size = size;
	codec->playback.fragsize = count;
	codec->playback.position = offset;
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	snd_ac97_write_lock(codec->ac97, AC97_XA_FRONT_DAC_RATE, pcm1->playback.rate);
	/* enable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0000);
	snd_mutex_lock(codec, reg_lock);
	snd_via686a_setup_fragments(codec, &codec->playback, &pcm1->playback);
	snd_mutex_unlock(codec, reg_lock);
	return;
}

static void snd_via686a_record_prepare(snd_pcm1_t *pcm1,
					unsigned char *buffer,
					unsigned int size,
					unsigned int offset,
					unsigned int count)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	*((int *)buffer) = 0x55aa55aa;
	memset(buffer, 0, size);
	codec->record.physbuf = virt_to_bus(buffer);
	codec->record.size = size;
	codec->record.fragsize = count;
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	snd_ac97_write_lock(codec->ac97, AC97_XA_LINE_ADC_RATE, pcm1->record.rate);
	/* enable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0000);
	snd_mutex_lock(codec, reg_lock);
	snd_via686a_setup_fragments(codec, &codec->record, &pcm1->record );
	snd_mutex_unlock(codec, reg_lock);
	return;
}

static unsigned int snd_via686a_playback_pointer(snd_pcm1_t * pcm1,
						unsigned int used_size)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	unsigned int result;

	snd_mutex_lock(codec, reg_lock);
	if (inb(VIAREG(codec, PLAYBACK_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->playback.position +
			 (codec->playback.fragsize -
			  inl(VIAREG(codec, PLAYBACK_CURR_COUNT)));
	} else {
		result = 0;
	}
	snd_mutex_unlock(codec, reg_lock);
	return used_size - result;
}

static unsigned int snd_via686a_record_pointer(snd_pcm1_t * pcm1,
						unsigned int used_size)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	unsigned int result;

	snd_mutex_lock(codec, reg_lock);
	if (inb(VIAREG(codec, CAPTURE_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->record.position +
			 (codec->record.fragsize -
			  inl(VIAREG(codec, CAPTURE_CURR_COUNT)));
	} else {
		result = 0;
	}
	snd_mutex_unlock(codec, reg_lock);
	return used_size - result;
}

static struct snd_stru_pcm1_hardware snd_via686a_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 */
        31,                     /* align value */
        32,                     /* minimal fragment */
        8000,                   /* min. rate */
        48000,                  /* max. rate */
        2,                      /* max. voices */
        snd_via686a_playback_open,
        snd_via686a_playback_close,
        snd_via686a_playback_ioctl,
        snd_via686a_playback_prepare,
        snd_via686a_playback_trigger,
        snd_via686a_playback_pointer,
        snd_pcm1_playback_dma_ulaw_loud,
        snd_pcm1_dma_move,
        snd_pcm1_playback_dma_neutral
};

static struct snd_stru_pcm1_hardware snd_via686a_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 */
        31,                     /* align value */
        32,                     /* minimal fragment */
        8000,                   /* min. rate */
        48000,                  /* max. rate */
        2,                      /* max. voices */
        snd_via686a_record_open,
        snd_via686a_record_close,
        snd_via686a_record_ioctl,
        snd_via686a_record_prepare,
        snd_via686a_record_trigger,
        snd_via686a_record_pointer,
        snd_pcm1_record_dma_ulaw,
        snd_pcm1_dma_move,
        NULL 
};



static void snd_via686a_pcm_free(void *private_data)
{
	via686a_t *codec = (via686a_t*) private_data;
	codec->pcm = NULL;
}

int snd_via686a_pcm(via686a_t * codec, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;

	*rpcm = NULL;
	pcm = snd_pcm1_new_device(codec->card, "VIA 82C686A");
	if (!pcm)
		return NULL;

	pcm1 = (snd_pcm1_t*) pcm->private_data;
	memcpy(&pcm1->playback.hw, &snd_via686a_playback, sizeof(snd_via686a_playback));
	memcpy(&pcm1->record.hw, &snd_via686a_record, sizeof(snd_via686a_record));

	/* Update the hardware structure based on autodetected values */
	if( pcm1->playback.hw.min_rate < codec->playback.min_rate )
		pcm1->playback.hw.min_rate = codec->playback.min_rate;
	if( pcm1->playback.hw.max_rate > codec->playback.max_rate )
		pcm1->playback.hw.max_rate = codec->playback.max_rate; 
	if( pcm1->record.hw.min_rate < codec->record.min_rate )
		pcm1->record.hw.min_rate = codec->record.min_rate; 
	if( pcm1->record.hw.max_rate > codec->record.max_rate )
		pcm1->record.hw.max_rate = codec->record.max_rate; 

	pcm1->private_data = codec;
	pcm1->private_free = snd_via686a_pcm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP | SND_PCM_INFO_TRIGGER |
		SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD | SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "VIA 82C686A");
	codec->playback.pcm1 = pcm1;
	codec->record.pcm1 = pcm1;
	*rpcm = codec->pcm = pcm;
	return 0;
}

#if 0

/*
 *  PCM code - MIC
 */

static int snd_via686a_playback_fm_open(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	int err;

	if ((err = snd_pcm1_dma_alloc(pcm1, SND_PCM1_PLAYBACK, codec->dma_fm,
				     "VIA 82C686A - FM DAC")) < 0) {
		return err;
	}

	return 0;
}

static int snd_via686a_playback_fm_close(snd_pcm1_t *pcm1)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	snd_pcm1_dma_free(pcm1, SND_PCM1_PLAYBACK, codec->dma_fm );
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	return 0;
}

static int snd_via686a_playback_fm_ioctl( snd_pcm1_t *pcm1,
					  unsigned int cmd,
					  unsigned long *arg)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	switch (cmd) {
#if 00
	case SND_PCM1_IOCTL_FRAGS:
		// max number of frags == 32
		if( *(unsigned int*)arg < 32 ) {
			*(unsigned int*)arg = 32;
		break;
#endif
	}

	/* to get rid of "unreferenced" compiler warning */
	codec = codec;

	return -ENXIO;
}

static int snd_via686a_playback_fm_trigger(snd_pcm1_t *pcm1, int up)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	return snd_via686a_trigger(codec, &codec->playback_fm, up);
}

static int snd_via686a_playback_fm_prepare( snd_pcm1_t *pcm1,
						unsigned char *buffer,
						unsigned int size,
						unsigned int offset,
						unsigned int count)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;

	codec->playback_fm.physbuf = virt_to_bus(buffer);
	codec->playback_fm.size = size;
	codec->playback_fm.fragsize = offset;
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	snd_ac97_write_lock(codec->ac97, AC97_XA_FRONT_DAC_RATE, pcm1->playback.rate);
	/* enable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0000);
	snd_mutex_lock(codec, reg_lock);
	snd_via686a_setup_fragments(codec, &codec->playback_fm, pcm1->playback );
	snd_mutex_unlock(codec, reg_lock);
	return 0;
}

static unsigned int snd_via686a_playback_fm_pointer( snd_pcm1_t * pcm1,
						     unsigned int used_size)
{
	via686a_t *codec = (via686a_t*) pcm1->private_data;
	unsigned int result;

	snd_mutex_lock(codec, reg_lock);
	if (inb(VIAREG(codec, FM_STATUS)) & VIA_REG_STAT_ACTIVE) {
		result = codec->playback_fm.position +
			 (codec->playback_fm.fragsize -
			  inl(VIAREG(codec, FM_CURR_COUNT)));
	} else {
		result = 0;
	}
	snd_mutex_unlock(codec, reg_lock);
	return used_size - result;
}

static struct snd_stru_pcm1_hardware snd_via686a_playback_fm =
{
        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_S16_LE,    /* hardware formats */
        31,                     /* align value */
        32,                     /* minimal fragment */
        24000,                   /* min. rate */
        24000,                  /* max. rate */
        2,                      /* max. voices */
        snd_via686a_playback_fm_open,
        snd_via686a_playback_fm_close,
        snd_via686a_playback_fm_ioctl,
        snd_via686a_playback_fm_prepare,
        snd_via686a_playback_fm_trigger,
        snd_via686a_playback_fm_pointer,
        snd_pcm1_playback_dma_ulaw_loud,
        snd_pcm1_dma_move,
        snd_pcm1_playback_dma_neutral
};


static void snd_via686a_pcm_fm_free(void *private_data)
{
	via686a_t *codec = (via686a_t*) private_data;
	codec->pcm_fm = NULL;
}

int snd_via686a_pcm_fm(via686a_t * codec, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;

	*rpcm = NULL;
	pcm = snd_pcm_new(codec->card, "VIA 82C686A - FM DAC");
	if (!pcm)
		return -1;

	pcm1 = (snd_pcm1_t*) pcm->private_data;
	memcpy(&pcm1->playback.hw, &snd_via686a_playback_fm, sizeof(snd_via686a_playback_fm));

	pcm1->private_data = codec;
	pcm1->private_free = snd_via686a_pcm_fm_free;
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP | SND_PCM_INFO_TRIGGER |
		SND_PCM_INFO_PLAYBACK;
	strcpy(pcm->name, "VIA 82C686A - FM DAC");

	*rpcm = codec->pcm_fm = pcm;	
	codec->pcm_fm->pcm1 = pcm1;
	return 0;
}

#endif

/*
 *  Mixer part
 */

static void snd_via686a_codec_init(void *private_data, ac97_t *ac97)
{
	via686a_t *codec = (via686a_t*) private_data;

	if (codec->ac97_ext_id & 0x0001)	/* VRA */
		snd_ac97_write_lock(ac97, AC97_XA_CTRL, 0x0009);	
	/* disable DAC & ADC power */
	snd_ac97_write_bitmask_lock(ac97, AC97_POWERDOWN, ~0x0300, 0x0300);
	/* disable center DAC/surround DAC/LFE DAC/MIC ADC */
	snd_ac97_write_bitmask_lock(ac97, AC97_XA_CTRL, (short)~0xe800, 0xe800);
}

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

static int snd_via686a_mixer(via686a_t *codec, snd_kmixer_t **rmixer)
{
	snd_kmixer_t *mixer;
	ac97_t *ac97;

	ac97 = snd_calloc(sizeof(ac97_t));
	if (!ac97)
		return NULL;

	memset(ac97, 0, sizeof(ac97_t));
	ac97->write = snd_via686a_codec_write;
	ac97->read = snd_via686a_codec_read;
	ac97->private_data = codec;
	ac97->private_free = snd_via686a_mixer_free_ac97;
	mixer = snd_ac97_mixer(codec->card, ac97);
	if (!mixer) {
		snd_free(ac97, sizeof(ac97));
		return NULL;
	}
	snd_via686a_codec_init(codec, ac97);
	codec->ac97 = (ac97_t*) mixer->private_data;
	*rmixer = codec->mixer = mixer;
	return 0;
}

/*
 *
 */

static int snd_via686a_test_rate(via686a_t *codec, int reg, int rate)
{
	unsigned short val;

	snd_via686a_codec_write(codec, reg, rate);
	val = snd_via686a_codec_read(codec, reg);
	return val == rate;
}

static void snd_via686a_determine_rates(via686a_t *codec, int reg, 
		unsigned int *min_rate, unsigned int *max_rate)
{
	int std_freq[] = { 8000, 11025, 16000, 22050, 32000, 44100, 48000, -1 };
	int min = -1,
	    max = -1,
	    idx;

	/* test a non-standard frequency */
//	if (snd_via686a_test_rate(codec, reg, 11000))
//		result |= SND_PCM_RATE_CONTINUOUS;
	/* let's try to obtain standard frequencies */
	for( idx = 0; std_freq[idx] > 0; idx++) {
		if (!snd_via686a_test_rate(codec, reg, std_freq[idx])) 
			continue;
		if (min == -1)
			min = std_freq[idx];
		max = std_freq[idx];	
	}
	if (min == -1)
		min = 48000;
	if (max == -1)
		max = 48000;

	snd_printd( "min rate = %i max rate = %i\n", min, max );

	*min_rate = min;
	*max_rate = max;
}

static int snd_via686a_chip_init(via686a_t *codec)
{
	unsigned short ext_id;

	/* disable all legacy ports */
	snd_pci_write_config_byte(codec->pci, 0x42, 0);

	/* deassert ACLink reset */
	snd_pci_write_config_byte(codec->pci, 0x41, 0x40);
	snd_delay(100);
	/* deassert ACLink reset, force SYNC (warm AC'97 reset) */
	snd_pci_write_config_byte(codec->pci, 0x41, 0x60);
	snd_delay(2);
	/* ACLink on, deassert ACLink reset, VSR, SGD data out */
	/* note - FM data out has trouble with non VRA codecs !! */
	snd_pci_write_config_byte(codec->pci, 0x41, 0xcc);

	snd_via686a_codec_ready(codec, 0, 1);
	snd_via686a_codec_write(codec, AC97_POWERDOWN, 0x0000);
	snd_via686a_codec_ready(codec, 0, 1);

#if 0
	{
	unsigned char cmdb;

	snd_pci_read_config_byte(codec->pci, 0x40, &cmdb);
	printk("PCI[0x40] = 0x%x\n", cmdb);
	snd_pci_read_config_byte(codec->pci, 0x42, &cmdb);
	printk("PCI[0x42] = 0x%x\n", cmdb);
	snd_pci_read_config_byte(codec->pci, 0x43, &cmdb);
	printk("PCI[0x43] = 0x%x\n", cmdb);
	snd_pci_read_config_byte(codec->pci, 0x44, &cmdb);
	printk("PCI[0x44] = 0x%x\n", cmdb);
	snd_pci_read_config_byte(codec->pci, 0x48, &cmdb);
	printk("PCI[0x48] = 0x%x\n", cmdb);
	}
#endif

	codec->ac97_ext_id = 
	ext_id = snd_via686a_codec_read(codec, AC97_XA_ID);
	if (ext_id == 0xffff) {
		snd_printk("snd_via686a_chip_init: invalid AC'97 codec\n");
		return -EIO;
	}
	if (ext_id & 0x0001) {
		snd_via686a_codec_write(codec, AC97_XA_CTRL, 0x0009);
		snd_via686a_determine_rates(codec, AC97_XA_FRONT_DAC_RATE, 
					&codec->playback.min_rate,
					&codec->playback.max_rate);
		snd_via686a_determine_rates(codec, AC97_XA_LINE_ADC_RATE, 
					&codec->record.min_rate,
					&codec->record.min_rate);
	} else {
		codec->playback.min_rate = 
		codec->playback.max_rate = 48000;
		codec->record.min_rate = 
		codec->record.max_rate = 48000;
	}
	if (ext_id & 0x0008) {
		snd_via686a_determine_rates(codec, AC97_XA_MIC_ADC_RATE, 
					&codec->playback_fm.min_rate,
					&codec->playback_fm.min_rate);
	} else {
		codec->playback_fm.min_rate = 
		codec->playback_fm.max_rate = 48000;
	}
	/* disable interrupts */
	snd_via686a_channel_reset(codec, &codec->playback);
	snd_via686a_channel_reset(codec, &codec->record);
	snd_via686a_channel_reset(codec, &codec->playback_fm);
	return 0;
}

static int snd_via686a_free(via686a_t * codec)
{
	/* disable interrupts */
	snd_via686a_channel_reset(codec, &codec->playback);
	snd_via686a_channel_reset(codec, &codec->record);
	snd_via686a_channel_reset(codec, &codec->playback_fm);
	/* --- */
//	synchronize_irq();
	if (codec->tables)
		snd_free(codec->tables, 8 + sizeof(unsigned int) * 64 * 2);
	snd_free(codec, sizeof(via686a_t));
	return 0;
}

static int snd_via686a_create(snd_card_t * card,
				     struct snd_pci_dev *pci,
				     snd_dma_t * dma_pbk,
				     snd_dma_t * dma_rec,
				     snd_dma_t * dma_fm,
				     snd_irq_t * irqptr,
				     via686a_t ** r_via)
{
	via686a_t *codec;
	unsigned short cmdw;
	int err;
   
	if ((codec = snd_calloc(sizeof(via686a_t))) == NULL)
		return -ENOMEM;

	snd_mutex_prepare(codec, reg_lock);
	codec->card = card;
	codec->pci = pci;
	codec->dma_pbk = dma_pbk;
	codec->dma_rec = dma_rec;
	codec->dma_fm = dma_fm;
	codec->irqptr = irqptr;
	codec->port = snd_pci_resource_start(pci, 0);
	snd_pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if ((cmdw & PCI_COMMAND_IO) != PCI_COMMAND_IO) {
		cmdw |= PCI_COMMAND_IO;
		snd_pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
#if 0
	{
	unsigned char cmdb;
	snd_pci_set_master(pci);
	snd_pci_read_config_byte(pci, PCI_LATENCY_TIMER, &cmdb);
	if (cmdb < 64)
		cmdb = 64;
	snd_pci_write_config_byte(pci, PCI_LATENCY_TIMER, cmdb);
	}
#endif
	snd_pci_read_config_byte(pci, PCI_REVISION_ID, &codec->revision);
//	synchronize_irq();

	/* initialize offsets */
	codec->playback.reg_offset = VIA_REG_PLAYBACK_STATUS;
	codec->record.reg_offset = VIA_REG_CAPTURE_STATUS;
	codec->playback_fm.reg_offset = VIA_REG_FM_STATUS;

	/* allocate buffer descriptor lists */
	/* the start of each lists must be aligned to 8 bytes */
	codec->tables = (unsigned int *)snd_calloc(8 + sizeof(unsigned int) * 64 * 2);
	if (codec->tables == NULL) {
		snd_via686a_free(codec);
		return -ENOMEM;
	}
	codec->playback.table = (unsigned int *)(((unsigned long)codec->tables + 7) & ~7);
	codec->record.table = codec->playback.table + 64 * 2;
	codec->playback_fm.table = codec->record.table + 64 * 2;

	if ((err = snd_via686a_chip_init(codec)) < 0) {
		snd_via686a_free(codec);
		return err;
	}

	*r_via = codec;
	return 0;
}

static int snd_via686a_resources(snd_card_t * card,
					via686a_card_t *scard,
					int dev)
{
	int err;

	if ((err = snd_register_ioport(card, 
			snd_pci_resource_start(&scard->pci, 0), 256, 
			"VIA 82C686A - AC'97", NULL)) < 0)
		return err;
	if ((err = snd_register_interrupt(card,
			"VIA 82C686A", scard->pci.irq,
			SND_IRQ_TYPE_PCI, snd_via686a_card_interrupt,
			scard, NULL, &scard->irqptr)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - playback", 0,
			SND_DMA_TYPE_PCI, snd_pbk_frame_size[dev],
			NULL, &scard->dma_pbk)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - record", 1,
			SND_DMA_TYPE_PCI, snd_cap_frame_size[dev],
			NULL, &scard->dma_rec)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"VIA 82C686A - FM playback", 2,
			SND_DMA_TYPE_PCI, snd_fm_frame_size[dev],
			NULL, &scard->dma_fm)) < 0)
		return err;
	return 0;
}

static int snd_via686a_probe(int dev, via686a_card_t *scard, int device, int pci_index)
{
	snd_card_t *card;
	snd_pcm_t *pcm = NULL, *pcm_fm = NULL;
	snd_kmixer_t *mixer = NULL;
	snd_rawmidi_t *midi_uart = NULL;
	via686a_t *codec;
	unsigned char legacy;
	unsigned char legacy_cfg;
	int rev_h = 0;

	card = snd_card_new(snd_index[dev], snd_id[dev]);
	if (card == NULL)
		return -ENOMEM;

	card->static_data = scard;
	if (snd_via686a_detect(card, &scard->pci, pci_index, device) < 0) {
		snd_card_free(card);
		return -ENODEV;
	}

	if (snd_via686a_resources(card, scard, dev) < 0) { 
		snd_card_free(card);
		return -ENODEV;
	}

	snd_pci_read_config_byte(&scard->pci, 0x42, &legacy);
	snd_pci_read_config_byte(&scard->pci, 0x43, &legacy_cfg);

	if (snd_via686a_create(card,
			       &scard->pci,
			       scard->dma_pbk,
			       scard->dma_rec,
			       scard->dma_fm,
			       scard->irqptr,
			       &scard->codec) < 0)
		goto __nodev;
	codec = scard->codec;

	if (snd_via686a_mixer(codec, &mixer) < 0)
		goto __nodev;
	if (snd_via686a_pcm(codec, &pcm) < 0)
		goto __nodev;
#if 0
	if (snd_via686a_pcm_fm(codec, &pcm_fm) < 0)
		goto __nodev;
#endif

	snd_pci_write_config_byte(&scard->pci, 0x42, legacy);
	snd_pci_write_config_byte(&scard->pci, 0x43, legacy_cfg);

	if (codec->revision >= 0x20) {
		if (snd_register_ioport(card, snd_pci_resource_start(&scard->pci, 2), 4, "VIA 82C686A - MPU401 config", NULL) < 0) {
			rev_h = 0;
			legacy &= ~0x40;
		}
		rev_h = 1;
	}
	if (snd_joystick[dev] > 0) {
		if (rev_h && (legacy & 0x40) && (legacy & 8) == 0)
			outw(0x200, snd_pci_resource_start(&scard->pci, 2) + 2);
		legacy |= 8;
	}
	if (rev_h && (legacy & 0x40) && snd_mpu_port[dev] >= 0x200) { /* rev H+ */
		legacy |= 0x02;	/* enable MPU */
		outw(snd_mpu_port[dev], snd_pci_resource_start(&scard->pci, 2) + 0);
	} else {
		if (rev_h & (legacy & 0x40) && (legacy & 0x02)) {
			snd_mpu_port[dev] = inw(snd_pci_resource_start(&scard->pci, 2) + 0);
		} else {
			switch ((legacy >> 2) & 3) {
			case 0:		snd_mpu_port[dev] = 0x300; break;
			case 1:		snd_mpu_port[dev] = 0x310; break;
			case 2:		snd_mpu_port[dev] = 0x320; break;
			case 3:		snd_mpu_port[dev] = 0x330; break;
			}
		}
	}
	switch (snd_mpu_port[dev]) {
	case 0x300:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 0 << 2; legacy |= 0x02; break;
	case 0x310:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 1 << 2; legacy |= 0x02; break;
	case 0x320:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 2 << 2; legacy |= 0x02; break;
	case 0x330:	legacy_cfg &= ~(3 << 2); legacy_cfg |= 3 << 2; legacy |= 0x02; break;
	}
	snd_pci_write_config_byte(&scard->pci, 0x42, legacy);
	snd_pci_write_config_byte(&scard->pci, 0x43, legacy_cfg);

#if 00
	if (legacy & 0x02) {
		if (snd_register_ioport(card, snd_mpu_port[dev], 2, "VIA 82C686A - MPU401", NULL) < 0) {
			snd_printk("VIA686A: unable to register MPU-401 port at 0x%x, skipping\n", snd_mpu_port[dev]);
			legacy &= ~0x02;
			snd_pci_write_config_byte(&scard->pci, 0x42, legacy);
			goto __skip_mpu;
		}
		if (snd_mpu401_uart_new(card, 0, MPU401_HW_VIA686A,
					snd_mpu_port[dev],
					-1,
					&midi_uart) < 0) {
			snd_printk("VIA686A: unable to initialize MPU-401 at 0x%x, skipping\n", snd_mpu_port[dev]);
			legacy &= ~0x02;
			snd_pci_write_config_byte(&scard->pci, 0x42, legacy);
		}
	      __skip_mpu:
	}
#endif

	if (snd_mixer_register(mixer, 0) < 0)
		goto __nodev;
	if (snd_pcm_register(pcm, 0) < 0) {
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}
#if 0
	if (snd_pcm_register(pcm_fm, 0) < 0) {
		snd_pcm_unregister(pcm);
		pcm = NULL;
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}
#endif
#if 00
	if (snd_rawmidi_register(midi_uart, 0) < 0) {
		snd_pcm_unregister(pcm_fm);
		pcm_fm = NULL;
		snd_pcm_unregister(pcm);
		pcm = NULL;
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}
#endif

	snd_enable_irq(scard->irqptr);
	
	strcpy(card->abbreviation, "VIA686A");
	strcpy(card->shortname, "VIA 82C686A");
	
	sprintf(card->longname, "%s at 0x%lx, irq %i",
		card->shortname, (long unsigned int)codec->port, scard->irqptr->irq);

	if (!snd_card_register(card)) {
		scard->card = card;
		scard->mixer = mixer;
		scard->pcm = pcm;
		scard->pcm_fm = pcm_fm;
		scard->midi_uart = midi_uart;
		return 0;
	}
	goto __nodev;

      __nodev:
	snd_pci_write_config_byte(&scard->pci, 0x42, legacy);
	snd_pci_write_config_byte(&scard->pci, 0x43, legacy_cfg);
	snd_card_free(card);
	return -ENXIO;
}


static int snd_via686a_card_free(int dev)
{
	via686a_card_t *scard;

	scard = snd_via686a_cards[dev];
	snd_via686a_cards[dev] = NULL;
	if (scard) {
		snd_card_unregister(scard->card);
		snd_free(scard, sizeof(via686a_card_t));
	}
	return 0;
}


int alsa_card_via686a_init(void)
{
	int dev, cards;
	via686a_card_t *scard = NULL;

	for (dev = cards = 0; dev < SND_CARDS; dev++) {
		if( cards > SND_CARDS)
			break;

		if (scard == NULL) {
			scard = (via686a_card_t *)
					snd_malloc(sizeof(via686a_card_t));
			if (scard == NULL)
				continue;
			memset(scard, 0, sizeof(via686a_card_t));
		}
		if (snd_via686a_probe(cards, scard, SND_CARD_TYPE_VIA82C686A, dev) < 0) {
			snd_free(scard, sizeof(via686a_card_t));
			break;
		}
		snd_via686a_cards[dev] = scard;
		cards++;
		scard = NULL;
	}
	if (!cards) {
		snd_printk("VIA 82C686A soundcard #%i not found or device busy\n", dev + 1);
		return -ENODEV;
	}
	return 0;
}

void alsa_card_via686a_cleanup(void)
{
	int dev;

	for (dev = 0; dev < SND_CARDS; dev++)
		snd_via686a_card_free(dev);
}
