/*
 *  Digital Audio (PCM) abstract 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.
 *
 */

#include "../include/driver.h"
#include "../include/pcm.h"
#include "../include/timer.h"

/*
 *  Timer functions
 */

void snd_pcm_timer_resolution_change(snd_pcm_subchn_t *subchn)
{
	unsigned int bps, sec, bsize;
	snd_pcm_runtime_t *runtime = subchn->runtime;
	
	if (runtime == NULL || runtime->mode != SND_PCM_MODE_BLOCK) {
                runtime->timer_resolution = 100;
		return;
	}
	/* this is not very good algorithm */
        sec = 1000000000;
	bps = (snd_pcm_format_width(runtime->format.format) * runtime->format.rate * runtime->format.voices) / 8;
	bsize = runtime->buf.block.frag_size;
	if (!bps) {
		runtime->timer_resolution = 100;
		return;
	}
	while (!(bps % 5) && !(sec % 5)) {
		bps /= 5;
		sec /= 5;
	}
	while (!(bps % 3) && !(sec % 3)) {
		bps /= 3;
		sec /= 3;
	}
	while (!(bps & 1) && !(sec & 1)) {
		bps >>= 1;
		sec >>= 1;
	}
	while (!(bps % 5) && !(bsize % 5)) {
		bps /= 5;
		bsize /= 5;
	}
	while (!(bps % 3) && !(bsize % 3)) {
		bps /= 3;
		bsize /= 3;
	}
	while (!(bps & 1) && !(bsize & 1)) {
		bps >>= 1;
		bsize >>= 1;
	}
	while ((sec * bsize) / bsize != sec) {
		if (bsize > sec) {
			bsize >>= 1;
		} else {
			sec >>= 1;
		}
		bps >>= 1;
	}
	runtime->timer_resolution = !bps ? 100 : (sec * bsize) / bps;
}

static unsigned long snd_pcm_timer_resolution(snd_timer_t * timer)
{
	snd_pcm_subchn_t * subchn;
	
	subchn = snd_magic_cast(snd_pcm_subchn_t, timer->private_data, -ENXIO);
	return subchn->runtime->timer_resolution;
}

static void snd_pcm_timer_start(snd_timer_t * timer)
{
	unsigned long flags;
	snd_pcm_subchn_t * subchn;
	snd_pcm_runtime_t * runtime;
	
	subchn = snd_magic_cast(snd_pcm_subchn_t, timer->private_data, );
	runtime = subchn->runtime;
	spin_lock_irqsave(&runtime->timer_lock, flags);
	if (runtime->mode == SND_PCM_MODE_BLOCK)
		runtime->timer_running = 1;
	spin_unlock_irqrestore(&runtime->timer_lock, flags);
}

static void snd_pcm_timer_stop(snd_timer_t * timer)
{
	unsigned long flags;
	snd_pcm_subchn_t * subchn;
	snd_pcm_runtime_t * runtime;
	
	subchn = snd_magic_cast(snd_pcm_subchn_t, timer->private_data, );
	runtime = subchn->runtime;
	spin_lock_irqsave(&runtime->timer_lock, flags);
	runtime->timer_running = 0;
	spin_unlock_irqrestore(&runtime->timer_lock, flags);
}

static struct snd_stru_timer_hardware snd_pcm_timer =
{
	flags:		SND_TIMER_HW_AUTO | SND_TIMER_HW_SLAVE,
	resolution:	0,
	ticks:		1,
	c_resolution:	snd_pcm_timer_resolution,
	start:		snd_pcm_timer_start,
	stop:		snd_pcm_timer_stop,
};

/*
 *  Init functions
 */

void snd_pcm_timer_init(snd_pcm_subchn_t *subchn)
{
	int device;
	snd_timer_t *timer;
	
	device = subchn->pcm->device;
	device <<= SND_TIMER_PCM_DEV_SHIFT;
	device |= (subchn->number << 1) | (subchn->channel & 1);
	if (snd_timer_new(subchn->pcm->card, "PCM", SND_TIMER_DEV_FLG_PCM | device, &timer) < 0)
		return;
	sprintf(timer->name, "PCM %s %i-%i-%i",
			subchn->channel == SND_PCM_CHANNEL_CAPTURE ?
				"capture" : "playback",
			SND_TIMER_PCM_CARD(device),
			SND_TIMER_PCM_DEV(device),
			SND_TIMER_PCM_SUBDEV(device));
	memcpy(&timer->hw, &snd_pcm_timer, sizeof(timer->hw));
	if (snd_device_register(timer->card, timer) < 0)
		snd_device_free(timer->card, timer);
	timer->private_data = subchn;
	subchn->timer = timer;
}

void snd_pcm_timer_done(snd_pcm_subchn_t *subchn)
{
	if (subchn->timer) {
		snd_device_unregister(subchn->pcm->card, subchn->timer);
		subchn->timer = NULL;
	}
}
