/*
 *  OSS emulation layer for the mixer interface
 *  Copyright (c) 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_OSS_COMPAT__
#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "minors.h"
#include "mixer.h"
#include "info.h"

extern snd_kmixer_t *snd_mixers[];

static int snd_mixer_oss_open(unsigned short minor, int cardnum, int device,
                              struct file *file)
{
	snd_kmixer_t *mixer;

	if (!(mixer = snd_mixers[(cardnum * SND_MINOR_MIXERS) + device]))
		return -ENODEV;
	file->private_data = mixer;
	MOD_INC_USE_COUNT;
	mixer->card->use_inc(mixer->card);
	return 0;
}

static int snd_mixer_oss_release(unsigned short minor, int cardnum, int device,
                                 struct file *file)
{
	snd_kmixer_t *mixer;

	if (file->private_data) {
		mixer = (snd_kmixer_t *) file->private_data;
		mixer->card->use_dec(mixer->card);
		MOD_DEC_USE_COUNT;
	}
	return 0;
}

static int snd_mixer_oss_info(snd_kmixer_t *mixer, struct snd_oss_mixer_info *_info)
{
	struct snd_oss_mixer_info info;
	
	memset(&info, 0, sizeof(info));
	strncpy(info.id, mixer->id, sizeof(info.id) - 1);
	strncpy(info.name, mixer->name, sizeof(info.name) - 1);
	info.modify_counter = mixer->oss_change_count;
	if (verify_area(VERIFY_WRITE, _info, sizeof(*_info)))
		return -EFAULT;
	copy_to_user(_info, &info, sizeof(info));
	return 0;
}

static int snd_mixer_oss_info_obsolete(snd_kmixer_t *mixer, struct snd_oss_mixer_info_obsolete *_info)
{
	struct snd_oss_mixer_info_obsolete info;
	
	memset(&info, 0, sizeof(info));
	strncpy(info.id, mixer->id, sizeof(info.id) - 1);
	strncpy(info.name, mixer->name, sizeof(info.name) - 1);
	if (verify_area(VERIFY_WRITE, _info, sizeof(*_info)))
		return -EFAULT;
	copy_to_user(_info, &info, sizeof(info));
	return 0;
}

static snd_kmixer_group_t *snd_mixer_oss_group_generic(snd_kmixer_t *mixer, int channel)
{
	switch (channel) {
	case SND_MIXER_OSS_VOLUME:
		return snd_mixer_group_find(mixer, SND_MIXER_OUT_MASTER, 0);
	case SND_MIXER_OSS_BASS:
	case SND_MIXER_OSS_TREBLE:
		return snd_mixer_group_find(mixer, SND_MIXER_GRP_TONE_CONTROL, 0);
	case SND_MIXER_OSS_SYNTH:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_SYNTHESIZER, 0);
	case SND_MIXER_OSS_PCM:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_PCM, 0);
	case SND_MIXER_OSS_SPEAKER:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_SPEAKER, 0);
	case SND_MIXER_OSS_LINE:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_LINE, 0);
	case SND_MIXER_OSS_MIC:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_MIC, 0);
	case SND_MIXER_OSS_CD:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_CD, 0);
	case SND_MIXER_OSS_IMIX:
		return snd_mixer_group_find(mixer, SND_MIXER_GRP_IGAIN, 0);
	case SND_MIXER_OSS_ALTPCM:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_PCM, 1);
	case SND_MIXER_OSS_RECLEV:
		return snd_mixer_group_find(mixer, SND_MIXER_GRP_IGAIN, 0);
	case SND_MIXER_OSS_IGAIN:
		return snd_mixer_group_find(mixer, SND_MIXER_GRP_IGAIN, 0);
	case SND_MIXER_OSS_OGAIN:
		return snd_mixer_group_find(mixer, SND_MIXER_GRP_OGAIN, 0);
	case SND_MIXER_OSS_LINE1:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_AUX, 0);
	case SND_MIXER_OSS_LINE2:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_AUX, 1);
	case SND_MIXER_OSS_LINE3:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_AUX, 2);
	case SND_MIXER_OSS_DIGITAL1:
	case SND_MIXER_OSS_DIGITAL2:
	case SND_MIXER_OSS_DIGITAL3:
		return NULL;
	case SND_MIXER_OSS_PHONEIN:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_PHONE, 0);
	case SND_MIXER_OSS_PHONEOUT:
		return snd_mixer_group_find(mixer, SND_MIXER_OUT_PHONE, 0);
	case SND_MIXER_OSS_VIDEO:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_VIDEO, 0);
	case SND_MIXER_OSS_RADIO:
		return snd_mixer_group_find(mixer, SND_MIXER_IN_RADIO, 0);
	case SND_MIXER_OSS_MONITOR:
		return NULL;
	default:
		return NULL;
	}
}

static snd_kmixer_group_t *snd_mixer_oss_group(snd_kmixer_t *mixer, int channel)
{
	return snd_mixer_oss_group_generic(mixer, channel);
}

static int snd_mixer_oss_set_recsrc(snd_kmixer_t *mixer, int recsrc)
{
	return -EIO;
}

static int snd_mixer_oss_devmask(snd_kmixer_t *mixer)
{
	int idx, result;

	snd_mixer_lock(mixer, 0);
	for (idx = result = 0; idx < 32; idx++)
		if (snd_mixer_oss_group(mixer, idx))
			result |= 1 << idx;
	snd_mixer_lock(mixer, 1);
	return result;
}

static int snd_mixer_oss_stereodevs(snd_kmixer_t *mixer)
{
	int idx, idx1, result;
	snd_kmixer_group_t *group;
	snd_kmixer_element_t *element;
#if 0
	snd_mixer_element_t selement;
#endif
	snd_mixer_element_info_t info;
	snd_kmixer_file_t mfile;

	memset(&mfile, 0, sizeof(mfile));
	mfile.mixer = mixer;
	snd_mixer_lock(mixer, 0);
	for (idx = result = 0; idx < 32; idx++) {
		if ((group = snd_mixer_oss_group(mixer, idx)) != NULL) {
			for (idx1 = 0; idx1 < group->elements_count; idx1++) {
				element = group->elements[idx1];
				if (element->type == SND_MIXER_ETYPE_VOLUME1) {
					memset(&info, 0, sizeof(info));
					if (element->info(&mfile, &info, element->private_data) >= 0) {
						if (info.data.volume1.range_over == 2) {
							result |= 1 << idx;
							break;
						}
					}
				}
#if 0
				  else if (element->type == SND_MIXER_ETYPE_SWITCH1) {
					memset(&selement, 0, sizeof(selement));
					if (element->control(&mfile, SND_MIXER_CMD_READ, &selement, sizeof(selement), element->private_data) >= 0) {
						if (selement.data.switch1.sw_over == 2) {
							result |= 1 << idx;
							break;
						}
					}
				}
#endif
			}
		}
	}
	snd_mixer_lock(mixer, 1);
	return result;
}

static int snd_mixer_oss_recmask(snd_kmixer_t *mixer)
{
	return 0;
}

static int snd_mixer_oss_caps(snd_kmixer_t *mixer)
{
	int result;
	snd_kmixer_element_t *element;

	result = 0;
	snd_mixer_lock(mixer, 0);
	for (element = mixer->elements; element; element = element->next) {
		if ((element->type == SND_MIXER_ETYPE_MUX1 ||
		     element->type == SND_MIXER_ETYPE_MUX2) &&
		    (!strstr(element->name, "Input") ||
		     !strstr(element->name, "In-"))) {
			result |= SND_MIXER_OSS_CAP_EXCL_INPUT;
			break;
		}
	}
	snd_mixer_lock(mixer, 1);
	return result;
}

static int snd_mixer_oss_recsrc(snd_kmixer_t *mixer)
{
	return 0;
}

static int snd_mixer_oss_conv(int val, int omin, int omax, int nmin, int nmax)
{
	int orange = omax - omin, nrange = nmax - nmin;
	
	if (orange == 0)
		return 0;
	return ((nrange * (val - omin)) + (orange / 2)) / orange + nmin;
}

static int snd_mixer_oss_conv1(int val, int min, int max)
{
	return snd_mixer_oss_conv(val, min, max, 0, 100);
}

static int snd_mixer_oss_conv2(int val, int min, int max)
{
	return snd_mixer_oss_conv(val, 0, 100, min, max);
}

static int snd_mixer_oss_output_element(snd_kmixer_t *mixer, snd_kmixer_element_t *element)
{
	/* this code should be better */
	if (!strstr(element->name, "Input") ||
	    !strstr(element->name, "In-"))
		return 0;
	return 1;
}

static int snd_mixer_oss_get_volume(snd_kmixer_t *mixer, int channel)
{
	int left, right, idx;
	snd_kmixer_group_t *group;
	snd_kmixer_element_t *element;
	snd_kmixer_file_t mfile;
	snd_mixer_element_info_t info;
	snd_mixer_element_t selement;
	mm_segment_t fs;

	memset(&mfile, 0, sizeof(mfile));
	mfile.mixer = mixer;
	left = right = 0;
	snd_mixer_lock(mixer, 0);
	if ((group = snd_mixer_oss_group(mixer, channel)) == NULL) {
		snd_mixer_lock(mixer, 1);
		return -ENXIO;
	}
	for (idx = 0; idx < group->elements_count; idx++) {
		element = group->elements[idx];
		if (element->type == SND_MIXER_ETYPE_VOLUME1) {
			int values[2];
			struct snd_mixer_element_volume1_range ranges[2];
			
			memset(&info, 0, sizeof(info));
			info.data.volume1.range_size = 2;
			info.data.volume1.prange = ranges;
			fs = snd_enter_user();
			if (element->info(&mfile, &info, element->private_data) < 0) {
				snd_leave_user(fs);
				continue;
			}
			snd_leave_user(fs);
			if (info.data.volume1.range_over > 0)
				continue;
			memset(&selement, 0, sizeof(selement));
			selement.data.volume1.voices_size = info.data.volume1.range;
			selement.data.volume1.pvoices = values;
			fs = snd_enter_user();
			element->control(&mfile, SND_MIXER_CMD_READ, &selement, sizeof(selement), element->private_data);
			snd_leave_user(fs);
			left = snd_mixer_oss_conv1(values[0], ranges[0].min, ranges[0].max);
			if (info.data.volume1.range > 1) {
				right = snd_mixer_oss_conv1(values[1], ranges[1].min, ranges[1].max);
			} else {
				right = left;
			}
		} else if (element->type == SND_MIXER_ETYPE_SWITCH1) {
			unsigned int value;

			if (!snd_mixer_oss_output_element(mixer, element))
				continue;
			if (group->elements_count > 1)
				continue;
			memset(&selement, 0, sizeof(selement));
			selement.data.switch1.sw_size = 2;
			selement.data.switch1.psw = &value;
			value = 0;
			if (element->control(&mfile, SND_MIXER_CMD_READ, &selement, sizeof(selement), element->private_data) < 0)
				continue;
			if (selement.data.switch1.sw_over > 0)
				continue;
 			left = snd_mixer_get_bit(&value, 0) ? 100 : 0;
			if (selement.data.switch1.sw > 1)
				right = snd_mixer_get_bit(&value, 1) ? 100 : 0;
		}
	}
	snd_mixer_lock(mixer, 1);
 	return (left & 0xff) | ((right & 0xff) << 8);
}

static int snd_mixer_oss_set_volume(snd_kmixer_t *mixer, int channel, int volume)
{
	int left, right, idx;
	snd_kmixer_group_t *group;
	snd_kmixer_element_t *element;
	snd_kmixer_file_t mfile;
	snd_mixer_element_info_t info;
	snd_mixer_element_t selement;
	mm_segment_t fs;

	memset(&mfile, 0, sizeof(mfile));
	mfile.mixer = mixer;
	left = volume & 0xff;
	right = (volume >> 8) & 0xff;
	snd_mixer_lock(mixer, 0);
	if ((group = snd_mixer_oss_group(mixer, channel)) == NULL) {
		snd_mixer_lock(mixer, 1);
		return -ENXIO;
	}
	for (idx = 0; idx < group->elements_count; idx++) {
		element = group->elements[idx];
		if (element->type == SND_MIXER_ETYPE_VOLUME1) {
			int values[2];
			struct snd_mixer_element_volume1_range ranges[2];
			
			memset(&info, 0, sizeof(info));
			info.data.volume1.range_size = 2;
			info.data.volume1.prange = ranges;
			fs = snd_enter_user();
			if (element->info(&mfile, &info, element->private_data) < 0) {
				snd_leave_user(fs);
				continue;
			}
			snd_leave_user(fs);
			if (info.data.volume1.range_over > 0)
				continue;
			values[0] = snd_mixer_oss_conv2(left, ranges[0].min, ranges[0].max);
			if (info.data.volume1.range > 1)
				values[1] = snd_mixer_oss_conv2(right, ranges[1].min, ranges[1].max);
			memset(&selement, 0, sizeof(selement));
			selement.data.volume1.voices_size =
			  selement.data.volume1.voices = info.data.volume1.range;
			selement.data.volume1.pvoices = values;
			fs = snd_enter_user();
			element->control(&mfile, SND_MIXER_CMD_WRITE, &selement, sizeof(selement), element->private_data);
			snd_leave_user(fs);
		} else if (element->type == SND_MIXER_ETYPE_SWITCH1) {
			unsigned int value;

			if (snd_mixer_oss_output_element(mixer, element))
				continue;
			memset(&selement, 0, sizeof(selement));
			if (element->control(&mfile, SND_MIXER_CMD_READ, &selement, sizeof(selement), element->private_data) < 0)
				continue;
			if (selement.data.switch1.sw_over > 2)
				continue;
			selement.data.switch1.sw_size =
			  selement.data.switch1.sw = selement.data.switch1.sw_over;
			selement.data.switch1.sw_over = 0;
			value = 0;
			snd_mixer_set_bit(&value, 0, left > 0);
			if (selement.data.switch1.sw > 1)
				snd_mixer_set_bit(&value, 1, right > 0);
			selement.data.switch1.psw = &value;
			fs = snd_enter_user();
			element->control(&mfile, SND_MIXER_CMD_WRITE, &selement, sizeof(selement), element->private_data);
			snd_leave_user(fs);
		}
	}
	snd_mixer_lock(mixer, 1);
	return snd_mixer_oss_get_volume(mixer, channel);
}

int snd_mixer_oss_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	snd_kmixer_t *mixer;
	int tmp;

	mixer = (snd_kmixer_t *) file->private_data;
	if (!mixer)
		return -EIO;
	if (((cmd >> 8) & 0xff) == 'M') {
		switch (cmd) {
		case SND_MIXER_OSS_INFO:
			return snd_mixer_oss_info(mixer, (struct snd_oss_mixer_info *)arg);
		case SND_MIXER_OSS_OLD_INFO:
 			return snd_mixer_oss_info_obsolete(mixer, (struct snd_oss_mixer_info_obsolete *)arg);
		case SND_MIXER_OSS_SET_RECSRC:
			tmp = snd_mixer_oss_set_recsrc(mixer, snd_ioctl_in((long *) arg));
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		case SND_OSS_GETVERSION:
			return snd_ioctl_out((long *) arg, SND_OSS_VERSION);
		case SND_MIXER_OSS_DEVMASK:
			tmp = snd_mixer_oss_devmask(mixer);
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		case SND_MIXER_OSS_STEREODEVS:
			tmp = snd_mixer_oss_stereodevs(mixer);
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		case SND_MIXER_OSS_RECMASK:
			tmp = snd_mixer_oss_recmask(mixer);
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		case SND_MIXER_OSS_CAPS:
			tmp = snd_mixer_oss_caps(mixer);
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		case SND_MIXER_OSS_RECSRC:
			tmp = snd_mixer_oss_recsrc(mixer);
			return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
		}
	}
	if (cmd & IOC_IN) {
		tmp = snd_mixer_oss_set_volume(mixer, cmd & 0xff, snd_ioctl_in((long *) arg));
		return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
	} else if (cmd & IOC_OUT) {
		tmp = snd_mixer_oss_get_volume(mixer, cmd & 0xff);
		return tmp >= 0 ? snd_ioctl_out((long *) arg, tmp) : tmp;
	}
	return -ENXIO;
}

/*
 *  REGISTRATION PART
 */

static snd_minor_t snd_mixer_oss_reg =
{
	"mixer",		/* comment */
	NULL,			/* reserved */

	NULL,			/* unregister */

	NULL,			/* lseek */
	NULL,			/* read */
	NULL,			/* write */
	snd_mixer_oss_open,	/* open */
	snd_mixer_oss_release,	/* release */
#ifdef SND_POLL
	NULL,			/* poll */
#else
	NULL,			/* select */
#endif
	snd_mixer_oss_ioctl,	/* ioctl */
	NULL			/* mmap */
};

/*
 *  INIT PART
 */

static int snd_mixer_oss_register_minor(unsigned short native_minor,
					snd_kmixer_t * mixer)
{
	char name[32];
	
	mixer->ossreg = 0;
	if (mixer->device < 1) {
		sprintf(name, "mixer%i%i", mixer->card->number, mixer->device);
		if (snd_register_oss_device(SND_OSS_DEVICE_TYPE_MIXER,
				mixer->card, mixer->device, &snd_mixer_oss_reg,
				name) < 0) {
			snd_printk("unable to register OSS mixer device %i:%i\n", mixer->card->number, mixer->device);
		} else {
			snd_oss_info_register(SND_OSS_INFO_DEV_MIXERS,
					      mixer->device,
					      mixer->name);
			mixer->ossreg = 1;
		}
	}
	return 0;
}

static int snd_mixer_oss_unregister_minor(unsigned short native_minor,
					  snd_kmixer_t * mixer)
{
	if (mixer->ossreg) {
		snd_unregister_oss_device(SND_OSS_DEVICE_TYPE_MIXER,
				mixer->card, mixer->device);
		snd_oss_info_unregister(SND_OSS_INFO_DEV_MIXERS, mixer->device);
		mixer->ossreg = 0;
	}
	return 0;
}

static struct snd_stru_mixer_notify snd_mixer_oss_notify =
{
	snd_mixer_oss_register_minor,
	snd_mixer_oss_unregister_minor,
	NULL
};

int init_module(void)
{
	int err;

#ifndef LINUX_2_1
	if (register_symtab(NULL) < 0)
		return -ENOMEM;
#endif
	if ((err = snd_mixer_notify(&snd_mixer_oss_notify, 0)) < 0)
		return err;
	return 0;
}

void cleanup_module(void)
{
	snd_mixer_notify(&snd_mixer_oss_notify, 1);
}
