/*
 *  Driver for Yamaha OPL3-SA soundcard
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.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.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "pcm.h"
#include "mixer.h"
#include "cs4231.h"

int snd_index[ SND_CARDS ] = SND_DEFAULT_IDX;	/* Index 1-MAX */
char *snd_id[ SND_CARDS ] = SND_DEFAULT_STR;	/* ID for this card */
int snd_port[ SND_CARDS ] = SND_DEFAULT_PORT;	/* 0xf86,0x370,0x100 */
int snd_wss_port[ SND_CARDS ] = SND_DEFAULT_PORT; /* 0x530,0xe80,0xf40,0x604 */
int snd_fm_port[ SND_CARDS ] = SND_DEFAULT_PORT; /* 0x388 */
int snd_midi_port[ SND_CARDS ] = SND_DEFAULT_PORT; /* 0x330,0x300 */
int snd_irq[ SND_CARDS ] = SND_DEFAULT_IRQ;	/* 0,1,3,5,9,11,12,15 */
int snd_dma1[ SND_CARDS ] = SND_DEFAULT_DMA;	/* 1,3,5,6,7 */
int snd_dma2[ SND_CARDS ] = SND_DEFAULT_DMA;	/* 1,3,5,6,7 */
int snd_dma1_size[ SND_CARDS ] = SND_DEFAULT_DMA_SIZE; /* 8,16,32,64,128 */
int snd_dma2_size[ SND_CARDS ] = SND_DEFAULT_DMA_SIZE; /* 8,16,32,64,128 */
#ifdef MODULE_PARM
MODULE_PARM( snd_index, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_index, "Index value for OPL3-SA soundcard." );
MODULE_PARM( snd_id, "1-" __MODULE_STRING(SND_CARDS) "s" );
MODULE_PARM_DESC( snd_id, "ID string for OPL3-SA soundcard." );
MODULE_PARM( snd_port, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_port, "Port # for OPL3-SA driver." );
MODULE_PARM( snd_wss_port, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_wss_port, "WSS port # for OPL3-SA driver." );
MODULE_PARM( snd_fm_port, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_fm_port, "FM port # for OPL3-SA driver." );
MODULE_PARM( snd_midi_port, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_midi_port, "MIDI port # for OPL3-SA driver." );
MODULE_PARM( snd_irq, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_irq, "IRQ # for OPL3-SA driver." );
MODULE_PARM( snd_dma1, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_dma1, "DMA1 # for OPL3-SA driver." );
MODULE_PARM( snd_dma2, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_dma2, "DMA2 # for OPL3-SA driver." );
MODULE_PARM( snd_dma1_size, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_dma1_size, "DMA1 size in kB for OPL3-SA driver." );
MODULE_PARM( snd_dma2_size, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_dma2_size, "DMA2 size in kB for OPL3-SA driver." );
#endif

struct snd_opl3sa {
  int irqnum;
  int dma1num;
  int dma2num;
  unsigned short port;
  snd_card_t *card;
  snd_pcm_t *pcm;
  snd_kmixer_t *mixer;
  unsigned short pcm_status_reg;
};

static struct snd_opl3sa *snd_opl3sa_cards[ SND_CARDS ] = SND_DEFAULT_PTR;

static void snd_opl3sa_use_inc( void )
{
  MOD_INC_USE_COUNT;
}

static void snd_opl3sa_use_dec( void )
{
  MOD_DEC_USE_COUNT;
}

static unsigned char snd_opl3sa_read( unsigned short port, unsigned char reg )
{
  unsigned long flags;
  unsigned char result;
  
  CLI( &flags );
  OUTB( 0x1d, port );	/* password */
  printk( "read [0x%x] = 0x%x\n", port, INB( port ) );
  OUTB( reg, port );	/* register */
  result = INB( port + 1 );
  STI( &flags );
  printk( "read [0x%x] = 0x%x [0x%x]\n", port, result, INB( port ) );
  return result;
}

static void snd_opl3sa_write( unsigned short port, unsigned char reg, unsigned char value )
{
  unsigned long flags;
  
  CLI( &flags );
  OUTB( 0x1d, port );	/* password */
  OUTB( reg, port );	/* register */
  OUTB( value, port + 1 );
  STI( &flags );
}

static void snd_opl3sa_disable( unsigned short port )
{
  snd_opl3sa_write( port, 0x01, 0x00 );	/* Disable MSS */
  snd_opl3sa_write( port, 0x02, 0x00 );	/* Disable SB */
  snd_opl3sa_write( port, 0x03, 0x00 );	/* Disable MPU */
}

static int snd_opl3sa_detect( snd_card_t *card,
                              unsigned short port,
                              unsigned short wss_port,
                              unsigned short fm_port,
                              unsigned short midi_port )
{
  unsigned char tmp;

  if ( snd_register_ioport( card, port, 2, "OPL3-SA control" ) < 0 )
    return -EBUSY;
#if 0
#if 1
  PRINTK( "REG 0 = 0x%x\n", snd_opl3sa_read( port, 0x00 ) );
  PRINTK( "REG 1 = 0x%x\n", snd_opl3sa_read( port, 0x01 ) );
  PRINTK( "REG 2 = 0x%x\n", snd_opl3sa_read( port, 0x02 ) );
  PRINTK( "REG 3 = 0x%x\n", snd_opl3sa_read( port, 0x03 ) );
  PRINTK( "REG 4 = 0x%x\n", snd_opl3sa_read( port, 0x04 ) );
#endif
  if ( ((tmp = snd_opl3sa_read( port, 0x01 )) & 0xc4 ) == 0xc4 ) {
    PRINTDD( "OPL3-SA [0x%x] detect (1) = 0x%x\n", port, tmp );
    goto __nodev;
  }
  if ( INB( port + 1 ) == tmp ) {
    PRINTDD( "OPL3-SA [0x%x] detect (2) = 0x%x, 0x%x\n", port, tmp, INB( port + 1 ) );
    goto __nodev;
  }
  tmp = (snd_opl3sa_read( port, 0x04 ) & 0xe0) >> 5;
  PRINTDD( "OPL3-SA [0x%x] mode = %i\n", port, tmp );
  if ( tmp != 0 || tmp != 1 ) {
    PRINTDD( "OPL3-SA [0x%x] mode failed\n", port );
    goto __nodev;
  }
#endif
  snd_opl3sa_disable( port );
#if 1
  snd_opl3sa_write( port, 0x01, 0x24 );
  PRINTK( "REG 1 = 0x%x\n", snd_opl3sa_read( port, 0x01 ) );
  return 0;
#endif

  __nodev:
  snd_unregister_ioports( card );
  return -ENODEV;
}

static void snd_opl3sa_interrupt( int irq, void *dev_id, struct pt_regs *regs )
{
  register unsigned short status;
  struct snd_opl3sa *oplcard = (struct snd_opl3sa *)dev_id;

  if ( !oplcard || !oplcard -> pcm ) return;

  if ( (status = INB( oplcard -> pcm_status_reg )) & 0x01 )	/* IRQ bit is set? */
    snd_cs4231_interrupt( oplcard -> pcm, status );
}

static int snd_opl3sa_resources( int dev, struct snd_opl3sa *oplcard, snd_card_t *card )
{
  static int possible_irqs[] = { -1 };	/* must be specified by user */
  static int possible_dmas[] = { -1 };	/* must be specified by user */

  if ( (oplcard -> irqnum = snd_register_interrupt( card, "OPL3-SA", snd_irq[ dev ], snd_opl3sa_interrupt, oplcard, possible_irqs )) < 0 ) {
    return oplcard -> irqnum;
  }
  if ( (oplcard -> dma1num = snd_register_dma_channel( card, "OPL3-SA playback", snd_dma1[ dev ], snd_dma1_size[ dev ], possible_dmas )) < 0 ) {
    return oplcard -> dma1num;
  }
  if ( snd_dma2[ dev ] >= 0 ) {
    if ( (oplcard -> dma2num = snd_register_dma_channel( card, "OPL3-SA record", snd_dma2[ dev ], snd_dma2_size[ dev ], possible_dmas )) < 0 ) {
      return oplcard -> dma2num;
    }
  } else {
    oplcard -> dma2num = SND_DMA_DISABLE;
  }
  return 0;
}

#define CS4231_PRIVATE( left, right, shift, mute ) ((left << 24)|(right << 16)|(shift<<8)|mute)

static int snd_opl3sa_mixer( snd_kmixer_t *mixer )
{
  snd_kmixer_channel_t *channel;
  int idx;
  
  channel = NULL;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    switch ( channel -> hw.priority ) {
      case SND_MIXER_PRI_AUXA:
        channel -> hw.priority = SND_MIXER_PRI_SYNTHESIZER;
        channel -> hw.ossdev = SND_MIXER_SYNTH;
        strcpy( channel -> hw.name, SND_MIXER_ID_SYNTHESIZER );
        snd_mixer_reorder_channel( mixer, channel );
        break;
      case SND_MIXER_PRI_AUXB:
        channel -> hw.priority = SND_MIXER_PRI_CD;
        channel -> hw.ossdev = SND_MIXER_CD;
        strcpy( channel -> hw.name, SND_MIXER_ID_CD );
        snd_mixer_reorder_channel( mixer, channel );
        break;
    }
  }
  return 0;
}

static int snd_opl3sa_probe( int dev, struct snd_opl3sa *oplcard )
{
  snd_card_t *card;
  snd_pcm_t *pcm;
  snd_kmixer_t *mixer;
    
  if ( snd_port[ dev ] == SND_AUTO_PORT ||
       snd_wss_port[ dev ] == SND_AUTO_PORT ||
       snd_fm_port[ dev ] == SND_AUTO_PORT ||
       snd_midi_port[ dev ] == SND_AUTO_PORT ) {
    PRINTK( "sound: probing for Yamaha OPL3-SA isn't supported\n" );
    return -EINVAL;
  }
  card = snd_card_new( snd_index[ dev ], snd_id[ dev ],
                       "Yamaha OPL3-SA",
                       snd_opl3sa_use_inc, snd_opl3sa_use_dec );
  if ( !card ) return -ENOMEM;
  if ( snd_opl3sa_resources( dev, oplcard, card ) < 0 ) {
    snd_card_free( card );
    return -EBUSY;
  }
  if ( snd_opl3sa_detect( card, snd_port[ dev ], snd_wss_port[ dev ],
                                snd_fm_port[ dev ], snd_midi_port[ dev ] ) ) {
    snd_card_free( card );
    return -ENODEV;
  }
  oplcard -> port = snd_port[ dev ];
  if ( snd_card_register( card ) ) {
    snd_opl3sa_disable( snd_port[ dev ] );
    snd_card_free( card );
    return -ENOMEM;
  }
  oplcard -> pcm_status_reg = snd_wss_port[ dev ] + 2;
  pcm = snd_cs4231_new_device( card,
                               snd_wss_port[ dev ],
                               oplcard -> irqnum,
                               oplcard -> dma1num,
                               oplcard -> dma2num == SND_DMA_DISABLE ? oplcard -> dma1num : oplcard -> dma2num,
                               CS4231_HW_DETECT );
  if ( !pcm ) {
    __return1:
    snd_opl3sa_disable( snd_port[ dev ] );
    snd_card_free( card );
    return -ENXIO;
  }
  if ( snd_cs4231_probe( pcm ) < 0 ) {
    PRINTD( "sound: Oops, seems that WSS emulation of OPL3-SA wasn't detected at 0x%x?\n", snd_wss_port[ dev ] );
    __return2:
    snd_pcm_free( pcm );
    goto __return1;
  }
  if ( snd_pcm_register( pcm, 0 ) < 0 ) goto __return2;
  mixer = snd_cs4231_new_mixer( pcm );
  if ( !mixer ) {
    goto __return2;
  }
  if ( snd_opl3sa_mixer( mixer ) < 0 ) {
    __return3:
    snd_mixer_free( mixer );
    goto __return2;
  }    
  if ( snd_mixer_register( mixer, 0 ) < 0 ) {
    goto __return3;
  }
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_SYNTHESIZER, 75, 75, 0 );
  snd_mixer_set_kernel_mute( mixer, SND_MIXER_PRI_PCM, SND_MIX_MUTE );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_PCM, 85, 85, 0 );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_LINE, 0, 0, SND_MIXER_FLG_MUTE );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_MIC, 0, 0, SND_MIXER_FLG_MUTE );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_CD, 0, 0, SND_MIXER_FLG_MUTE );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_GAIN, 0, 0, 0 );
  snd_mixer_channel_init( mixer, SND_MIXER_PRI_LOOPBACK, 0, 0, SND_MIXER_FLG_MUTE );
  snd_enable_irq( card, oplcard -> irqnum );
#if 0
  snd_cs4231_debug( (cs4231_t *)pcm -> private_data );
#endif
  sprintf( card -> name + strlen( card -> name ), " at 0x%x, irq %i, dma %i",
    oplcard -> port,
    card -> irqs[ oplcard -> irqnum ] -> irq, 
    card -> dmas[ oplcard -> dma1num ] -> dma );
  if ( oplcard -> dma2num != SND_DMA_DISABLE )
    sprintf( card -> name + strlen( card -> name ), "&%i",
      card -> dmas[ oplcard -> dma2num ] -> dma );
  oplcard -> card = card;
  oplcard -> pcm = pcm;
  oplcard -> mixer = mixer;
  return 0;
}

int init_module( void )
{
  int dev, cards;
  struct snd_opl3sa *oplcard;

#ifndef LINUX_2_1
  register_symtab( NULL );
#endif
  for ( dev = cards = 0; dev < SND_CARDS && snd_port[ dev ] > 0; dev++ ) {
    oplcard = (struct snd_opl3sa *)snd_malloc( sizeof( struct snd_opl3sa ) );
    if ( !oplcard ) continue;
    MEMSET( oplcard, 0, sizeof( struct snd_opl3sa ) );
    if ( snd_opl3sa_probe( dev, oplcard ) < 0 ) {
      snd_free( oplcard, sizeof( struct snd_opl3sa ) );
      if ( snd_port[ dev ] == SND_AUTO_PORT ) break;
      PRINTK( "sound: Yamaha OPL3-SA soundcard #%i not found at 0x%x or device busy\n", dev + 1, snd_port[ dev ] );
      continue;
    }
    snd_opl3sa_cards[ dev ] = oplcard;
    cards++;
  }
  if ( !cards ) { 
    PRINTK( "sound: Yamaha OPL3-SA soundcard not found or device busy\n" );
    return -ENODEV;
  }
  return 0;
}

void cleanup_module( void )
{
  int idx;
  struct snd_opl3sa *oplcard;
  snd_pcm_t *pcm;

  for ( idx = 0; idx < SND_CARDS; idx++ ) {
    oplcard = snd_opl3sa_cards[ idx ];
    if ( oplcard ) {
      snd_opl3sa_disable( oplcard -> port );
      if ( oplcard -> mixer )
        snd_mixer_unregister( oplcard -> mixer );
      if ( oplcard -> pcm ) {
        pcm = oplcard -> pcm;
        oplcard -> pcm = NULL;	/* turn off interrupts */
        snd_pcm_unregister( pcm );
      }
      snd_card_unregister( oplcard -> card );
      snd_free( oplcard, sizeof( struct snd_opl3sa ) );
    }
  }
}
