/*
 *   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 "../../include/driver.h"
#include "seq_system.h"
#include "seq_ports.h"
#include "seq_clientmgr.h"
#include "seq_lock.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;
	spin_lock_irqsave(&group->use_lock, flags);
	group->use++;
	spin_unlock_irqrestore(&group->use_lock, 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;
	spin_lock_irqsave(&group->use_lock, flags);
	if (!(--group->use) && waitqueue_active(&group->use_sleep))
		wake_up(&group->use_sleep);
	spin_unlock_irqrestore(&group->use_lock, flags);
}

/* lock subscribers and or wait for wakeup */
static void snd_seq_subscribers_wait(subscribers_group_t *group, unsigned long *rflags)
{

	if (!group || !rflags)
		return;
	spin_lock_irqsave(&group->use_lock, *rflags);
	while (group->use) {
		snd_seq_sleep_in_lock(&group->use_sleep, &group->use_lock);
	}
}

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

	spin_lock_irqsave(&group->use_lock, flags);
	s = group->list;
	group->list = NULL;
	spin_unlock_irqrestore(&group->use_lock, flags);
	/* release resources...*/
	while (s) {
		o = s;
		s = s->next;
		snd_kfree(o);
	}
	return 0;	/* success */
}


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

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

	if (!client)
		return;
	spin_lock_irqsave(&client->ports_lock, flags);
	client->ports_use++;
	spin_unlock_irqrestore(&client->ports_lock, flags);
}

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

	if (!client)
		return;
	spin_lock_irqsave(&client->ports_lock, flags);
	if (!(--client->ports_use) && waitqueue_active(&client->ports_sleep))
		wake_up(&client->ports_sleep);
	spin_unlock_irqrestore(&client->ports_lock, flags);
}

/* lock ports and or wait for wakeup */
static void snd_seq_ports_wait(client_t *client, unsigned long *rflags)
{

	if (!client || !rflags)
		return;
	spin_lock_irqsave(&client->ports_lock, *rflags);
	while (client->ports_use) {
		snd_seq_sleep_in_lock(&client->ports_sleep, &client->ports_lock);
	}
}

/* return pointer to port structure - ports are locked if found */
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);
	for (p = client->ports; p; p = p->next) {
		if (p->port == num)
			return p;
	}
	snd_seq_ports_unlock(client);
	return NULL;		/* not found */
}


/* search for next port - ports are locked if found */
client_port_t *snd_seq_port_query_nearest(client_t *client, snd_seq_port_info_t *pinfo)
{
	int num, len;
	client_port_t *p, *found;

	num = pinfo->port;
	pinfo->name[sizeof(pinfo->name)-1] = 0;
	len = strlen(pinfo->name);

	found = NULL;
	snd_seq_ports_lock(client);
	for (p = client->ports; p; p = p->next) {
		if (p->port < num)
			continue;
		if (p->port == num || found == NULL || p->port < found->port) {
			if ((len && strncmp(pinfo->name, p->name, len)) ||
			    (*pinfo->group && strcmp(pinfo->group, p->group)))
				continue;
			if (p->port == num)
				return p;
			found = p;
		}
	}
	if (found)
		return found;
	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_kcalloc(sizeof(client_port_t), GFP_KERNEL);
	if (new_port) {
		new_port->port = -1;
		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);
	strncpy(new_port->group, client->group, sizeof(new_port->group));
	new_port->input.use_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&new_port->input.use_sleep);
	new_port->itrack.use_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&new_port->itrack.use_sleep);
	new_port->output.use_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&new_port->output.use_sleep);
	new_port->otrack.use_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&new_port->otrack.use_sleep);
	init_MUTEX(&new_port->subscribe_mutex);
	init_MUTEX(&new_port->use_mutex);

	/* 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;
		}
		if (num >= SND_SEQ_MAX_PORTS - 1) {
			spin_unlock_irqrestore(&client->ports_lock, flags);
			snd_kfree(new_port);
			snd_printd("too many ports\n");
			return NULL;
		}
		p->next = new_port;
		num++;
	}

	client->num_ports++;
	new_port->port = num;	/* store the port number in the port */
	sprintf(new_port->name, "port-%d", num);
	spin_unlock_irqrestore(&client->ports_lock, flags);

	return new_port;
}

/* */
enum group_type_t {
	GROUP_TYPE_INPUT,
	GROUP_TYPE_ITRACK,
	GROUP_TYPE_OUTPUT,
	GROUP_TYPE_OTRACK
};

/* clear subscription list */
static void clear_subscribers(client_t *client, client_port_t *port, subscribers_group_t *group, int grptype)
{
	subscribers_t *s;
	snd_seq_port_subscribe_t subs;
	snd_seq_addr_t addr;

	/* address of the removed port (queue is overwritten later) */
	addr.client = client->number;
	addr.port = port->port;
	memset(&subs, 0, sizeof(subs));

	/* remove each subscriber */
	for (s = group->list; s; s = s->next) {
		client_t *c = NULL;
		client_port_t *p = NULL;
		if ((c = snd_seq_client_use_ptr(s->addr.client)) == NULL)
			goto __skip;
		if ((p = snd_seq_port_use_ptr(c, s->addr.port)) == NULL)
			goto __skip;
		if (p == port) {
			snd_printd("Huh? subscribed in same port.");
			goto __skip;
		}
		addr.queue = s->addr.queue;
		if (s->tracked) {
			switch (grptype) {
			case GROUP_TYPE_INPUT:
				snd_seq_port_remove_subscriber(&p->itrack, &addr);
				break;
			case GROUP_TYPE_OUTPUT:
				snd_seq_port_remove_subscriber(&p->otrack, &addr);
				break;
			}
		} else {
			switch (grptype) {
			case GROUP_TYPE_OUTPUT:
			case GROUP_TYPE_ITRACK:
				memcpy(&subs.dest, &addr, sizeof(addr));
				memcpy(&subs.sender, &s->addr, sizeof(s->addr));
				snd_seq_port_unsubscribe(p, &subs);
				snd_seq_port_remove_subscriber(&p->input, &addr);
				if (c->type == USER_CLIENT)
					snd_seq_client_notify_subscription(client, &subs, SND_SEQ_EVENT_PORT_UNSUBSCRIBED);
				break;
			case GROUP_TYPE_INPUT:
			case GROUP_TYPE_OTRACK:
				memcpy(&subs.sender, &addr, sizeof(addr));
				memcpy(&subs.dest, &s->addr, sizeof(s->addr));
				snd_seq_port_unuse(p, &subs);
				snd_seq_port_remove_subscriber(&p->output, &addr);
				if (c->type == USER_CLIENT)
					snd_seq_client_notify_subscription(client, &subs, SND_SEQ_EVENT_PORT_UNUSED);
				break;
			}
		}
	__skip:
		snd_seq_ports_unlock(c);
		snd_seq_client_unlock(c);
	}

	/* release list */
	snd_seq_subscribers_delete(group);
}
	

/* delete port data */
static int snd_seq_port_delete(client_t *client, client_port_t *port)
{
	/* sanity check */
	if (client == NULL || port == NULL) {
		snd_printd("oops: port_delete() got NULL\n");
		return -EINVAL;
	}

	/* clear subscribers info */
	clear_subscribers(client, port, &port->input, GROUP_TYPE_INPUT);
	clear_subscribers(client, port, &port->itrack, GROUP_TYPE_ITRACK);
	clear_subscribers(client, port, &port->output, GROUP_TYPE_OUTPUT);
	clear_subscribers(client, port, &port->otrack, GROUP_TYPE_OTRACK);

	if (port->private_free)
		port->private_free(port->private_data);

	snd_kfree(port);
	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, *prev;

	snd_seq_ports_wait(client, &flags);
	if ((p = client->ports) != NULL) {
		prev = NULL;
		for (; p; prev = p, p = p->next) {
			if (p->port == port) {
				if (prev)
					prev->next = p->next;
				else
					client->ports = p->next;
				client->num_ports--;
				spin_unlock_irqrestore(&client->ports_lock, flags);
				return snd_seq_port_delete(client, p);
			}
		}
	}
	spin_unlock_irqrestore(&client->ports_lock, 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;
	client->num_ports = 0;
	spin_unlock_irqrestore(&client->ports_lock, 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 */
	if (info->name[0]) {
		strncpy(port->name, info->name, sizeof(port->name)-1);
		port->name[sizeof(port->name)-1] = '\0';
	}
	if (info->group[0]) {
		strncpy(port->group, info->group, sizeof(port->group)-1);
		port->group[sizeof(port->group)-1] = '\0';
	}
	
	/* set capabilities */
	port->capability = info->capability;
	port->cap_group = info->cap_group;
	/* group capability always inherits */
	port->cap_group |= port->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));
	strncpy(info->group, port->group, sizeof(port->group));
	
	/* get capabilities */
	info->capability = port->capability;
	info->cap_group = port->cap_group;

	/* 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->read_use = port->subscribe_count;
	info->write_use = port->use_count;
	
	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, int tracked)
{
	unsigned long flags;
	subscribers_t *n, *s;

	if (!group || !addr)
		return -EINVAL;
	n = snd_kcalloc(sizeof(subscribers_t), GFP_KERNEL);
	if (!n)
		return -ENOMEM;
	n->addr = *addr;
	n->realtime = realtime ? 1 : 0;
	n->tracked = tracked ? 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_kfree(n);
			spin_unlock_irqrestore(&group->use_lock, flags);
			return -EBUSY;
		}
		/* add to the end of the list */
		while (s->next) {
			s = s->next;
		}
		s->next = n;
	}
	group->count++;
	spin_unlock_irqrestore(&group->use_lock, 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);
	p = NULL;
	for (s = group->list; s; p = s, s = s->next) {
		if (addr_compare(&s->addr, addr)) {
			if (p)
				p->next = s->next;
			else
				group->list = s->next;
			if (group->exclusive)
				group->exclusive = 0;
			snd_kfree(s);
			spin_unlock_irqrestore(&group->use_lock, flags);
			return 0;
		}
	}
	group->count--;
	spin_unlock_irqrestore(&group->use_lock, flags);
	return -ENOENT;
}


/* check if the address was already subscribed */
int snd_seq_port_is_subscribed(subscribers_group_t *group, snd_seq_addr_t *addr)
{
	subscribers_t *s;

	snd_seq_subscribers_lock(group);
	for (s = group->list; s; s = s->next)
		if (addr_compare(&s->addr, addr)) {
			snd_seq_subscribers_unlock(group);
			return 1;
		}
	snd_seq_subscribers_unlock(group);
	return 0;
}


/* get matched subscriber - must unlock after use it! */
subscribers_t *snd_seq_port_get_subscription(subscribers_group_t *group,
					     snd_seq_addr_t *addr)
{
	subscribers_t *s;

	snd_seq_subscribers_lock(group);
	for (s = group->list; s; s = s->next)
		if (addr_compare(&s->addr, addr)) {
			return s;
		}
	snd_seq_subscribers_unlock(group);
	return NULL;
}


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

	if (!port->subscribe)
		return 0;
	down(&port->subscribe_mutex);
	port->subscribe_count++;
	if (port->subscribe_count > 1) {
		up(&port->subscribe_mutex);
		return 0;
	}
	result = port->subscribe(port->private_data, info);
	if (result < 0)
		port->subscribe_count--;
	up(&port->subscribe_mutex);
	return result;
}

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

	if (!port->subscribe)
		return 0;
	down(&port->subscribe_mutex);
	port->subscribe_count--;
	if (port->subscribe_count > 0) {
		up(&port->subscribe_mutex);
		return 0;
	}
	result = port->unsubscribe(port->private_data, info);
	if (result < 0)
		port->subscribe_count++;	/* wrong? */
	up(&port->subscribe_mutex);
	return result;
}

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

	if (!port->use)
		return 0;
	down(&port->use_mutex);
	port->use_count++;
	if (port->use_count > 1) {
		up(&port->use_mutex);
		return 0;
	}
	result = port->use(port->private_data, info);
	if (result < 0)
		port->use_count--;
	up(&port->use_mutex);
	return result;
}

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

	if (!port->use)
		return 0;
	down(&port->use_mutex);
	if (port->use_count == 0) {
		up(&port->use_mutex);
		return 0;
	}
	port->use_count--;
	if (port->use_count) {
		up(&port->use_mutex);
		return 0;
	}
	result = port->unuse(port->private_data, info);
	if (result < 0)
		port->use_count++;	/* wrong? */
	up(&port->use_mutex);
	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_kmalloc(sizeof(snd_seq_port_callback_t), GFP_KERNEL);
	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 = 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;
}
