/*
 *  Copyright (C) 2004 Christopher John Purnell
 *                     cjp@lost.org.uk
 *
 *  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 <sys/types.h>
#include <sys/fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <ansidecl.h>
#include <alsa/asoundlib.h>

#include "alc_device.h"
#include "alc_context.h"
#include "alc_error.h"

#define _ALC_DEF_FREQ 44100
#define _ALC_DEF_REFRESH 20
#define _ALC_NUM_PERIODS 2

static int send_routing[] = {
	0, 1, 2, 3,
	0, 2, 6, 7,
	1, 3, 6, 7
};

ALCboolean _alcOpenSource(AL_source *src)
{
	snd_pcm_hw_params_t *hw_params;
	snd_pcm_info_t *info;
	snd_ctl_elem_value_t *route_ctl;
	snd_pcm_uframes_t size;
	unsigned i;
	AL_context *ctx = src->context;
	ALCdevice *dev = ctx->device;

	if (snd_pcm_open(&src->handle, dev->device, SND_PCM_STREAM_PLAYBACK,
			 SND_PCM_NONBLOCK))
		return ALC_FALSE;

	snd_pcm_hw_params_alloca(&hw_params);

	if (snd_pcm_hw_params_any(src->handle, hw_params))
		return ALC_FALSE;
	if (snd_pcm_hw_params_set_access(src->handle, hw_params,
					 SND_PCM_ACCESS_MMAP_INTERLEAVED))
		return ALC_FALSE;
	if (snd_pcm_hw_params_set_format(src->handle, hw_params,
					 SND_PCM_FORMAT_S16))
		return ALC_FALSE;
	if (snd_pcm_hw_params_set_channels(src->handle, hw_params, 2))
		return ALC_FALSE;

	src->freq = dev->freq;
	if (snd_pcm_hw_params_set_rate_near(src->handle, hw_params,
					    &src->freq, 0))
		return ALC_FALSE;

	if (snd_pcm_hw_params_set_periods(src->handle, hw_params,
					  _ALC_NUM_PERIODS, 0))
		return ALC_FALSE;

	size = src->freq * _ALC_NUM_PERIODS / dev->refresh;
	if (snd_pcm_hw_params_set_buffer_size_near(src->handle, hw_params,
						   &size))
		return ALC_FALSE;

	if (snd_pcm_hw_params(src->handle, hw_params))
		return ALC_FALSE;

	snd_pcm_info_alloca(&info);

	if (snd_pcm_info(src->handle, info))
		return ALC_FALSE;

	src->subdev = snd_pcm_info_get_subdevice(info);

	if (snd_ctl_elem_value_malloc(&src->vol_ctl))
		return ALC_FALSE;		
	snd_ctl_elem_value_clear(src->vol_ctl);
	snd_ctl_elem_value_set_interface(src->vol_ctl,
					 SND_CTL_ELEM_IFACE_MIXER);
	snd_ctl_elem_value_set_name(src->vol_ctl,
				    "EMU10K1 PCM Volume");
	snd_ctl_elem_value_set_index(src->vol_ctl, src->subdev);

	if (snd_ctl_elem_value_malloc(&src->send_ctl))
		return ALC_FALSE;
	snd_ctl_elem_value_clear(src->send_ctl);
	snd_ctl_elem_value_set_interface(src->send_ctl,
					 SND_CTL_ELEM_IFACE_MIXER);
	snd_ctl_elem_value_set_name(src->send_ctl,
				    "EMU10K1 PCM Send Volume");
	snd_ctl_elem_value_set_index(src->send_ctl, src->subdev);

	snd_ctl_elem_value_alloca(&route_ctl);
	snd_ctl_elem_value_clear(route_ctl);
	snd_ctl_elem_value_set_interface(route_ctl,
					 SND_CTL_ELEM_IFACE_MIXER);
	snd_ctl_elem_value_set_name(route_ctl,
				    "EMU10K1 PCM Send Routing");
	snd_ctl_elem_value_set_index(route_ctl, src->subdev);

	for (i = 0; i < 12; i++)
	{	
		snd_ctl_elem_value_set_integer(route_ctl, i, send_routing[i]);
	}

	if (snd_ctl_elem_write(dev->ctl, route_ctl))
		return ALC_FALSE;

	return ALC_TRUE;
}

ALCvoid _alcCloseSource(AL_source *src)
{
	if (src->send_ctl) snd_ctl_elem_value_free(src->send_ctl);
	if (src->vol_ctl) snd_ctl_elem_value_free(src->vol_ctl);
	if (src->handle) snd_pcm_close(src->handle);
}

static ALCboolean _alcOpenDevice(ALCdevice *dev)
{
	snd_pcm_info_t *info;
	int card = -1;

	snd_pcm_info_alloca(&info);

	while (snd_card_next(&card) >= 0 && card >= 0)
	{
		sprintf(dev->device, "hw:%d", card);

		if (snd_ctl_open(&dev->ctl, dev->device, 0))
			break;

		if (snd_ctl_pcm_info(dev->ctl, info))
			break;
		if (strcmp(snd_pcm_info_get_id(info), "emu10k1"))
			continue;

		dev->subdevs = snd_pcm_info_get_subdevices_count(info);

		return ALC_TRUE;
	}

 	return ALC_FALSE;
}

static ALCvoid _alcCloseDevice(ALCdevice *dev)
{
	if (dev->ctl) snd_ctl_close(dev->ctl);
	free(dev);
}

ALCdevice *alcOpenDevice(const ALubyte *spec ATTRIBUTE_UNUSED)
{
	ALCdevice *dev;

	if (!(dev = malloc(sizeof(ALCdevice))))
	{
		_alcSetError(ALC_OUT_OF_MEMORY);
		return 0;
	}

	dev->ctl = 0;
	dev->sync = ALC_FALSE;
	dev->freq = _ALC_DEF_FREQ;
	dev->refresh = _ALC_DEF_REFRESH;

	if (_alcOpenDevice(dev))
		return dev;

	_alcCloseDevice(dev);

	_alcSetError(ALC_INVALID_DEVICE);

	return 0;
}

ALCvoid alcCloseDevice(ALCdevice *dev)
{
	if (dev)
	{
		_alcCloseDevice(dev);
	}
	else
	{
		_alcSetError(ALC_INVALID_DEVICE);
	}
}
