/*
 *  Hardware dependent layer
 *  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_MAIN_OBJECT_FILE
#include "../include/driver.h"
#include "../include/control.h"
#include "../include/minors.h"
#include "../include/hwdep.h"
#include "../include/info.h"

snd_hwdep_t *snd_hwdep_devices[SND_CARDS * SND_MINOR_HWDEPS];

static DECLARE_MUTEX(register_mutex);

/*

 */

static long long snd_hwdep_lseek(struct file * file, long long offset, int orig)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return -EINVAL;
	if (hw->ops.lseek)
		return hw->ops.lseek(hw, file, offset, orig);
	return -ENXIO;
}

static long snd_hwdep_read(struct file * file, char *buf, long count)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return -EINVAL;
	if (hw->ops.read)
		return hw->ops.read(hw, buf, count);
	return -ENXIO;	
}

static long snd_hwdep_write(struct file * file, const char *buf, long count)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return -EINVAL;
	if (hw->ops.write)
		return hw->ops.write(hw, buf, count);
	return -ENXIO;	
}

static int snd_hwdep_open(unsigned short minor, int cardnum, int device, struct file * file)
{
	snd_hwdep_t *hw = snd_hwdep_devices[(cardnum * SND_MINOR_HWDEPS) + device];
	int err;
	
	if (hw == NULL)
		return -ENXIO;
	if (hw->ops.open) {
		err = hw->ops.open(hw, file);
		if (err >= 0)
			file->private_data = hw;
		return err;
	}
	return -ENXIO;	
}

static int snd_hwdep_release(unsigned short minor, int cardnum, int device, struct file * file)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;

	if (hw == NULL)
		return -EINVAL;
	if (hw->ops.release)
		return hw->ops.release(hw, file);
	return -ENXIO;	
}

static unsigned int snd_hwdep_poll(struct file * file, poll_table * wait)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return 0;
	if (hw->ops.poll)
		return hw->ops.poll(hw, file, wait);
	return 0;
}

static int snd_hwdep_info(snd_hwdep_t *hw, snd_hwdep_info_t *_info)
{
	snd_hwdep_info_t info;
	
	memset(&info, 0, sizeof(info));
	info.type = hw->card->type;
	strncpy(info.id, hw->id, sizeof(info.id) - 1);	
	strncpy(info.name, hw->name, sizeof(info.name) - 1);
	info.hw_type = hw->type;
	if (copy_to_user(_info, &info, sizeof(info)))
		return -EFAULT;
	return 0;
}

static int snd_hwdep_ioctl(struct file * file, unsigned int cmd, unsigned long arg)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return 0;
	if (cmd == SND_HWDEP_IOCTL_PVERSION)
		return snd_ioctl_out((long *)arg, SND_HWDEP_VERSION);
	if (cmd == SND_HWDEP_IOCTL_INFO)
		return snd_hwdep_info(hw, (snd_hwdep_info_t *)arg);
	if (hw->ops.ioctl)
		return hw->ops.ioctl(hw, file, cmd, arg);
	return 0;
}

static int snd_hwdep_mmap(struct inode * inode, struct file * file, struct vm_area_struct * vma)
{
	snd_hwdep_t *hw = (snd_hwdep_t *)file->private_data;
	
	if (hw == NULL)
		return -ENXIO;
	if (hw->ops.mmap)
		return hw->ops.mmap(hw, inode, file, vma);
	return -ENXIO;
}

static int snd_hwdep_control_ioctl(snd_card_t * card, snd_control_t * control,
				   unsigned int cmd, unsigned long arg)
{
	int idx;
	unsigned int tmp;
	snd_hwdep_t *hw;
	
	tmp = card->number * SND_MINOR_HWDEPS;
	switch (cmd) {
	case SND_CTL_IOCTL_HW_INFO:
		{
			struct snd_ctl_hw_info *ptr = (struct snd_ctl_hw_info *) arg;
			
			ptr->hwdepdevs = 0;
			for (idx = SND_MINOR_HWDEPS - 1; idx >= 0; idx--) {
				if (snd_hwdep_devices[tmp + idx]) {
					ptr->hwdepdevs = idx + 1;
					break;
				}
			}
			return 0;
		}
	case SND_CTL_IOCTL_HWDEP_DEVICE:
		{
			int val = snd_ioctl_in((long *) arg);
			if (val < 0 || val >= SND_MINOR_HWDEPS)
				return -EINVAL;
			if (!snd_hwdep_devices[tmp + val])
				return -EINVAL;
			control->hwdep_device = val;
			return 0;
		}
	case SND_CTL_IOCTL_HWDEP_INFO:
		{
			hw = snd_hwdep_devices[tmp + control->hwdep_device];
			if (!hw)
				return -ENOENT;
			return snd_hwdep_info(hw, (snd_hwdep_info_t *)arg);
		}
	}
	return -EAGAIN;
}

/*

 */

static snd_minor_t snd_hwdep_reg =
{
	"hardware dependent",	/* comment */
	NULL,			/* reserved */

	NULL,			/* unregister */

	snd_hwdep_lseek,	/* lseek */
	snd_hwdep_read,		/* read */
	snd_hwdep_write,	/* write */
	snd_hwdep_open,		/* open */
	snd_hwdep_release,	/* release */
	snd_hwdep_poll,		/* poll */
	snd_hwdep_ioctl,	/* ioctl */
	snd_hwdep_mmap,		/* mmap */
};

snd_hwdep_t *snd_hwdep_new_device(snd_card_t * card, char *id)
{
	snd_hwdep_t *hwdep;

	hwdep = (snd_hwdep_t *) snd_kcalloc(sizeof(snd_hwdep_t), GFP_KERNEL);
	if (!hwdep)
		return NULL;
	hwdep->card = card;
	if (id) {
		strncpy(hwdep->id, id, sizeof(hwdep->id) - 1);
	}
	return hwdep;
}

int snd_hwdep_free(snd_hwdep_t * hwdep)
{
	if (!hwdep)
		return -EINVAL;
	if (hwdep->private_free)
		hwdep->private_free(hwdep->private_data);
	snd_kfree(hwdep);
	return 0;
}

int snd_hwdep_register(snd_hwdep_t * hwdep, int device)
{
	int idx, err;
	char name[32];

	if (!hwdep || device < 0 || device >= SND_MINOR_HWDEPS)
		return -EINVAL;
	down(&register_mutex);
	idx = (hwdep->card->number * SND_MINOR_HWDEPS) + device;
	if (snd_hwdep_devices[idx]) {
		up(&register_mutex);
		return -EBUSY;
	}
	hwdep->device = device;
	snd_hwdep_devices[idx] = hwdep;
	sprintf(name, "hwC%iD%i", hwdep->card->number, device);
	if ((err = snd_register_device(SND_DEVICE_TYPE_HWDEP,
				       hwdep->card, device,
				       &snd_hwdep_reg, name)) < 0) {
		snd_hwdep_devices[idx] = NULL;
		up(&register_mutex);
		return err;
	}
	up(&register_mutex);
	return 0;
}

int snd_hwdep_unregister(snd_hwdep_t * hwdep)
{
	int idx;

	if (!hwdep)
		return -EINVAL;
	down(&register_mutex);
	idx = (hwdep->card->number * SND_MINOR_HWDEPS) + hwdep->device;
	if (snd_hwdep_devices[idx] != hwdep) {
		up(&register_mutex);
		return -EINVAL;
	}
	snd_unregister_device(SND_DEVICE_TYPE_HWDEP, hwdep->card, hwdep->device);
	snd_hwdep_devices[idx] = NULL;
	up(&register_mutex);
	return snd_hwdep_free(hwdep);
}

/*
 *  Info interface
 */

static void snd_hwdep_proc_read(snd_info_buffer_t * buffer, void *private_data)
{
	int idx;
	snd_hwdep_t *hwdep;

	down(&register_mutex);
	for (idx = 0; idx < SND_CARDS * SND_MINOR_HWDEPS; idx++) {
		hwdep = snd_hwdep_devices[idx];
		if (hwdep == NULL)
			continue;
		snd_iprintf(buffer, "%02i-%02i: %s\n",
					idx / SND_MINOR_HWDEPS,
					idx % SND_MINOR_HWDEPS,
					hwdep->name);
	}
	up(&register_mutex);
}

/*
 *  ENTRY functions
 */

static snd_info_entry_t *snd_hwdep_proc_entry = NULL;

int init_module(void)
{
	snd_info_entry_t *entry;

	memset(snd_hwdep_devices, 0, sizeof(snd_hwdep_devices));
	if ((entry = snd_info_create_entry(NULL, "hwdep")) != NULL) {
		entry->t.text.read_size = 512;
		entry->t.text.read = snd_hwdep_proc_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	snd_hwdep_proc_entry = entry;
	snd_control_register_ioctl(snd_hwdep_control_ioctl);
	return 0;
}

void cleanup_module(void)
{
	snd_control_unregister_ioctl(snd_hwdep_control_ioctl);
	if (snd_hwdep_proc_entry) {
		snd_info_unregister(snd_hwdep_proc_entry);
		snd_hwdep_proc_entry = NULL;
	}
}

EXPORT_SYMBOL(snd_hwdep_new_device);
EXPORT_SYMBOL(snd_hwdep_free);
EXPORT_SYMBOL(snd_hwdep_register);
EXPORT_SYMBOL(snd_hwdep_unregister);
