/*                                                       -*- linux-c -*-
 *  Driver for Aztech Sound Galaxy cards
 *  Copyright (c) by Christopher Butler <chrisb@sandy.force9.co.uk.
 *
 *  I don't have documentation for this card, I based this driver on the
 *  driver for OSS/Free included in the kernel source (drivers/sound/sgalaxy.c)
 *
 *   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_OSS_COMPAT__
#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "sb.h"
#include "ad1848.h"
#include "initval.h"
#include "sndspinlock.h"

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_sbport[SND_CARDS] = SND_DEFAULT_PORT;	/* 0x220,0x240 */
int snd_wssport[SND_CARDS] = SND_DEFAULT_PORT;	/* 0x530,0xe80,0xf40,0x604 */
int snd_irq[SND_CARDS] = SND_DEFAULT_IRQ;	/* 7,9,10,11 */
int snd_dma1[SND_CARDS] = SND_DEFAULT_DMA;	/* 0,1,3 */
int snd_dma1_size[SND_CARDS] = SND_DEFAULT_DMA_SIZE; /* 8,16,32,64 */

#ifdef MODULE_PARM
MODULE_PARM(snd_index, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_index, "Index value for Sound Galaxy soundcard.");
MODULE_PARM(snd_id, "1-" __MODULE_STRING(SND_CARDS) "s");
MODULE_PARM_DESC(snd_id, "ID string for Sound Galaxy soundcard.");
MODULE_PARM(snd_sbport, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_sbport, "Port # for Sound Galaxy SB driver.");
MODULE_PARM(snd_wssport, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_wssport, "Port # for Sound Galaxy WSS driver.");
MODULE_PARM(snd_irq, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_irq, "IRQ # for Sound Galaxy driver.");
MODULE_PARM(snd_dma1, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_dma1, "DMA1 # for Sound Galaxy driver.");
MODULE_PARM(snd_dma1_size, "1-" __MODULE_STRING(SND_CARDS) "i");
MODULE_PARM_DESC(snd_dma1_size, "DMA1 size in kB for Sound Galaxy driver.");
#endif

#define SGALAXY_AUXC_LEFT 18
#define SGALAXY_AUXC_RIGHT 19

/*

 */

static struct snd_sgalaxy {
	snd_irq_t *irqptr;
	snd_dma_t *dma1ptr;
	snd_card_t *card;
	snd_pcm_t *pcm;
	snd_kmixer_t *mixer;
	unsigned short pcm_status_reg;
} *snd_sgalaxy_cards[SND_CARDS] = SND_DEFAULT_PTR;

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

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

snd_spin_define_static(ad1848_detect);

#define AD1848P1( port, x ) ( port + c_d_c_AD1848##x )

/* copied from ad1848.c */

static void snd_sgalaxy_ad1848_out(unsigned short port, 
				   unsigned char reg,
				   unsigned char value)
{
	int timeout;

#if 0
	snd_printk("snd_sgalaxy_ad1848_out: port = 0x%x, reg = 0x%x, value = 0x%x\n", port, reg, value);
#endif

	for (timeout = 250; timeout > 0 && (inb(AD1848P1(port, REGSEL)) & AD1848_INIT); timeout--)
		snd_delay(1);
	outb(reg, AD1848P1(port, REGSEL));
	outb(value, AD1848P1(port, REG));
	mb();
}

static unsigned char snd_sgalaxy_ad1848_in(unsigned short port, 
					   unsigned char reg)
{
	int timeout;

#if 0
	snd_printk("snd_sgalaxy_ad1848_in: port = 0x%x, reg = 0x%x\n", port, reg);
#endif
	for (timeout = 250; timeout > 0 && (inb(AD1848P1(port, REGSEL)) & AD1848_INIT); timeout--)
		snd_delay(1);
	outb(reg, AD1848P1(port, REGSEL));
	mb();
	return inb(AD1848P1(port, REG));
}

static int snd_sgalaxy_ad1848_detect(snd_card_t * card,
				     unsigned short port)
{
	unsigned long flags;
	int i, id, rev;

	if (snd_register_ioport(card, port, 4, "Sound Galaxy - AD1848", NULL) < 0)
		return -EBUSY;

	id = 0;
	for (i = 0; i < 100; i++) {
		mb();
		if (inb(AD1848P1(port, REGSEL)) & AD1848_INIT)
			snd_delay(1);
		else {
			snd_spin_lock_static(ad1848_detect, &flags);
			snd_sgalaxy_ad1848_out(port, AD1848_MISC_INFO, 0x00);
			snd_sgalaxy_ad1848_out(port, AD1848_LEFT_INPUT, 0xaa);
			snd_sgalaxy_ad1848_out(port, AD1848_RIGHT_INPUT, 0x45);
			rev = snd_sgalaxy_ad1848_in(port, AD1848_RIGHT_INPUT);
			if (rev == 0x65) {
				id = 1;
				break;
			}
			if (snd_sgalaxy_ad1848_in(port, AD1848_LEFT_INPUT) == 0xaa && rev == 0x45) {
				id = 1;
				break;
			}
			snd_spin_unlock_static(ad1848_detect, &flags);
		}
	}
	snd_spin_unlock_static(ad1848_detect, &flags);

	if (id != 1) {
		snd_unregister_ioports(card);
		return -ENODEV;	/* no valid device found */
	}

	return 0;
}

/* from lowlevel/sb/sb.c - to avoid having to allocate a sbdsp_t for the */
/* short time we actually need it..                                      */

static int snd_sgalaxy_sbdsp_reset(unsigned short port)
{
	int i;

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

static int snd_sgalaxy_sbdsp_command(unsigned short port, unsigned char val)
{
	int i;
       	
	for (i = 10000; i; i--)
		if ((inb(SBP1(port, STATUS)) & 0x80) == 0) {
			outb(val, SBP1(port, COMMAND));
			return 1;
		}

	return 0;
}

snd_spin_define_static(wss_setup);

static int snd_sgalaxy_setup_wss(unsigned short port,
				 snd_irq_t *irqptr,
				 snd_dma_t *dma1ptr)
{
	static int interrupt_bits[] = {-1, -1, -1, -1, -1, -1, -1, 0x08, -1, 
				       0x10, 0x18, 0x20, -1, -1, -1, -1};
	static int dma_bits[] = {1, 2, 0, 3};
	int tmp, tmp1;
	unsigned long flags;

	if ((tmp = inb(port + 3)) == 0xff)
	{
		snd_printdd("I/O address dead (%x)\n", tmp);
		return 0;
	}
#if 0
	snd_printdd("WSS signature = 0x%x\n", tmp);
#endif

        if ((tmp & 0x3f) != 0x04 &&
            (tmp & 0x3f) != 0x0f &&
            (tmp & 0x3f) != 0x00) {
		snd_printdd("No WSS signature detected on port 0x%x\n",
			    port + 3);
		return 0;
	}

#if 0
	snd_printdd("sgalaxy - setting up IRQ/DMA for WSS\n");
#endif

        /* initialize IRQ for WSS codec */
        tmp = interrupt_bits[irqptr->irq];
        if (tmp < 0)
                return -EINVAL;
	snd_spin_lock_static(wss_setup, &flags);
        outb(tmp | 0x40, port);
        tmp1 = dma_bits[dma1ptr->dma];
        outb(tmp | tmp1, port);
	snd_spin_unlock_static(wss_setup, &flags);
	return 0;
}

static int snd_sgalaxy_detect(int dev, struct snd_sgalaxy * acard, 
			      snd_card_t *card)
{
	/* try detect AD1848 chip
	if (snd_sgalaxy_ad1848_detect(card, snd_wssport[dev] + 4))
		return snd_sgalaxy_setup_wss(snd_wssport[dev], 
					     acard->irqptr, acard->dma1ptr); 
	* WSS mode already active */

#if 0
	snd_printdd("sgalaxy - switching to WSS mode\n");
#endif

	/* switch to WSS mode */
	snd_sgalaxy_sbdsp_reset(snd_sbport[dev]);

	snd_sgalaxy_sbdsp_command(snd_sbport[dev], 9);
	snd_sgalaxy_sbdsp_command(snd_sbport[dev], 0);

	snd_delay(HZ/10);
	return snd_sgalaxy_setup_wss(snd_wssport[dev], 
				     acard->irqptr, acard->dma1ptr); 
}

static void snd_sgalaxy_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	int loop;
	unsigned char status;
	struct snd_sgalaxy *acard;

	acard = (struct snd_sgalaxy *)dev_id;
	if (acard == NULL)
		return;

	do {
		loop = 0;
		if ((status = inb(acard->pcm_status_reg)) & 0x01) {
			snd_ad1848_interrupt(acard->pcm, status);
			loop++;
		}
	} while (loop);
}

static int snd_sgalaxy_resources(int dev, struct snd_sgalaxy *acard, 
				 snd_card_t *card)
{
	static int possible_irqs[] = {7, 9, 10, 11, -1};
	static int possible_dmas[] = {1, 3, 0, -1};
	int err;

#if 0
	snd_printdd("sgalaxy_resources\n");
#endif

	if ((err = snd_register_interrupt(card, "Sound Galaxy",
				snd_irq[dev], SND_IRQ_TYPE_ISA,
				snd_sgalaxy_interrupt, acard,
				possible_irqs, &acard->irqptr)) < 0)
		return err;
	if ((err = snd_register_dma_channel(card, "Sound Galaxy",
			snd_dma1[dev], SND_DMA_TYPE_ISA,
			snd_dma1_size[dev], possible_dmas, &acard->dma1ptr)) < 0)
		return err;
#if 0
	snd_printdd("sgalaxy_resources - return\n");
#endif
	return 0;
}

static int snd_sgalaxy_aux2_switch(int w_flag, unsigned int *bitmap, ad1848_t *codec)
{
	return snd_ad1848_mixer_stereo_switch(w_flag, bitmap, codec,
					      7, 1,
					      SGALAXY_AUXC_LEFT,
					      SGALAXY_AUXC_RIGHT);
}

static int snd_sgalaxy_aux2_volume(int w_flag, int *voices, ad1848_t *codec)
{
	return snd_ad1848_mixer_stereo_volume(w_flag, voices, codec,
					      31, 1, 0,
					      SGALAXY_AUXC_LEFT,
					      SGALAXY_AUXC_RIGHT);
}

static int snd_sgalaxy_mixer(snd_kmixer_t * mixer, ad1848_t * codec)
{
	snd_kmixer_group_t *group;
	snd_kmixer_element_t *element1, *element2, *element3, *accu;
	static struct snd_mixer_element_volume1_range aux_range[2] = {
		{0, 31, -3450, 1200},
		{0, 31, -3450, 1200}
	};

	/* reassign AUX0 to LINE */
	if (snd_mixer_group_rename(mixer,
				SND_MIXER_IN_AUX, 0,
				SND_MIXER_IN_LINE, 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				SND_MIXER_IN_AUX, 0, SND_MIXER_ETYPE_INPUT,
				SND_MIXER_IN_LINE, 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				"Aux Input Volume", 0, SND_MIXER_ETYPE_VOLUME1,
				"Line Input Volume", 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				"Aux Input Switch", 0, SND_MIXER_ETYPE_SWITCH1,
				"Line Input Switch", 0) < 0)
		goto __error;
	/* reassign AUX1 to FM */
	if (snd_mixer_group_rename(mixer,
				SND_MIXER_IN_AUX, 1,
				SND_MIXER_IN_FM, 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				SND_MIXER_IN_AUX, 1, SND_MIXER_ETYPE_INPUT,
				SND_MIXER_IN_FM, 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				"Aux Input Volume", 1, SND_MIXER_ETYPE_VOLUME1,
				"FM Input Volume", 0) < 0)
		goto __error;
	if (snd_mixer_element_rename(mixer,
				"Aux Input Switch", 1, SND_MIXER_ETYPE_SWITCH1,
				"FM Input Switch", 0) < 0)
		goto __error;
	/* build AUX2 input */
	if ((accu = snd_mixer_element_find(mixer, SND_MIXER_ELEMENT_OUTPUT_ACCU, 0, SND_MIXER_ETYPE_ACCU1)) == NULL)
		goto __error;
	if ((group = snd_mixer_lib_group(mixer, SND_MIXER_IN_AUX, 2)) == NULL)
		goto __error;
	if ((element1 = snd_mixer_lib_io_stereo(mixer, SND_MIXER_IN_AUX, 2, SND_MIXER_ETYPE_INPUT, 0)) == NULL)
		goto __error;
	if ((element2 = snd_mixer_lib_volume1(mixer, "Aux Input Volume", 2, 2, aux_range, (snd_mixer_volume1_control_t *)snd_sgalaxy_aux2_volume, codec)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, element2) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element1, element2) < 0)
		goto __error;
	if ((element3 = snd_mixer_lib_sw1(mixer, "Aux Input Switch", 2, 2, (snd_mixer_sw1_control_t *)snd_sgalaxy_aux2_switch, codec)) == NULL)
		goto __error;
	if (snd_mixer_group_element_add(mixer, group, element3) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element2, element3) < 0)
		goto __error;
	if (snd_mixer_element_route_add(mixer, element3, accu) < 0)
		goto __error;
	return 0;

      __error:
      	return -ENOMEM;
}

static int snd_sgalaxy_probe(int dev, struct snd_sgalaxy *acard)
{
	int err;
	snd_card_t *card;
	snd_pcm_t *pcm = NULL;
	snd_kmixer_t *mixer = NULL;
	ad1848_t *codec;

	card = snd_card_new(snd_index[dev], snd_id[dev],
			    snd_sgalaxy_use_inc, snd_sgalaxy_use_dec);
	if (!card)
		return -ENOMEM;
	card->type = SND_CARD_TYPE_SGALAXY;
	if (snd_sgalaxy_resources(dev, acard, card) < 0) {
		snd_card_free(card);
		return -EBUSY;
	}
	if ((err = snd_sgalaxy_detect(dev, acard, card)) < 0) {
#if 0
		snd_printdd("sgalaxy - error detecting sound galaxy card\n");
#endif
		snd_card_free(card);
		return err;
	}

#if 0
	snd_printdd("snd_sgalaxy_detect - successful\n");
#endif

	acard->pcm_status_reg = snd_wssport[dev] + 4 + 2;
	pcm = snd_ad1848_new_device(card, snd_wssport[dev] + 4,
				    acard->irqptr, acard->dma1ptr,
				    AD1848_HW_DETECT);
	if (!pcm) {
#if 0
		snd_printdd("sgalaxy - error creating new ad1848 device\n");
#endif
		goto __nodev;
	}

#if 0
	snd_printdd("sgalaxy - creating mixer device\n");
#endif
	mixer = snd_ad1848_new_mixer(pcm, 0);
	if (!mixer) {
		snd_printdd("sgalaxy - error creating new ad1848 mixer\n");
		goto __nodev;
	}
#if 0
	snd_printdd("sgalaxy - created mixer device\n");
	snd_printdd("sgalaxy - changing mixer settings\n");
#endif
	codec = (ad1848_t *)(((snd_pcm1_t *)pcm->private_data)->private_data);
	if (snd_sgalaxy_mixer(mixer, codec) < 0) {
		snd_printdd("sgalaxy - the mixer rewrite failed\n");
		goto __nodev;
	}

#if 0
	snd_printdd("sgalaxy - registering mixer device\n");
#endif
	if (snd_mixer_register(mixer, 0) < 0)
		goto __nodev;
#if 0
	snd_printdd("sgalaxy - registering pcm device\n");
#endif
	if (snd_pcm_register(pcm, 0) < 0) {
		snd_mixer_unregister(mixer);
		mixer = NULL;
		goto __nodev;
	}

#if 0
	snd_printdd("sgalaxy - initialising device names\n");
#endif
	strcpy(card->abbreviation, "Sound Galaxy");
	strcpy(card->shortname, "Sound Galaxy");
	sprintf(card->longname, "Sound Galaxy at 0x%x, irq %i, dma %i",
		snd_wssport[dev],
		acard->irqptr->irq,
		acard->dma1ptr->dma);

#if 0
	snd_printdd("sgalaxy - registering card\n");
#endif
	if (!snd_card_register(card)) {
		acard->card = card;
		acard->pcm = pcm;
		acard->mixer = mixer;
		return 0;
	}

	snd_pcm_unregister(pcm);
	pcm = NULL;
	if (mixer)
		snd_mixer_unregister(mixer);
	mixer = NULL;
	snd_card_free(card);
	return -ENOMEM;

      __nodev:
	if (pcm)
		snd_pcm_free(pcm);
	snd_card_free(card);
	return -ENXIO;
}

int init_module(void)
{
	int dev, cards;
	struct snd_sgalaxy *acard;

	for (dev = cards = 0; dev < SND_CARDS && snd_sbport[dev] > 0; dev++) {
		acard = (struct snd_sgalaxy *)snd_malloc(sizeof(struct snd_sgalaxy));
		if (!acard)
			continue;
		memset(acard, 0, sizeof(struct snd_sgalaxy));
		if (snd_sgalaxy_probe(dev, acard) < 0) {
			snd_free(acard, sizeof(struct snd_sgalaxy));
			if (snd_wssport[dev] == SND_AUTO_PORT)
				break;
			snd_printk("Sound Galaxy soundcard #%i not found at 0x%x or device busy\n", dev + 1, snd_wssport[dev]);
			continue;
		}
		snd_sgalaxy_cards[dev] = acard;
		cards++;
	}

	if (!cards) {
		snd_printk("Sound Galaxy soundcard not found or device busy\n");
		return -ENODEV;
	}

#if 0
	snd_printk("pointer listing - snd-sgalaxy.o\n");
	for (dev = 0; dev < SND_CARDS; dev++) {
		if (snd_sgalaxy_cards[dev] == NULL) continue;
		snd_printk("snd_sgalaxy_cards[%i] = 0x%p\n", dev, snd_sgalaxy_cards[dev]);
		snd_printk("snd_sgalaxy_cards[%i]->irqptr = 0x%p\n", dev, snd_sgalaxy_cards[dev]->irqptr);
		snd_printk("snd_sgalaxy_cards[%i]->dma1ptr = 0x%p\n", dev, snd_sgalaxy_cards[dev]->dma1ptr);
		snd_printk("snd_sgalaxy_cards[%i]->card = 0x%p\n", dev, snd_sgalaxy_cards[dev]->card);
		snd_printk("snd_sgalaxy_cards[%i]->pcm = 0x%p\n", dev, snd_sgalaxy_cards[dev]->pcm);
		snd_printk("snd_sgalaxy_cards[%i]->mixer = 0x%p\n", dev, snd_sgalaxy_cards[dev]->mixer);
	}
#endif
	return 0;
}

void cleanup_module(void)
{
	int idx;
	struct snd_sgalaxy *acard;

	for (idx = 0; idx < SND_CARDS; idx++) {
		acard = snd_sgalaxy_cards[idx];
		if (acard) {
			if (acard->card)
				snd_card_unregister(acard->card);
			if (acard->mixer)
				snd_mixer_unregister(acard->mixer);
			if (acard->pcm)
				snd_pcm_unregister(acard->pcm);
			if (acard->card)
				snd_card_free(acard->card);
			snd_free(acard, sizeof(struct snd_sgalaxy));
		}
	}
}
