/*
 *  Advanced Linux Sound Architecture
 *  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 "minors.h"
#include "info.h"
#include "sndpersist.h"
#ifdef CONFIG_KMOD
#include <linux/kmod.h>
#endif
#ifdef CONFIG_KERNELD
#include <linux/kerneld.h>
#endif

#if 0
#define DEBUG_ACCESS( d, m ) printk( ">>" d "[%i]\n", m )
#else
#define DEBUG_ACCESS( d, m )
#endif

int snd_major = SNDCFG_MAJOR;
int snd_cards_limit = SND_CARDS, snd_ecards_limit;
char snd_soundman_path[64] = "/sbin/soundman";
#ifdef MODULE_PARM              /* hey - we have new 2.1.18+ kernel... */
MODULE_AUTHOR( "Jaroslav Kysela <perex@jcu.cz>" );
MODULE_DESCRIPTION( "Advanced Linux Sound Architecture driver for soundcards." );
MODULE_SUPPORTED_DEVICE( "sound" );
MODULE_PARM( snd_major, "i" );
MODULE_PARM_DESC( snd_major, "Major # for sound driver. (14 by default)" );
MODULE_PARM( snd_cards_limit, "1-" __MODULE_STRING(SND_CARDS) "i" );
MODULE_PARM_DESC( snd_cards_limit, "Count of soundcards installed in the system." );
MODULE_PARM( snd_soundman_path, "64s" );
MODULE_PARM_DESC( snd_soundman_path, "Path for sound manager program." );
#endif

#define SND_OS_MINORS		256

static int snd_minors_card[ SND_OS_MINORS ] = { [ 0 ... (SND_OS_MINORS-1) ] = -1 };
static int snd_minors_device[ SND_OS_MINORS ] = { [ 0 ... (SND_OS_MINORS-1) ] = -1 };
static snd_minor_t *snd_minors[ SND_OS_MINORS ] = { [ 0 ... (SND_OS_MINORS-1) ] = NULL };

snd_mutex_define_static( sound );

static inline snd_minor_t *snd_verify_card( unsigned short minor )
{
  register int card;
  
  if ( ( card = snd_minors_card[ minor ] ) >= 0 ) {
    if ( !(snd_cards_bitmap & (1 << card)) ) return NULL;
  }
  return snd_minors[ minor ];
}

static inline snd_minor_t *snd_verify_card_open( unsigned short minor )
{
  register int card;
  snd_minor_t *mptr;

  if ( ( card = snd_minors_card[ minor ] ) >= 0 ) {
    if ( !(snd_cards_bitmap & (1 << card)) ) {
#if defined( CONFIG_KERNELD ) || defined( CONFIG_KMOD )
      char str[32]; 

      sprintf( str, "snd-card-%i", card );
      request_module( str );
      if ( !(snd_cards_bitmap & (1 << card)) )
#endif
        return NULL;
    }
  }
  mptr = snd_minors[ minor ];
#if defined( CONFIG_KERNELD ) || defined( CONFIG_KMOD )
  if ( !mptr && minor < SND_MINOR_BEGIN ) {	/* only OSS minors should be add-on modules */
    char str[32];
    
    sprintf( str, "snd-minor-oss-%i", minor & SND_MINOR_OSS_MASK );
    request_module( str );
    mptr = snd_minors[ minor ];
  }
#endif
  return mptr;
}

#ifdef LINUX_2_1
static loff_t snd_lseek( struct file *file, loff_t offset, int orig )
#else
static int snd_lseek( struct inode *inode, struct file *file, off_t offset, int orig )
#endif /* LINUX_2_1 */
{
#ifdef LINUX_2_1
  unsigned short minor = MINOR( file -> f_dentry -> d_inode -> i_rdev );
#else
  unsigned short minor = MINOR( inode -> i_rdev );
#endif
  snd_minor_t *mptr;
  snd_lseek_t *ptr;

  DEBUG_ACCESS( "lseek", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> lseek) == NULL ) return -ENXIO;
  return ptr( file, offset, orig );
}    

#ifdef LINUX_2_1
static ssize_t snd_read( struct file *file, char *buf, size_t count, loff_t *offset )
#else
static int snd_read( struct inode *inode, struct file *file, char *buf, int count )
#endif
{
#ifdef LINUX_2_1
  unsigned short minor = MINOR( file -> f_dentry -> d_inode -> i_rdev );
#else
  unsigned short minor = MINOR( inode -> i_rdev );
#endif
  snd_minor_t *mptr;
  snd_read_t *ptr;

  DEBUG_ACCESS( "read", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> read) == NULL ) return -ENXIO;
  return ptr( file, buf, count );
}

#ifdef LINUX_2_1
static ssize_t snd_write( struct file *file, const char *buf, size_t count, loff_t *offset )
#else
static int snd_write( struct inode *inode, struct file *file, const char *buf, int count )
#endif
{
#ifdef LINUX_2_1
  unsigned short minor = MINOR( file -> f_dentry -> d_inode -> i_rdev );
#else
  unsigned short minor = MINOR( inode -> i_rdev );
#endif
  snd_minor_t *mptr;
  snd_write_t *ptr;

  DEBUG_ACCESS( "write", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> write) == NULL ) return -ENXIO;
  return ptr( file, buf, count );
}

static int snd_open( struct inode *inode, struct file *file )
{
  unsigned short minor = MINOR( inode -> i_rdev );
  snd_minor_t *mptr;
  snd_open_t *ptr;

  DEBUG_ACCESS( "open", minor );
  if ( (mptr = snd_verify_card_open( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> open) == NULL ) return -ENXIO;
  return ptr( minor, snd_minors_card[ minor ], snd_minors_device[ minor ], file );
}

static int snd_release( struct inode * inode, struct file *file )
{
  unsigned short minor = MINOR( inode -> i_rdev );
  snd_minor_t *mptr;
  snd_release_t *ptr;

  DEBUG_ACCESS( "release", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> release) == NULL ) return -ENXIO;
  return ptr( minor, snd_minors_card[ minor ], snd_minors_device[ minor ], file );
}

#ifdef SND_POLL
static unsigned int snd_poll( struct file *file, poll_table *wait )
{
#ifdef LINUX_2_1
  unsigned short minor = MINOR( file -> f_dentry -> d_inode -> i_rdev );
#else
  unsigned short minor = MINOR( file -> f_inode -> i_rdev );
#endif
  snd_minor_t *mptr;
  snd_poll_t *ptr;

  DEBUG_ACCESS( "poll", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> poll) == NULL ) return -ENODEV;
  return ptr( file, wait );
}
#else
static int snd_select( struct inode *inode, struct file *file, 
                       int sel_type, select_table *wait )
{
  unsigned short minor = MINOR( inode -> i_rdev );
  snd_minor_t *mptr;
  snd_select_t *ptr;

  DEBUG_ACCESS( "select", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> select) == NULL ) return -ENXIO;
  return ptr( file, sel_type, wait );
}                       
#endif

static int snd_ioctl( struct inode *inode, struct file *file,
		      unsigned int cmd, unsigned long arg )
{
  unsigned short minor = MINOR( inode -> i_rdev );
  snd_minor_t *mptr;
  snd_ioctl_t *ptr;

  DEBUG_ACCESS( "ioctl", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> ioctl) == NULL ) return -ENXIO;
  return ptr( file, cmd, arg );
}

#ifdef LINUX_2_1
static int snd_mmap( struct file *file, struct vm_area_struct *vma )
#else
static int snd_mmap( struct inode *inode, struct file *file, struct vm_area_struct *vma )
#endif
{
#ifdef LINUX_2_1
  struct inode *inode = file -> f_dentry -> d_inode;
  unsigned short minor = MINOR( inode -> i_rdev );
#else
  unsigned short minor = MINOR( file -> f_inode -> i_rdev );
#endif
  snd_minor_t *mptr;
  snd_mmap_t *ptr;

  DEBUG_ACCESS( "mmap", minor );
  if ( (mptr = snd_verify_card( minor )) == NULL ) return -ENODEV;
  if ( (ptr = mptr -> mmap) == NULL ) return -ENXIO;
  return ptr( inode, file, vma );
}

static struct file_operations snd_fops = {
  snd_lseek,	/* (l)lseek */
  snd_read,	/* read */
  snd_write,	/* write */
  NULL,		/* readdir */
#ifdef SND_POLL
  snd_poll,	/* poll */
#else
  snd_select,	/* select */
#endif
  snd_ioctl,	/* ioctl */
  snd_mmap,	/* mmap */
  snd_open,	/* open */
#ifdef LINUX_2_1
  snd_release,	/* release */
#else
  (void (*)( struct inode * inode, struct file *file ))snd_release,
#endif
  NULL,		/* fsync */
#ifdef LINUX_2_1
  NULL,		/* fasync */
  NULL,		/* check_media_change */
  NULL,		/* revalidate */
  NULL,		/* lock */
#endif
};

#ifndef LINUX_2_1

int snd_ioctl_in( long *addr )
{
  if ( verify_area( VERIFY_READ, addr, sizeof( int ) ) != 0 )
    {
      snd_printd( "snd_ioctl_in: bad address 0x%lx\n", (long)addr );
      return 0;
    }
  return get_fs_long( addr );	/* not really long - 32-bit int */
}

int snd_ioctl_out( long *addr, int value )
{
  int err;
  
  if ( ( err = verify_area( VERIFY_WRITE, addr, sizeof( int ) ) ) != 0 )
    return err;
  put_fs_long( value, addr );	/* not really long - 32-bit int */
  return 0;
}

#else

int snd_ioctl_in( long *addr )
{
  int value;
  
  if ( verify_area( VERIFY_READ, addr, sizeof( int ) ) != 0 ) {
    snd_printd( "snd_ioctl_in: bad address 0x%lx\n", (long)addr );
    return 0;
  }
  get_user( value, addr );
  return value;
}

int snd_ioctl_out( long *addr, int value )
{
  int err;

  if ( ( err = verify_area( VERIFY_WRITE, addr, sizeof( int ) ) ) != 0 )
    return err;
  put_user( value, addr );
  return 0;
}

#endif /* !LINUX_2_1 */

static void snd_init_minors( void )
{
  int idx, card, device;
  
  for ( idx = 0; idx < SND_MINOR_BEGIN; idx++ ) { 
    card = idx >> 4;
    device = -1;
    switch ( idx & SND_MINOR_OSS_MASK ) {
      case SND_MINOR_OSS_SEQUENCER:
      case SND_MINOR_OSS_MUSIC:
        if ( idx > 15 ) card = -1;
        break;
      case SND_MINOR_OSS_SNDSTAT:
        card = -1;
        break;
      case SND_MINOR_OSS_MIXER:
      case SND_MINOR_OSS_MIDI:
      case SND_MINOR_OSS_PCM_8:
      case SND_MINOR_OSS_AUDIO:
      case SND_MINOR_OSS_PCM_16:
      case SND_MINOR_OSS_DMMIDI:
      case SND_MINOR_OSS_DMFM:
        device = 0;
        break;
      case SND_MINOR_OSS_MIXER1:
      case SND_MINOR_OSS_PCM1:
      case SND_MINOR_OSS_MIDI1:
      case SND_MINOR_OSS_DMMIDI1:
        device = 1;
        break;
      default:
        card = -1;
    }
    snd_minors_card[ idx ] = card;
    snd_minors_device[ idx ] = device;
  }
  for ( idx = SND_MINOR_BEGIN; idx < SND_OS_MINORS; idx++ ) {
    snd_minors_card[ idx ] =
    snd_minors_device[ idx ] = -1;
  }
  for ( idx = 0; idx < 8; idx++ ) {
    snd_minors_card[ idx + SND_MINOR_CONTROL ] =
    snd_minors_card[ idx + SND_MINOR_FM ] = idx;
  }
  for ( idx = 0; idx < 16; idx++ ) {
    snd_minors_card[ idx + SND_MINOR_MIXER ] = idx >> 1;
    snd_minors_device[ idx + SND_MINOR_MIXER ] = idx & 1;
  }
  for ( idx = 0; idx < 32; idx++ ) {
    snd_minors_card[ idx + SND_MINOR_PCM ] =
    snd_minors_card[ idx + SND_MINOR_RAWMIDI ] = idx >> 2;
    snd_minors_device[ idx + SND_MINOR_PCM ] =
    snd_minors_device[ idx + SND_MINOR_RAWMIDI ] = idx & 3;
  }
}

int snd_register_minor( unsigned short minor, snd_minor_t *reg )
{
  snd_mutex_down_static( sound );
  if ( minor > SND_OS_MINORS ) {
    snd_mutex_up_static( sound );
    return -EINVAL;
  }
  if ( snd_minors[ minor ] ) {
    snd_mutex_up_static( sound );
    return -EBUSY;
  }
  snd_minors[ minor ] = reg;
  snd_mutex_up_static( sound );
  return 0;
}

int snd_unregister_minor( unsigned short minor )
{
  snd_mutex_down_static( sound );
  if ( minor > SND_OS_MINORS ) {
    snd_mutex_up_static( sound );
    return -EINVAL;
  }
  if ( snd_minors[ minor ] == NULL ) {
    snd_mutex_up_static( sound );
    return -EINVAL;
  }
  snd_minors[ minor ] = NULL;
  snd_mutex_up_static( sound );
  return 0;
}

/*
 *  INFO PART
 */

static snd_info_entry_t *snd_minor_info_entry = NULL;
static snd_info_entry_t *snd_minor_info_oss_entry = NULL;

static void snd_minor_info_read( snd_info_buffer_t *buffer, void *private_data )
{
  int idx, card, device;
  snd_minor_t *mptr;

  snd_mutex_down_static( sound );
  for ( idx = SND_MINOR_BEGIN; idx < SND_OS_MINORS; idx++ ) {
    if ( (mptr = snd_minors[ idx ]) == NULL ) continue;
    if ( (card = snd_minors_card[ idx ]) >= 0 ) {
      if ( (device = snd_minors_device[ idx ]) >= 0 )
        snd_iprintf( buffer, "%3i: [%i-%2i]: %s\n", idx, card, device, mptr -> comment );
       else
        snd_iprintf( buffer, "%3i: [%i]   : %s\n", idx, card, mptr -> comment );
    } else {
      snd_iprintf( buffer, "%3i:       : %s\n", idx, mptr -> comment );
    }
  }
  snd_mutex_up_static( sound );
}

static void snd_minor_info_oss_read( snd_info_buffer_t *buffer, void *private_data )
{
  int idx, card;
  snd_minor_t *mptr;

  snd_mutex_down_static( sound );
  for ( idx = 0; idx < SND_MINOR_BEGIN; idx++ ) {
    if ( (mptr = snd_minors[ idx ]) == NULL ) continue;
    if ( (card = snd_minors_card[ idx ]) >= 0 )
      snd_iprintf( buffer, "%3i: [%i-%2i]: %s\n", idx, card, idx & SND_MINOR_OSS_MASK, mptr -> comment );
     else
      snd_iprintf( buffer, "%3i:       : %s\n", idx, mptr -> comment );
  }
  snd_mutex_up_static( sound );
}

int snd_minor_info_init( void )
{
  snd_info_entry_t *entry;
  snd_info_entry_t *oss_entry;

  entry = snd_info_create_entry( NULL, "devices" );
  if ( !entry ) return -ENOMEM;
  entry -> t.text.read_size = PAGE_SIZE;
  entry -> t.text.read = snd_minor_info_read;
  if ( snd_info_register( entry ) < 0 ) {
    snd_info_free_entry( entry );
    return -ENOMEM;
  }
  oss_entry = snd_info_create_entry( NULL, "oss-devices" );
  if ( !oss_entry ) return -ENOMEM;
  oss_entry -> t.text.read_size = PAGE_SIZE;
  oss_entry -> t.text.read = snd_minor_info_oss_read;
  if ( snd_info_register( oss_entry ) < 0 ) {
    snd_info_unregister( entry );
    snd_info_free_entry( oss_entry );
    return -ENOMEM;
  }
  snd_minor_info_entry = entry;
  snd_minor_info_oss_entry = oss_entry;
  return 0;
}

int snd_minor_info_done( void )
{
  if ( snd_minor_info_entry )
    snd_info_unregister( snd_minor_info_entry );
  if ( snd_minor_info_oss_entry )
    snd_info_unregister( snd_minor_info_oss_entry );
  return 0;
}

/*
 *  persistant part
 */

#if defined( LINUX_2_1 ) || LinuxVersionCode( 2, 0, 34 ) <= LINUX_VERSION_CODE

void (*snd_L_persist_lock)( int lock );
int (*snd_L_persist_store)( char *key, const char *data, int data_len ) = NULL;
int (*snd_L_persist_restore)( char *key, char *data, int data_len ) = NULL;
int (*snd_L_persist_present)( char *key ) = NULL;
int (*snd_L_persist_remove)( char *key ) = NULL;

static void snd_persist_init( void )
{
  unsigned long flags;

  snd_cli( &flags );
  snd_L_persist_lock = (void *)get_module_symbol( NULL, "persist_lock" );
  if ( snd_L_persist_lock ) {
    snd_L_persist_lock( 1 );
  } else {
    snd_sti( &flags );
    return;
  }
  snd_sti( &flags );
  snd_L_persist_store = (void *)get_module_symbol( NULL, "persist_store" );
  snd_L_persist_restore = (void *)get_module_symbol( NULL, "persist_restore" );
  snd_L_persist_present = (void *)get_module_symbol( NULL, "persist_present" );
  snd_L_persist_remove = (void *)get_module_symbol( NULL, "persist_remove" );
#if 0
  printk( "persist ok - remove = 0x%lx\n", (long)snd_L_persist_remove );
#endif
}

static void snd_persist_done( void )
{
  if ( snd_L_persist_lock )
    snd_L_persist_lock( 0 );
}

int snd_persist_store( char *key, const char *data, int data_len )
{
  if ( snd_L_persist_store )
    return snd_L_persist_store( key, data, data_len );
  return -ENXIO;
}

int snd_persist_restore( char *key, char *data, int data_len )
{
  if ( snd_L_persist_store )
    return snd_L_persist_restore( key, data, data_len );
  return -ENXIO;
}

int snd_persist_present( char *key )
{
  if ( snd_L_persist_present )
    return snd_L_persist_present( key );
  return -ENXIO;
}

int snd_persist_remove( char *key )
{
  if ( snd_L_persist_remove )
    return snd_L_persist_remove( key );
  return -ENXIO;
}

#endif

/*
 *  INIT PART
 */
 
#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_sndexport;
#endif

int init_module( void )
{
  snd_ecards_limit = snd_cards_limit;
#ifndef LINUX_2_1 
  if ( register_symtab( &snd_symbol_table_sndexport ) < 0 )
    return -ENOMEM;
#endif
  snd_init_minors();
  if ( register_chrdev( snd_major, "snd", &snd_fops ) ) {
    printk( "unable to register major device number %d\n", snd_major );
    return -EIO;
  }
  snd_malloc_init();
  snd_driver_init();
  if ( snd_info_init() < 0 ) {
    snd_malloc_done();
    return -ENOMEM;
  }
  snd_info_minor_register();
#if defined( LINUX_2_1 ) || LinuxVersionCode( 2, 0, 34 ) <= LINUX_VERSION_CODE
  snd_persist_init();
#endif
  return 0;
}

void cleanup_module( void )
{
  int idx;
  snd_minor_t *minor;
  
  for ( idx = 0; idx < SND_OS_MINORS; idx++ ) {
    minor = snd_minors[ idx ];
#if 0
    snd_printk( "unregistering minor %i\n", idx );
#endif
    if ( minor ) {
      if ( minor -> unregister )
        minor -> unregister( idx );
       else
        snd_unregister_minor( idx );
    }
#if 0
    snd_printk( "done\n" );
#endif
  }
  for ( idx = 0; idx < SND_OS_MINORS; idx++ ) {
    minor = snd_minors[ idx ];
    if ( minor && minor -> unregister )
      printk( "%s device minor %i not unregistered!!!\n", idx < SND_MINOR_BEGIN ? "Native" : "OSS", idx < SND_MINOR_BEGIN ? idx & SND_MINOR_OSS_MASK : idx );
  }
  snd_info_done();
  snd_malloc_done();
#if defined( LINUX_2_1 ) || LinuxVersionCode( 2, 0, 34 ) <= LINUX_VERSION_CODE
  snd_persist_done();
#endif
  if ( unregister_chrdev( snd_major, "snd" ) != 0 )
    printk( "unable to unregister major device number %d\n", snd_major );
}
