/*	MikMod sound library
	(c) 1998, 1999 Miodrag Vallat and others - see file AUTHORS for
	complete list.

	This library is free software; you can redistribute it and/or modify
	it under the terms of the GNU Library 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 Library General Public License for more details.

	You should have received a copy of the GNU Library General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
	02111-1307, USA.
*/

/*==============================================================================

  Windows Sound System I/O routines (CS42XX, ESS18XX, GUS+DaughterBoard etc)
  Written by Andrew Zabolotny <bit@eltech.ru>

==============================================================================*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef DRV_WSS

#include <stdlib.h>
#include <string.h>
#include <dos.h>
#ifdef __DJGPP__
#include <dpmi.h>
#include <go32.h>
#include <sys/nearptr.h>
#include <sys/farptr.h>
#else
#define _farsetsel(seg)
#define _farnspeekl(addr)  (*((unsigned long *)(addr)))
#endif

#include "doswss.h"

/********************************************* Private variables/routines *****/

__wss_state wss;

/* WSS frequency rates... lower bit selects one of two frequency generators */
static unsigned int wss_rates[14][2] = {
	{5510, 0x00 | WSSM_XTAL2},
	{6620, 0x0E | WSSM_XTAL2},
	{8000, 0x00 | WSSM_XTAL1},
	{9600, 0x0E | WSSM_XTAL1},
	{11025, 0x02 | WSSM_XTAL2},
	{16000, 0x02 | WSSM_XTAL1},
	{18900, 0x04 | WSSM_XTAL2},
	{22050, 0x06 | WSSM_XTAL2},
	{27420, 0x04 | WSSM_XTAL1},
	{32000, 0x06 | WSSM_XTAL1},
	{33075, 0x0C | WSSM_XTAL2},
	{37800, 0x08 | WSSM_XTAL2},
	{44100, 0x0A | WSSM_XTAL2},
	{48000, 0x0C | WSSM_XTAL1}
};

#if defined(__WATCOMC__)
static void nop (void);
#pragma aux nop = "nop"
#else
#define nop()
#endif

static void INTERRUPT_ATTRIBUTES NO_REORDER wss_irq()
{
	/* Make sure its not a spurious IRQ */
	if (!irq_check(wss.irq_handle))
		return;

	wss.irqcount++;

	/* Clear IRQ status */
	outportb(WSS_STATUS, 0);

	/* Write transfer count again */
	__wss_outreg(WSSR_COUNT_LOW, wss.samples & 0xff);
	__wss_outreg(WSSR_COUNT_HIGH, wss.samples >> 8);
	irq_ack(wss.irq_handle);

	enable();
	if (wss.timer_callback)
		wss.timer_callback();
}

static void INTERRUPT_ATTRIBUTES NO_REORDER wss_irq_end()
{
	nop();
}

/* WSS accepts some conventional values instead of frequency in Hz... */
static unsigned char __wss_getrate(unsigned int *freq)
{
	int i, best = -1, delta = 0xffff;

	for (i = 0; i < 14; i++) {
		int newdelta = abs(wss_rates[i][0] - *freq);
		if (newdelta < delta)
			best = i, delta = newdelta;
	}

	*freq = wss_rates[best][0];
	return wss_rates[best][1];
}

/* Check if we really have a WSS compatible card on given address */
static boolean __wss_ping()
{
	/* Disable CODEC operations first */
	__wss_regbit_reset(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);
	/* Now put some harmless values in registers and check them */
	__wss_outreg(WSSR_COUNT_LOW, 0xaa);
	__wss_outreg(WSSR_COUNT_HIGH, 0x55);
	return (__wss_inreg(WSSR_COUNT_LOW) == 0xaa)
	  && (__wss_inreg(WSSR_COUNT_HIGH) == 0x55);
}

static boolean __wss_reset()
{
	int count;

	/* Disable output */
	wss_output(FALSE);

	/* Now select the test/initialization register */
	count = 10000;
	while (inportb(WSS_ADDR) != WSSR_TEST_INIT) {
		outportb(WSS_ADDR, WSSR_TEST_INIT);
		if (!--count)
			return FALSE;
	}

	count = 10000;
	while (inportb(WSS_DATA) & WSSM_CALIB_IN_PROGRESS) {
		outportb(WSS_ADDR, WSSR_TEST_INIT);
		if (!--count)
			return FALSE;
	}

	/* Enable playback IRQ */
	__wss_regbit_set(WSSR_PIN_CTRL, WSSM_IRQ_ENABLE);
	__wss_outreg(WSSR_IRQ_STATUS, WSSM_PLAYBACK_IRQ);

	/* Clear IRQ status */
	outportb(WSS_STATUS, 0);

	return TRUE;
}

static boolean __wss_setformat(unsigned char format)
{
	int count;

	outportb(WSS_ADDR, WSSM_MCE | WSSR_PLAY_FORMAT);
	outportb(WSS_DATA, format);
	inportb(WSS_DATA);			/* ERRATA SHEETS ... */
	inportb(WSS_DATA);			/* ERRATA SHEETS ... */

	/* Wait end of syncronization ... */
	if (!__wss_wait())
		return FALSE;

	/* Turn off the ModeChangeEnable bit: do it until it works */
	count = 10000;
	while (inportb(WSS_ADDR) != WSSR_PLAY_FORMAT) {
		outportb(WSS_ADDR, WSSR_PLAY_FORMAT);
		if (!--count)
			return FALSE;
	}

	return __wss_reset();
}

/**************************************************** WSS detection stuff *****/

static int __wss_irq_irqdetect(int irqno)
{
	unsigned char status = inportb(WSS_STATUS);
	/* Clear IRQ status */
	outportb(WSS_STATUS, 0);
	/* Reset transfer counter */
	__wss_outreg(WSSR_COUNT_LOW, 0);
	__wss_outreg(WSSR_COUNT_HIGH, 0);
	return (status & WSSM_INT);
}

static boolean __wss_detect()
{
	/* First find the port number */
	if (!wss.port) {
		static unsigned int wss_ports[] =
		  { 0x32c, 0x530, 0x604, 0xE80, 0xF40 };
		int i;
		for (i = 0; i < 5; i++) {
			wss.port = wss_ports[i];
			if (__wss_ping())
				break;
		}
		if (i < 0) {
			wss.port = 0;
			return FALSE;
		}
	}

	/* Now disable output */
	wss_output(FALSE);

	/* Detect the DMA channel */
	if (!wss.dma) {
		static int __dma[] = { 0, 1, 3 };
		int i;

		/* Enable playback IRQ */
		__wss_regbit_set(WSSR_PIN_CTRL, WSSM_IRQ_ENABLE);
		__wss_outreg(WSSR_IRQ_STATUS, WSSM_PLAYBACK_IRQ);

		/* Start a short DMA transfer and check if DMA count is zero */
		for (i = 0; i < 3; i++) {
			unsigned int timer, status, freq = 44100;

			wss.dma = __dma[i];

			dma_disable(wss.dma);
			dma_set_mode(wss.dma, DMA_MODE_WRITE);
			dma_clear_ff(wss.dma);
			dma_set_count(wss.dma, 10);
			dma_enable(wss.dma);

			/* Clear IRQ status */
			outportb(WSS_STATUS, 0);

			__wss_setformat(__wss_getrate(&freq));
			__wss_outreg(WSSR_COUNT_LOW, 1);
			__wss_outreg(WSSR_COUNT_HIGH, 0);
			/* Tell codec to start transfer */
			__wss_regbit_set(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);

			_farsetsel(_dos_ds);
			timer = _farnspeekl(0x46c);

			while (_farnspeekl(0x46c) - timer <= 2)
				if (dma_get_count(wss.dma) == 0)
					break;
			__wss_regbit_reset(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);
			dma_disable(wss.dma);

			/* Now check if DMA transfer count is zero and an IRQ is pending */
			status = inportb(WSS_STATUS);
			outportb(WSS_STATUS, 0);
			if ((dma_get_count(wss.dma) == 0) && (status & WSSM_INT))
				break;

			wss.dma = 0;
		}

		if (!wss.dma)
			return FALSE;
	}

	/* Now detect the IRQ number */
	if (!wss.irq) {
		unsigned int i, irqmask, freq = 5510;
		unsigned long timer, delta = 0x7fffffff;

		/* IRQ can be one of 2,3,5,7,10 */
		irq_detect_start(0x04ac, __wss_irq_irqdetect);

		dma_disable(wss.dma);
		dma_set_mode(wss.dma, DMA_MODE_WRITE | DMA_MODE_AUTOINIT);
		dma_clear_ff(wss.dma);
		dma_set_count(wss.dma, 1);
		dma_enable(wss.dma);

		__wss_setformat(__wss_getrate(&freq));

		/* Clear IRQ status */
		outportb(WSS_STATUS, 0);

		__wss_outreg(WSSR_COUNT_LOW, 0);
		__wss_outreg(WSSR_COUNT_HIGH, 0);

		/* Prepare timeout counter */
		_farsetsel(_dos_ds);
		timer = _farnspeekl(0x46c);
		while (timer == _farnspeekl(0x46c)) {
			nop();
		}
		timer = _farnspeekl(0x46c);

		/* Reset all IRQ counters */
		irq_detect_clear();

		/* Tell codec to start transfer */
		__wss_regbit_set(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);

		/* Now wait 1/18 seconds */
		while (timer == _farnspeekl(0x46c)) {
			nop();
		}
		__wss_regbit_reset(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);
		dma_disable(wss.dma);

		/* Given frequency 5510Hz, a buffer size of 1 byte and a time interval
		   of 1/18.2 second, we should have received about 302 interrupts */
		for (i = 2; i <= 10; i++) {
			int count = abs(302 - irq_detect_get(i, &irqmask));
			if (count < delta)
				wss.irq = i, delta = count;
		}
		if (delta > 150)
			wss.irq = 0;

		irq_detect_end();
		if (!wss.irq)
			return FALSE;
	}

	return TRUE;
}

/*************************************************** High-level interface *****/

/* Detect whenever WSS is present and fill "wss" structure */
boolean wss_detect()
{
	char *env;

	/* Try to find the port and DMA from environment */
	env = getenv("WSS");

	while (env && *env) {
		/* Skip whitespace */
		while ((*env == ' ') || (*env == '\t'))
			env++;
		if (!*env)
			break;

		switch (*env++) {
		  case 'A':
		  case 'a':
			if (!wss.port)
				wss.port = strtol(env, &env, 16);
			break;
		  case 'I':
		  case 'i':
			if (!wss.irq)
				wss.irq = strtol(env, &env, 10);
			break;
		  case 'D':
		  case 'd':
			if (!wss.dma)
				wss.dma = strtol(env, &env, 10);
			break;
		  default:
			/* Skip other values */
			while (*env && (*env != ' ') && (*env != '\t'))
				env++;
			break;
		}
	}

	/* Try to fill the gaps in wss hardware parameters */
	__wss_detect();

	if (!wss.port || !wss.irq || !wss.dma)
		return FALSE;

	if (!__wss_ping())
		return FALSE;

	if (!__wss_reset())
		return FALSE;

	wss.ok = 1;
	return TRUE;
}

/* Reset WSS */
void wss_reset()
{
	wss_stop_dma();
	__wss_reset();
}

/* Open WSS for usage */
boolean wss_open()
{
	if (!wss.ok)
		if (!wss_detect())
			return FALSE;

	if (wss.open)
		return FALSE;

	/* Now lock the wss structure in memory */
	if (dpmi_lock_linear_region_base(&wss, sizeof(wss)))
		return FALSE;

	/* Hook the WSS IRQ */
	wss.irq_handle = irq_hook(wss.irq, wss_irq, wss_irq_end);
	if (!wss.irq_handle) {
		dpmi_unlock_linear_region_base(&wss, sizeof(wss));
		return FALSE;
	}

	/* Enable the interrupt */
	irq_enable(wss.irq_handle);
	if (wss.irq > 7)
		_irq_enable(2);

	wss.open++;

	return TRUE;
}

/* Finish working with WSS */
boolean wss_close()
{
	if (!wss.open)
		return FALSE;

	wss.open--;

	/* Stop/free DMA buffer */
	wss_stop_dma();

	/* Unhook IRQ */
	irq_unhook(wss.irq_handle);
	wss.irq_handle = NULL;

	/* Unlock the wss structure */
	dpmi_unlock_linear_region_base(&wss, sizeof(wss));

	return TRUE;
}

/* Adjust frequency rate to nearest WSS available */
unsigned int wss_adjust_freq(unsigned int freq)
{
	__wss_getrate(&freq);
	return freq;
}

/* Enable/disable speaker output */
/* Start playing from DMA buffer in either 8/16 bit mono/stereo */
boolean wss_start_dma(unsigned char mode, unsigned int freq)
{
	int dmabuffsize;
	unsigned char format;

	/* Stop DMA transfer if it is enabled */
	wss_stop_dma();

	/* Sanity check: we support only 8-bit unsigned and 16-bit signed formats */
	if (((mode & WSSMODE_16BITS) && !(mode & WSSMODE_SIGNED))
		|| (!(mode & WSSMODE_16BITS) && (mode & WSSMODE_SIGNED)))
		return FALSE;

	/* Find the nearest frequency divisor (rate) */
	format = __wss_getrate(&freq);
	wss.mode = mode;

	/* Get a DMA buffer enough for a 1sec interval... 4K <= dmasize <= 32K */
	dmabuffsize = freq;
	if (mode & WSSMODE_STEREO)
		dmabuffsize *= 2;
	if (mode & WSSMODE_16BITS)
		dmabuffsize *= 2;
	dmabuffsize >>= 2;
	if (dmabuffsize < 4096)
		dmabuffsize = 4096;
	if (dmabuffsize > 32768)
		dmabuffsize = 32768;
	dmabuffsize = (dmabuffsize + 255) & 0xffffff00;

	wss.dma_buff = dma_allocate(wss.dma, dmabuffsize);
	if (!wss.dma_buff)
		return FALSE;

	/* Fill DMA buffer with silence */
	dmabuffsize = wss.dma_buff->size;
	if (mode & WSSMODE_SIGNED)
		memset(wss.dma_buff->linear, 0, dmabuffsize);
	else
		memset(wss.dma_buff->linear, 0x80, dmabuffsize);

	/* Check data size and build a WSSR_PLAY_FORMAT value accordingly */
	wss.samples = dmabuffsize;
	if (mode & WSSMODE_16BITS) {
		wss.samples >>= 1;
		format |= WSSM_16BITS;
	}

	if (mode & WSSMODE_STEREO) {
		wss.samples >>= 1;
		format |= WSSM_STEREO;
	}

	if (!__wss_setformat(format)) {
		wss_stop_dma();
		return FALSE;
	}

	/* Prime DMA for transfer */
	dma_start(wss.dma_buff, dmabuffsize, DMA_MODE_WRITE | DMA_MODE_AUTOINIT);

	/* Tell codec how many samples to transfer */
	wss.samples = (wss.samples >> 1) - 1;
	__wss_outreg(WSSR_COUNT_LOW, wss.samples & 0xff);
	__wss_outreg(WSSR_COUNT_HIGH, wss.samples >> 8);

	/* Tell codec to start transfer */
	__wss_regbit_set(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);

	return TRUE;
}

/* Stop playing from DMA buffer */
void wss_stop_dma()
{
	if (!wss.dma_buff)
		return;

	__wss_regbit_reset(WSSR_IFACE_CTRL, WSSM_PLAYBACK_ENABLE);
	dma_disable(wss.dma);
	dma_free(wss.dma_buff);
	wss.dma_buff = NULL;
}

/* Query current position/total size of the DMA buffer */
void wss_query_dma(unsigned int *dma_size, unsigned int *dma_pos)
{
	unsigned int dma_left;
	*dma_size = wss.dma_buff->size;
	/* It can happen we try to read DMA count when HI/LO bytes will be
	   inconsistent */
	for (;;) {
		unsigned int dma_left_test;
		dma_clear_ff(wss.dma);
		dma_left_test = dma_get_count(wss.dma);
		dma_left = dma_get_count(wss.dma);
		if ((dma_left >= dma_left_test) && (dma_left - dma_left_test < 10))
			break;
	}
	*dma_pos = *dma_size - dma_left;
}

void wss_output(boolean enable)
{
	if (enable)
		wss.curlevel = wss.level;
	else
		wss.curlevel = 0x3f;

	__wss_outreg(WSSR_MASTER_L, wss.curlevel);
	__wss_outreg(WSSR_MASTER_R, wss.curlevel);
}

void wss_level(int level)
{
	if (level < 0)
		level = 0;
	if (level > 63)
		level = 63;
	wss.curlevel = wss.level = level ^ 63;

	__wss_outreg(WSSR_MASTER_L, wss.curlevel);
	__wss_outreg(WSSR_MASTER_R, wss.curlevel);
}

#endif /* DRV_WSS */
