/*
 *   ALSA sequencer Ports
 *   Copyright (c) 1998 by Frank van de Pol <frank@vande-pol.demon.nl>
 *                         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.
 *
 */

#include "driver.h"
#include "seq_system.h"
#include "seq_ports.h"
#include "seq_clientmgr.h"

/*

   registration of client ports

 */


/* 

NOTE: the current implementation of the port structure as a linked list is
not optimal for clients that have many ports. For sending messages to all
subscribers of a port we first need to find the address of the port
structure, which means we have to traverse the list. A direct access table
(array) would be better, but big preallocated arrays waste memory.

Possible actions:

1) leave it this way, a client does normaly does not have more than a few
ports

2) replace the linked list of ports by a array of pointers which is
dynamicly kmalloced. When a port is added or deleted we can simply allocate
a new array, copy the corresponding pointers, and delete the old one. We
then only need a pointer to this array, and an integer that tells us how
much elements are in array.

*/


/*---------------------------------------------------------------------------*/
/* List of subscribers */

/* lock subscribers - to prevent create/delete operations */
void snd_seq_subscribers_lock(subscribers_group_t *group)
{
	unsigned long flags;

	if (!group)
		return;
	snd_spin_lock(group, use, &flags);
	group->use++;
	snd_spin_unlock(group, use, &flags);
}

/* unlock subscribers (and wakeup) - to allow create/delete operations */
void snd_seq_subscribers_unlock(subscribers_group_t *group)
{
	unsigned long flags;

	if (!group)
		return;
	snd_spin_lock(group, use, &flags);
	if (!(--group->use) && (snd_getlock(group, use) & SND_WK_SLEEP)) {
		snd_getlock(group, use) &= ~SND_WK_SLEEP;
		snd_wakeup(group, use);
	}
	snd_spin_unlock(group, use, &flags);
}

/* lock subscribers and or wait for wakeup */
static void snd_seq_subscribers_wait(subscribers_group_t *group, unsigned long *flags)
{
	if (!group || !flags)
		return;
	snd_spin_lock(group, use, flags);
	while (group->use) {
		snd_getlock(group, use) |= SND_WK_SLEEP;
		snd_spin_unlock(group, use, flags);
		snd_sleep(group, use, HZ);
		snd_spin_lock(group, use, flags);
		snd_getlock(group, use) &= ~SND_WK_SLEEP;
	}
}

/* destructor */
static int snd_seq_subscribers_delete(subscribers_group_t *group)
{
	unsigned long flags;
	subscribers_t *s, *o;

	snd_spin_lock(group, use, &flags);
	s = group->list;
	group->list = NULL;
	snd_spin_unlock(group, use, &flags);
	/* release resources...*/
	while (s) {
		o = s;
		s = s->next;
		snd_free(o, sizeof(subscribers_t));
	}
	return 0;	/* success */
}


/*---------------------------------------------------------------------------*/

/* lock ports - to prevent create/delete operations */
void snd_seq_ports_lock(client_t *client)
{
	unsigned long flags;

	if (!client)
		return;
	snd_spin_lock(client, ports, &flags);
	client->ports_use++;
	snd_spin_unlock(client, ports, &flags);
}

/* unlock ports (and wakeup) - to allow create/delete operations */
void snd_seq_ports_unlock(client_t *client)
{
	unsigned long flags;

	if (!client)
		return;
	snd_spin_lock(client, ports, &flags);
	if (!(--client->ports_use) &&
	    (snd_getlock(client, ports) & SND_WK_SLEEP)) {
		snd_getlock(client, ports) &= ~SND_WK_SLEEP;
		snd_wakeup(client, ports);
	}
	snd_spin_unlock(client, ports, &flags);
}

/* lock ports and or wait for wakeup */
static void snd_seq_ports_wait(client_t *client, unsigned long *flags)
{
	if (!client)
		return;
	snd_spin_lock(client, ports, flags);
	while (client->ports_use) {
		snd_getlock(client, ports) |= SND_WK_SLEEP;
		snd_spin_unlock(client, ports, flags);
		snd_sleep(client, ports, HZ);
		snd_spin_lock(client, ports, flags);
		snd_getlock(client, ports) &= ~SND_WK_SLEEP;
	}
}

/* return pointer to port structure */
client_port_t *snd_seq_port_use_ptr(client_t *client, int num)
{
	client_port_t *p;

	if (!client)
		return NULL;
	snd_seq_ports_lock(client);
	p = client->ports;
	if (p != NULL) {
		if (p->port == num)
			return p;
		while (p->next) {
			p = p->next;
			if (p->port == num)
				return p;
		}
	}
	snd_seq_ports_unlock(client);
	return NULL;		/* not found */
}


/* create a port, port number is returned (-1 on failure) */
client_port_t *snd_seq_create_port(client_t *client)
{
	unsigned long flags;
	client_port_t *new_port;
	int num = -1;
	
	/* sanity check */
	if (client == NULL) {
		snd_printd("oops: snd_seq_create_port() got NULL client\n");
		return NULL;
	}

	/* create a new port */
	new_port = snd_calloc(sizeof(client_port_t));
	if (new_port) {
		new_port->port = -1;
		strcpy(new_port->name, "");
		new_port->next = NULL;
	} else {
		snd_printd("malloc failed for registering client port\n");
		return NULL;	/* failure, out of memory */
	}

	/* init port data */
	sprintf(new_port->name, "port-%d", num);
	snd_spin_prepare(&new_port->input, use);
	snd_sleep_prepare(&new_port->input, use);
	snd_spin_prepare(&new_port->itrack, use);
	snd_sleep_prepare(&new_port->itrack, use);
	snd_spin_prepare(&new_port->output, use);
	snd_sleep_prepare(&new_port->output, use);
	snd_spin_prepare(&new_port->otrack, use);
	snd_sleep_prepare(&new_port->otrack, use);
	snd_mutex_prepare(new_port, subscribe);
	snd_mutex_prepare(new_port, use);

	/* wait if we can do some operation - spinlock is enabled!! */
	snd_seq_ports_wait(client, &flags);

	/* add the port to the list of registed ports for this client */
	if (client->ports == NULL) {
		/* first port in the list */
		client->ports = new_port;
		num = 0;
	} else {
		/* add to end of list */
		client_port_t *p = client->ports;
		num = p->port;
		while (p->next) {
			p = p->next;
			if (p->port > num)
				num = p->port;
		}
		p->next = new_port;
		num++;
	}
	new_port->port = num;	/* store the port number in the port */
	snd_spin_unlock(client, ports, &flags);

	return new_port;
}


/* delete port data */
static int snd_seq_port_delete(client_t *client, client_port_t *port)
{
	snd_seq_addr_t addr;
	subscribers_t *s;
	client_t *c;
	client_port_t *p;

	/* sanity check */
	if (client == NULL || port == NULL) {
		snd_printd("oops: port_delete() got NULL\n");
		return -EINVAL;
	}

	/* clear subscribers info */
	snd_seq_subscribers_delete(&port->input);
	for (s = port->itrack.list; s; s = s->next) {
		c = snd_seq_client_use_ptr(s->addr.client);
		if (!c)
			continue;
		p = snd_seq_port_use_ptr(c, s->addr.port);
		if (!p)
			continue;
		addr.queue = s->addr.queue;
		addr.client = client->number;
		addr.port = port->port;
		snd_seq_port_unsubscribe(p);
		snd_seq_port_remove_subscriber(&p->input, &addr);
		snd_seq_ports_unlock(c);
		snd_seq_client_unlock(c);
	}
	snd_seq_subscribers_delete(&port->itrack);
	snd_seq_subscribers_delete(&port->output);
	for (s = port->otrack.list; s; s = s->next) {
		c = snd_seq_client_use_ptr(s->addr.client);
		if (!c)
			continue;
		p = snd_seq_port_use_ptr(c, s->addr.port);
		if (!p)
			continue;
		addr.queue = s->addr.queue;
		addr.client = client->number;
		addr.port = port->port;
		snd_seq_port_unuse(p);
		snd_seq_port_remove_subscriber(&p->output, &addr);
		snd_seq_ports_unlock(c);
		snd_seq_client_unlock(c);
	}
	snd_seq_subscribers_delete(&port->otrack);
	
	if (port->private_free)
		port->private_free(port->private_data);

	snd_free(port, sizeof(client_port_t));
	return 0;
}


/* delete a port from port structure */
int snd_seq_delete_port(client_t *client, int port)
{
	unsigned long flags;
	client_port_t *p;

	snd_seq_ports_wait(client, &flags);
	if ((p = client->ports) != NULL) {
		if (p->port == port) {
			client->ports = p->next;
			snd_spin_unlock(client, ports, &flags);
			return snd_seq_port_delete(client, p);
		}
		while (p->next) {
			if (p->next) {
				if (p->next->port == port) {
					client_port_t *del = p->next;
					p->next = del->next;
					snd_spin_unlock(client, ports, &flags);
					return snd_seq_port_delete(client, del);
				}
			}
			p = p->next;
		}
	}
	snd_spin_unlock(client, ports, &flags);
	return -ENOENT; 	/* error, port not found */
}

/* delete whole port list */
int snd_seq_delete_ports(client_t *client)
{
	unsigned long flags;
	client_port_t *p, *next;
	
	if (client == NULL)
		return -EINVAL;
	snd_seq_ports_wait(client, &flags);
	p = client->ports;
	client->ports = NULL;
	snd_spin_unlock(client, ports, &flags);
	while (p != NULL) {
		next = p->next;
		snd_seq_port_delete(client, p);
		snd_seq_system_client_ev_port_exit(client->number, p->port);
		p = next;
	}
	return 0;
}

/* set port info fields */
int snd_seq_set_port_info(client_port_t * port, snd_seq_port_info_t * info)
{
	if ((port == NULL) || (info == NULL))
		return -1;

	/* set port name */
	strncpy(port->name, info->name, sizeof(port->name)-1);
	port->name[sizeof(port->name)-1] = '\0';
	
	/* set capabilities */
	port->capability = info->capability;
	
	/* get port type */
	port->type = info->type;

	/* information about supported channels/voices */
	port->midi_channels = info->midi_channels;
	port->synth_voices = info->synth_voices;

	return 0;
}

/* get port info fields */
int snd_seq_get_port_info(client_port_t * port, snd_seq_port_info_t * info)
{
	if ((port == NULL) || (info == NULL))
		return -1;

	/* get port name */
	strncpy(info->name, port->name, sizeof(info->name));
	
	/* get capabilities */
	info->capability = port->capability;

	/* get port type */
	info->type = port->type;

	/* information about supported channels/voices */
	info->midi_channels = port->midi_channels;
	info->synth_voices = port->synth_voices;

	/* get subscriber counts */
	info->out_use = port->input.use;
	info->in_use = port->output.use;
	
	return 0;
}


/* add subscriber to subscription list */
int snd_seq_port_add_subscriber(subscribers_group_t *group, snd_seq_addr_t *addr, int exclusive, int realtime)
{
	unsigned long flags;
	subscribers_t *n, *s;

	if (!group || !addr)
		return -EINVAL;
	n = snd_calloc(sizeof(subscribers_t));
	if (!n)
		return -ENOMEM;
	n->addr = *addr;
	n->realtime = realtime ? 1 : 0;
	snd_seq_subscribers_wait(group, &flags);
	s = group->list;
	if (s == NULL) {
		/* first subscriber */
		group->list = n;
		group->exclusive = exclusive ? 1 : 0;
	} else {
		if (exclusive) {
			snd_free(n, sizeof(subscribers_t));
			snd_spin_unlock(group, use, &flags);
			return -EBUSY;
		}
		/* add to the end of the list */
		while (s->next) {
			s = s->next;
		}
		s->next = n;
	}
	snd_spin_unlock(group, use, &flags);
	return 0;
}


static inline int addr_compare(snd_seq_addr_t *r, snd_seq_addr_t *s)
{
	return (r->queue == s->queue) &&
	       (r->client == s->client) &&
	       (r->port == s->port);
}

/* remove subscriber from subscription list */ 
int snd_seq_port_remove_subscriber(subscribers_group_t *group, snd_seq_addr_t *addr)
{
	unsigned long flags;
	subscribers_t *s, *p;

	snd_seq_subscribers_wait(group, &flags);
	s = group->list;
	if (s) {
		if (addr_compare(&s->addr, addr)) {
			group->list = s->next;
			if (group->exclusive)
				group->exclusive = 0;
			snd_free(s, sizeof(subscribers_t));
			snd_spin_unlock(group, use, &flags);
			return 0;
		}
		p = s;
		while (s) {
			if (addr_compare(&s->addr, addr)) {
				p->next = s->next;
				if (group->exclusive)
					group->exclusive = 0;
				snd_free(s, sizeof(subscribers_t));
				snd_spin_unlock(group, use, &flags);
				return 0;
			}
			p = s;
			s = s->next;
		}
	}
	snd_spin_unlock(group, use, &flags);
	return -ENOENT;
}


/* subscribe port */
int snd_seq_port_subscribe(client_port_t *port)
{
	int result;

	if (!port->subscribe)
		return 0;
	snd_mutex_down(port, subscribe);
	port->subscribe_count++;
	if (port->subscribe_count > 1) {
		snd_mutex_up(port, subscribe);
		return 0;
	}
	result = port->subscribe(port->private_data);
	if (result < 0)
		port->subscribe_count--;
	snd_mutex_up(port, subscribe);
	return result;
}

/* unsubscribe port */
int snd_seq_port_unsubscribe(client_port_t *port)
{
	int result;

	if (!port->subscribe)
		return 0;
	snd_mutex_down(port, subscribe);
	port->subscribe_count--;
	if (port->subscribe_count > 0) {
		snd_mutex_up(port, subscribe);
		return 0;
	}
	result = port->unsubscribe(port->private_data);
	if (result < 0)
		port->subscribe_count++;	/* wrong? */
	snd_mutex_up(port, subscribe);
	return result;
}

/* use port */
int snd_seq_port_use(client_port_t *port)
{
	int result;

	if (!port->use)
		return 0;
	snd_mutex_down(port, use);
	port->use_count++;
	if (port->use_count > 1) {
		snd_mutex_up(port, use);
		return 0;
	}
	result = port->use(port->private_data);
	if (result < 0)
		port->use_count--;
	snd_mutex_up(port, use);
	return result;
}

/* unuse port */
int snd_seq_port_unuse(client_port_t *port)
{
	int result;

	if (!port->use)
		return 0;
	snd_mutex_down(port, use);
	port->use_count--;
	if (port->use_count) {
		snd_mutex_up(port, use);
		return 0;
	}
	result = port->unuse(port->private_data);
	if (result < 0)
		port->use_count++;	/* wrong? */
	snd_mutex_up(port, use);
	return result;
}

/*
 * Initialise a port callback structure.
 */
void snd_port_init_callback(snd_seq_port_callback_t *p)
{
	if (!p)
		return;
	p->use = NULL;
	p->unuse = NULL;
	p->subscribe = NULL;
	p->unsubscribe = NULL;
	p->event_input = NULL;
	p->private_data = NULL;
	p->private_free = NULL;
}

/*
 * Allocate and initialise a port callback
 */
snd_seq_port_callback_t * snd_port_alloc_callback(void)
{
	snd_seq_port_callback_t *p;

	p = snd_malloc(sizeof(snd_seq_port_callback_t));
	snd_port_init_callback(p);

	return p;
}

/*
 * Attach a device driver that wants to receive events from the
 * sequencer.  Returns the new port number on success.
 * A driver that wants to receive the events converted to midi, will
 * use snd_seq_midisynth_register_port().
 */
int snd_seq_event_port_attach(int client,
			      snd_seq_port_callback_t *pcbp,
			      int cap,
			      int type,
			      char *portname)
{
	snd_seq_port_info_t portinfo;
	int  ret;

	/* Set up the port */
	memset(&portinfo, 0, sizeof(portinfo));
	portinfo.client = client;
	if (portname)
		strncpy(portinfo.name, portname, sizeof(portinfo.name));
	else
		sprintf(portinfo.name, "Unamed port");

	portinfo.capability = SND_SEQ_PORT_CAP_SUBSCRIPTION;
	portinfo.capability |= cap;
	portinfo.type = type;
	portinfo.kernel = pcbp;

	/* Create it */
	ret = snd_seq_kernel_client_ctl(client,
					SND_SEQ_IOCTL_CREATE_PORT,
					&portinfo);

	if (ret >= 0)
		ret = portinfo.port;

	return ret;
}


/*
 * Detach the driver from a port.
 */
int snd_seq_event_port_detach(int client, int port)
{
	snd_seq_port_info_t portinfo;
	int  err;

	memset(&portinfo, 0, sizeof(portinfo));
	portinfo.client = client;
	portinfo.port   = port;
	err = snd_seq_kernel_client_ctl(client,
					SND_SEQ_IOCTL_DELETE_PORT,
					&portinfo);

	return err;
}
