/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Routines for control of Intel ICH (i8x0) chips
 *
 *  BUGS:
 *    --
 *
 *  TODO:
 *    --
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public Lcodecnse as published by
 *   the Free Software Foundation; either version 2 of the Lcodecnse, 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 Lcodecnse for more details.
 *
 *   You should have received a copy of the GNU General Public Lcodecnse
 *   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 "../../include/driver.h"
#include "../../include/control.h"
#include "../../include/info.h"
#include "../../include/intel8x0.h"

/*
 *  Basic I/O
 */
 
static int snd_intel8x0_codec_semaphore(intel8x0_t *codec, int secondary, int sched)
{
	signed long end_time;
	int stat = !secondary ? ICH_PCR : ICH_SCR;
	
	end_time = jiffies + 3 * (HZ >> 2);
	do {
		if (inl(ICHREG(codec, GLOB_STA)) & stat)
			goto __ok;
		if (sched) {
			set_current_state(TASK_UNINTERRUPTIBLE);
			schedule_timeout(1);
		}
	} while (end_time - (signed long)jiffies >= 0);
	snd_printk("snd_intel8x0_codec_semaphore: codec %i is not ready [0x%x]\n", secondary, inl(ICHREG(codec, GLOB_STA)));
	return -EIO;

      __ok:
	end_time = jiffies + 10 * (HZ >> 2);
      	do {
      		if (!(inb(ICHREG(codec, ACC_SEMA)) & ICH_CAS))
      			return 0;
      		if (sched) {
			set_current_state(TASK_UNINTERRUPTIBLE);
			schedule_timeout(1);
      		}
	} while (end_time - (signed long)jiffies >= 0);
	snd_printk("snd_intel8x0_codec_semaphore: codec %i semaphore is not ready [0x%x][0x%x]\n", secondary, inb(ICHREG(codec, ACC_SEMA)), inl(ICHREG(codec, GLOB_STA)));
	return -EIO;
}
 
static void snd_intel8x0_codec_write(void *private_data,
				     unsigned short reg,
				     unsigned short val)
{
	intel8x0_t *codec = (intel8x0_t *)private_data;
	
	snd_intel8x0_codec_semaphore(codec, 0, 0);
	return outw(val, codec->port + reg);
}

static unsigned short snd_intel8x0_codec_read(void *private_data,
					      unsigned short reg)
{
	intel8x0_t *codec = (intel8x0_t *)private_data;

	if (snd_intel8x0_codec_semaphore(codec, 0, 0) < 0)
		return ~0;
	return inw(codec->port + reg);
}

static int snd_intel8x0_trigger(intel8x0_t *codec, ichdev_t *ichdev, int cmd)
{
	unsigned char val = 0;
	unsigned int port = codec->bmport + ichdev->reg_offset;
	
	switch (cmd) {
	case SND_PCM_TRIGGER_GO:
		val = ICH_IOCE | ICH_STARTBM;
		break;
	case SND_PCM_TRIGGER_STOP:
		val = 0;
		break;
	case SND_PCM_TRIGGER_PAUSE_PUSH:
		val = ICH_IOCE;
		break;
	case SND_PCM_TRIGGER_PAUSE_RELEASE:
		val = ICH_IOCE | ICH_STARTBM;
		break;
	default:
		return -EINVAL;
	}
	outb(val, port + ICH_REG_PI_CR);
	if (cmd == SND_PCM_TRIGGER_STOP) {
		/* reset whole DMA things */
		while (!(inb(port + ICH_REG_PI_SR) & ICH_DCH)) ;
		outb(ICH_RESETREGS, port + ICH_REG_PI_CR);
	}
	return 0;
}

static void snd_intel8x0_setup_fragments(intel8x0_t *codec, ichdev_t *ichdev) 
{
	int idx;
	unsigned int *bdbar = ichdev->bdbar;
	unsigned int port = codec->bmport + ichdev->reg_offset;

	outl(virt_to_bus(ichdev->bdbar), port + ICH_REG_PI_BDBAR);
	if (ichdev->size == ichdev->fragsize) {
		ichdev->ack_reload = ichdev->ack = 2;
		ichdev->fragsize1 = ichdev->fragsize >> 1;
		for (idx = 0; idx < (ICH_REG_LVI_MASK + 1) * 2; idx += 4) {
			bdbar[idx + 0] = ichdev->physbuf;
			bdbar[idx + 1] = 0x80000000 | /* interrupt on completion */
					 ichdev->fragsize1 >> 1;
			bdbar[idx + 2] = ichdev->physbuf + (ichdev->size >> 1);
			bdbar[idx + 3] = 0x80000000 | /* interrupt on completion */
					 ichdev->fragsize1 >> 1;
		}
		ichdev->frags = 2;
	} else {
		ichdev->ack_reload = ichdev->ack = 1;
		ichdev->fragsize1 = ichdev->fragsize;
		for (idx = 0; idx < (ICH_REG_LVI_MASK + 1) * 2; idx += 2) {
			bdbar[idx + 0] = ichdev->physbuf + (((idx >> 1) * ichdev->fragsize) % ichdev->size);
			bdbar[idx + 1] = 0x80000000 | /* interrupt on completion */
					 ichdev->fragsize >> 1;
			// printk("bdbar[%i] = 0x%x [0x%x]\n", idx + 0, bdbar[idx + 0], bdbar[idx + 1]);
		}
		ichdev->frags = ichdev->size / ichdev->fragsize;
	}
	outb(ichdev->lvi = ICH_REG_LVI_MASK, port + ICH_REG_PI_LVI);
	ichdev->lvi_frag = ICH_REG_LVI_MASK % ichdev->frags;
	ichdev->position = 0;
#if 0
	printk("lvi_frag = %i, frags = %i, frag_size = 0x%x, frag_size1 = 0x%x\n",
			ichdev->lvi_frag, ichdev->frags, ichdev->fragsize, ichdev->fragsize1);
#endif
	/* clear interrupts */
	outb(ICH_FIFOE | ICH_BCIS | ICH_LVBCI, port + ICH_REG_PI_SR);
}

/*
 *  Interrupt handler
 */

static inline void snd_intel8x0_update(intel8x0_t *codec, ichdev_t *ichdev)
{
	unsigned int port = codec->bmport + ichdev->reg_offset;
	int ack = 0;

	spin_lock(&codec->reg_lock);
	ichdev->position += ichdev->fragsize1;
	ichdev->position %= ichdev->size;
	ichdev->lvi++;
	ichdev->lvi &= ICH_REG_LVI_MASK;
	outb(ichdev->lvi, port + ICH_REG_PI_LVI);
	ichdev->lvi_frag++;
	ichdev->lvi_frag %= ichdev->frags;
	ichdev->bdbar[ichdev->lvi * 2] = ichdev->physbuf + ichdev->lvi_frag * ichdev->fragsize1;
	// printk("new: bdbar[%i] = 0x%x [0x%x], prefetch = %i, all = 0x%x, 0x%x\n", ichdev->lvi * 2, ichdev->bdbar[ichdev->lvi * 2], ichdev->bdbar[ichdev->lvi * 2 + 1], inb(ICH_REG_PI_PIV + port), inl(port + 4), inb(port + ICH_REG_PI_CR));
	if ((ack = (--ichdev->ack == 0)) != 0)
		ichdev->ack = ichdev->ack_reload;
	spin_unlock(&codec->reg_lock);
	if (ack && ichdev->subchn)
		snd_pcm_transfer_done(ichdev->subchn);
	outb(ICH_FIFOE | ICH_BCIS | ICH_LVBCI, port + ICH_REG_PI_SR);
}

void snd_intel8x0_interrupt(intel8x0_t *codec)
{
	unsigned int status;
	
	status = inl(ICHREG(codec, GLOB_STA));
	if ((status & (ICH_MCINT | ICH_POINT | ICH_PIINT)) == 0)
		return;
	if (status & ICH_POINT)
		snd_intel8x0_update(codec, &codec->playback);
	if (status & ICH_PIINT)
		snd_intel8x0_update(codec, &codec->capture);
	if (status & ICH_MCINT)
		snd_intel8x0_update(codec, &codec->capture_mic);
}

/*
 *  PCM part
 */

static int snd_intel8x0_playback_ioctl(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned int cmd,
				      unsigned long *arg)
{
	int result;
	// intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	return 0;
}

static int snd_intel8x0_capture_ioctl(void *private_data,
				      snd_pcm_subchn_t * subchn,
				      unsigned int cmd,
				      unsigned long *arg)
{
	int result;
	// intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	return 0;
}

static int snd_intel8x0_playback_trigger(void *private_data,
					snd_pcm_subchn_t * pcm,
					int cmd)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	return snd_intel8x0_trigger(codec, &codec->playback, cmd);
}

static int snd_intel8x0_capture_trigger(void *private_data,
				       snd_pcm_subchn_t * subchn,
				       int cmd)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	return snd_intel8x0_trigger(codec, &codec->capture, cmd);
}

static int snd_intel8x0_playback_prepare(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->playback.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->playback.size = snd_pcm_lib_transfer_size(subchn);
	codec->playback.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_FRONT_DAC_RATE, runtime->format.rate);
	/* enable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0000);
#if 0
	printk("prepare: AC97_PCM_FRONT_DAC_RATE=0x%x (%i), AC97_EXTENDED_STATUS=0x%x\n",
		snd_ac97_read_lock(codec->ac97, AC97_PCM_FRONT_DAC_RATE),
		snd_ac97_read_lock(codec->ac97, AC97_PCM_FRONT_DAC_RATE),
		snd_ac97_read_lock(codec->ac97, AC97_EXTENDED_STATUS));
#endif
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_intel8x0_setup_fragments(codec, &codec->playback);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static int snd_intel8x0_capture_prepare(void *private_data,
				       snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->capture.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->capture.size = snd_pcm_lib_transfer_size(subchn);
	codec->capture.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_LR_ADC_RATE, runtime->format.rate);
	/* enable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0000);
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_intel8x0_setup_fragments(codec, &codec->capture);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static unsigned int snd_intel8x0_playback_pointer(void *private_data,
						  snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	result = codec->playback.fragsize1 - (inw(ICHREG(codec, PO_PICB)) << 1);
	result += codec->playback.position;
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static unsigned int snd_intel8x0_capture_pointer(void *private_data,
						snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	result = codec->capture.fragsize1 - (inw(ICHREG(codec, PI_PICB)) << 1);
	result += codec->capture.position;
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static snd_pcm_hardware_t snd_intel8x0_playback =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID |
	SND_PCM_CHNINFO_PAUSE,	/* flags */
	SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* overwritten */
	8000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* min. voices */
	2,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_intel8x0_playback_ioctl,
	snd_intel8x0_playback_prepare,
	snd_intel8x0_playback_trigger,
	snd_intel8x0_playback_pointer
};

static snd_pcm_hardware_t snd_intel8x0_capture =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID,	/* flags */
	SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* overwritten */
	8000,			/* min. rate */
	48000,			/* max. rate */
	2,			/* min. voices */
	2,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_intel8x0_capture_ioctl,
	snd_intel8x0_capture_prepare,
	snd_intel8x0_capture_trigger,
	snd_intel8x0_capture_pointer
};

static int snd_intel8x0_playback_open(void *private_data,
			             snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_pbk,
				     "Intel ICH - DAC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->playback.subchn = subchn;
	memcpy(hw, &snd_intel8x0_playback, sizeof(*hw));
	hw->rates = codec->playback.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->ac97->me_playback);
	return 0;
}

static int snd_intel8x0_capture_open(void *private_data,
				    snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_cap,
				     "Intel ICH - ADC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->capture.subchn = subchn;
	memcpy(hw, &snd_intel8x0_capture, sizeof(*hw));
	hw->rates = codec->capture.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	snd_pcm_set_mixer(subchn, codec->mixer->device, codec->ac97->me_capture);
	return 0;
}

static int snd_intel8x0_playback_close(void *private_data,
				      snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	codec->playback.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable DAC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0200, 0x0200);
	return 0;
}

static int snd_intel8x0_capture_close(void *private_data,
				     snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	codec->capture.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_POWERDOWN, ~0x0100, 0x0100);
	return 0;
}

static void snd_intel8x0_pcm_free(void *private_data)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, );
	codec->pcm = NULL;
}

int snd_intel8x0_pcm(intel8x0_t * codec, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;
	err = snd_pcm_new(codec->card, "Intel ICH", device, 1, 1, &pcm);
	if (err < 0)
		return err;

	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].open = snd_intel8x0_playback_open;
	pcm->chn[SND_PCM_CHANNEL_PLAYBACK].close = snd_intel8x0_playback_close;

	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_intel8x0_capture_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_intel8x0_capture_close;

	pcm->private_data = codec;
	pcm->private_free = snd_intel8x0_pcm_free;
	pcm->info_flags = SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_CAPTURE |
			  SND_PCM_INFO_DUPLEX;
	strcpy(pcm->name, "Intel ICH");
	*rpcm = codec->pcm = pcm;
	return 0;
}

/*
 *  PCM code - MIC
 */

static int snd_intel8x0_capture_mic_ioctl(void *private_data,
					  snd_pcm_subchn_t * subchn,
					  unsigned int cmd,
					  unsigned long *arg)
{
	int result;
	// intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	result = snd_pcm_lib_ioctl(private_data, subchn, cmd, arg);
	if (result < 0)
		return result;
	return 0;
}

static int snd_intel8x0_capture_mic_trigger(void *private_data,
					    snd_pcm_subchn_t * subchn,
					    int cmd)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	return snd_intel8x0_trigger(codec, &codec->capture_mic, cmd);
}

static int snd_intel8x0_capture_mic_prepare(void *private_data,
					    snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_runtime_t *runtime = subchn->runtime;

	codec->capture_mic.physbuf = virt_to_bus(runtime->dma_area->buf);
	codec->capture_mic.size = snd_pcm_lib_transfer_size(subchn);
	codec->capture_mic.fragsize = snd_pcm_lib_transfer_fragment(subchn);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_EXTENDED_STATUS, ~0x4000, 0x4000);
	snd_ac97_write_lock(codec->ac97, AC97_PCM_MIC_ADC_RATE, runtime->format.rate);
	/* enable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_EXTENDED_STATUS, ~0x4000, 0x0000);
	spin_lock_irqsave(&codec->reg_lock, flags);
	snd_intel8x0_setup_fragments(codec, &codec->capture_mic);
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return 0;
}

static unsigned int snd_intel8x0_capture_mic_pointer(void *private_data,
						     snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	unsigned int result;

	spin_lock_irqsave(&codec->reg_lock, flags);
	result = codec->capture_mic.fragsize1 - (inw(ICHREG(codec, MC_PICB)) << 1);
	result += codec->capture_mic.position;
	spin_unlock_irqrestore(&codec->reg_lock, flags);
	return result;
}

static snd_pcm_hardware_t snd_intel8x0_capture_mic =
{
	SND_PCM_CHNINFO_MMAP | SND_PCM_CHNINFO_STREAM |
	SND_PCM_CHNINFO_BLOCK | SND_PCM_CHNINFO_INTERLEAVE |
	SND_PCM_CHNINFO_BLOCK_TRANSFER |
	SND_PCM_CHNINFO_MMAP_VALID |
	SND_PCM_CHNINFO_PAUSE,	/* flags */
	SND_PCM_FMT_S16_LE,	/* hardware formats */
	0,			/* overwritten */
	8000,			/* min. rate */
	48000,			/* max. rate */
	1,			/* min. voices */
	1,			/* max. voices */
	32,			/* min. fragment size */
	128 * 1024,		/* max. fragment size */
	31,			/* fragment align */
	0,			/* FIFO size (unknown) */
	16,			/* transfer block size */
	snd_intel8x0_capture_mic_ioctl,
	snd_intel8x0_capture_mic_prepare,
	snd_intel8x0_capture_mic_trigger,
	snd_intel8x0_capture_mic_pointer
};

static int snd_intel8x0_capture_mic_open(void *private_data,
					 snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);
	snd_pcm_hardware_t *hw;
	int err;

	hw = (snd_pcm_hardware_t *)snd_kmalloc(sizeof(snd_pcm_hardware_t), GFP_KERNEL);
	if (hw == NULL)
		return -ENOMEM;
	if ((err = snd_pcm_dma_alloc(subchn, codec->dma_mic,
				     "Intel ICH - MIC ADC")) < 0) {
		snd_kfree(hw);
		return err;
	}
	codec->capture_mic.subchn = subchn;
	memcpy(hw, &snd_intel8x0_capture_mic, sizeof(*hw));
	hw->rates = codec->capture_mic.rates;
	if (!(hw->rates & SND_PCM_RATE_8000))
		hw->min_rate = 48000;
	subchn->runtime->hw = hw;
	subchn->runtime->hw_free = _snd_kfree;
	return 0;
}

static int snd_intel8x0_capture_mic_close(void *private_data,
					  snd_pcm_subchn_t * subchn)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, -ENXIO);

	codec->capture_mic.subchn = NULL;
	snd_pcm_dma_free(subchn);
	/* disable ADC power */
	snd_ac97_write_bitmask_lock(codec->ac97, AC97_EXTENDED_STATUS, ~0x4000, 0x4000);
	return 0;
}

static void snd_intel8x0_pcm_mic_free(void *private_data)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, );
	codec->pcm_mic = NULL;
}

int snd_intel8x0_pcm_mic(intel8x0_t * codec, int device, snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;

	*rpcm = NULL;
	err = snd_pcm_new(codec->card, "Intel ICH - MIC ADC", device, 1, 1, &pcm);
	if (err < 0)
		return err;

	pcm->chn[SND_PCM_CHANNEL_CAPTURE].private_data = codec;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].open = snd_intel8x0_capture_mic_open;
	pcm->chn[SND_PCM_CHANNEL_CAPTURE].close = snd_intel8x0_capture_mic_close;

	pcm->private_data = codec;
	pcm->private_free = snd_intel8x0_pcm_mic_free;
	pcm->info_flags = SND_PCM_INFO_CAPTURE;
	strcpy(pcm->name, "Intel ICH - MIC ADC");

	*rpcm = codec->pcm_mic = pcm;	
	return 0;
}

/*
 *  Mixer part
 */

static void snd_intel8x0_codec_init(void *private_data, ac97_t *ac97)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, );

	if (codec->ac97_ext_id & 0x0001)	/* VRA */
		snd_ac97_write_lock(ac97, AC97_EXTENDED_STATUS, 0x0009);	
	/* disable DAC & ADC power */
	snd_ac97_write_bitmask_lock(ac97, AC97_POWERDOWN, ~0x0300, 0x0300);
	/* disable center DAC/surround DAC/LFE DAC/MIC ADC */
	snd_ac97_write_bitmask_lock(ac97, AC97_EXTENDED_STATUS, (short)~0xe800, 0xe800);
}

static void snd_intel8x0_mixer_free_ac97(void *private_data)
{
	intel8x0_t *codec = snd_magic_cast(intel8x0_t, private_data, );
	codec->mixer = NULL;
}

int snd_intel8x0_mixer(intel8x0_t * codec, int device, int pcm_count, int *pcm_devs, snd_kmixer_t ** rmixer)
{
	snd_kmixer_t *mixer;
	ac97_t ac97;
	int err;

	*rmixer = NULL;
	memset(&ac97, 0, sizeof(ac97));
	ac97.write = snd_intel8x0_codec_write;
	ac97.read = snd_intel8x0_codec_read;
	ac97.init = snd_intel8x0_codec_init;
	ac97.private_data = codec;
	ac97.private_free = snd_intel8x0_mixer_free_ac97;
	if ((err = snd_ac97_mixer(codec->card, device, &ac97, pcm_count, pcm_devs, &mixer)) < 0) {
		return err;
	}
	codec->ac97 = snd_magic_cast(ac97_t, mixer->private_data, -ENXIO);
	*rmixer = codec->mixer = mixer;
	return 0;
}

/*
 *
 */

static int snd_intel8x0_test_rate(intel8x0_t *codec, int reg, int rate)
{
	unsigned short val;

	snd_intel8x0_codec_write(codec, reg, rate);
	val = snd_intel8x0_codec_read(codec, reg);
	return val == rate;
}

static void snd_intel8x0_determine_rates(intel8x0_t *codec, int reg, unsigned int *r_result)
{
	unsigned int result = 0;

	/* test a non-standard frequency */
	if (snd_intel8x0_test_rate(codec, reg, 11000))
		result |= SND_PCM_RATE_CONTINUOUS;
	/* let's try to obtain standard frequencies */
	if (snd_intel8x0_test_rate(codec, reg, 8000))
		result |= SND_PCM_RATE_8000;
	if (snd_intel8x0_test_rate(codec, reg, 11025))
		result |= SND_PCM_RATE_11025;
	if (snd_intel8x0_test_rate(codec, reg, 16000))
		result |= SND_PCM_RATE_16000;
	if (snd_intel8x0_test_rate(codec, reg, 22050))
		result |= SND_PCM_RATE_22050;
	if (snd_intel8x0_test_rate(codec, reg, 32000))
		result |= SND_PCM_RATE_32000;
	if (snd_intel8x0_test_rate(codec, reg, 44100))
		result |= SND_PCM_RATE_44100;
	if (snd_intel8x0_test_rate(codec, reg, 48000))
		result |= SND_PCM_RATE_48000;
	*r_result = result;
}

static int snd_intel8x0_chip_init(intel8x0_t *codec)
{
	unsigned short ext_id;
	signed long end_time;
	
	/* ACLink on, warm reset */
	outl(ICH_AC97WARM | ICH_AC97COLD, ICHREG(codec, GLOB_CNT));
	end_time = jiffies + (HZ >> 2);
	do {
		if ((inl(ICHREG(codec, GLOB_CNT)) & ICH_AC97WARM) == 0)
			goto __ok;
	} while (end_time - (signed long)jiffies >= 0);
	snd_printk("AC'97 warm reset still in progress? [0x%x]\n", inl(ICHREG(codec, GLOB_CNT)));

      __ok:	
	/* try to wait to the ready state */
	inw(codec->port);	/* clear semaphore flag */
	snd_intel8x0_codec_semaphore(codec, 0, 1);
	inw(codec->port);	/* clear semaphore flag */

	codec->ac97_ext_id = 
	ext_id = snd_intel8x0_codec_read(codec, AC97_EXTENDED_ID);
	if (ext_id == 0xffff) {
		snd_printk("snd_intel8x0_chip_init: invalid AC'97 codec\n");
		return -EIO;
	}
	if (ext_id & 0x0001) {
		snd_intel8x0_codec_write(codec, AC97_EXTENDED_STATUS, 0x0009);
		snd_intel8x0_determine_rates(codec, AC97_PCM_FRONT_DAC_RATE, &codec->playback.rates);
		snd_intel8x0_determine_rates(codec, AC97_PCM_LR_ADC_RATE, &codec->capture.rates);
	} else {
		codec->playback.rates = SND_PCM_RATE_48000;
		codec->capture.rates = SND_PCM_RATE_48000;
	}
	if (ext_id & 0x0008) {
		snd_intel8x0_determine_rates(codec, AC97_PCM_MIC_ADC_RATE, &codec->capture_mic.rates);
	} else {
		codec->capture_mic.rates = SND_PCM_RATE_48000;
	}
	/* disable interrupts */
	outb(0x00, ICHREG(codec, PI_CR));
	outb(0x00, ICHREG(codec, PO_CR));
	outb(0x00, ICHREG(codec, MC_CR));
	/* reset channels */
	outb(ICH_RESETREGS, ICHREG(codec, PI_CR));
	outb(ICH_RESETREGS, ICHREG(codec, PO_CR));
	outb(ICH_RESETREGS, ICHREG(codec, MC_CR));
	/* initialize Buffer Descriptor Lists */
	outl(virt_to_bus(codec->playback.bdbar), ICHREG(codec, PO_BDBAR));
	outl(virt_to_bus(codec->capture.bdbar), ICHREG(codec, PI_BDBAR));
	outl(virt_to_bus(codec->capture_mic.bdbar), ICHREG(codec, MC_BDBAR));
	return 0;
}

int snd_intel8x0_create(snd_card_t * card,
		        struct pci_dev *pci,
		        snd_dma_t * dma_pbk,
		        snd_dma_t * dma_cap,
		        snd_dma_t * dma_mic,
			snd_irq_t * irqptr,
		        intel8x0_t ** r_intel8x0)
{
	intel8x0_t *codec;
	unsigned short cmdw;
	unsigned char cmdb;
	int err;
	static snd_device_ops_t ops = {
		(snd_dev_free_t *)snd_intel8x0_free,
		NULL,
		NULL
	};

	*r_intel8x0 = NULL;
	codec = snd_magic_kcalloc(intel8x0_t, 0, GFP_KERNEL);
	if (codec == NULL)
		return -ENOMEM;
	spin_lock_init(&codec->reg_lock);
	codec->card = card;
	codec->pci = pci;
	codec->dma_pbk = dma_pbk;
	codec->dma_cap = dma_cap;
	codec->dma_mic = dma_mic;
	codec->irqptr = irqptr;
	codec->port = pci_resource_start(pci, 0);
	codec->bmport = pci_resource_start(pci, 1);
	pci_read_config_word(pci, PCI_COMMAND, &cmdw);
	if ((cmdw & PCI_COMMAND_IO) != PCI_COMMAND_IO) {
		cmdw |= PCI_COMMAND_IO;
		pci_write_config_word(pci, PCI_COMMAND, cmdw);
	}
	pci_set_master(pci);
	pci_read_config_byte(pci, PCI_LATENCY_TIMER, &cmdb);
	if (cmdb < 64)
		cmdb = 64;
	pci_write_config_byte(pci, PCI_LATENCY_TIMER, cmdb);
	synchronize_irq();

	/* initialize offsets */
	codec->playback.reg_offset = 0x10;
	codec->capture.reg_offset = 0;
	codec->capture_mic.reg_offset = 0x20;

	/* allocate buffer descriptor lists */
	/* the start of each lists must be aligned to 8 bytes */
	codec->bdbars = (unsigned int *)snd_kcalloc(8 + sizeof(unsigned int) * 32 * 2, GFP_KERNEL);
	if (codec->bdbars == NULL) {
		snd_intel8x0_free(codec);
		return -ENOMEM;
	}
	codec->playback.bdbar = (unsigned int *)(((unsigned long)codec->bdbars + 7) & ~7);
	codec->capture.bdbar = codec->playback.bdbar + 32 * 2;
	codec->capture_mic.bdbar = codec->capture.bdbar + 32 * 2;

	if ((err = snd_intel8x0_chip_init(codec)) < 0) {
		snd_intel8x0_free(codec);
		return err;
	}

	if ((err = snd_device_new(card, SND_DEV_LOWLEVEL, codec, 0, &ops, NULL)) < 0) {
		snd_intel8x0_free(codec);
		return err;
	}

	*r_intel8x0 = codec;
	return 0;
}

int snd_intel8x0_free(intel8x0_t * codec)
{
	/* disable interrupts */
	outb(0x00, ICHREG(codec, PI_CR));
	outb(0x00, ICHREG(codec, PO_CR));
	outb(0x00, ICHREG(codec, MC_CR));
	/* reset channels */
	outb(ICH_RESETREGS, ICHREG(codec, PI_CR));
	outb(ICH_RESETREGS, ICHREG(codec, PO_CR));
	outb(ICH_RESETREGS, ICHREG(codec, MC_CR));
	/* --- */
	synchronize_irq();
	if (codec->bdbars)
		snd_kfree(codec->bdbars);
	snd_magic_kfree(codec);
	return 0;
}

EXPORT_SYMBOL(snd_intel8x0_interrupt);
EXPORT_SYMBOL(snd_intel8x0_create);
EXPORT_SYMBOL(snd_intel8x0_pcm);
EXPORT_SYMBOL(snd_intel8x0_pcm_mic);
EXPORT_SYMBOL(snd_intel8x0_mixer);

/*
 *  INIT part
 */

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

static void __exit alsa_intel8x0_exit(void)
{
}

module_init(alsa_intel8x0_init)
module_exit(alsa_intel8x0_exit)
