/*
 * FILE:    auddev_linux.c
 * PROGRAM: RAT
 * AUTHOR:  Jaroslav Kysela <perex@jcu.cz>
 * DESCRIPTION: Interface for Advanced Linux Sound Architecture driver
 *
 *   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 "assert.h"
#include "bat_include.h"

#ifdef Linux

#include <stdlib.h>
#include <ctype.h>
#include <sys/asoundlib.h>

static int iport = AUDIO_MICROPHONE;
static audio_format format;
static void *alsa_handle = NULL;
static int alsa_mode = SND_PCM_OPEN_DUPLEX;
static int alsa_block_mode = 1;
static int alsa_duplex = -1;

static int audio_card( void )
{
  char *tmp;
  int cardno = 0;

  tmp = getenv( "RAT_ALSA_CARD" );
  if ( tmp ) {
    cardno = snd_card_name( tmp );
    if ( cardno < 0 ) {
      fprintf( stderr, "RAT ERROR: Invalid soundcard '%s'...\n", tmp );
      exit(1);
    }
  }
  return cardno;
}

static int audio_device( void )
{
  char *tmp;
  int device = 0;
  
  tmp = getenv( "RAT_ALSA_DEVICE" );
  if ( tmp ) {
    device = atoi( tmp );
    if ( !isdigit(tmp) || device < 0 || device > 31 ) {
      fprintf( stderr, "RAT ERROR: Invalid device '%s'...\n", tmp );
      exit(1);
    }
  }
  return device;
}

static int audio_open_rw( int mode )
{
  snd_pcm_format_t format;
  snd_pcm_playback_params_t pparams;
  snd_pcm_record_params_t rparams;
  int cardno, device, err;

  cardno = audio_card();
  device = audio_device();
  if ( (err = snd_pcm_open( &alsa_handle, cardno, device, alsa_mode = mode )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Unable to open sound device: %s\n", snd_strerror( err ) );
    exit(1);
  }
  if ( (err = snd_pcm_block_mode( alsa_handle, alsa_block_mode = 1 )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Unable to set block mode: %s\n", snd_strerror( err ) );
    exit(1);
  }
  bzero( &format, sizeof( format ) );
  format.format = SND_PCM_SFMT_S16_LE;
  format.rate = 8000;
  format.channels = 1;	/* mono */
  if ( (err = snd_pcm_playback_format( alsa_handle, &format )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Unable to set playback format: %s\n", snd_strerror( err ) );
    exit(1);
  }
  if ( alsa_mode == SND_PCM_OPEN_DUPLEX ) {
    if ( (err = snd_pcm_record_format( alsa_handle, &format )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Unable to set record format: %s\n", snd_strerror( err ) );
      exit(1);
    }
  }
  bzero( &pparams, sizeof( pparams ) );
  bzero( &rparams, sizeof( rparams ) );
  pparams.fragment_size = rparams.fragment_size = 160;	/* ok, 1/100Hz: 16000 / 100 */
  pparams.fragments_max = 4000 / 160;		/* 0.25 sec */
  pparams.fragments_room = 1;
  rparams.fragments_min = 1;
  if ( (err = snd_pcm_playback_params( alsa_handle, &pparams )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Unable to set playback parameters: %s\n", snd_strerror( err ) );
    exit(1);
  }
  if ( alsa_mode == SND_PCM_OPEN_DUPLEX ) {
    if ( (err = snd_pcm_record_params( alsa_handle, &rparams )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Unable to set record parameters: %s\n", snd_strerror( err ) );
      exit(1);
    }
  }
  return 100000 + (cardno * 256) + device;
}

/* Try to open the audio device.              */
/* Return TRUE if successful FALSE otherwise. */
int audio_open(audio_format fmt)
{
  format = fmt;
  if (audio_duplex(-1)) {
    return audio_open_rw( SND_PCM_OPEN_DUPLEX );
  } else {
    return audio_open_rw( SND_PCM_OPEN_PLAYBACK );
  }
}

/* Close the audio device */
void audio_close(int audio_fd)
{
  if (audio_fd < 0) return;
  if (!alsa_handle) return;
  snd_pcm_drain_playback( alsa_handle );
  snd_pcm_close( alsa_handle );
  alsa_handle = NULL;
  alsa_duplex = -1;
}

/* Flush input buffer */
void audio_drain(int audio_fd)
{
  snd_pcm_drain_playback( alsa_handle );
}

int audio_duplex( int audio_fd )
{
  int err;
  snd_pcm_info_t info;

  if (audio_fd < 0) {
    void *handle;
  
    if ( (err = snd_ctl_open( &handle, audio_card() )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Cannot open control device: %s\n", snd_strerror( err ) );
      exit(1);
    }
    if ( (err = snd_ctl_pcm_info( handle, audio_device(), &info )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Cannot get info about PCM device: %s\n", snd_strerror( err ) );
      exit(1);
    }
    snd_ctl_close( handle );
    alsa_duplex = -1;
  } else {
    /* ok.. use cached value if available */
    if ( alsa_duplex >= 0 ) return alsa_duplex;
    if ( (err = snd_pcm_info( alsa_handle, &info )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Cannot get info about PCM device: %s\n", snd_strerror( err ) );
      exit( 1 );
    }
    alsa_duplex = (info.flags & SND_PCM_INFO_DUPLEX) ? 1 : 0;
  }

  return (info.flags & SND_PCM_INFO_DUPLEX) ? 1 : 0;
}

/* Gain and volume values are in the range 0 - MAX_AMP */
void audio_set_gain( int audio_fd, int gain )
{
  /* not implemented yet - we can leave this function to external mixer */
}

int audio_get_gain( int audio_fd )
{
  /* not implemented yet - we can leave this function to external mixer */
  return MAX_AMP / 2;
}

void audio_set_volume( int audio_fd, int vol )
{
  /* not implemented yet - we can leave this function to external mixer */
}

int audio_get_volume( int audio_fd )
{
  /* not implemented yet - we can leave this function to external mixer */
  return MAX_AMP / 2;
}

int audio_read( int audio_fd, sample *buf, int samples )
{
  if ( audio_fd >= 0 && alsa_handle && alsa_mode != SND_PCM_OPEN_PLAYBACK ) {
    int err, len, read_len;
    snd_pcm_record_status_t status;

    /* Figure out how many bytes we can read before blocking... */
    if ( (err = snd_pcm_record_status( alsa_handle, &status )) < 0 ) {
      fprintf( stderr, "RAT ERROR: Cannot get record status: %s\n", snd_strerror( err ) );
      exit(1);
    }
    if ( status.count > (samples * BYTES_PER_SAMPLE) ) {
      read_len = (samples * BYTES_PER_SAMPLE);
    } else {
      read_len = status.count;
    }
    if ( !read_len ) {		/* kick record */
      int fd;
      fd_set in;
      struct timeval tv;
      
      FD_ZERO( &in );
      tv.tv_sec = 0;
      tv.tv_usec = 0;
      fd = snd_pcm_file_descriptor( alsa_handle );
      if ( fd >= 0 ) {
        FD_SET( fd, &in );
        select( fd + 1, &in, NULL, NULL, &tv );
      }
      return 0;
    }
    /* Read the data... */
    if ( (len = snd_pcm_read( alsa_handle, (char *)buf, read_len)) < 0 ) {
      fprintf( stderr, "RAT ERROR: Audio read: %s\n", snd_strerror( len ) );
      return 0;
    }
    return len / BYTES_PER_SAMPLE;
  } else {
    /* The value returned should indicate the time (in audio samples) */
    /* since the last time read was called.                           */
    int                   i;
    int                   diff;
    static struct timeval last_time;
    static struct timeval curr_time;
    static int            first_time = 0;

    if (first_time == 0) {
      gettimeofday(&last_time, NULL);
      first_time = 1;
    }
    gettimeofday(&curr_time, NULL);
    diff = (((curr_time.tv_sec - last_time.tv_sec) * 1e6) + (curr_time.tv_usec - last_time.tv_usec)) / 125;
    if (diff > samples) diff = samples;
    if (diff <      80) diff = 80;
    xmemchk();
    for (i=0; i<diff; i++) {
      buf[i] = L16_AUDIO_ZERO;
    }
    xmemchk();
    last_time = curr_time;
    return diff;
  }
}

int audio_write( int audio_fd, sample *buf, int samples )
{
  int done, len;
  char *p;

  if ( audio_fd >= 0 && alsa_handle && alsa_mode != SND_PCM_OPEN_RECORD ) {
    p   = (char *) buf;
    len = samples * BYTES_PER_SAMPLE;
    if ( (done = snd_pcm_write(alsa_handle, p, len)) == len ) return samples;
    if ( errno && errno != EINTR )
      fprintf( stderr, "RAT ERROR: Audio write (len = %i, done = %i): %s\n", len, done, done < 0 ? snd_strerror( done ) : "Success" );
    return samples - (done / BYTES_PER_SAMPLE);
  } else {
    return samples;
  }
}

/* Check if the audio output has run out of data */
int audio_is_dry(int audio_fd)
{
  int err;
  snd_pcm_playback_status_t status;
  
  if (audio_fd >= 0 && alsa_handle) {
    if ( (err = snd_pcm_playback_status( alsa_handle, &status )) < 0 )
      return 0;
    return status.queue == 0;
  }
  return 0;
}

/* Set ops on audio device to be non-blocking */
void audio_non_block(int audio_fd)
{
#if 0 /* we have better way for stream control */
  int err;

  if (audio_fd < 0 || !alsa_handle) {
    return;
  }
  if ( (err = snd_pcm_block_mode( alsa_handle, alsa_block_mode = 0 )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Cannot set non-blocking mode: %s\n", snd_strerror( err ) );
  }
#endif
}

/* Set ops on audio device to block */
void audio_block(int audio_fd)
{
  int err;

  if (audio_fd < 0 || !alsa_handle) {
    return;
  }
  if ( (err = snd_pcm_block_mode( alsa_handle, alsa_block_mode = 1 )) < 0 ) {
    fprintf( stderr, "RAT ERROR: Cannot set blocking mode: %s\n", snd_strerror( err ) );
  }
}

void audio_set_oport( int audio_fd, int port )
{
  /* not implemeted yet */
}

int audio_get_oport(int audio_fd)
{
  /* There appears to be no-way to select this with ALSA... */
  return AUDIO_HEADPHONE;
}

int audio_next_oport(int audio_fd)
{
  /* There appears to be no-way to select this with ALSA... */
  return AUDIO_HEADPHONE;
}

void audio_set_iport( int audio_fd, int port )
{
  /* not implemented yet - we can leave this function to external mixer */
}

int audio_get_iport( int audio_fd )
{
  /* not implemented yet - we can leave this function to external mixer */
  return iport;
}

int audio_next_iport(int audio_fd)
{
  /* not implemented yet - we can leave this function to external mixer */
  return iport;
}

void audio_switch_out( int audio_fd, cushion_struct *ap )
{
  if ( audio_fd < 0 || !alsa_handle ) return;
  if ( !audio_duplex(audio_fd) && alsa_mode == SND_PCM_OPEN_RECORD ) {
    audio_close(audio_fd);
    audio_open_rw(O_WRONLY);
  }
}

void audio_switch_in(int audio_fd)
{
  if ( audio_fd < 0 || !alsa_handle ) return;
  if ( !audio_duplex(audio_fd) && alsa_mode == SND_PCM_OPEN_PLAYBACK ) {
    audio_close(audio_fd);
    audio_open_rw(O_RDONLY);
  }
}

#endif /* Linux */
