/*
 *  Driver for S3 SonicVibes soundcard
 *  Copyright (c) by 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.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "sonicvibes.h"
#include "opl3.h"
#include "initval.h"

#ifndef PCI_VENDOR_ID_S3
#define PCI_VENDOR_ID_S3             0x5333
#endif
#ifndef PCI_DEVICE_ID_S3_SONICVIBES
#define PCI_DEVICE_ID_S3_SONICVIBES  0xca00
#endif

int snd_index[SND_CARDS] = SND_DEFAULT_IDX;	/* Index 1-MAX */
char *snd_id[SND_CARDS] = SND_DEFAULT_STR;	/* ID for this card */
int snd_dmaa_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_dmac_size[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 128};
int snd_reverb[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 0};
int snd_mge[SND_CARDS] = {[0 ... (SND_CARDS - 1)] = 0};
unsigned int snd_dmaio = 0x7a00;	/* DDMA i/o address */
#ifdef MODULE_PARM
MODULE_PARM(snd_index, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_index, "Index value for S3 SonicVibes soundcard.");
MODULE_PARM(snd_id, "1-" __MODULE_STRING(SND_CARDS) "s");
MODULE_PARM_DESC(snd_id, "ID string for S3 SonicVibes soundcard.");
MODULE_PARM(snd_dmaa_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_dmaa_size, "DDMA-A size in kB for S3 SonicVibes soundcard.");
MODULE_PARM(snd_dmac_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_dmac_size, "DDMA-C size in kB for S3 SonicVibes soundcard.");
MODULE_PARM(snd_reverb, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_reverb, "Enable reverb (SRAM is present) for S3 SonicVibes soundcard.");
MODULE_PARM(snd_mge, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_mge, "MIC Gain Enable for S3 SonicVibes soundcard.");
MODULE_PARM(snd_dmaio, "i");
MODULE_PARM_DESC(snd_dmaio, "DDMA i/o base address for S3 SonicVibes soundcard.");
#endif

struct snd_sonic {
	struct snd_pci_dev pci;
	snd_irq_t *irqptr;
	snd_dma_t *dma1ptr;
	snd_dma_t *dma2ptr;
	snd_card_t *card;
	sonicvibes_t *sonic;
	snd_pcm_t *pcm;
	snd_kmixer_t *mixer;
	snd_rawmidi_t *midi_uart;
	snd_synth_t *synth;
};

static struct snd_sonic *snd_sonic_cards[SND_CARDS] = SND_DEFAULT_PTR;

static void snd_sonic_use_inc(snd_card_t * card)
{
	MOD_INC_USE_COUNT;
}

static void snd_sonic_use_dec(snd_card_t * card)
{
	MOD_DEC_USE_COUNT;
}

static int snd_sonic_detect(snd_card_t * card,
			    struct snd_sonic *scard,
			    int pci_index)
{
	int idx;
	unsigned int dmaa, dmac;

	if (snd_pci_find_device(PCI_VENDOR_ID_S3,
	                        PCI_DEVICE_ID_S3_SONICVIBES,
	                        pci_index, &scard->pci) < 0)
		return -ENODEV;
	for (idx = 0; idx < 5; idx++) {
		if (scard->pci.base_address[idx] == 0 ||
		    (scard->pci.base_address[idx] & PCI_BASE_ADDRESS_SPACE) != PCI_BASE_ADDRESS_SPACE_IO)
			return -ENODEV;
	}
	if (snd_register_ioport(card, scard->pci.base_address[1] & ~PCI_BASE_ADDRESS_SPACE, 0x10, "S3 SonicVibes PCM", NULL) < 0)
		goto __nodev;
	if (snd_register_ioport(card, scard->pci.base_address[2] & ~PCI_BASE_ADDRESS_SPACE, 4, "S3 SonicVibes Synth", NULL) < 0)
		goto __nodev;
	if (snd_register_ioport(card, scard->pci.base_address[3] & ~PCI_BASE_ADDRESS_SPACE, 4, "S3 SonicVibes Midi", NULL) < 0)
		goto __nodev;
	snd_pci_read_config_dword(&scard->pci, 0x40, &dmaa);
	snd_pci_read_config_dword(&scard->pci, 0x48, &dmac);
	snd_dmaio &= ~0x0f;
	dmaa &= ~0x0f;
	dmac &= ~0x0f;
	if (!dmaa) {
		dmaa = snd_dmaio;
		snd_dmaio += 0x10;
		snd_printk("SV: BIOS didn't allocate DDMA channel A i/o, allocated at 0x%x\n", dmaa);
	}
	if (!dmac) {
		dmac = snd_dmaio;
		snd_dmaio += 0x10;
		snd_printk("SV: BIOS didn't allocate DDMA channel C i/o, allocated at 0x%x\n", dmac);
	}
	snd_pci_write_config_dword(&scard->pci, 0x40, dmaa);
	snd_pci_write_config_dword(&scard->pci, 0x48, dmac);
	if (snd_register_ioport(card, dmaa, 0x10, "S3 SonicVibes DDMA-A", NULL) < 0)
		goto __nodev;
	if (snd_register_ioport(card, dmac, 0x10, "S3 SonicVibes DDMA-C", NULL) < 0)
		goto __nodev;
	return 0;
      __nodev:
	snd_unregister_ioports(card);
	return -ENODEV;
}

static void snd_sonic_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	struct snd_sonic *scard = (struct snd_sonic *) dev_id;

	if (!scard || !scard->sonic)
		return;
	snd_sonicvibes_interrupt(scard->sonic);
}

static int snd_sonic_resources(snd_card_t * card,
			       struct snd_sonic *scard, int dev)
{
	int err;

	if ((err = snd_register_interrupt(card,
			"S3 SonicVibes", scard->pci.irq,			
			SND_IRQ_TYPE_PCI, snd_sonic_interrupt,
			scard, NULL, &scard->irqptr)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"S3 SonicVibes DDMA-A", 0,
			SND_DMA_TYPE_PCI_16MB, snd_dmaa_size[dev],
			NULL, &scard->dma1ptr)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card,
			"S3 SonicVibes DDMA-C", 1,
			SND_DMA_TYPE_PCI_16MB, snd_dmac_size[dev],
			NULL, &scard->dma2ptr)) < 0)
		return err;
	return 0;
}

static int snd_sonic_probe(int dev, struct snd_sonic *scard)
{
	snd_card_t *card;
	snd_pcm_t *pcm = NULL;
	snd_kmixer_t *mixer = NULL;
	snd_rawmidi_t *midi_uart = NULL;
	snd_synth_t *synth = NULL;
	int pci_index;

	card = snd_card_new(snd_index[dev], snd_id[dev],
			    snd_sonic_use_inc, snd_sonic_use_dec);
	if (!card)
		return -ENOMEM;
	card->type = SND_CARD_TYPE_S3_SONICVIBES;
	card->static_data = scard;
	for (pci_index = 0; pci_index < SND_CARDS; pci_index++) {
		if (!snd_sonic_detect(card, scard, pci_index))
			break;
	}
	if (pci_index >= SND_CARDS) {
		snd_card_free(card);
		return -ENODEV;
	}
	if (snd_sonic_resources(card, scard, dev) < 0) {
		snd_card_free(card);
		return -ENODEV;
	}
	scard->sonic = snd_sonicvibes_create(card, &scard->pci,
					     scard->dma1ptr, scard->dma2ptr,
					     scard->irqptr,
					     snd_reverb[dev] ? 1 : 0,
					     snd_mge[dev] ? 1 : 0);
	if (!scard->sonic)
		goto __nodev;
	mixer = snd_sonicvibes_mixer(scard->sonic);
	if (!mixer)
		goto __nodev;
	pcm = snd_sonicvibes_pcm(scard->sonic);
	if (!pcm)
		goto __nodev;
	midi_uart = snd_mpu401_uart_new_device(card, MPU401_HW_SONICVIBES,
					       scard->sonic->midi_port,
					       scard->irqptr->irq);
	if (!midi_uart)
		goto __nodev;
	snd_sonicvibes_midi(scard->sonic, (mpu401_t *) midi_uart->private_data);
	synth = snd_opl3_new_device(card, scard->sonic->synth_port,
				    scard->sonic->synth_port + 2,
				    OPL3_HW_OPL3_SV, -1);
	if (!synth)
		goto __nodev;
	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 (snd_rawmidi_register(midi_uart, 0) < 0) {
		snd_pcm_unregister(pcm);
		pcm = NULL;
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}
	if (snd_synth_register(synth) < 0) {
		snd_rawmidi_unregister(midi_uart);
		midi_uart = NULL;
		snd_pcm_unregister(pcm);
		pcm = NULL;
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}
	strcpy(card->abbreviation, "SonicVibes");
	strcpy(card->shortname, "S3 SonicVibes");
	sprintf(card->longname, "%s rev %i at 0x%lx, irq %i",
		card->shortname,
		scard->sonic->revision,
		scard->pci.base_address[1] & ~PCI_BASE_ADDRESS_SPACE,
		scard->irqptr->irq);

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

      __nodev:
	if (synth)
		snd_synth_free(synth);
	if (midi_uart)
		snd_rawmidi_free(midi_uart);
	if (mixer)
		snd_mixer_free(mixer);
	if (pcm)
		snd_pcm_free(pcm);
	if (scard->sonic) {
		snd_sonicvibes_free(scard->sonic);
		scard->sonic = NULL;
	}
	snd_card_free(card);
	return -ENXIO;
}

static int snd_sonic_free(int dev)
{
	struct snd_sonic *scard;
	snd_pcm_t *pcm;

	scard = snd_sonic_cards[dev];
	snd_sonic_cards[dev] = NULL;
	if (scard) {
		snd_card_unregister(scard->card);
		if (scard->synth)
			snd_synth_unregister(scard->synth);
		if (scard->midi_uart) {
			scard->sonic->rmidi = NULL;
			snd_rawmidi_unregister(scard->midi_uart);
		}
		if (scard->mixer)
			snd_mixer_unregister(scard->mixer);
		if (scard->pcm) {
			pcm = scard->pcm;
			scard->pcm = NULL;	/* turn off interrupts */
			snd_pcm_unregister(pcm);
		}
		if (scard->sonic) {
			snd_sonicvibes_free(scard->sonic);
			scard->sonic = NULL;
		}
		snd_card_free(scard->card);
		snd_free(scard, sizeof(struct snd_sonic));
	}
	return 0;
}

int init_module(void)
{
	int dev, cards;
	struct snd_sonic *scard;

#ifndef LINUX_2_1
	register_symtab(NULL);
#endif
	for (dev = cards = 0; dev < SND_CARDS; dev++) {
		scard = (struct snd_sonic *)
				snd_malloc(sizeof(struct snd_sonic));
		if (!scard)
			continue;
		memset(scard, 0, sizeof(struct snd_sonic));
		if (snd_sonic_probe(dev, scard) < 0) {
			snd_free(scard, sizeof(struct snd_sonic));
			break;
		}
		snd_sonic_cards[dev] = scard;
		cards++;
	}
	if (!cards) {
		snd_printk("S3 SonicVibes soundcard #%i not found or device busy\n", dev + 1);
		return -ENODEV;
	}
	return 0;
}

void cleanup_module(void)
{
	int dev;

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