/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of SoundBlaster cards
 *
 *
 *   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 "sb.h"

extern struct snd_stru_pcm1_hardware snd_sb16_playback;
extern struct snd_stru_pcm1_hardware snd_sb16_record;

int snd_sb16dsp_command(sbdsp_t * codec, unsigned char val)
{
	int i;

	for (i = 10000; i; i--)
		if ((inb(SBP(codec, STATUS)) & 0x80) == 0) {
			outb(val, SBP(codec, COMMAND));
			return 1;
		}
	snd_printd("snd_sb16dsp_command: timeout (0x%x)\n", val);
	return 0;
}

int snd_sb16dsp_get_byte(sbdsp_t * codec)
{
	int i;

	for (i = 1000; i; i--)
		if (inb(SBP(codec, DATA_AVAIL)) & 0x80)
			return inb(SBP(codec, READ));
	snd_printd("snd_sb16dsp_get_byte failed: 0x%x = 0x%x!!!\n", SBP(codec, DATA_AVAIL), inb(SBP(codec, DATA_AVAIL)));
	return -ENODEV;
}

int snd_sb16dsp_reset(sbdsp_t * codec)
{
	int i;

	outb(1, SBP(codec, RESET));
	snd_delay(1);
	outb(0, SBP(codec, RESET));
	snd_delay(3);
	for (i = 0; i < 1000 && !(inb(SBP(codec, DATA_AVAIL)) & 0x80); i++);
	if (inb(SBP(codec, READ)) != 0xaa) {
		snd_printd("sb_reset: failed at 0x%x!!!\n", codec->port);
		return -ENODEV;
	}
	return 0;
}

int snd_sb16dsp_version(snd_pcm_t * pcm)
{
	unsigned long flags;
	snd_pcm1_t *pcm1;
	sbdsp_t *codec;
	int i;
	unsigned int result = -1;

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
	snd_spin_lock(codec, reg, &flags);
	snd_sb16dsp_command(codec, SB_DSP_GET_VERSION);
	for (i = 100000; i; i--) {
		if (inb(SBP(codec, DATA_AVAIL)) & 0x80) {
			result = (short) inb(SBP(codec, READ)) << 8;
			break;
		}
	}
	for (i = 100000; i; i--) {
		if (inb(SBP(codec, DATA_AVAIL)) & 0x80) {
			result |= (short) inb(SBP(codec, READ));
			break;
		}
	}
	snd_spin_unlock(codec, reg, &flags);
	return result;
}

int snd_sb16dsp_probe(snd_pcm_t * pcm)
{
	unsigned long flags;
	snd_pcm1_t *pcm1;
	sbdsp_t *codec;
	int version, hw;
	char *str;

	/*
	 *  initialization sequence
	 */

	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;

	snd_spin_lock(codec, reg, &flags);
	if (snd_sb16dsp_reset(codec) < 0) {
		snd_printdd("SB: [0x%x] reset failed... 0x%x\n", codec->port, inb(SBP(codec, READ)));
		snd_spin_unlock(codec, reg, &flags);
		return -ENODEV;
	}
	snd_spin_unlock(codec, reg, &flags);

	version = snd_sb16dsp_version(pcm);
	if (version < 0)
		return -ENODEV;

	snd_printdd("SB [0x%x]: DSP chip found, version = %i.%i\n", codec->port, version >> 8, version & 0xff);

	hw = SB_HW_AUTO;
	switch (version >> 8) {
	case 1:
	case 2:
	case 3:
		snd_printk("SB [0x%x]: DSP chip version %i.%i is not supported with the SB16 code\n", codec->port, version >> 8, version & 0xff);
		return -ENODEV;
	case 4:
		hw = SB_HW_16;
		break;
	default:
		snd_printk("SB [0x%x]: uknown DSP chip version %i.%i\n", codec->port, version >> 8, version & 0xff);
		return -ENODEV;
	}
	switch (codec->hardware = hw) {
	case SB_HW_16:
		str = "Sound Blaster 16";
		break;
	default:
		str = "???";
	}
	strcpy(codec->name, str);
	sprintf(pcm->name, "DSP v%i.%i", version >> 8, version & 0xff);

	return 0;
}

int snd_sb16dsp_configure(snd_pcm_t * pcm)
{
	snd_pcm1_t *pcm1;
	sbdsp_t *codec;
	unsigned long flags, flags1;
	unsigned char irqreg = 0, dmareg = 0, mpureg, realirq, realdma,
	 realmpureg;

	/* note: mpu register should be present only on SB16 Vibra soundcards */
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) pcm1->private_data;
#if 0
	printk("codec -> irq = %i, codec -> dma8 = %i, codec -> dma16 = %i\n", codec->irq, codec->dma8, codec->dma16);
#endif
	snd_spin_lock(codec, reg, &flags);
	snd_spin_lock(&codec->mixer, mixer, &flags1);
	mpureg = snd_sb16mixer_read(&codec->mixer, 0x84) & ~0x06;
	snd_spin_unlock(&codec->mixer, mixer, &flags1);
	snd_spin_unlock(codec, reg, &flags);
	switch (codec->irq) {
	case 2:
	case 9:
		irqreg |= 0x01;
		break;
	case 5:
		irqreg |= 0x02;
		break;
	case 7:
		irqreg |= 0x04;
		break;
	case 10:
		irqreg |= 0x08;
		break;
	default:
		return -EINVAL;
	}
	if (codec->dma8ptr) {
		switch (codec->dma8) {
		case 0:
			dmareg |= 0x01;
			break;
		case 1:
			dmareg |= 0x02;
			break;
		case 3:
			dmareg |= 0x08;
			break;
		default:
			return -EINVAL;
		}
	}
	if (codec->dma16ptr) {
		switch (codec->dma16) {
		case 5:
			dmareg |= 0x20;
			break;
		case 6:
			dmareg |= 0x40;
			break;
		case 7:
			dmareg |= 0x80;
			break;
		default:
			return -EINVAL;
		}
	}
	switch (codec->mpu_port) {
	case 0x300:
		mpureg |= 0x04;
		break;
	case 0x330:
		mpureg |= 0x00;
		break;
	default:
		mpureg |= 0x02;	/* disable MPU */
	}
	snd_spin_lock(codec, reg, &flags);
	snd_spin_lock(&codec->mixer, mixer, &flags1);
	snd_sb16mixer_write(&codec->mixer, 0x80, irqreg);
	realirq = snd_sb16mixer_read(&codec->mixer, 0x80);
	snd_sb16mixer_write(&codec->mixer, 0x81, dmareg);
	realdma = snd_sb16mixer_read(&codec->mixer, 0x81);
	snd_sb16mixer_write(&codec->mixer, 0x84, mpureg);
	realmpureg = snd_sb16mixer_read(&codec->mixer, 0x84);
	snd_spin_unlock(&codec->mixer, mixer, &flags1);
	snd_spin_unlock(codec, reg, &flags);
	if ((~realirq) & irqreg || (~realdma) & dmareg) {
		snd_printdd("SB16 [0x%x]: unable to set DMA & IRQ (PnP device?)\n", codec->port);
		snd_printdd("SB16 [0x%x]: irqreg=0x%x, dmareg=0x%x, mpureg = 0x%x\n", codec->port, realirq, realdma, realmpureg);
		snd_printdd("SB16 [0x%x]: new: irqreg=0x%x, dmareg=0x%x, mpureg = 0x%x\n", codec->port, irqreg, dmareg, mpureg);
#if 0		/* this test doesn't work for all SB16 PnP soundcards */
		return -ENODEV;
#endif
	}
	return 0;
}

void snd_sb16dsp_free(void *private_data)
{
	sbdsp_t *codec;

	codec = (sbdsp_t *) private_data;
	snd_sb16dsp_proc_done(codec->pcm);
	snd_free(codec, sizeof(sbdsp_t));
}

snd_pcm_t *snd_sb16dsp_new_device(snd_card_t * card,
				  unsigned short port,
				  snd_irq_t * irqptr,
				  snd_dma_t * dma8ptr,
				  snd_dma_t * dma16ptr,
				  unsigned short hardware)
{
	snd_pcm_t *pcm;
	snd_pcm1_t *pcm1;
	sbdsp_t *codec;

	pcm = snd_pcm1_new_device(card, "SB16 DSP");
	if (!pcm)
		return NULL;
	pcm1 = (snd_pcm1_t *) pcm->private_data;
	codec = (sbdsp_t *) snd_calloc(sizeof(sbdsp_t));
	if (!codec)
		return NULL;
	snd_spin_prepare(codec, reg);
	snd_spin_prepare(codec, open8);
	snd_spin_prepare(codec, open16);
	snd_spin_prepare(codec, midi_input);
	snd_spin_prepare(&codec->mixer, mixer);
	codec->pcm = pcm;
	codec->card = pcm->card;
	codec->port = port;
	codec->mixer.port = port;
	codec->irqptr = irqptr;
	codec->irq = irqptr->irq;
	codec->dma8ptr = dma8ptr;
	if (dma8ptr)
		codec->dma8 = dma8ptr->dma;
	codec->dma16ptr = dma16ptr;
	if (dma16ptr)
		codec->dma16 = dma16ptr->dma;
	codec->hardware = hardware;
	pcm1->private_data = codec;
	pcm1->private_free = snd_sb16dsp_free;
	strcpy(pcm->name, "Sound Blaster");
	if (snd_sb16dsp_probe(pcm) < 0) {
		snd_pcm_free(pcm);
		return NULL;
	}
	pcm->info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
	  		  SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD |
	  		  SND_PCM_INFO_DUPLEX | SND_PCM_INFO_DUPLEX_LIMIT;
	codec->force_mode16 = SB_MODE16_AUTO;
	memcpy(&pcm1->playback.hw, &snd_sb16_playback, sizeof(snd_sb16_playback));
	memcpy(&pcm1->record.hw, &snd_sb16_record, sizeof(snd_sb16_record));
	snd_sb16dsp_proc_init(pcm);
	return pcm;
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_sb16_export;
#endif

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

void cleanup_module(void)
{
}
