/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of EMU10K1 MPU-401 in UART mode
 *
 *
 *   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.
 *
 */

#include "driver.h"
#include "emu10k1.h"

#define EMU10K1_MIDI_MODE_INPUT		(1<<0)
#define EMU10K1_MIDI_MODE_OUTPUT	(1<<1)

/*

 */

static void snd_emu10k1_midi_interrupt(emu10k1_t *emu, unsigned int status)
{
	snd_rawmidi_t *rmidi;
	unsigned char byte, bstatus;

	if ((rmidi = emu->rmidi) == NULL) {
		snd_emu10k1_intr_disable(emu, status & (ENB_RX|ENB_TX));
		return;
	}
	snd_mutex_lock(emu, midi_input);
	bstatus = inb(emu->port + MUSTAT);
	if ((status & INT_RX) && !(bstatus & 0x80)) {
		byte = inb(emu->port + MUDATA);
		snd_mutex_unlock(emu, midi_input);
		rmidi->input.data(rmidi, &byte, 1);
		snd_mutex_lock(emu, midi_input);
	}
	if ((status & INT_TX) && !(bstatus & 0x40)) {
		if (rmidi->output.data(rmidi, &byte, 1) == 1) 
			outb(byte, emu->port + MUDATA);
	}
	snd_mutex_unlock(emu, midi_input);
}

/*

 */

static void snd_emu10k1_midi_cmd(emu10k1_t * emu, unsigned char cmd, int ack)
{
	int timeout, ok;

	snd_mutex_lock(emu, midi_input);
	outb(0x00, emu->port + MUDATA);
	for (timeout = 100000; timeout > 0 && !(inb(emu->port + MUSTAT) & 0x80); timeout--)
		inb(emu->port + MUDATA);
#ifdef CONFIG_SND_DEBUG
	if (timeout <= 0)
		snd_printk("snd_emu10k1_midi_cmd: clear rx timeout (status = 0x%x)\n", inb(emu->port + MUSTAT));
#endif
	outb(cmd, emu->port + MUSTAT);
	if (ack) {
		ok = 0;
		timeout = 10000;
		while (!ok && timeout-- > 0) {
			if (!(inb(emu->port + MUSTAT) & 0x80)) {
				if (inb(emu->port + MUDATA) == 0xfe)
					ok = 1;
			}
		}
	} else {
		ok = 1;
	}	
	snd_mutex_unlock(emu, midi_input);
	if (!ok)
		snd_printk("snd_emu10k1_midi_cmd: 0x%x failed at 0x%x (status = 0x%x, data = 0x%x)!!!\n", cmd, emu->port, inb(emu->port + MUSTAT), inb(emu->port + MUDATA));
}

static int snd_emu10k1_midi_input_open(snd_rawmidi_t * rmidi)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
#if 0
	snd_printk("[0x%x] MPU-401 command port - 0x%x\n", (emu), inb(MPU401C(emu)));
	snd_printk("[0x%x] MPU-401 data port - 0x%x\n", MPU401D(emu), inb(MPU401D(emu)));
#endif
	snd_mutex_lock(emu, midi_open);
	emu->midi_mode |= EMU10K1_MIDI_MODE_INPUT;
	if (!(emu->midi_mode & EMU10K1_MIDI_MODE_OUTPUT)) {
		snd_mutex_unlock(emu, midi_open);
		snd_emu10k1_midi_cmd(emu, 0xff, 1);	/* reset */
		snd_emu10k1_midi_cmd(emu, 0x3f, 1);	/* enter UART mode */
	} else {
		snd_mutex_unlock(emu, midi_open);
	}
	snd_emu10k1_intr_enable(emu, INT_RX);
	return 0;
}

static int snd_emu10k1_midi_output_open(snd_rawmidi_t * rmidi)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
	snd_mutex_lock(emu, midi_open);
	emu->midi_mode |= EMU10K1_MIDI_MODE_OUTPUT;
	if (!(emu->midi_mode & EMU10K1_MIDI_MODE_INPUT)) {
		snd_mutex_unlock(emu, midi_open);
		snd_emu10k1_midi_cmd(emu, 0xff, 1);	/* reset */
		snd_emu10k1_midi_cmd(emu, 0x3f, 1);	/* enter UART mode */
	} else {
		snd_mutex_unlock(emu, midi_open);
	}
	snd_emu10k1_intr_enable(emu, INT_TX);
	return 0;
}

static int snd_emu10k1_midi_input_close(snd_rawmidi_t * rmidi)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
	snd_mutex_lock(emu, midi_open);
	snd_emu10k1_intr_disable(emu, INT_RX);
	emu->midi_mode &= ~EMU10K1_MIDI_MODE_INPUT;
	if (!(emu->midi_mode & EMU10K1_MIDI_MODE_OUTPUT)) {
		snd_mutex_unlock(emu, midi_open);
		snd_emu10k1_midi_cmd(emu, 0xff, 0);	/* reset */
	} else {
		snd_mutex_unlock(emu, midi_open);
	}
	return 0;
}

static int snd_emu10k1_midi_output_close(snd_rawmidi_t * rmidi)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
	snd_mutex_lock(emu, midi_open);
	snd_emu10k1_intr_disable(emu, INT_TX);
	emu->midi_mode &= ~EMU10K1_MIDI_MODE_OUTPUT;
	if (!(emu->midi_mode & EMU10K1_MIDI_MODE_INPUT)) {
		snd_mutex_unlock(emu, midi_open);
		snd_emu10k1_midi_cmd(emu, 0xff, 0);	/* reset */
	} else {
		snd_mutex_unlock(emu, midi_open);
	}
	return 0;
}

static void snd_emu10k1_midi_input_trigger(snd_rawmidi_t * rmidi, int up)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
	if (up)
		snd_emu10k1_intr_enable(emu, ENB_RX);
	else
		snd_emu10k1_intr_disable(emu, ENB_TX);
}

static void snd_emu10k1_midi_output_trigger(snd_rawmidi_t * rmidi, int up)
{
	emu10k1_t *emu;

	emu = (emu10k1_t *) rmidi->private_data;
	if (up) {
		int max = 4;
		unsigned char byte;
	
		while (max > 0) {
			snd_mutex_lock(emu, midi_output);
			if (!(inb(emu->port + MUSTAT) & 0x40)) {
				if (!(emu->midi_mode & EMU10K1_MIDI_MODE_OUTPUT) ||
				   (rmidi->output.data(rmidi, &byte, 1) != 1)) {
					snd_mutex_unlock(emu, midi_output);
					return;
				}
				outb(byte, emu->port + MUDATA);
				snd_mutex_unlock(emu, midi_output);
				// snd_printk("emu10k1_uart: tx = 0x%x\n", byte);
				max--;
			} else {
				snd_mutex_unlock(emu, midi_output);
				break;
			}
		}
		snd_emu10k1_intr_enable(emu, ENB_TX);
	} else {
		snd_emu10k1_intr_disable(emu, ENB_TX);
	}
}

/*

 */

static struct snd_stru_rawmidi_direction_hw snd_emu10k1_midi_output =
{
	0,				/* flags */
	NULL,				/* private_data */
	NULL,				/* private_free */
	snd_emu10k1_midi_output_open,	/* open */
	snd_emu10k1_midi_output_close,	/* close */
	snd_emu10k1_midi_output_trigger, /* trigger */
	{NULL},				/* io.write */
	NULL,				/* abort */
};

static struct snd_stru_rawmidi_direction_hw snd_emu10k1_midi_input =
{
	0,				/* flags */
	NULL,				/* private_data */
	NULL,				/* private_free */
	snd_emu10k1_midi_input_open,	/* open */
	snd_emu10k1_midi_input_close,	/* close */
	snd_emu10k1_midi_input_trigger,	/* trigger */
	{NULL},				/* io.read */
	NULL,				/* abort */
};

static void snd_emu10k1_midi_free(void *private_data)
{
	if (private_data == NULL)
		return;
	((emu10k1_t *)private_data)->mpu401_interrupt = NULL;
	((emu10k1_t *)private_data)->rmidi = NULL;
}

snd_rawmidi_t *snd_emu10k1_midi(emu10k1_t *emu)
{
	snd_rawmidi_t *rmidi;

	if ((rmidi = snd_rawmidi_new_device(emu->card, "EMU10k1 MPU")) == NULL)
		return NULL;
	snd_mutex_prepare(emu, midi_open);
	snd_mutex_prepare(emu, midi_input);
	snd_mutex_prepare(emu, midi_output);
	strcpy(rmidi->name, "EMU10K1 MPU-401 (UART)");
	memcpy(&rmidi->output.hw, &snd_emu10k1_midi_output, sizeof(snd_emu10k1_midi_output));
	memcpy(&rmidi->input.hw, &snd_emu10k1_midi_input, sizeof(snd_emu10k1_midi_input));
	rmidi->info_flags |= SND_RAWMIDI_INFO_OUTPUT |
	                     SND_RAWMIDI_INFO_INPUT |
	                     SND_RAWMIDI_INFO_DUPLEX;
	rmidi->private_data = emu;
	rmidi->private_free = snd_emu10k1_midi_free;
	emu->mpu401_interrupt = snd_emu10k1_midi_interrupt;
	return rmidi;
}

