/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *     and (c) 1999 Steve Ratcliffe <steve@parabola.demon.co.uk>
 *
 *  Routines for control of EMU8000 chip
 *
 *   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.
 *
 * Changes
 * 19990306   Takashi Iwai	use all 30 channels for sample loading
 */

#define SND_MAIN_OBJECT_FILE
#include "../../include/driver.h"
#include "../../include/emu8000.h"
#include "../../include/emu8000_reg.h"
#include "../../include/soundfont.h"
#include "../../include/info.h"
#include "emu8000_port.h"
#include "emu8000_voice.h"
#include "../../include/seq_device.h"

static int snd_emu8000_detect(emu8000_t * emu);
static void snd_emu8000_free(void *private_data);

/*
 * The following routines read and write registers on the emu8000.  They
 * should always be called via the EMU8000*READ/WRITE macros and never
 * directly.  The macros handle the port number and command word.
 */
/* Write a word */
void snd_emu8000_poke(emu8000_t *emu, unsigned int port, unsigned int reg, unsigned int val)
{
	//printk("poke %x, reg %d,%d --> %x\n", port, reg>>5, reg&0x1f, val);
	if (reg != emu->last_reg) {
		outw((unsigned short)reg, EMU8000_PTR(emu)); /* Set register */
		emu->last_reg = reg;
	}
	outw((unsigned short)val, port); /* Send data */
}

/* Read a word */
unsigned short snd_emu8000_peek(emu8000_t *emu, unsigned int port, unsigned int reg)
{
	unsigned short res;

	//outw((unsigned short)reg, EMU8000_PTR(emu)); /* Set register */
	if (reg != emu->last_reg) {
		outw((unsigned short)reg, EMU8000_PTR(emu)); /* Set register */
		emu->last_reg = reg;
	}
	res = inw(port);	/* Read data */
	//printk("peek %x, reg %d,%d --> %x\n", port, reg>>5, reg&0x1f, res);
	return res;
}

/* Write a double word */
void snd_emu8000_poke_dw(emu8000_t *emu, unsigned int port, unsigned int reg, unsigned int val)
{
	//printk("pokedw %x, reg %d,%d --> %x\n", port, reg>>5, reg&0x1f, val);
	if (reg != emu->last_reg) {
		outw((unsigned short)reg, EMU8000_PTR(emu)); /* Set register */
		emu->last_reg = reg;
	}
	outw((unsigned short)val, port); /* Send low word of data */
	outw((unsigned short)(val>>16), port+2); /* Send high word of data */
}

/* Read a double word */
unsigned int snd_emu8000_peek_dw(emu8000_t *emu, unsigned int port, unsigned int reg)
{
	unsigned short low;
	unsigned int res;

	//outw((unsigned short) reg, EMU8000_PTR(emu)); /* Set register */
	if (reg != emu->last_reg) {
		outw((unsigned short)reg, EMU8000_PTR(emu)); /* Set register */
		emu->last_reg = reg;
	}
	low = inw(port);	/* Read low word of data */

	res = low + (inw(port+2) << 16);
	//printk("peekdw %x, reg %d,%d --> %x\n", port, reg>>5, reg&0x1f, res);
	return res;
}

static int snd_emu8000_detect(emu8000_t * emu)
{
	/* Initialise */
	EMU8000_HWCF1_WRITE(emu, 0x0059);
	EMU8000_HWCF2_WRITE(emu, 0x0020);
	EMU8000_HWCF3_WRITE(emu, 0x0000);
	/* Check for a recognisable emu8000 */
	//if ((EMU8000_U1_READ(emu) & 0x000f) != 0x000c)
		//return -ENODEV;
	if ((EMU8000_HWCF1_READ(emu) & 0x007e) != 0x0058)
		return -ENODEV;
	if ((EMU8000_HWCF2_READ(emu) & 0x0003) != 0x0003)
		return -ENODEV;

	return 0;
}

/*

 */

static void snd_emu8000_free(void *private_data)
{
	emu8000_t *emu;

	emu = (emu8000_t *)private_data;

#ifdef CONFIG_SND_OSSEMUL
	snd_emu8000_free_seq_oss(emu);
#endif
	snd_emu8000_detach_seq(emu);

	if (emu->ram_proc_entry)
		snd_emu8000_proc_free(emu->ram_proc_entry);

	if (emu->rom_proc_entry)
		snd_emu8000_proc_free(emu->rom_proc_entry);

	if (emu->patch_proc_entry)
		snd_emu8000_proc_free(emu->patch_proc_entry);

	if (emu->sflist) {
		snd_sf_free(emu->sflist);
	}

	if (emu->voices) {
		snd_kfree(emu->voices);
	}

	snd_kfree(private_data);
}

/*
 * create a new hardware dependent device for Emu8000
 */
int snd_emu8000_new_device(snd_card_t *card, int device, char *name,
			   char *id, void *arg, int size,
			   snd_seq_dev_entry_t *result)
{
	int base = (int)arg;
	snd_hwdep_t *hw;
	emu8000_t *emu;
	snd_sf_callback_t sf_cb;

	if ((hw = snd_hwdep_new_device(card, id)) == NULL)
		return -ENOMEM;
	strcpy(hw->name, name);
	hw->type = SND_HWDEP_TYPE_EMU8000;

	emu = snd_kcalloc(sizeof(emu8000_t), GFP_KERNEL);
	if (!emu) {
		snd_hwdep_free(hw);
		return -ENOMEM;
	}

	emu->magic = EMU8000_MAGIC;

	emu->port1 = base;
	emu->port2 = base + 0x400;
	emu->port3 = base + 0x800;

	emu->card = card;

	emu->mem_size = -1;
	emu->max_channels = EMU8000_CHANNELS;
	emu->max_voices   = EMU8000_CHANNELS;

	emu->voice_lock = SPIN_LOCK_UNLOCKED;
	emu->reg_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&emu->wait);
	init_MUTEX(&emu->patch_mutex);
	init_MUTEX(&emu->register_mutex);

	emu->client = -1;
#ifdef CONFIG_SND_OSSEMUL
	emu->oss_synth = NULL;
#endif

	hw->private_data = emu;
	hw->private_free = snd_emu8000_free;

	emu->voices = snd_kcalloc(sizeof(emu8000_voice_t) * emu->max_channels, GFP_KERNEL);
	if (! emu->voices) {
		snd_hwdep_free(hw);
		return -ENOMEM;
	}

	if (snd_emu8000_detect(emu) < 0) {
		snd_hwdep_free(hw);
		return -ENXIO;
	}

	memset(&sf_cb, 0, sizeof(sf_cb));
	sf_cb.private_data = emu;
	sf_cb.sample_write = snd_emu8000_sample_write;
	sf_cb.sample_reset = (snd_sf_sample_reset_t)snd_emu8000_terminate_all;
	emu->sflist = snd_sf_new(&sf_cb);
	if (! emu->sflist) {
		snd_hwdep_free(hw);
		return -ENOMEM;
	}

	emu->bass_level = 5;
	emu->treble_level = 9;
	emu->chorus_mode = 2;
	emu->reverb_mode = 4;

	snd_emu8000_init(emu);

	snd_emu8000_proc_init(emu);
	snd_emu8000_init_seq(emu);
#ifdef CONFIG_SND_OSSEMUL
	snd_emu8000_init_seq_oss(emu);
#endif

	snd_hwdep_register(hw, device);
	*result = (snd_seq_dev_entry_t)hw;
	return 0;
}

int snd_emu8000_delete_device(snd_card_t *card, int device, void *arg,
			      int size, snd_seq_dev_entry_t entry)
{
	return snd_hwdep_unregister((snd_hwdep_t*)entry);
}


/*
 * Set up a channel to be used for DMA.
 */
void
snd_emu8000_dma_chan(emu8000_t *emu, int ch, int write)
{
	EMU8000_DCYSUSV_WRITE(emu, ch, 0x80);
	EMU8000_VTFT_WRITE(emu, ch, 0);
	EMU8000_CVCF_WRITE(emu, ch, 0);
	EMU8000_PTRX_WRITE(emu, ch, 0x40000000);
	EMU8000_CPF_WRITE(emu, ch, 0x40000000);
	EMU8000_PSST_WRITE(emu, ch, 0);
	EMU8000_CSL_WRITE(emu, ch, 0);
	if (write == EMU8000_RAM_WRITE) /* DMA write */
		EMU8000_CCCA_WRITE(emu, ch, 0x06000000);
	else	   /* DMA read */
		EMU8000_CCCA_WRITE(emu, ch, 0x04000000);
	emu->voices[ch].state = EMU8000_ST_DRAM;
}

/*
 * Open up channels.
 */
int
snd_emu8000_open_dma(emu8000_t *emu, int write)
{
#if 0
	int  i;
	int  found;

	found = 0;

	/* Only use channels that are not being used for any voices */
	for (i = 0; i < emu->max_voices; i++) {
		if (emu->voices[i].state == EMU8000_ST_OFF) {
			snd_emu8000_dma_chan(emu, i, write);
			found++;
		}
	}


	if (found == 0) {
		/* Maybe we should force some to be freed up */
snd_printd("NO NO NO, none found\n");
	for (i = 0; i < emu->max_voices; i++) {
		if (emu->voices[i].state == EMU8000_ST_RELEASED) {
			snd_emu8000_dma_chan(emu, i, write);
			found++;
		}
	}
		//return -EIO;
	}
#else
	int i;

	/* reserve all 30 voices for loading */
	for (i = 0; i < emu->max_voices; i++) {
		snd_emu8000_dma_chan(emu, i, write);
	}
#endif

	EMU8000_VTFT_WRITE(emu, 30, 0);
	EMU8000_PSST_WRITE(emu, 30, 0x1d8);
	EMU8000_CSL_WRITE(emu, 30, 0x1e0);
	EMU8000_CCCA_WRITE(emu, 30, 0x1d8);
	EMU8000_VTFT_WRITE(emu, 31, 0);
	EMU8000_PSST_WRITE(emu, 31, 0x1d8);
	EMU8000_CSL_WRITE(emu, 31, 0x1e0);
	EMU8000_CCCA_WRITE(emu, 31, 0x1d8);

	return 0;
}

/*
 * Close all dram channels.
 * XXX Do we ever want to just close a few?
 */
void
snd_emu8000_close_dma(emu8000_t *emu)
{
	int i;

	/* wait until FULL bit in SMAxW register is false */
	for (i = 0; i < 10000; i++) {
		if ((EMU8000_SMALW_READ(emu) & 0x80000000) == 0)
			break;
		snd_emu8000_wait(emu, 10);
	}

	for (i = 0; i < emu->max_voices; i++) {
		if (emu->voices[i].state == EMU8000_ST_DRAM) {
			EMU8000_CCCA_WRITE(emu, i, 0);
			EMU8000_DCYSUSV_WRITE(emu, i, 0x807F);
			emu->voices[i].state = EMU8000_ST_OFF;
		}
	}
}

void snd_emu8000_read_wait(emu8000_t *emu)
{
	while ((EMU8000_SMALR_READ(emu) & 0x80000000) != 0) {
		//if (i++ < 5)
		//	continue;
		//printk("readwait\n");
		snd_emu8000_wait(emu, 5);
	}
}

void snd_emu8000_write_wait(emu8000_t *emu)
{
	while ((EMU8000_SMALW_READ(emu) & 0x80000000) != 0) {
		//printk("writewait\n");
		snd_emu8000_wait(emu, 5);
	}
}

void snd_emu8000_wait(emu8000_t *emu, unsigned short delay)
{
	unsigned long waittime;

	waittime = (HZ * (unsigned long)delay + 44099) / 44100;
	//printk("wait %ld\n", waittime);
	interruptible_sleep_on_timeout(&emu->wait, waittime);
}

/*
 *  INIT part
 */

int init_module(void)
{
	
	static snd_seq_dev_ops_t ops = {
		snd_emu8000_new_device,
		snd_emu8000_delete_device,
	};
	return snd_seq_device_register_driver(SND_SEQ_DEV_EMU8000, &ops);
}

void cleanup_module(void)
{
	snd_seq_device_unregister_driver(SND_SEQ_DEV_EMU8000);
}
