/*
 *  Abstract layer for sequenced MIDI v1.0 stream
 *  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 "midi.h"
#include "control.h"

extern snd_rawmidi_t *snd_rawmidi_devices[];

extern int snd_rawmidi_init_buffer(snd_rawmidi_t * rmidi, int direction);
extern int snd_rawmidi_done_buffer(snd_rawmidi_t * rmidi, int direction);
extern void snd_rawmidi_trigger_output(snd_rawmidi_t * rmidi, int up);
extern void snd_rawmidi_trigger_input(snd_rawmidi_t * rmidi, int up);
extern int snd_rawmidi_drain_output(snd_rawmidi_t * rmidi);
extern int snd_rawmidi_flush_output(snd_rawmidi_t * rmidi);
extern int snd_rawmidi_transmit(snd_rawmidi_t * rmidi, char *buffer, int count);

/*

 */

static int snd_midi_input_reset(snd_rawmidi_t * rmidi)
{
	rmidi->input.u.p.cused = 0;
	rmidi->input.u.p.cprev = 0;
	rmidi->input.u.p.cleft = 0;
	return 0;
}

static int snd_midi_init_input_buffer(snd_rawmidi_t * rmidi)
{
	rmidi->input.mode = SND_RAWMIDI_MODE_SEQ;
	rmidi->input.reset = snd_midi_input_reset;
	rmidi->input.u.p.csize = 512;
	snd_midi_input_reset(rmidi);
	rmidi->input.u.p.cbuffer = snd_malloc(rmidi->input.u.p.csize);
	if (!rmidi->input.u.p.cbuffer)
		return -ENOMEM;
	return 0;
}

static int snd_midi_done_input_buffer(snd_rawmidi_t * rmidi)
{
	if (rmidi->input.u.p.cbuffer) {
		snd_free(rmidi->input.u.p.cbuffer, rmidi->input.u.p.csize);
		rmidi->input.u.p.cbuffer = NULL;
	}
	return 0;
}

static int snd_midi_receive_byte(snd_rawmidi_t * rmidi, unsigned char b)
{
	/* # of data bytes following a status */
	static unsigned char len_tab[] =
	{
		2,	/* 8x - note off */
		2,	/* 9x - note on */
		2,	/* Ax - note pressure */
		2,	/* Bx - control */
		1,	/* Cx - program change */
		1,	/* Dx - channel pressure */
		2,	/* Ex - bender */
		0,	/* Fx - common message */
	};
	/* # of data bytes following a common status */
	static unsigned char len_tab_c[] =
	{
		254,	/* F0 - sysex - wait for next data */
		1,	/* F1 - mtc quarter */
		2,	/* F2 - song position */
		1,	/* F3 - song select */

		255,	/* F4 - unknown == reset */
		255,	/* F5 - unknown == reset */
		0,	/* F6 - tune request */
		255,	/* F7 - sysex end == reset */

		0,	/* F8 - clock */
		0,	/* F9 - start */
		0,	/* FA - continue */
		255,	/* FB - unknown == reset */

		0,	/* FC - stop */
		255,	/* FD - unknown == reset */
		0,	/* FE - sensing */
		0,	/* FF - reset */
	};
	unsigned long flags;
	unsigned char tmp;

	snd_spin_lock(rmidi, input, &flags);
	if (rmidi->input.u.p.cused >= rmidi->input.u.p.csize) {
		snd_midi_input_reset(rmidi);
		snd_spin_unlock(rmidi, input, &flags);
		snd_printk("snd_midi_receive: rx ignore data 0x%x (and command)\n", b);
		return -ENOMEM;
	}
	rmidi->input.u.p.cbuffer[rmidi->input.u.p.cused++] = b;
	if (rmidi->input.u.p.cused == 1) {	/* init byte? */
		if (b & 0x80) {	/* MIDI status byte */
			if ((b & 0xf0) == 0xf0) {	/* common message */
				tmp = len_tab_c[b & 0x0f];
				switch (tmp) {
				case 254:	/* wait for next data */
					snd_spin_unlock(rmidi, input, &flags);
					return 0;
				case 255:	/* reset */
					snd_midi_input_reset(rmidi);
					snd_spin_unlock(rmidi, input, &flags);
					return 0;
				case 0:
					if (rmidi->input.u.p.command)
						rmidi->input.u.p.command(rmidi,
						 rmidi->input.u.p.cmd_private_data,
						 rmidi->input.u.p.cbuffer,
						 rmidi->input.u.p.cused);
					snd_midi_input_reset(rmidi);
					snd_spin_unlock(rmidi, input, &flags);
					return 0;
				default:
					rmidi->input.u.p.cleft = tmp;
					snd_spin_unlock(rmidi, input, &flags);
					return 0;
				}
			} else {
				rmidi->input.u.p.cleft = len_tab[(b >> 4) & 0x07];
				rmidi->input.u.p.cprev = b;
				snd_spin_unlock(rmidi, input, &flags);
				return 0;
			}
		} else {
			if (!rmidi->input.u.p.cprev) {
				/* probably unexpected data */
				snd_midi_input_reset(rmidi);
				snd_spin_unlock(rmidi, input, &flags);
				return 0;
			}
			rmidi->input.u.p.cbuffer[0] = rmidi->input.u.p.cprev;
			rmidi->input.u.p.cbuffer[1] = b;
			rmidi->input.u.p.cused = 2;
			rmidi->input.u.p.cleft =
				len_tab[(rmidi->input.u.p.cprev >> 4) & 0x07]-1;
			if (!rmidi->input.u.p.cleft) {
				if (rmidi->input.u.p.command)
					rmidi->input.u.p.command(rmidi,
					 rmidi->input.u.p.cmd_private_data,
					 rmidi->input.u.p.cbuffer,
					 rmidi->input.u.p.cused);
				rmidi->input.u.p.cused =
				 rmidi->input.u.p.cleft = 0;
			}
			snd_spin_unlock(rmidi, input, &flags);
			return 0;
		}
	}
	/* ok.. look if we wait for next data */
	if (rmidi->input.u.p.cleft > 0) {	/* ok. some next data */
		if (!(--rmidi->input.u.p.cleft)) {	/* finished */
			if (rmidi->input.u.p.command)
				rmidi->input.u.p.command(rmidi,
				 rmidi->input.u.p.cmd_private_data,
				 rmidi->input.u.p.cbuffer,
				 rmidi->input.u.p.cused);
			rmidi->input.u.p.cused = rmidi->input.u.p.cleft = 0;
		}
		snd_spin_unlock(rmidi, input, &flags);
		return 0;
	}
	/* sysex in progress? */
	if (rmidi->input.u.p.cbuffer[0] == 0xf0) {	/* SysEx */
		if (b == 0xf7) {	/* SysEx end */
			rmidi->input.u.p.command(rmidi,
			 rmidi->input.u.p.cmd_private_data,
			 rmidi->input.u.p.cbuffer,
			 rmidi->input.u.p.cused);
			snd_midi_input_reset(rmidi);
		}
	} else {
		snd_midi_input_reset(rmidi);
	}
	snd_spin_unlock(rmidi, input, &flags);
	return 0;
}

static int snd_midi_receive(snd_rawmidi_t * rmidi, char *buffer, int count)
{
	if (!rmidi->input.u.p.cbuffer) {
		snd_printd("snd_midi_receive: input isn't active!!\n");
		return -EINVAL;
	}
	rmidi->input.bytes += count;
	while (count-- > 0)
		snd_midi_receive_byte(rmidi, *(unsigned char *) buffer++);
	return 0;
}


int snd_midi_info(int cardnum, int device, snd_rawmidi_info_t *info)
{
	snd_rawmidi_t *rmidi;

	if (!info)
		return -EINVAL;
	rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (!rmidi)
		return -ENODEV;
	memset(info, 0, sizeof(info));
	info->type = rmidi->card->type;
	info->flags = rmidi->info_flags;
	strcpy(info->id, rmidi->id);
	strcpy(info->name, rmidi->name);
	return 0;
}

int snd_midi_open(int cardnum, int device, int mode, snd_rawmidi_t ** out)
{
	int err;
	snd_rawmidi_t *rmidi;

	rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (!rmidi)
		return -ENODEV;
	snd_mutex_down(rmidi, open);
	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		if (!(rmidi->info_flags & SND_RAWMIDI_INFO_INPUT)) {
			snd_mutex_up(rmidi, open);
			return -ENXIO;
		}
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		if (!(rmidi->info_flags & SND_RAWMIDI_INFO_OUTPUT)) {
			snd_mutex_up(rmidi, open);
			return -ENXIO;
		}
	}
	if (rmidi->flags & mode) {
		snd_mutex_up(rmidi, open);
		return -EBUSY;
	}
	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_midi_init_input_buffer(rmidi) < 0) {
			snd_mutex_up(rmidi, open);
			return -ENOMEM;
		}
		if ((err = rmidi->input.hw.open(rmidi)) < 0) {
			snd_midi_done_input_buffer(rmidi);
			snd_mutex_up(rmidi, open);
			return err;
		}
		rmidi->input.data = snd_midi_receive;
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		if (snd_rawmidi_init_buffer(rmidi, SND_RAWMIDI_LFLG_OUTPUT) < 0) {
			if (mode & SND_RAWMIDI_LFLG_INPUT) {
				rmidi->input.hw.close(rmidi);
				snd_midi_done_input_buffer(rmidi);
				rmidi->input.data = NULL;
			}
			snd_mutex_up(rmidi, open);
			return -ENOMEM;
		}
		rmidi->output.mode = SND_RAWMIDI_MODE_SEQ;
		if ((err = rmidi->output.hw.open(rmidi)) < 0) {
			if (mode & SND_RAWMIDI_LFLG_INPUT) {
				rmidi->input.hw.close(rmidi);
				snd_midi_done_input_buffer(rmidi);
				rmidi->input.data = NULL;
			}
			snd_mutex_up(rmidi, open);
			return err;
		}
		rmidi->output.data = snd_rawmidi_transmit;
	}
	rmidi->flags |= mode;
	MOD_INC_USE_COUNT;
	rmidi->card->use_inc(rmidi->card);
	snd_mutex_up(rmidi, open);
	if (out)
		*out = rmidi;
	return 0;
}

int snd_midi_close(int cardnum, int device, int mode)
{
	snd_rawmidi_t *rmidi;

	rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (!rmidi)
		return -ENODEV;
	snd_mutex_down(rmidi, open);
	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		snd_rawmidi_trigger_input(rmidi, 0);
		rmidi->input.hw.close(rmidi);
		rmidi->input.data = NULL;
		snd_midi_done_input_buffer(rmidi);
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		snd_rawmidi_flush_output(rmidi);
		rmidi->output.hw.close(rmidi);
		rmidi->output.data = NULL;
		snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_OUTPUT);
	}
	rmidi->flags &= ~mode;
	rmidi->card->use_dec(rmidi->card);
	MOD_DEC_USE_COUNT;
	snd_mutex_up(rmidi, open);
	return 0;
}

int snd_midi_drain_output(snd_rawmidi_t * rmidi)
{
	return snd_rawmidi_drain_output(rmidi);
}

int snd_midi_flush_output(snd_rawmidi_t * rmidi)
{
	return snd_rawmidi_flush_output(rmidi);
}

int snd_midi_stop_input(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_trigger_input(rmidi, 0);
	return snd_midi_input_reset(rmidi);
}

int snd_midi_start_input(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_trigger_input(rmidi, 1);
	return 0;
}

int snd_midi_transmit(snd_rawmidi_t * rmidi, char *buf, int count)
{
	unsigned long flags;
	int count1, result;

	result = 0;
	while (count > 0 && rmidi->output.u.s.used < rmidi->output.u.s.size) {
		count1 = rmidi->output.u.s.size - rmidi->output.u.s.head;
		if (count1 > count)
			count1 = count;
		snd_spin_lock(rmidi, output, &flags);
		if (count1 > rmidi->output.u.s.size - rmidi->output.u.s.used)
			count1 = rmidi->output.u.s.size - rmidi->output.u.s.used;
		snd_spin_unlock(rmidi, output, &flags);
		memcpy(rmidi->output.u.s.buffer + rmidi->output.u.s.head,
		       buf, count1);
		snd_spin_lock(rmidi, output, &flags);
		rmidi->output.u.s.head += count1;
		rmidi->output.u.s.head %= rmidi->output.u.s.size;
		rmidi->output.u.s.used += count1;
		snd_spin_unlock(rmidi, output, &flags);
		result += count1;
		buf += count1;
		count -= count1;
	}
	snd_rawmidi_trigger_output(rmidi, 1);
	return result;
}

/*
 *  ENTRY functions
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_midi_export;
#endif

int init_module(void)
{
#ifndef LINUX_2_1
	if (register_symtab(&snd_symbol_table_midi_export) < 0)
		return -ENOMEM;
#endif
	snd_control_register_ioctl(snd_rawmidi_control_ioctl);
	return 0;
}

void cleanup_module(void)
{
	snd_control_unregister_ioctl(snd_rawmidi_control_ioctl);
}
