/*
 *  Digital Audio (PCM) - /proc interface
 *  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.
 *
 */

#include "driver.h"
#include "pcm.h"
#include "info.h"

#define SND_PCM_PROC_BUFFER_SIZE	(128*1024)

struct snd_pcm_proc_private {
  unsigned char *buffer;		/* allocated buffer */
  unsigned int size;
  unsigned int used;
  unsigned int head;
  unsigned int tail;
  unsigned int xrun;
  unsigned int sleep: 1;
  snd_spin_define( ptr );
  snd_spin_define( sleep );
  snd_sleep_define( read );
  struct snd_pcm_proc_private *next;
};

static void snd_pcm_proc_file_write( struct snd_pcm_proc_private *proc, const void *buffer, unsigned int count )
{
  unsigned long flags;
  unsigned int tmp, size, size1;

  if ( !count ) return;
  snd_spin_lock( proc, ptr, &flags );
  tmp = proc -> size - proc -> used;		/* free */
  if ( tmp < count ) {
    proc -> xrun += count - tmp;
    count = tmp;
  }
  tmp = proc -> size - proc -> head;
  snd_spin_unlock( proc, ptr, &flags );
  size = count;
  if ( tmp < size ) size = tmp;
  size1 = count - size;
  copy_from_user( proc -> buffer + proc -> head, buffer, size );
  if ( size1 > 0 ) {
    copy_from_user( proc -> buffer, buffer + size, size1 );
  }
  snd_spin_lock( proc, ptr, &flags );
  proc -> head += count; proc -> head %= proc -> size;
  proc -> used += count;
  snd_spin_unlock( proc, ptr, &flags );
  if ( proc -> sleep )
    snd_wakeup( proc, read );
}

void snd_pcm_proc_write( snd_pcm_channel_t *pchn, const void *buffer, unsigned int count )
{
  struct snd_pcm_proc_private *first;

  snd_mutex_down( pchn, proc );
  first = (struct snd_pcm_proc_private *)pchn -> proc_private;
  while ( first ) {
    snd_pcm_proc_file_write( first, buffer, count );
    first = first -> next;
  }
  snd_mutex_up( pchn, proc );
}

static long snd_pcm_proc_read( void *private_data, void *file_private_data, struct file *file, char *buf, long count )
{
  unsigned long flags;
  struct snd_pcm_proc_private *proc;
  unsigned int size, size1;
  long count1, result = 0;
  
  proc = (struct snd_pcm_proc_private *)file_private_data;
  if ( count > proc -> size ) count = proc -> size;
  if ( !count ) return result;
  while ( count > 0 ) {
    while ( !proc -> used ) {
      if ( file -> f_flags & O_NONBLOCK ) return result;
      snd_spin_lock( proc, sleep, &flags );
      proc -> sleep = 1;
      snd_sleep( proc, read, 10 * HZ );
      proc -> sleep = 0;
      snd_spin_unlock( proc, sleep, &flags );
      if ( snd_sleep_abort( proc, read ) ) {
        return -EINTR;
      }
    }
    count1 = count;
    snd_spin_lock( proc, ptr, &flags );
    if ( count1 > proc -> used ) count1 = proc -> used;
    size = count1;
    if ( proc -> size - proc -> tail < size )
      size = proc -> size - proc -> tail;
    size1 = count1 - size;
    snd_spin_unlock( proc, ptr, &flags );
    if ( verify_area( VERIFY_WRITE, buf, count1 ) ) return -EFAULT;
    copy_to_user( buf, proc -> buffer + proc -> tail, size );
    buf += size;
    if ( size1 > 0 ) {
      copy_to_user( buf, proc -> buffer, size1 );
      buf += size1;
    }
    snd_spin_lock( proc, ptr, &flags );
    proc -> tail += count1; proc -> tail %= proc -> size;
    proc -> used -= count1;
    snd_spin_unlock( proc, ptr, &flags );
    count -= count1;
    result += count1;
  }
  return result;
}

static int snd_pcm_proc_open( void *private_data, snd_info_entry_t *entry, unsigned short mode, void **file_private_data )
{
  struct snd_pcm_proc_private *proc, *proc1;
  snd_pcm_t *pcm;
  snd_pcm_channel_t *pchn;
  int record;
  
  if ( mode == O_RDWR || mode == O_WRONLY ) return -EIO;
  proc = (struct snd_pcm_proc_private *)snd_malloc( sizeof( struct snd_pcm_proc_private ) );
  if ( !proc ) return -ENOMEM;
  memset( proc, 0, sizeof( struct snd_pcm_proc_private ) );
  proc -> buffer = vmalloc( proc -> size = SND_PCM_PROC_BUFFER_SIZE );
  if ( !proc -> buffer ) {
    snd_free( proc, sizeof( struct snd_pcm_proc_private ) );
    return -ENOMEM;
  }
  snd_spin_prepare( proc, ptr );
  snd_spin_prepare( proc, sleep );
  snd_sleep_prepare( proc, read );
  pcm = (snd_pcm_t *)private_data;
  record = entry -> name[ strlen( entry -> name ) - 1 ] == 'r';
  pchn = !record ? &pcm -> playback : &pcm -> record;
  snd_mutex_down( pchn, proc );
  proc1 = (struct snd_pcm_proc_private *)pchn -> proc_private;
  if ( !proc1 ) {
    pchn -> proc_private = proc;
  } else {
    while ( proc1 -> next ) proc1 = proc1 -> next;
    proc1 -> next = proc;
  } 
  snd_mutex_up( pchn, proc );
  *file_private_data = proc;
  MOD_INC_USE_COUNT;
  return 0;
}

static int snd_pcm_proc_release( void *private_data, snd_info_entry_t *entry, unsigned short mode, void *file_private_data )
{
  struct snd_pcm_proc_private *proc, *proc1;
  snd_pcm_t *pcm;
  snd_pcm_channel_t *pchn;
  int record;

  proc = (struct snd_pcm_proc_private *)file_private_data;
  if ( !proc ) {
    MOD_DEC_USE_COUNT;
    return -EIO;
  }
  pcm = (snd_pcm_t *)private_data;
  record = entry -> name[ strlen( entry -> name ) - 1 ] == 'r';
  pchn = !record ? &pcm -> playback : &pcm -> record;
  snd_mutex_down( pchn, proc );
  proc1 = (struct snd_pcm_proc_private *)pchn -> proc_private;
  if ( proc == proc1 ) {
    pchn -> proc_private = proc -> next;
  } else {
    while ( proc1 -> next != proc ) proc1 = proc1 -> next;
    proc1 -> next = proc -> next; 
  }
  snd_mutex_up( pchn, proc );
  vfree( proc -> buffer );
  snd_free( proc, sizeof( struct snd_pcm_proc_private ) );
  MOD_DEC_USE_COUNT;  
  return 0;
}

#ifdef LINUX_2_1
static unsigned int snd_pcm_proc_poll( void *private_data, void *file_private_data, struct file *file, poll_table *wait )
{
  unsigned int mask;
  unsigned long flags;
  struct snd_pcm_proc_private *proc;
  
  proc = (struct snd_pcm_proc_private *)file_private_data;

  snd_spin_lock( proc, sleep, &flags );
  proc -> sleep = 1;
  snd_poll_wait( file, proc, read, wait );
  snd_spin_unlock( proc, sleep, &flags );

  mask = 0;
  if ( proc -> used )
    mask |= POLLIN | POLLRDNORM; 
  return mask;
}
#else
static int snd_pcm_proc_select( void *private_data, void *file_private_data, struct file *file, int sel_type, select_table *wait )
{
  unsigned long flags;
  struct snd_pcm_proc_private *proc;
  
  proc = (struct snd_pcm_proc_private *)file_private_data;
  if ( sel_type == SEL_IN ) {
    snd_spin_lock( proc, sleep, &flags );
    if ( !proc -> used ) {
      proc -> sleep = 1;
      snd_select_wait( proc, read, wait );
      snd_spin_unlock( proc, sleep, &flags );
      return 0;
    }
    proc -> sleep = 0;
    snd_spin_unlock( proc, sleep, &flags );
  }
  return 0;
}
#endif

static const char *snd_pcm_proc_get_format( unsigned int mode )
{
  if ( mode & SND_PCM_MODE_GSM ) return "GSM";
  if ( mode & SND_PCM_MODE_MPEG ) return "MPEG";
  if ( mode & SND_PCM_MODE_ADPCM ) return "Ima-ADPCM";
  if ( mode & SND_PCM_MODE_ALAW ) return "A-Law";
  if ( mode & SND_PCM_MODE_ULAW ) return "Mu-Law";
  if ( mode & SND_PCM_MODE_U ) {
    if ( mode & SND_PCM_MODE_16 ) {
      return mode & SND_PCM_MODE_BIG ? "Unsigned 16-bit Big Endian" : "Unsigned 16-bit Little Endian";
    } else {
      return "Unsigned 8-bit";
    }
  } else {
    if ( mode & SND_PCM_MODE_16 ) {
      return mode & SND_PCM_MODE_BIG ? "Signed 16-bit Big Endian" : "Signed 16-bit Little Endian";
    } else {
      return "Signed 8-bit";
    }
  }
  return "Unknown";
}

static void snd_pcm_proc_info_read( snd_info_buffer_t *buffer, void *private_data )
{
  snd_pcm_t *pcm;
  
  pcm = (snd_pcm_t *)private_data;
  snd_iprintf( buffer, "%s\n\n", pcm -> name );
  if ( pcm -> info_flags & SND_PCM_INFO_PLAYBACK ) {
    if ( pcm -> flags & SND_PCM_LFLG_PLAY ) {
      snd_iprintf( buffer,
"Playback\n"
"  Mode           : %s\n"
"  Format         : %s\n"
"  Rate           : %dHz [requested %dHz]\n"
"  Channels       : %d\n"
"  Buffer size    : %d\n"
"  Buffer used    : %d\n"
"  Fragments      : %d\n"
"  Fragment size  : %d\n",
	pcm -> flags & SND_PCM_LFLG_OSS_PLAY ? "OSS compatible" : "native",
	snd_pcm_proc_get_format( pcm -> playback.mode ),
	pcm -> playback.real_rate, pcm -> playback.rate,
	pcm -> playback.voices,
	pcm -> playback.size,
	pcm -> playback.used_size,
	pcm -> playback.blocks,
	pcm -> playback.block_size );
      if ( !(pcm -> flags & SND_PCM_LFLG_OSS_PLAY) ) {
        snd_iprintf( buffer,
"  Fragments room : %d\n"
"  Fragments max  : %d\n",
	pcm -> playback.blocks_room,
	pcm -> playback.blocks_max );
      }
      snd_iprintf( buffer,
"  Underruns      : %d\n"
"  Total underruns: %d\n",
	pcm -> playback.xruns,
	pcm -> playback.total_xruns );
    } else {
      snd_iprintf( buffer, "Playback isn't active.\n" );
    }
  }
  if ( pcm -> info_flags & SND_PCM_INFO_RECORD ) {
    if ( pcm -> flags & SND_PCM_LFLG_RECORD ) {
      snd_iprintf( buffer,
"Record\n"
"  Mode           : %s\n"
"  Format         : %s\n"
"  Rate           : %dHz [requested %dHz]\n"
"  Channels       : %d\n"
"  Buffer size    : %d\n"
"  Buffer used    : %d\n"
"  Fragments      : %d\n"
"  Fragment size  : %d\n",
	pcm -> flags & SND_PCM_LFLG_OSS_RECORD ? "OSS compatible" : "native",
	snd_pcm_proc_get_format( pcm -> record.mode ),
	pcm -> record.real_rate, pcm -> record.rate,
	pcm -> record.voices,
	pcm -> record.size,
	pcm -> record.used_size,
	pcm -> record.blocks,
	pcm -> record.block_size );
      if ( !(pcm -> flags & SND_PCM_LFLG_OSS_RECORD) ) {
        snd_iprintf( buffer,
"  Fragments min  : %d\n",
	pcm -> record.blocks_min );
      }
      snd_iprintf( buffer,
"  Overruns       : %d\n"
"  Total overruns : %d\n",
	pcm -> record.xruns,
	pcm -> record.total_xruns );
    } else {
      snd_iprintf( buffer, "Record isn't active.\n" );
    }
  }
}

static void snd_pcm_proc_init_direction( snd_pcm_t *pcm, int direction )
{
  char name[16];
  snd_info_entry_t *entry;
  snd_pcm_channel_t *pchn;

  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  sprintf( name, "pcm%d%s", pcm -> device, direction == SND_PCM_PLAYBACK ? "p" : "r" );
  entry = snd_info_create_entry( pcm -> card, name );
  if ( entry ) {
    entry -> type = SND_INFO_ENTRY_DATA;
    entry -> private_data = pcm;
    entry -> t.data.open = snd_pcm_proc_open;
    entry -> t.data.release = snd_pcm_proc_release;
    entry -> t.data.read = snd_pcm_proc_read;
#ifdef LINUX_2_1
    entry -> t.data.poll = snd_pcm_proc_poll;
#else
    entry -> t.data.select = snd_pcm_proc_select;
#endif
    if ( snd_info_register( entry ) < 0 ) {
      snd_info_free_entry( entry );
      entry = NULL;
    }
  }
  pchn -> proc_entry = entry;
}


void snd_pcm_proc_init( snd_pcm_t *pcm )
{
  char name[16];
  snd_info_entry_t *entry;

  snd_mutex_prepare( &pcm -> playback, proc );
  snd_mutex_prepare( &pcm -> record, proc );
  if ( pcm -> info_flags & SND_PCM_INFO_PLAYBACK ) {
    snd_pcm_proc_init_direction( pcm, SND_PCM_PLAYBACK );
  }
  if ( pcm -> info_flags & SND_PCM_INFO_RECORD ) {
    snd_pcm_proc_init_direction( pcm, SND_PCM_RECORD );
  }
  sprintf( name, "pcm%d", pcm -> device );
  entry = snd_info_create_entry( pcm -> card, name );
  if ( entry ) {
    entry -> private_data = pcm;
    entry -> t.text.read_size = 1024;
    entry -> t.text.read = snd_pcm_proc_info_read;
    if ( snd_info_register( entry ) < 0 ) {
      snd_info_free_entry( entry );
      entry = NULL;
    }
    pcm -> proc_entry = entry;
  }
}

void snd_pcm_proc_done( snd_pcm_t *pcm )
{
  if ( pcm -> proc_entry ) {
    snd_info_unregister( pcm -> proc_entry );
    pcm -> proc_entry = NULL;
  }
  if ( (pcm -> info_flags & SND_PCM_INFO_RECORD) && pcm -> record.proc_entry ) {
    snd_info_unregister( (snd_info_entry_t *)pcm -> record.proc_entry );
    pcm -> record.proc_entry = NULL;
  }
  if ( (pcm -> info_flags & SND_PCM_INFO_PLAYBACK) && pcm -> playback.proc_entry ) {
    snd_info_unregister( (snd_info_entry_t *)pcm -> playback.proc_entry );
    pcm -> playback.proc_entry = NULL;
  }
}
