/*
 * OSS compatible sequencer driver
 *
 * MIDI device handlers
 *
 * Copyright (C) 1998,99 Takashi Iwai <iwai@ww.uni-erlangen.de>
 *
 * 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 "seq_oss_midi.h"
#include "seq_oss_readq.h"
#include "seq_oss_timer.h"
#include "midi_coder.h"


/*
 * constants
 */
#define SND_SEQ_OSS_MAX_MIDI_NAME	30

/*
 * definition of midi device record
 */
struct seq_oss_midi_t {
	int seq_device;		/* device number */
	int client;		/* sequencer client number */
	int port;		/* sequencer port number */
	unsigned int flags;	/* port capability */
	int opened;		/* flag for opening */
	unsigned char name[SND_SEQ_OSS_MAX_MIDI_NAME];
	snd_midi_coder_t *coder;	/* MIDI event coder */
	seq_oss_devinfo_t *devinfo;	/* assigned OSSseq device */
};


/*
 * midi device table
 */
static int max_midi_devs = 0;
static seq_oss_midi_t *midi_devs[SND_SEQ_OSS_MAX_MIDI_DEVS];

static DECLARE_MUTEX(register_mutex);

/*
 * prototypes
 */
static int find_slot(int client, int port);
static seq_oss_midi_t *get_mididev(seq_oss_devinfo_t *dp, int dev);
static int receive_midi(unsigned char *buf, int len, void *private, void *opt);

/*
 * look up the existing ports
 * this looks a very exhausting job.
 */
int
snd_seq_oss_midi_lookup_ports(int client)
{
	snd_seq_system_info_t sysinfo;
	snd_seq_client_info_t clinfo;
	snd_seq_port_info_t pinfo;
	int rc, i, p;

	rc = snd_seq_kernel_client_ctl(client, SND_SEQ_IOCTL_SYSTEM_INFO, &sysinfo);
	if (rc < 0)
		return rc;
	
	memset(&clinfo, 0, sizeof(clinfo));
	memset(&pinfo, 0, sizeof(pinfo));
	for (i = 0; i < sysinfo.clients; i++) {
		clinfo.client = i;
		if (snd_seq_kernel_client_ctl(client, SND_SEQ_IOCTL_GET_CLIENT_INFO, &clinfo) < 0)
			continue;
		pinfo.client = i;
		for (p = 0; p < sysinfo.ports; p++) {
			pinfo.port = p;
			if (snd_seq_kernel_client_ctl(client, SND_SEQ_IOCTL_GET_PORT_INFO, &pinfo) >= 0)
				snd_seq_oss_midi_check_new_port(&pinfo);
		}
	}
	return 0;
}


/*
 * look for the identical slot
 */
static int
find_slot(int client, int port)
{
	int i;
	seq_oss_midi_t *mdev;

	for (i = 0; i < max_midi_devs; i++) {
		if ((mdev = midi_devs[i]) != NULL &&
		    mdev->client == client && mdev->port == port)
			/* already exists */
			return i;
	}
	return -1;
}


#define PERM_WRITE (SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE)
#define PERM_READ (SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_SUBS_READ)
/*
 * register a new port if it doesn't exist yet
 */
int
snd_seq_oss_midi_check_new_port(snd_seq_port_info_t *pinfo)
{
	int i;
	seq_oss_midi_t *mdev;

	debug_printk(("seq_oss: check for MIDI client %d port %d\n", pinfo->client, pinfo->port));
	/* the port must be device group */
	if (strcmp(pinfo->group, SND_SEQ_GROUP_DEVICE) != 0)
		return 0;
	/* either read or write subscribable */
	if ((pinfo->capability & PERM_WRITE) != PERM_WRITE &&
	    (pinfo->capability & PERM_READ) != PERM_READ)
		return 0;

	down(&register_mutex);
	/*
	 * look for the identical slot
	 */
	if (find_slot(pinfo->client, pinfo->port) >= 0) {
		/* already exists */
		up(&register_mutex);
		return 0;
	}

	/*
	 * allocate midi info record
	 */
	if ((mdev = snd_kcalloc(sizeof(*mdev), GFP_KERNEL)) == NULL) {
		snd_printk("sequencer: can't malloc midi info\n");
		up(&register_mutex);
		return -ENOMEM;
	}

	/*
	 * look for en empty slot
	 */
	for (i = 0; i < max_midi_devs; i++) {
		if (midi_devs[i] == NULL)
			break;
	}
	if (i >= max_midi_devs) {
		if (max_midi_devs >= SND_SEQ_OSS_MAX_MIDI_DEVS) {
			up(&register_mutex);
			return -ENOMEM;
		}
		max_midi_devs++;
	}

	mdev->seq_device = i;
	
	/* copy the port information */
	mdev->client = pinfo->client;
	mdev->port = pinfo->port;
	mdev->flags = pinfo->capability;
	mdev->opened = 0;

	/* copy and truncate the name of synth device */
	strncpy(mdev->name, pinfo->name, sizeof(mdev->name));
	mdev->name[sizeof(mdev->name) - 1] = 0;

	/* create MIDI coder */
	mdev->coder = snd_midi_coder_new();
	if (! mdev->coder) {
		snd_printk("sequencer: can't malloc midi coder\n");
		snd_kfree(mdev);
		up(&register_mutex);
		return -ENOSPC;
	}
	mdev->coder->input = receive_midi;
	mdev->coder->private = mdev;
	mdev->coder->private_free = NULL;

	midi_devs[mdev->seq_device] = mdev;

	/*MOD_INC_USE_COUNT;*/
	up(&register_mutex);
	return 0;
}

/*
 * release the midi device if it was registered
 */
int
snd_seq_oss_midi_check_exit_port(int client, int port)
{
	seq_oss_midi_t *mdev;
	int index;

	down(&register_mutex);
	if ((index = find_slot(client, port)) >= 0) {
		mdev = midi_devs[index];
		if (mdev->coder)
			snd_midi_coder_delete(mdev->coder);
		snd_kfree(mdev);
		midi_devs[index] = NULL;
	}
	if (index == max_midi_devs - 1) {
		for (index--; index >= 0; index--) {
			if (midi_devs[index])
				break;
		}
		max_midi_devs = index + 1;
	}
	up(&register_mutex);
	return 0;
}


/*
 * release the midi device if it was registered
 */
void
snd_seq_oss_midi_clear_all(void)
{
	int i;
	seq_oss_midi_t *mdev;

	down(&register_mutex);
	for (i = 0; i < max_midi_devs; i++) {
		if ((mdev = midi_devs[i]) != NULL) {
			if (mdev->coder)
				snd_midi_coder_delete(mdev->coder);
			snd_kfree(mdev);
			midi_devs[i] = NULL;
		}
	}
	max_midi_devs = 0;
	up(&register_mutex);
}


/*
 * set up midi tables
 */
void
snd_seq_oss_midi_setup(seq_oss_devinfo_t *dp)
{
	down(&register_mutex);
	dp->max_mididev = max_midi_devs;
	up(&register_mutex);
}

void
snd_seq_oss_midi_cleanup(seq_oss_devinfo_t *dp)
{
	int i;
	down(&register_mutex);
	for (i = 0; i < dp->max_mididev; i++)
		snd_seq_oss_midi_close(dp, i);
	up(&register_mutex);
	dp->max_mididev = 0;
}


/*
 * get the midi device information
 */
static seq_oss_midi_t *
get_mididev(seq_oss_devinfo_t *dp, int dev)
{
	if (dev < 0 || dev >= dp->max_mididev)
		return NULL;
	return midi_devs[dev];
}

/*
 * open the midi device if not opened yet
 */
int
snd_seq_oss_midi_open(seq_oss_devinfo_t *dp, int dev, int fmode)
{
	int perm;
	seq_oss_midi_t *mdev;
	snd_seq_port_subscribe_t subs;

	if ((mdev = get_mididev(dp, dev)) == NULL)
		return -ENODEV;

	/* already used? */
	if (mdev->opened && mdev->devinfo != dp)
		return -EBUSY;

	perm = 0;
	if (is_write_mode(fmode))
		perm |= PERM_WRITE;
	if (is_read_mode(fmode))
		perm |= PERM_READ;
	perm &= mdev->flags;
	if (perm == 0)
		return -ENXIO;

	/* already opened? */
	if ((mdev->opened & perm) == perm)
		return 0;

	perm &= ~mdev->opened;

	memset(&subs, 0, sizeof(subs));

	if (perm & PERM_WRITE) {
		subs.sender = dp->addr;
		subs.dest.client = mdev->client;
		subs.dest.port = mdev->port;
		subs.dest.queue = dp->addr.queue;
		if (snd_seq_kernel_client_ctl(dp->cseq, SND_SEQ_IOCTL_SUBSCRIBE_PORT, &subs) >= 0)
			mdev->opened |= PERM_WRITE;
	}
	if (perm & PERM_READ) {
		subs.sender.client = mdev->client;
		subs.sender.port = mdev->port;
		subs.sender.queue = dp->addr.queue;
		subs.dest = dp->addr;
		if (snd_seq_kernel_client_ctl(dp->cseq, SND_SEQ_IOCTL_SUBSCRIBE_PORT, &subs) >= 0)
			mdev->opened |= PERM_READ;
	}
	if (! mdev->opened)
		return -ENXIO;

	mdev->devinfo = dp;
	return 0;
}

/*
 * close the midi device if already opened
 */
int
snd_seq_oss_midi_close(seq_oss_devinfo_t *dp, int dev)
{
	seq_oss_midi_t *mdev;
	snd_seq_port_subscribe_t subs;

	if ((mdev = get_mididev(dp, dev)) == NULL)
		return -ENODEV;
	if (! mdev->opened || mdev->devinfo != dp)
		return 0;

	memset(&subs, 0, sizeof(subs));
	if (mdev->opened & PERM_WRITE) {
		subs.sender = dp->addr;
		subs.dest.client = mdev->client;
		subs.dest.port = mdev->port;
		subs.dest.queue = dp->addr.queue;
		snd_seq_kernel_client_ctl(dp->cseq, SND_SEQ_IOCTL_UNSUBSCRIBE_PORT, &subs);
	}
	if (mdev->opened & PERM_READ) {
		subs.sender.client = mdev->client;
		subs.sender.port = mdev->port;
		subs.sender.queue = dp->addr.queue;
		subs.dest = dp->addr;
		snd_seq_kernel_client_ctl(dp->cseq, SND_SEQ_IOCTL_UNSUBSCRIBE_PORT, &subs);
	}

	mdev->opened = 0;
	mdev->devinfo = NULL;

	return 0;
}

/*
 * change seq capability flags to file mode flags
 */
int
snd_seq_oss_midi_filemode(seq_oss_devinfo_t *dp, int dev)
{
	seq_oss_midi_t *mdev;
	int mode;

	if ((mdev = get_mididev(dp, dev)) == NULL)
		return 0;

	mode = 0;
	if (mdev->opened & PERM_WRITE)
		mode |= SND_SEQ_OSS_FILE_WRITE;
	if (mdev->opened & PERM_READ)
		mode |= SND_SEQ_OSS_FILE_READ;
	return mode;
}

/*
 * reset the midi device and close it:
 * so far, only close the device.
 */
void
snd_seq_oss_midi_reset(seq_oss_devinfo_t *dp, int dev)
{
	snd_seq_oss_midi_close(dp, dev);
}


/*
 * input callback
 */
int
snd_seq_oss_midi_input(snd_seq_event_t *ev, int direct, void *private_data)
{
	seq_oss_devinfo_t *dp = (seq_oss_devinfo_t *)private_data;
	seq_oss_midi_t *mdev;
	seq_oss_readq_t *readq;
	int i;

	if ((readq = dp->readq) == NULL)
		return 0;
	if ((i = find_slot(ev->source.client, ev->source.port)) < 0)
		return 0;
	mdev = midi_devs[i];
	if (! (mdev->opened & PERM_READ))
		return 0;

	snd_seq_oss_readq_put_timestamp(readq, snd_seq_oss_timer_get_tick(dp->timer), dp->seq_mode);
	return snd_midi_coder_decode(mdev->coder, ev, NULL);
}

/*
 * callback from midi_coder:
 * send MIDI input data to read queue
 */
static int
receive_midi(unsigned char *buf, int len, void *private, void *opt)
{
	int i;
	seq_oss_midi_t *mdev = (seq_oss_midi_t *)private;

	if (! mdev->devinfo || ! mdev->devinfo->readq)
		return 0;
	
	for (i = 0; i < len; i++)
		snd_seq_oss_readq_putc(mdev->devinfo->readq, mdev->seq_device, buf[i]);

	return 0;
}


/*
 * dump midi data
 * return 0 : enqueued
 *        non-zero : invalid - ignored
 */
int
snd_seq_oss_midi_putc(seq_oss_devinfo_t *dp, int dev, unsigned char c, snd_seq_event_t *ev)
{
	seq_oss_midi_t *mdev;

	if ((mdev = get_mididev(dp, dev)) == NULL)
		return -ENODEV;
	if (snd_midi_coder_encode(mdev->coder, c, ev) > 0) {
		snd_seq_oss_fill_addr(dp, ev, mdev->client, mdev->port);
		return 0;
	}
	return -EINVAL;
}

/*
 * create OSS compatible midi_info record
 */
int
snd_seq_oss_midi_make_info(seq_oss_devinfo_t *dp, int dev, oss_midi_info_t *inf)
{
	seq_oss_midi_t *mdev;

	if ((mdev = get_mididev(dp, dev)) == NULL)
		return -ENXIO;
	inf->device = dev;
	inf->dev_type = 0; /* FIXME: ?? */
	inf->capabilities = 0; /* FIXME: ?? */
	strncpy(inf->name, mdev->name, sizeof(inf->name));
	return 0;
}


/*
 * proc interface
 */
static char *
capmode_str(int val)
{
	val &= PERM_READ|PERM_WRITE;
	if (val == (PERM_READ|PERM_WRITE))
		return "read/write";
	else if (val == PERM_READ)
		return "read";
	else if (val == PERM_WRITE)
		return "write";
	else
		return "none";
}

void
snd_seq_oss_midi_info_read(snd_info_buffer_t *buf)
{
	int i;
	seq_oss_midi_t *mdev;

	down(&register_mutex);
	snd_iprintf(buf, "\nNumber of MIDI devices: %d\n", max_midi_devs);
	for (i = 0; i < max_midi_devs; i++) {
		snd_iprintf(buf, "\nmidi %d: ", i);
		if ((mdev = midi_devs[i]) == NULL) {
			snd_iprintf(buf, "*empty*\n");
			continue;
		}
		snd_iprintf(buf, "%s\n", mdev->name);
		snd_iprintf(buf, "  client %d\n", mdev->client);
		snd_iprintf(buf, "  port %d\n", mdev->port);
		snd_iprintf(buf, "  capability %s\n", capmode_str(mdev->flags));
		snd_iprintf(buf, "  opened %s\n", capmode_str(mdev->opened));
	}
	up(&register_mutex);
}

