/*
 *   GF1 (GUS) Patch - Instrument routines
 *   Copyright (c) 1999 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 */
 
#include <sound/driver.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/ainstr_gf1.h>
#include <sound/initval.h>
#include <asm/uaccess.h>

MODULE_AUTHOR("Jaroslav Kysela <perex@suse.cz>");
MODULE_DESCRIPTION("Advanced Linux Sound Architecture GF1 (GUS) Patch support.");
MODULE_LICENSE("GPL");

static unsigned int snd_seq_gf1_size(unsigned int size, unsigned int format)
{
	unsigned int result = size;
	
	if (format & GF1_WAVE_16BIT)
		result <<= 1;
	if (format & GF1_WAVE_STEREO)
		result <<= 1;
	return format;
}

static int snd_seq_gf1_copy_wave_from_stream(snd_gf1_ops_t *ops,
					     gf1_instrument_t *ip,
					     char __user **data,
					     long *len,
					     int atomic)
{
	gf1_wave_t *wp, *prev;
	gf1_xwave_t xp;
	int err;
	unsigned int gfp_mask;
	unsigned int real_size;
	
	gfp_mask = atomic ? GFP_ATOMIC : GFP_KERNEL;
	if (*len < (long)sizeof(xp))
		return -EINVAL;
	if (copy_from_user(&xp, *data, sizeof(xp)))
		return -EFAULT;
	*data += sizeof(xp);
	*len -= sizeof(xp);
	wp = kcalloc(1, sizeof(*wp), gfp_mask);
	if (wp == NULL)
		return -ENOMEM;
	wp->share_id[0] = le32_to_cpu(xp.share_id[0]);
	wp->share_id[1] = le32_to_cpu(xp.share_id[1]);
	wp->share_id[2] = le32_to_cpu(xp.share_id[2]);
	wp->share_id[3] = le32_to_cpu(xp.share_id[3]);
	wp->format = le32_to_cpu(xp.format);
	wp->size = le32_to_cpu(xp.size);
	wp->start = le32_to_cpu(xp.start);
	wp->loop_start = le32_to_cpu(xp.loop_start);
	wp->loop_end = le32_to_cpu(xp.loop_end);
	wp->loop_repeat = le16_to_cpu(xp.loop_repeat);
	wp->flags = xp.flags;
	wp->sample_rate = le32_to_cpu(xp.sample_rate);
	wp->low_frequency = le32_to_cpu(xp.low_frequency);
	wp->high_frequency = le32_to_cpu(xp.high_frequency);
	wp->root_frequency = le32_to_cpu(xp.root_frequency);
	wp->tune = le16_to_cpu(xp.tune);
	wp->balance = xp.balance;
	memcpy(wp->envelope_rate, xp.envelope_rate, 6);
	memcpy(wp->envelope_offset, xp.envelope_offset, 6);
	wp->tremolo_sweep = xp.tremolo_sweep;
	wp->tremolo_rate = xp.tremolo_rate;
	wp->tremolo_depth = xp.tremolo_depth;
	wp->vibrato_sweep = xp.vibrato_sweep;
	wp->vibrato_rate = xp.vibrato_rate;
	wp->vibrato_depth = xp.vibrato_depth;
	wp->scale_frequency = le16_to_cpu(xp.scale_frequency);
	wp->scale_factor = le16_to_cpu(xp.scale_factor);
	real_size = snd_seq_gf1_size(wp->size, wp->format);
	if ((long)real_size > *len) {
		kfree(wp);
		return -ENOMEM;
	}
	if (ops->put_sample) {
		err = ops->put_sample(ops->private_data, wp,
				      *data, real_size, atomic);
		if (err < 0) {
			kfree(wp);
			return err;
		}
	}
	*data += real_size;
	*len -= real_size;
	prev = ip->wave;
	if (prev) {
		while (prev->next) prev = prev->next;
		prev->next = wp;
	} else {
		ip->wave = wp;
	}
	return 0;
}

static void snd_seq_gf1_wave_free(snd_gf1_ops_t *ops,
				  gf1_wave_t *wave,
				  int atomic)
{
	if (ops->remove_sample)
		ops->remove_sample(ops->private_data, wave, atomic);
	kfree(wave);
}

static void snd_seq_gf1_instr_free(snd_gf1_ops_t *ops,
				   gf1_instrument_t *ip,
				   int atomic)
{
	gf1_wave_t *wave;
	
	while ((wave = ip->wave) != NULL) {
		ip->wave = wave->next;
		snd_seq_gf1_wave_free(ops, wave, atomic);
	}
}

static int snd_seq_gf1_put(void *private_data, snd_seq_kinstr_t *instr,
			   char __user *instr_data, long len, int atomic,
			   int cmd)
{
	snd_gf1_ops_t *ops = (snd_gf1_ops_t *)private_data;
	gf1_instrument_t *ip;
	gf1_xinstrument_t ix;
	int err, gfp_mask;

	if (cmd != SNDRV_SEQ_INSTR_PUT_CMD_CREATE)
		return -EINVAL;
	gfp_mask = atomic ? GFP_ATOMIC : GFP_KERNEL;
	/* copy instrument data */
	if (len < (long)sizeof(ix))
		return -EINVAL;
	if (copy_from_user(&ix, instr_data, sizeof(ix)))
		return -EFAULT;
	if (ix.stype != GF1_STRU_INSTR)
		return -EINVAL;
	instr_data += sizeof(ix);
	len -= sizeof(ix);
	ip = (gf1_instrument_t *)KINSTR_DATA(instr);
	ip->exclusion = le16_to_cpu(ix.exclusion);
	ip->exclusion_group = le16_to_cpu(ix.exclusion_group);
	ip->effect1 = ix.effect1;
	ip->effect1_depth = ix.effect1_depth;
	ip->effect2 = ix.effect2;
	ip->effect2_depth = ix.effect2_depth;
	/* copy layers */
	while (len > (long)sizeof(__u32)) {
		__u32 stype;

		if (copy_from_user(&stype, instr_data, sizeof(stype)))
			return -EFAULT;
		if (stype != GF1_STRU_WAVE) {
			snd_seq_gf1_instr_free(ops, ip, atomic);
			return -EINVAL;
		}
		err = snd_seq_gf1_copy_wave_from_stream(ops,
							ip,
							&instr_data,
							&len,
							atomic);
		if (err < 0) {
			snd_seq_gf1_instr_free(ops, ip, atomic);
			return err;
		}
	}
	return 0;
}

static int snd_seq_gf1_copy_wave_to_stream(snd_gf1_ops_t *ops,
					   gf1_instrument_t *ip,
					   char __user **data,
					   long *len,
					   int atomic)
{
	gf1_wave_t *wp;
	gf1_xwave_t xp;
	int err;
	unsigned int real_size;
	
	for (wp = ip->wave; wp; wp = wp->next) {
		if (*len < (long)sizeof(xp))
			return -ENOMEM;
		memset(&xp, 0, sizeof(xp));
		xp.stype = GF1_STRU_WAVE;
		xp.share_id[0] = cpu_to_le32(wp->share_id[0]);
		xp.share_id[1] = cpu_to_le32(wp->share_id[1]);
		xp.share_id[2] = cpu_to_le32(wp->share_id[2]);
		xp.share_id[3] = cpu_to_le32(wp->share_id[3]);
		xp.format = cpu_to_le32(wp->format);
		xp.size = cpu_to_le32(wp->size);
		xp.start = cpu_to_le32(wp->start);
		xp.loop_start = cpu_to_le32(wp->loop_start);
		xp.loop_end = cpu_to_le32(wp->loop_end);
		xp.loop_repeat = cpu_to_le32(wp->loop_repeat);
		xp.flags = wp->flags;
		xp.sample_rate = cpu_to_le32(wp->sample_rate);
		xp.low_frequency = cpu_to_le32(wp->low_frequency);
		xp.high_frequency = cpu_to_le32(wp->high_frequency);
		xp.root_frequency = cpu_to_le32(wp->root_frequency);
		xp.tune = cpu_to_le16(wp->tune);
		xp.balance = wp->balance;
		memcpy(xp.envelope_rate, wp->envelope_rate, 6);
		memcpy(xp.envelope_offset, wp->envelope_offset, 6);
		xp.tremolo_sweep = wp->tremolo_sweep;
		xp.tremolo_rate = wp->tremolo_rate;
		xp.tremolo_depth = wp->tremolo_depth;
		xp.vibrato_sweep = wp->vibrato_sweep;
		xp.vibrato_rate = wp->vibrato_rate;
		xp.vibrato_depth = wp->vibrato_depth;
		xp.scale_frequency = cpu_to_le16(wp->scale_frequency);
		xp.scale_factor = cpu_to_le16(wp->scale_factor);
		if (copy_to_user(*data, &xp, sizeof(xp)))
			return -EFAULT;
		*data += sizeof(xp);
		*len -= sizeof(xp);
		real_size = snd_seq_gf1_size(wp->size, wp->format);
		if (*len < (long)real_size)
			return -ENOMEM;
		if (ops->get_sample) {
			err = ops->get_sample(ops->private_data, wp,
					      *data, real_size, atomic);
			if (err < 0)
				return err;
		}
		*data += wp->size;
		*len -= wp->size;
	}
	return 0;
}

static int snd_seq_gf1_get(void *private_data, snd_seq_kinstr_t *instr,
			   char __user *instr_data, long len, int atomic,
			   int cmd)
{
	snd_gf1_ops_t *ops = (snd_gf1_ops_t *)private_data;
	gf1_instrument_t *ip;
	gf1_xinstrument_t ix;
	
	if (cmd != SNDRV_SEQ_INSTR_GET_CMD_FULL)
		return -EINVAL;
	if (len < (long)sizeof(ix))
		return -ENOMEM;
	memset(&ix, 0, sizeof(ix));
	ip = (gf1_instrument_t *)KINSTR_DATA(instr);
	ix.stype = GF1_STRU_INSTR;
	ix.exclusion = cpu_to_le16(ip->exclusion);
	ix.exclusion_group = cpu_to_le16(ip->exclusion_group);
	ix.effect1 = cpu_to_le16(ip->effect1);
	ix.effect1_depth = cpu_to_le16(ip->effect1_depth);
	ix.effect2 = ip->effect2;
	ix.effect2_depth = ip->effect2_depth;
	if (copy_to_user(instr_data, &ix, sizeof(ix)))
		return -EFAULT;
	instr_data += sizeof(ix);
	len -= sizeof(ix);
	return snd_seq_gf1_copy_wave_to_stream(ops,
					       ip,
					       &instr_data,
					       &len,
					       atomic);
}

static int snd_seq_gf1_get_size(void *private_data, snd_seq_kinstr_t *instr,
				long *size)
{
	long result;
	gf1_instrument_t *ip;
	gf1_wave_t *wp;

	*size = 0;
	ip = (gf1_instrument_t *)KINSTR_DATA(instr);
	result = sizeof(gf1_xinstrument_t);
	for (wp = ip->wave; wp; wp = wp->next) {
		result += sizeof(gf1_xwave_t);
		result += wp->size;
	}
	*size = result;
	return 0;
}

static int snd_seq_gf1_remove(void *private_data,
			      snd_seq_kinstr_t *instr,
                              int atomic)
{
	snd_gf1_ops_t *ops = (snd_gf1_ops_t *)private_data;
	gf1_instrument_t *ip;

	ip = (gf1_instrument_t *)KINSTR_DATA(instr);
	snd_seq_gf1_instr_free(ops, ip, atomic);
	return 0;
}

static void snd_seq_gf1_notify(void *private_data,
			       snd_seq_kinstr_t *instr,
			       int what)
{
	snd_gf1_ops_t *ops = (snd_gf1_ops_t *)private_data;

	if (ops->notify)
		ops->notify(ops->private_data, instr, what);
}

int snd_seq_gf1_init(snd_gf1_ops_t *ops,
		     void *private_data,
		     snd_seq_kinstr_ops_t *next)
{
	memset(ops, 0, sizeof(*ops));
	ops->private_data = private_data;
	ops->kops.private_data = ops;
	ops->kops.add_len = sizeof(gf1_instrument_t);
	ops->kops.instr_type = SNDRV_SEQ_INSTR_ID_GUS_PATCH;
	ops->kops.put = snd_seq_gf1_put;
	ops->kops.get = snd_seq_gf1_get;
	ops->kops.get_size = snd_seq_gf1_get_size;
	ops->kops.remove = snd_seq_gf1_remove;
	ops->kops.notify = snd_seq_gf1_notify;
	ops->kops.next = next;
	return 0;
}

/*
 *  Init part
 */

static int __init alsa_ainstr_gf1_init(void)
{
	return 0;
}

static void __exit alsa_ainstr_gf1_exit(void)
{
}

module_init(alsa_ainstr_gf1_init)
module_exit(alsa_ainstr_gf1_exit)

EXPORT_SYMBOL(snd_seq_gf1_init);
