/*
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.cz>
 *  Routines for control of SoundBlaster cards
 *
 *
 *   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 "driver.h"
#include "pcm.h"
#include "mixer.h"
#include "sb.h"

extern struct snd_stru_pcm_hardware snd_sb8_playback;
extern struct snd_stru_pcm_hardware snd_sb8_record;
extern struct snd_stru_pcm_hardware snd_sb16_playback;
extern struct snd_stru_pcm_hardware snd_sb16_record;

int snd_sbdsp_command( sbdsp_t *codec, unsigned char val )
{
  int i;
  
  for ( i = 10000; i; i-- )
    if ( ( inb( SBP( codec, STATUS ) ) & 0x80 ) == 0 ) {
      outb( val, SBP( codec, COMMAND ) );
      return 1;
    }
#ifdef SNDCFG_DEBUG
  printk( "snd_sb_dsp_command: timeout (0x%x)\n", val );
#endif
  return 0;
}

int snd_sbdsp_get_byte( sbdsp_t *codec )
{
  int i;
  
  for ( i = 1000; i; i-- )
    if ( inb( SBP( codec, DATA_AVAIL ) ) & 0x80 )
      return inb( SBP( codec, READ ) );
  snd_printd( "sb get byte failed: 0x%x = 0x%x!!!\n", SBP( codec, DATA_AVAIL ), inb( SBP( codec, DATA_AVAIL ) ) );
  return -ENODEV;
}

int snd_sbdsp_reset( sbdsp_t *codec )
{
  int i;

  outb( 1, SBP( codec, RESET ) );
  snd_delay( 1 );
  outb( 0, SBP( codec, RESET ) );
  snd_delay( 3 );
  for ( i = 0; i < 1000 && !(inb( SBP( codec, DATA_AVAIL ) ) & 0x80); i++ );
  if ( inb( SBP( codec, READ ) ) != 0xaa ) {
#ifdef SNDCFG_DEBUG
    snd_printk( "sb_reset: failed!!!\n" );
#endif
    return -ENODEV;
  }
  return 0;
}

int snd_sbdsp_version( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int i;
  unsigned int result = -1;
  
  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_sbdsp_command( codec, SB_DSP_GET_VERSION );
  for ( i = 100000; i; i-- ) {
    if ( inb( SBP( codec, DATA_AVAIL ) ) & 0x80 ) {
      result = (short)inb( SBP( codec, READ ) ) << 8;
      break;
    }
  }
  for ( i = 100000; i; i-- ) {
    if ( inb( SBP( codec, DATA_AVAIL ) ) & 0x80 ) {
      result |= (short)inb( SBP( codec, READ ) );
      break;
    }
  }
  snd_spin_unlock( codec, reg, &flags );
  return result;
}

int snd_sbdsp_probe( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int version, hw;
  char *str;

  /*
   *  initialization sequence
   */
  
  codec = (sbdsp_t *)pcm -> private_data;

  snd_spin_lock( codec, reg, &flags );
  if ( snd_sbdsp_reset( codec ) < 0 ) {
    snd_printdd( "SB: [0x%x] reset failed... 0x%x\n", codec -> port, inb( SBP( codec, READ ) ) );
    snd_spin_unlock( codec, reg, &flags );
    return -ENODEV;
  }
  snd_spin_unlock( codec, reg, &flags );

  version = snd_sbdsp_version( pcm );
  if ( version < 0 ) return -ENODEV;
  
  snd_printdd( "SB [0x%x]: DSP chip found, version = %i.%i\n", codec -> port, version >> 8, version & 0xff );

  hw = SB_HW_AUTO;
  switch ( version >> 8 ) {
    case 1: hw = SB_HW_10; break;
    case 2: hw = !(version & 0xff) ? SB_HW_20 : SB_HW_201; break;
    case 3: hw = SB_HW_PRO; break;
    case 4: hw = SB_HW_16; break;
    default:
      snd_printk( "SB [0x%x]: uknown DSP chip version %i.%i\n", codec -> port, version >> 8, version & 0xff );
      return -ENODEV;
  }
  switch ( codec -> hardware = hw ) {
    case SB_HW_10: str = "Sound Blaster 1.0"; break;
    case SB_HW_20: str = "Sound Blaster 2.0"; break;
    case SB_HW_201: str = "Sound Blaster 2.01+"; break;
    case SB_HW_PRO: str = "Sound Blaster Pro"; break;
    case SB_HW_16: str = "Sound Blaster 16"; break;
    default: str = "???";
  }
  strcpy( codec -> name, str );
  sprintf( pcm -> name, "DSP v%i.%i", version >> 8, version & 0xff );
  
  return 0;
}

int snd_sbdsp_sb16_configure( snd_pcm_t *pcm )
{
  sbdsp_t *codec;
  unsigned long flags, flags1;
  unsigned char irqreg = 0, dmareg = 0, mpureg, realirq, realdma, realmpureg;
  
  /* note: mpu register should be present only on SB16 Vibra soundcards */
  codec = (sbdsp_t *)pcm -> private_data;
#if 0
  printk( "codec -> irq = %i, codec -> dma8 = %i, codec -> dma16 = %i\n", codec -> irq, codec -> dma8, codec -> dma16 );
#endif
  snd_spin_lock( codec, reg, &flags );
  snd_spin_lock( &codec -> mixer, mixer, &flags1 );
  mpureg = snd_sbmixer_read( &codec -> mixer, 0x84 ) & ~0x06;
  snd_spin_unlock( &codec -> mixer, mixer, &flags1 );
  snd_spin_unlock( codec, reg, &flags );
  switch ( codec -> irq ) {
    case 2:
    case 9: irqreg |= 0x01; break;
    case 5: irqreg |= 0x02; break;
    case 7: irqreg |= 0x04; break;
    case 10: irqreg |= 0x08; break;
    default:
      return -EINVAL;
  }
  if ( codec -> dma8num != SND_DMA_DISABLE ) {
    switch ( codec -> dma8 ) {
      case 0: dmareg |= 0x01; break;
      case 1: dmareg |= 0x02; break;
      case 3: dmareg |= 0x08; break;
      default:
        return -EINVAL;
    }
  }
  if ( codec -> dma16num != SND_DMA_DISABLE ) {
    switch ( codec -> dma16 ) {
      case 5: dmareg |= 0x20; break;
      case 6: dmareg |= 0x40; break;
      case 7: dmareg |= 0x80; break;
      default:
        return -EINVAL;
    }
  }
  switch ( codec -> mpu_port ) {
    case 0x300: mpureg |= 0x04; break;
    case 0x330: mpureg |= 0x00; break;
    default:
      mpureg |= 0x02;		/* disable MPU */
  }
  snd_spin_lock( codec, reg, &flags );
  snd_spin_lock( &codec -> mixer, mixer, &flags1 );
  snd_sbmixer_write( &codec -> mixer, 0x80, irqreg );
  realirq = snd_sbmixer_read( &codec -> mixer, 0x80 );
  snd_sbmixer_write( &codec -> mixer, 0x81, dmareg );
  realdma = snd_sbmixer_read( &codec -> mixer, 0x81 );
  snd_sbmixer_write( &codec -> mixer, 0x84, mpureg );
  realmpureg = snd_sbmixer_read( &codec -> mixer, 0x84 );
  snd_spin_unlock( &codec -> mixer, mixer, &flags1 );
  snd_spin_unlock( codec, reg, &flags );
  if ( (~realirq) & irqreg || (~realdma) & dmareg ) {
    snd_printdd( "SB16 [0x%x]: unable to set DMA & IRQ (PnP device?)\n", codec -> port );
    snd_printdd( "SB16 [0x%x]: irqreg=0x%x, dmareg=0x%x, mpureg = 0x%x\n", codec -> port, realirq, realdma, realmpureg );
    snd_printdd( "SB16 [0x%x]: new: irqreg=0x%x, dmareg=0x%x, mpureg = 0x%x\n", codec -> port, irqreg, dmareg, mpureg );
#if 0	/* this test doesn't work for all SB16 PnP soundcards */
    return -ENODEV;
#endif
  }
  return 0;
}

void snd_sbdsp_free( void *private_data )
{
  sbdsp_t *codec;

  codec = (sbdsp_t *)private_data;
  if ( codec -> hardware == SB_HW_16 ) {
    snd_sb16_proc_done( codec -> pcm );
  }
  snd_free( codec, sizeof( sbdsp_t ) );
}

snd_pcm_t *snd_sbdsp_new_device( snd_card_t *card,
                                 unsigned short port,
				 unsigned short irqnum,
				 unsigned short dma8num,
				 unsigned short dma16num,
				 unsigned short hardware )
{
  snd_pcm_t *pcm;
  sbdsp_t *codec;

  pcm = snd_pcm_new_device( card, "SB DSP" );
  if ( !pcm ) return NULL;
  codec = (sbdsp_t *)snd_malloc( sizeof( sbdsp_t ) );
  if ( !codec ) return NULL;
  memset( codec, 0, sizeof( sbdsp_t ) );
  snd_spin_prepare( codec, reg );
  snd_spin_prepare( codec, open8 );
  snd_spin_prepare( codec, open16 );
  snd_spin_prepare( codec, midi_input );
  snd_spin_prepare( &codec -> mixer, mixer );
  codec -> pcm = pcm;
  codec -> card = pcm -> card;
  codec -> port = port;
  codec -> mixer.port = port;
  codec -> irqnum = irqnum;
  codec -> irq = pcm -> card -> irqs[ irqnum ] -> irq;
  codec -> dma8num = dma8num;
  if ( dma8num != SND_DMA_DISABLE )
    codec -> dma8 = pcm -> card -> dmas[ dma8num ] -> dma;
  codec -> dma16num = dma16num;
  if ( dma16num != SND_DMA_DISABLE )
    codec -> dma16 = pcm -> card -> dmas[ dma16num ] -> dma;
  codec -> hardware = hardware;
  pcm -> private_data = codec;
  pcm -> private_free = snd_sbdsp_free;
  strcpy( pcm -> name, "Sound Blaster" );
  if ( snd_sbdsp_probe( pcm ) < 0 ) {
    snd_pcm_free( pcm );
    return NULL;
  }
  pcm -> info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
                      SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD;
  codec -> force_mode16 = SB_MODE16_AUTO;
  switch ( codec -> hardware ) {
    case SB_HW_10:
    case SB_HW_20:
    case SB_HW_201:
    case SB_HW_PRO:
      memcpy( &pcm -> playback.hw, &snd_sb8_playback, sizeof( snd_sb8_playback ) );
      memcpy( &pcm -> record.hw, &snd_sb8_record, sizeof( snd_sb8_record ) );
      break;
    case SB_HW_16:
      memcpy( &pcm -> playback.hw, &snd_sb16_playback, sizeof( snd_sb16_playback ) );
      memcpy( &pcm -> record.hw, &snd_sb16_record, sizeof( snd_sb16_record ) );
      snd_sb16_proc_init( pcm );
      break;
  }
  pcm -> info_flags = SND_PCM_INFO_DSP | SND_PCM_INFO_PLAYBACK |
                      SND_PCM_INFO_RECORD | SND_PCM_INFO_MMAP;
  switch ( codec -> hardware ) {
    case SB_HW_10:
      pcm -> playback.hw.flags &= ~SND_PCM_HW_AUTODMA;	/* ugh, antique !!! */
      pcm -> record.hw.flags &= ~SND_PCM_HW_AUTODMA;	/* ugh, antique !!! */
      break;
    case SB_HW_PRO:
      pcm -> playback.hw.max_voices = 
      pcm -> record.hw.max_voices = 2;
      /* follow thru */
    case SB_HW_201:
      pcm -> playback.hw.max_rate = 44100;
      pcm -> record.hw.max_rate = 15000;
      break;
    case SB_HW_16:
      pcm -> info_flags |= SND_PCM_INFO_DUPLEX | SND_PCM_INFO_DUPLEX_LIMIT;
      break;
  }
  return pcm;
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_sb_export;
#endif
 
int init_module( void )
{
#ifndef LINUX_2_1
  if ( register_symtab( &snd_symbol_table_sb_export ) < 0 )
    return -ENOMEM;
#endif
  return 0;
}

void cleanup_module( void )
{
}
