/*
 *  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 "driver.h"
#include "emu8000.h"
#include "emu8000_reg.h"
#include "soundfont.h"
#include "info.h"
#include "emu8000_port.h"
#include "emu8000_voice.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);

	/* It would be nice if info_free_entry would do this for us */
	if (emu->ram_proc_entry) {
		snd_emu8000_proc_free(emu->ram_proc_entry); /* TEMP */
		snd_info_free_entry(emu->ram_proc_entry);
	}
	if (emu->rom_proc_entry) {
		snd_emu8000_proc_free(emu->rom_proc_entry); /* TEMP */
		snd_info_free_entry(emu->rom_proc_entry);
	}
	if (emu->patch_proc_entry) {
		snd_emu8000_proc_free(emu->patch_proc_entry); /* TEMP */
		snd_info_free_entry(emu->patch_proc_entry);
	}

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

	if (emu->voices) {
		snd_free(emu->voices, sizeof(emu8000_voice_t) * emu->max_channels);
	}

	snd_free(private_data, sizeof(emu8000_t));
}

/*
 * create a new synth device for Emu8000
 */
snd_synth_t *snd_emu8000_new_device(snd_card_t * card, unsigned short base)
{
	snd_synth_t *synth;
	emu8000_t *emu;
	snd_sf_callback_t sf_cb;

	if ((synth = snd_synth_new_device(card, "EMU8000")) == NULL)
		return NULL;
	strcpy(synth->name, "EMU-8000");

	emu = snd_calloc(sizeof(emu8000_t));
	if (!emu) {
		snd_synth_free(synth);
		return NULL;
	}

	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;

	snd_spin_prepare(emu, voice);
	snd_spin_prepare(emu, reg);
	snd_sleep_prepare(emu, wait);
	snd_mutex_prepare(emu, patch);
	snd_mutex_prepare(emu, register);

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

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

	emu->voices = snd_calloc(sizeof(emu8000_voice_t) * emu->max_channels);
	if (! emu->voices) {
		snd_synth_free(synth);
		return NULL;
	}

	if (snd_emu8000_detect(emu) < 0) {
		snd_synth_free(synth);
		return NULL;
	}

	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_synth_free(synth);
		return NULL;
	}

	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

	return synth;
}

/*
 * 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);
	snd_sleep(emu, wait, waittime);
}

/*
 *  INIT part
 */
#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_emu8000_export;
#endif

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

	return 0;
}

void cleanup_module(void)
{
}
