/*
 *   Generic Instrument routines for ALSA sequencer
 *   Copyright (c) 1990 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 "seq_clientmgr.h"
#include "seq_instr.h"

snd_seq_kcluster_t *snd_seq_cluster_new(int atomic)
{
	snd_seq_kcluster_t *cluster;
	
	if (atomic) {
		snd_printk("snd_seq_cluster_new: atomic - TODO!!\n");
		return NULL;
	}
	cluster = (snd_seq_kcluster_t *) snd_calloc(sizeof(snd_seq_kcluster_t));
	return cluster;
}

void snd_seq_cluster_free(snd_seq_kcluster_t *cluster)
{
	if (cluster == NULL)
		return;
	snd_free(cluster, sizeof(snd_seq_kcluster_t));
}

snd_seq_kinstr_t *snd_seq_instr_new(int add_len, int atomic)
{
	snd_seq_kinstr_t *instr;
	
	if (atomic) {
		snd_printk("snd_seq_instr_new: atomic - TODO!!\n");
		return NULL;
	}
	instr = (snd_seq_kinstr_t *) snd_calloc(sizeof(snd_seq_kinstr_t) + add_len);
	if (instr == NULL)
		return NULL;
	instr->add_len = add_len;
	return instr;
}

void snd_seq_instr_free(snd_seq_kinstr_t *instr)
{
	if (instr == NULL)
		return;
	snd_free(instr, sizeof(snd_seq_kinstr_t) + instr->add_len);
}

snd_seq_kinstr_list_t *snd_seq_instr_list_new(void)
{
	snd_seq_kinstr_list_t *list;

	list = (snd_seq_kinstr_list_t *) snd_calloc(sizeof(snd_seq_kinstr_list_t));
	if (list == NULL)
		return NULL;
	snd_spin_prepare(list, lock);
	snd_spin_prepare(list, ops);
	snd_mutex_prepare(list, ops);
	return list;
}

void snd_seq_instr_list_free(snd_seq_kinstr_list_t **list_ptr)
{
	snd_seq_kinstr_list_t *list;
	snd_seq_kinstr_t *instr;
	snd_seq_kcluster_t *cluster;
	int idx;

	if (list_ptr == NULL)
		return;
	list = *list_ptr;
	*list_ptr = NULL;
	if (list == NULL)
		return;
	
	for (idx = 0; idx < SND_SEQ_INSTR_HASH_SIZE; idx++) {
		while ((instr = list->hash[idx]) != NULL) {
			list->hash[idx] = instr->next;
			list->count--;
			while (instr->use) ;
			snd_seq_instr_free(instr);
		}
		while ((cluster = list->chash[idx]) != NULL) {
			list->chash[idx] = cluster->next;
			list->ccount--;
			snd_seq_cluster_free(cluster);
		}
	}
	snd_free(list, sizeof(snd_seq_kinstr_list_t));
}

static int compute_hash_instr_key(snd_seq_instr_t *instr)
{
	int result;
	
	result = instr->cluster;
	result += instr->std;
	result += instr->bank;
	result += instr->prg;
	result += result >> 24;
	result += result >> 16;
	result += result >> 8;
	return result & (SND_SEQ_INSTR_HASH_SIZE-1);
}

static int compute_hash_cluster_key(snd_seq_instr_cluster_t cluster)
{
	int result;
	
	result = cluster;
	result += result >> 24;
	result += result >> 16;
	result += result >> 8;
	return result & (SND_SEQ_INSTR_HASH_SIZE-1);
}

static int compare_instr(snd_seq_instr_t *i1, snd_seq_instr_t *i2)
{
	return i1->cluster != i2->cluster ||
	       i2->std != i2->std ||
	       i2->bank != i2->bank ||
	       i2->prg != i2->prg;
}

snd_seq_kinstr_t *snd_seq_instr_find(snd_seq_kinstr_list_t *list,
				     snd_seq_instr_t *instr)
{
	unsigned long flags;
	int depth = 0;
	snd_seq_kinstr_t *result;

	if (list == NULL || instr == NULL)
		return NULL;
	snd_spin_lock(list, lock, &flags);
      __again:
	result = list->hash[compute_hash_instr_key(instr)];
	while (result) {
		if (!compare_instr(&result->instr, instr)) {
			if (result->type == SND_SEQ_INSTR_ATYPE_ALIAS) {
				instr = (snd_seq_instr_t *)KINSTR_DATA(result);
				if (++depth > 10)
					goto __not_found;
				goto __again;
			}
			result->use++;
			snd_spin_unlock(list, lock, &flags);
			return result;
		}
		result = result->next;
	}
      __not_found:
	snd_spin_unlock(list, lock, &flags);
	return NULL;
}

void snd_seq_instr_free_use(snd_seq_kinstr_list_t *list,
			    snd_seq_kinstr_t *instr)
{
	unsigned long flags;

	if (list == NULL || instr == NULL)
		return;
	snd_spin_lock(list, lock, &flags);
	if (instr->use <= 0) {
		snd_printk("snd_seq_instr_free_use: fatal!!! use = %i, name = '%s'\n", instr->use, instr->name);
	} else {
		instr->use--;
	}
	snd_spin_unlock(list, lock, &flags);
}

static void snd_instr_lock_ops(snd_seq_kinstr_list_t *list, int atomic)
{
	if (atomic) {
		snd_spin_lock(list, ops, &list->ops_flags);
	} else {
		snd_mutex_down(list, ops);
	}
}

static void snd_instr_unlock_ops(snd_seq_kinstr_list_t *list, int atomic)
{
	if (atomic) {
		snd_spin_unlock(list, ops, &list->ops_flags);
	} else {
		snd_mutex_up(list, ops);
	}
}

static snd_seq_kinstr_ops_t *instr_ops(snd_seq_kinstr_ops_t *ops, char *instr_type)
{
	while (ops) {
		if (!strcmp(ops->instr_type, instr_type))
			return ops;
		ops = ops->next;
	}
	return NULL;
}

static int instr_result(snd_seq_event_t *ev,
			int type, int result,
			int atomic)
{
	snd_seq_event_t sev;
	
	memset(&sev, 0, sizeof(sev));
	sev.type = type;
	sev.flags = SND_SEQ_TIME_STAMP_REAL | SND_SEQ_EVENT_LENGTH_FIXED |
	            SND_SEQ_PRIORITY_NORMAL | SND_SEQ_DEST_DIRECT;
	sev.source.queue = SND_SEQ_ADDRESS_UNKNOWN;
	sev.source.client = ev->dest.client;
	sev.source.port = ev->dest.port;
	sev.dest.queue = SND_SEQ_ADDRESS_UNKNOWN;
	sev.dest.client = ev->source.client;
	sev.dest.port = ev->source.port;
	sev.data.result = result;
	return snd_seq_kernel_client_dispatch(ev->source.client, &sev, atomic);
}

static int instr_begin(snd_seq_kinstr_ops_t *ops,
		       snd_seq_kinstr_list_t *list,
		       snd_seq_event_t *ev,
		       int atomic)
{
	unsigned long flags;

	snd_spin_lock(list, lock, &flags);
	if (list->owner >= 0 && list->owner != ev->source.client) {
		snd_spin_unlock(list, lock, &flags);
		return instr_result(ev, SND_SEQ_EVENT_INSTR_BEGIN_RESULT, -EBUSY, atomic);
	}
	list->owner = ev->source.client;
	snd_spin_unlock(list, lock, &flags);
	return instr_result(ev, SND_SEQ_EVENT_INSTR_BEGIN_RESULT, 0, atomic);
}

static int instr_end(snd_seq_kinstr_ops_t *ops,
		     snd_seq_kinstr_list_t *list,
		     snd_seq_event_t *ev,
		     int atomic)
{
	unsigned long flags;

	/* TODO: timeout handling */
	snd_spin_lock(list, lock, &flags);
	if (list->owner >= 0 && list->owner != ev->source.client) {
		list->owner = -1;
		snd_spin_unlock(list, lock, &flags);
		return instr_result(ev, SND_SEQ_EVENT_INSTR_END_RESULT, 0, atomic);
	}
	list->owner = ev->source.client;
	snd_spin_unlock(list, lock, &flags);
	return instr_result(ev, SND_SEQ_EVENT_INSTR_END_RESULT, -EINVAL, atomic);
}

static int instr_put(snd_seq_kinstr_ops_t *ops,
		     snd_seq_kinstr_list_t *list,
		     snd_seq_event_t *ev,
		     int atomic)
{
	unsigned long flags;
	int kernel_flag = 0;
	mm_segment_t fs;
	snd_seq_instr_put_t put;
	snd_seq_kinstr_t *instr;
	int result = 0, len, key;

	switch (ev->flags & SND_SEQ_EVENT_LENGTH_MASK) {
	case SND_SEQ_EVENT_LENGTH_FIXED:
	case SND_SEQ_EVENT_LENGTH_VARIPC:
		return -EINVAL;
	case SND_SEQ_EVENT_LENGTH_VARIABLE:
		kernel_flag = 1;
		break;
	case SND_SEQ_EVENT_LENGTH_VARUSR:
		kernel_flag = 0;
		break;
	}
	if (ev->data.ext.len < sizeof(snd_seq_instr_put_t))
		return -EINVAL;
	if (kernel_flag) {
		fs = snd_enter_user();
	} else {
		if (verify_area(VERIFY_READ, ev->data.ext.ptr, ev->data.ext.len))
			return -EPERM;
	}
	copy_from_user(&put, ev->data.ext.ptr, sizeof(snd_seq_instr_put_t));
	snd_instr_lock_ops(list, atomic);
	if ((instr = snd_seq_instr_find(list, &put.id))) {
		snd_seq_instr_free_use(list, instr);
		snd_instr_unlock_ops(list, atomic);
		result = -EBUSY;
		goto __return;
	}
	ops = instr_ops(ops, put.data.data.format);
	if (ops == NULL) {
		snd_instr_unlock_ops(list, atomic);
		result = -EINVAL;
		goto __return;
	}
	len = ops->add_len;
	if (put.data.type == SND_SEQ_INSTR_ATYPE_ALIAS)
		len = sizeof(snd_seq_instr_t);
	instr = snd_seq_instr_new(len, atomic);
	if (instr == NULL) {
		snd_instr_unlock_ops(list, atomic);
		result = -ENOMEM;
		goto __return;
	}
	instr->instr = put.id;
	strncpy(instr->name, put.data.name, sizeof(instr->name)-1);
	instr->name[sizeof(instr->name)-1] = '\0';
	instr->type = put.data.type;
	if (instr->type == SND_SEQ_INSTR_ATYPE_DATA) {
		result = ops->put(ops->private_data,
				  instr,
				  ev->data.ext.ptr,
				  ev->data.ext.len - sizeof(snd_seq_instr_put_t),
				  atomic);
		if (result < 0) {
			snd_seq_instr_free(instr);
			snd_instr_unlock_ops(list, atomic);
			goto __return;
		}
	}
	key = compute_hash_instr_key(&instr->instr);
	snd_spin_lock(list, lock, &flags);
	instr->next = list->hash[key];
	list->hash[key] = instr;
	snd_spin_unlock(list, lock, &flags);
	snd_instr_unlock_ops(list, atomic);
      __return:
	if (kernel_flag)
		snd_leave_user(fs);
	return result;
}

static int instr_get(snd_seq_kinstr_ops_t *ops,
		     snd_seq_kinstr_list_t *list,
		     snd_seq_event_t *ev,
		     int atomic)
{
	return -ENXIO;
}

static int instr_free(snd_seq_kinstr_ops_t *ops,
		      snd_seq_kinstr_list_t *list,
		      snd_seq_event_t *ev,
		      int atomic)
{
	return -ENXIO;
}

static int instr_list(snd_seq_kinstr_ops_t *ops,
		      snd_seq_kinstr_list_t *list,
		      snd_seq_event_t *ev,
		      int atomic)
{
	return -ENXIO;
}

static int instr_reset(snd_seq_kinstr_ops_t *ops,
		       snd_seq_kinstr_list_t *list,
		       snd_seq_event_t *ev,
		       int atomic)
{
	return -ENXIO;
}

static int instr_info(snd_seq_kinstr_ops_t *ops,
		      snd_seq_kinstr_list_t *list,
		      snd_seq_event_t *ev,
		      int atomic)
{
	return -ENXIO;
}

static int instr_status(snd_seq_kinstr_ops_t *ops,
			snd_seq_kinstr_list_t *list,
			snd_seq_event_t *ev,
			int atomic)
{
	return -ENXIO;
}

static int instr_cluster(snd_seq_kinstr_ops_t *ops,
			 snd_seq_kinstr_list_t *list,
			 snd_seq_event_t *ev,
			 int atomic)
{
	return -ENXIO;
}

int snd_seq_instr_event(snd_seq_kinstr_ops_t *ops,
			snd_seq_kinstr_list_t *list,
			snd_seq_event_t *ev,
			int atomic)
{
	if (ops == NULL || list == NULL || ev == NULL)
		return -EINVAL;
	if ((ev->flags & SND_SEQ_DEST_QUEUE) == SND_SEQ_DEST_DIRECT) {
		switch (ev->type) {
		case SND_SEQ_EVENT_INSTR_BEGIN:
			return instr_begin(ops, list, ev, atomic);
		case SND_SEQ_EVENT_INSTR_END:
			return instr_end(ops, list, ev, atomic);
		default:
			return -EINVAL;
		}
	}
	if (ops->flags & SND_SEQ_INSTR_OPS_DIRECT) {
		if ((ev->flags & SND_SEQ_DEST_MASK) != SND_SEQ_DEST_DIRECT)
			return -EINVAL;
	}
	switch (ev->type) {
	case SND_SEQ_EVENT_INSTR_PUT:
		return instr_put(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_GET:
		return instr_get(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_FREE:
		return instr_free(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_LIST:
		return instr_list(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_RESET:
		return instr_reset(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_INFO:
		return instr_info(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_STATUS:
		return instr_status(ops, list, ev, atomic);
	case SND_SEQ_EVENT_INSTR_CLUSTER:
		return instr_cluster(ops, list, ev, atomic);
	}
	return -EINVAL;
}
			
/*
 *  Init part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_seq_instr_export;
#endif

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

void cleanup_module(void)
{
}

#ifdef MODULE_PARM		/* hey - we have new 2.1.18+ kernel... */
MODULE_AUTHOR("Jaroslav Kysela <perex@suse.cz>");
MODULE_DESCRIPTION("Advanced Linux Sound Architecture sequencer instrument library.");
MODULE_SUPPORTED_DEVICE("sound");
#endif
