/* Evil evil evil hack to get OSS apps to cooperate with esd
 * Copyright (C) 1998, 1999 Manish Singh <yosh@gimp.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/* #define DSP_DEBUG */

/* This lets you run multiple instances of x11amp by setting the X11AMPNUM
   environment variable. Only works on glibc2.
 */
/* #define MULTIPLE_X11AMP */

#if defined(__GNUC__) && !defined(__STRICT_ANSI__)

#define _GNU_SOURCE 1

#ifdef DSP_DEBUG
#define DPRINTF(format, args...)	printf(format, ## args)
#else
#define DPRINTF(format, args...)
#endif

#include "config.h"

#include <dlfcn.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>

#ifdef HAVE_MACHINE_SOUNDCARD_H
#  include <machine/soundcard.h>
#else
#  ifdef HAVE_SOUNDCARD_H
#    include <soundcard.h>
#  else
#    include <sys/soundcard.h>
#  endif
#endif

#include "esd.h"

/* BSDI has this functionality, but not define :() */
#if defined(RTLD_NEXT)
#define REAL_LIBC RTLD_NEXT
#else
#define REAL_LIBC ((void *) -1L)
#endif

#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__bsdi__)
typedef unsigned long request_t;
#else
typedef int request_t;
#endif

static int sndfd = -1, mixfd = -1;
static int settings = 0, done = 0;

static char *ident = NULL, *mixer = NULL;
static int use_mixer = 0;

/*
 * memory mapping emulation
 */
static int mmapemu = 0;
static count_info mmapemu_ocount;
static char *mmapemu_obuffer = NULL;
static int mmapemu_osize;
static long long mmapemu_bytes_per_sec;
struct timeval mmapemu_last_flush;

#define MMAPEMU_FRAGSTOTAL 16
#define MMAPEMU_FRAGSIZE ESD_BUF_SIZE

#define OSS_VOLUME_BASE 50

#define ESD_VOL_TO_OSS(left, right) (short int)			\
    (((OSS_VOLUME_BASE * (right) / ESD_VOLUME_BASE) << 8) |	\
      (OSS_VOLUME_BASE * (left)  / ESD_VOLUME_BASE))

#define OSS_VOL_TO_ESD_LEFT(vol) 				\
    (ESD_VOLUME_BASE * (vol & 0xff) / OSS_VOLUME_BASE)
#define OSS_VOL_TO_ESD_RIGHT(vol) 				\
    (ESD_VOLUME_BASE * ((vol >> 8) & 0xff) / OSS_VOLUME_BASE)

static void
get_volume (int *left, int *right)
{
  int vol;

  if (read (mixfd, &vol, sizeof (vol)) != sizeof (vol))
    *left = *right = ESD_VOLUME_BASE;
  else
    {
      *left  = OSS_VOL_TO_ESD_LEFT  (vol);
      *right = OSS_VOL_TO_ESD_RIGHT (vol);
    }
}

static void
set_volume (int left, int right)
{
  int vol = ESD_VOL_TO_OSS (left, right);

  write (mixfd, &vol, sizeof (vol));
}


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

static void
dsp_init (void)
{
  pthread_mutex_lock(&mutex);
  if (!ident)
    {
      const char *str;
     
      str = getenv ("ESDDSP_NAME");
      ident = malloc (ESD_NAME_MAX+1);
      strncpy (ident, (str ? str : "esddsp"), ESD_NAME_MAX);
      ident[ESD_NAME_MAX] = '\0';

      str = getenv("ESDDSP_MMAP");
      mmapemu = str && !strcmp(str,"1");

      if (getenv ("ESDDSP_MIXER"))
	{
	  use_mixer = 1;

	  str = getenv ("HOME");
	  if (str)
 	    {
	      mixer = malloc (strlen (str) + strlen (ident) + 10);
	      sprintf (mixer, "%s/.esddsp_%s", str, ident);
	    }
	  else
	    {
	      fprintf (stderr, "esddsp: can't get home directory\n");
	      exit (1);
	    }

	  DPRINTF ("mixer settings file: %s\n", mixer);
	}
    }
  pthread_mutex_unlock(&mutex);
}

static void
mix_init (int *esd, int *player)
{
  esd_info_t *all_info;
  esd_player_info_t *player_info;

  if (*esd < 0 && (*esd = esd_open_sound (NULL)) < 0)
    return;
    
  if (*player < 0)
    {
      all_info = esd_get_all_info (*esd);
      if (all_info)
	{
	  for (player_info = all_info->player_list; player_info;
	       player_info = player_info->next)
	    if (!strcmp(player_info->name, ident))
	      {
		*player = player_info->source_id;
		break;
	      }

	  esd_free_all_info (all_info);
	}
    }
}

static void mmapemu_flush(void)
{
  struct timeval current_time;
  long long usec_since_last_flush;
  long long bytes_to_flush;
  int frags_to_flush;

  gettimeofday(&current_time, NULL);
  usec_since_last_flush = 1000000 * (current_time.tv_sec - mmapemu_last_flush.tv_sec) + (current_time.tv_usec - mmapemu_last_flush.tv_usec);

  bytes_to_flush = (mmapemu_bytes_per_sec * usec_since_last_flush) / 1000000;
  frags_to_flush  = bytes_to_flush / MMAPEMU_FRAGSIZE;

  if (frags_to_flush > MMAPEMU_FRAGSTOTAL)
    frags_to_flush = MMAPEMU_FRAGSTOTAL;

  if (frags_to_flush > 0)
    mmapemu_last_flush = current_time;

  while (frags_to_flush-- > 0)
    {
      write(sndfd, &mmapemu_obuffer[mmapemu_ocount.ptr], MMAPEMU_FRAGSIZE);
      mmapemu_ocount.ptr += MMAPEMU_FRAGSIZE;
      mmapemu_ocount.ptr %= mmapemu_osize;
      mmapemu_ocount.blocks++;
      mmapemu_ocount.bytes += MMAPEMU_FRAGSIZE;
    }
}

static int
open_wrapper (int (*func) (const char *, int, mode_t),
	      const char *pathname, int flags, ...)
{
  va_list args;
  mode_t mode;

  dsp_init ();

  va_start (args, flags);
  if (sizeof (mode_t) < sizeof (int))
	  mode = va_arg (args, int);
  else
	  mode = va_arg (args, mode_t);
  va_end (args);

  if (!strcmp (pathname, "/dev/dsp"))
    {
      if (!getenv ("ESPEAKER") && !mmapemu)
	{
          int ret;

	  flags |= O_NONBLOCK;
	  if ((ret = (*func) (pathname, flags, mode)) >= 0)
	    return ret;
	}

      DPRINTF ("hijacking /dev/dsp open, and taking it to esd...\n");
      settings = done = 0;
      return (sndfd = esd_open_sound (NULL));
    }
  else if (use_mixer && !strcmp (pathname, "/dev/mixer"))
    {
      DPRINTF ("hijacking /dev/mixer open, and taking it to esd...\n");
      return (mixfd = (*func) (mixer, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
    }
  else
    return (*func) (pathname, flags, mode);
}

int
open (const char *pathname, int flags, ...)
{
  static int (*func) (const char *, int, mode_t) = NULL;
  va_list args;
  mode_t mode;

  DPRINTF ("open\n");

  if (!func) 
    {
      func = (int (*) (const char *, int, mode_t)) dlsym (REAL_LIBC, "open");
      if (!func) 
	{
	  fprintf(stderr, "esddsp: error: Cannot find symbol 'open'\n");
	  errno = ENXIO;
	  return -1;
	}
    }

  va_start (args, flags);
  mode = va_arg (args, mode_t);
  va_end (args);

  return open_wrapper(func, pathname, flags, mode);
}

int
open64 (const char *pathname, int flags, ...)
{
  static int (*func) (const char *, int, mode_t) = NULL;
  va_list args;
  mode_t mode;

  DPRINTF ("open64\n");

  if (!func) 
    {
      func = (int (*) (const char *, int, mode_t)) dlsym (REAL_LIBC, "open64");
      if (!func) 
	{
	  fprintf(stderr, "esddsp: error: Cannot find symbol 'open64'\n");
	  errno = ENXIO;
	  return -1;
	}
    }

  va_start (args, flags);
  mode = va_arg (args, mode_t);
  va_end (args);

  return open_wrapper(func, pathname, flags, mode);
}

static FILE *
fopen_wrapper (FILE * (*func) (const char *, const char *),
	       const char *pathname, const char *mode)
{
  dsp_init ();

  if (!strcmp (pathname, "/dev/dsp"))
    {
      if (!getenv ("ESPEAKER") && !mmapemu)
	{
          FILE *ret;

	  if ((ret = (*func) (pathname, mode)) != NULL)
	    return ret;
	}

      DPRINTF ("hijacking /dev/dsp fopen, and taking it to esd...\n");
      settings = done = 0;
      return fdopen((sndfd = esd_open_sound (NULL)), mode);
    }
  else if (use_mixer && !strcmp (pathname, "/dev/mixer"))
    {
      FILE *ret;
      DPRINTF ("hijacking /dev/mixer fopen, and taking it to esd...\n");

      if ((ret = (*func) (pathname, mode)) != NULL)
	{
	  mixfd = fileno(ret);
	  return ret;
	}
      else
        return NULL;
    }
  else
    return (*func) (pathname, mode);
}

FILE *
fopen (const char *path, const char *mode)
{
  static FILE * (*func) (const char *, const char *) = NULL;

  DPRINTF ("fopen\n");

  if (!func) 
    {
      func = (FILE * (*) (const char *, const char *)) dlsym (REAL_LIBC, "fopen");
      if (!func) 
	{
	  fprintf(stderr, "esddsp: error: Cannot find symbol 'fopen'\n");
	  return NULL;
	}
    }

  return fopen_wrapper(func, path, mode);
}

FILE *
fopen64 (const char *path, const char *mode)
{
  static FILE * (*func) (const char *, const char *) = NULL;

  DPRINTF ("fopen64\n");

  if (!func) 
    {
      func = (FILE * (*) (const char *, const char *)) dlsym (REAL_LIBC, "fopen64");
      if (!func) 
	{
	  fprintf(stderr, "esddsp: error: Cannot find symbol 'fopen64'\n");
	  return NULL;
	}
    }

  return fopen_wrapper(func, path, mode);
}

static int
dspctl (int fd, request_t request, void *argp)
{
  static esd_format_t fmt = ESD_STREAM | ESD_PLAY | ESD_MONO;
  static int speed;

  int *arg = (int *) argp;

  DPRINTF ("hijacking /dev/dsp ioctl, and sending it to esd "
	   "(%d : %x - %p)\n", fd, request, argp);
  
  switch (request)
    {
    case SNDCTL_DSP_RESET:
    case SNDCTL_DSP_POST:
      break;

    case SNDCTL_DSP_SETFMT:
      fmt |= (*arg & 0x30) ? ESD_BITS16 : ESD_BITS8;
      settings |= 1;
      break;

    case SNDCTL_DSP_SPEED:
      speed = *arg;
      settings |= 2;
      break;

    case SNDCTL_DSP_STEREO:
      fmt &= ~ESD_MONO;
      fmt |= (*arg) ? ESD_STEREO : ESD_MONO;
      break;

    case SNDCTL_DSP_GETBLKSIZE:
      if (mmapemu)
      	*arg = MMAPEMU_FRAGSIZE;
      else
      	*arg = ESD_BUF_SIZE;
     
      break;

    case SNDCTL_DSP_GETFMTS:
      *arg = 0x38;
      break;

#ifdef SNDCTL_DSP_GETCAPS
    case SNDCTL_DSP_GETCAPS:
      if (mmapemu)
	*arg = DSP_CAP_MMAP | DSP_CAP_TRIGGER | DSP_CAP_REALTIME;
      else
	*arg = 0;
      break;
#endif

    case SNDCTL_DSP_GETOSPACE:
    case SNDCTL_DSP_GETISPACE:
      {
	audio_buf_info *bufinfo = (audio_buf_info *) argp;
	if (mmapemu)
	  {
	    /* Number of fragments allocated for buffering */
	    bufinfo->fragstotal = MMAPEMU_FRAGSTOTAL;
	    /* Size of a fragment in bytes (same as SNDCTL_DSP_GETBLKSIZE) */
	    bufinfo->fragsize = MMAPEMU_FRAGSIZE;
	    /* Number of bytes that can be read or written immediately without blocking */
	    bufinfo->bytes = MMAPEMU_FRAGSIZE;
	    /* Number of full fragments that can be read or written without blocking */
	    bufinfo->fragments = 1;
	  }
	else
	  {
	    /* Number of fragments allocated for buffering */
	    bufinfo->fragstotal = 1;
	    /* Size of a fragment in bytes (same as SNDCTL_DSP_GETBLKSIZE) */
	    bufinfo->fragsize = ESD_BUF_SIZE;
	    /* Number of bytes that can be read or written immediately without blocking */
	    bufinfo->bytes = ESD_BUF_SIZE;
	    /* Number of full fragments that can be read or written without blocking */
	    bufinfo->fragments = 1;
	  }
      }
      break;

    case SNDCTL_DSP_GETOPTR:
      if (mmapemu)
        {
	  DPRINTF("esddsp: mmapemu getoptr\n");
	  mmapemu_flush();
	  *((count_info *)arg) = mmapemu_ocount;
	  mmapemu_ocount.blocks = 0;
	}
      else
        {
	  DPRINTF ("esddsp: SNDCTL_DSP_GETOPTR unsupported\n");
	}
      break;

    default:
      DPRINTF ("unhandled /dev/dsp ioctl (%x - %p)\n", request, argp);
      break;
    }

  if (settings == 3 && !done)
    {
      int proto = ESD_PROTO_STREAM_PLAY;

      done = 1;

      if (write (sndfd, &proto, sizeof (proto)) != sizeof (proto))
        return -1;
      if (write (sndfd, &fmt, sizeof (fmt)) != sizeof (fmt))
        return -1;
      if (write (sndfd, &speed, sizeof (speed)) != sizeof (speed))
        return -1;
      if (write (sndfd, ident, ESD_NAME_MAX) != ESD_NAME_MAX)
        return -1;

      if (mmapemu)
	{
	  mmapemu_ocount.ptr=mmapemu_ocount.blocks=mmapemu_ocount.bytes=0;
	  mmapemu_bytes_per_sec = speed * ((fmt & ESD_BITS16) ? 2 : 1) * ((fmt & ESD_STEREO) ? 2 : 1);
	  gettimeofday(&mmapemu_last_flush, NULL);
	}

      fmt = ESD_STREAM | ESD_PLAY | ESD_MONO;
      speed = 0;

      if (use_mixer)
	{
  	  int esd = -1, player = -1;
	  int left, right;

	  while (player < 0)
	    mix_init (&esd, &player);

	  get_volume (&left, &right);

	  DPRINTF ("panning %d - %d %d\n", player, left, right);
	  esd_set_stream_pan (esd, player, left, right);

	  esd_close(esd);
	}
    }

  return 0;
}

int
mixctl (int fd, request_t request, void *argp)
{
  static int esd = -1, player = -1;
  static int left, right;

  int *arg = (int *) argp;

  DPRINTF ("hijacking /dev/mixer ioctl, and sending it to esd "
	   "(%d : %x - %p)\n", fd, request, argp);

  switch (request)
    {
    case SOUND_MIXER_READ_DEVMASK:
      *arg = 5113;
      break;

    case SOUND_MIXER_READ_PCM:
      mix_init (&esd, &player);

      if (player > 0)
	{
	  esd_info_t *all_info;

	  all_info  = esd_get_all_info (esd);

	  esd_close(esd);

	  if (all_info)
	    {
	      esd_player_info_t *player_info;

	      for (player_info = all_info->player_list; player_info;
		   player_info = player_info->next)
		if (player_info->source_id == player)
		  {
		    *arg = ESD_VOL_TO_OSS (player_info->left_vol_scale,
					   player_info->right_vol_scale);
		  }

	      esd_free_all_info (all_info);
	    }
	  else
	    return -1;
	}
      else
	{
          get_volume (&left, &right);
          esd_close(esd);
	  *arg = ESD_VOL_TO_OSS (left, right);
	}

      break;

    case SOUND_MIXER_WRITE_PCM:
      mix_init (&esd, &player);

      left  = OSS_VOL_TO_ESD_LEFT  (*arg);
      right = OSS_VOL_TO_ESD_RIGHT (*arg);

      set_volume (left, right);

      if (player > 0)
	{
	  DPRINTF ("panning %d - %d %d\n", player, left, right);
	  esd_set_stream_pan (esd, player, left, right);
	}

      esd_close(esd);

      break;

    default:
      DPRINTF ("unhandled /dev/mixer ioctl (%x - %p)\n", request, argp);
      break;
    }

  return 0;
}

int
ioctl (int fd, request_t request, ...)
{
  static int (*func) (int, request_t, void *) = NULL;
  va_list args;
  void *argp;

  if (!func)                                                                    
    func = (int (*) (int, request_t, void *)) dlsym (REAL_LIBC, "ioctl");             
  va_start (args, request);
  argp = va_arg (args, void *);
  va_end (args);

  if (fd == sndfd)
    return dspctl (fd, request, argp);
  else if (fd == mixfd) {
    if(use_mixer)
      return mixctl (fd, request, argp);
  } else { /* (fd != sndfd && fd != mixfd) */
        return (*func) (fd, request, argp); 
      }
  return 0;
}

int
close (int fd)
{
  static int (*func) (int) = NULL;

  if (!func)
    func = (int (*) (int)) dlsym (REAL_LIBC, "close");

  if (fd == sndfd)
    sndfd = -1;
  else if (fd == mixfd)
    mixfd = -1;
 
  return (*func) (fd);
}

void *
mmap (void  *start,  size_t length, int prot, int flags, int fd, off_t offset)
{
  static void *(*func) (void *, size_t, int, int, int, off_t) = NULL;
  
  if (!func)
    func = (void * (*) (void *, size_t, int, int, int, off_t)) dlsym (REAL_LIBC, "mmap");

  if(fd != sndfd || sndfd == -1)
    return (*func)(start,length,prot,flags,fd,offset);
  else
    {
      DPRINTF ("esddsp: mmap - start = %x, length = %d, prot = %d\n",
	       start, length, prot);
      DPRINTF ("      flags = %d, fd = %d, offset = %d\n",flags, fd,offset);
      if(mmapemu)
	{
	  mmapemu_osize = length;
	  mmapemu_obuffer = malloc(length);
	  mmapemu_ocount.ptr = mmapemu_ocount.blocks = mmapemu_ocount.bytes = 0;
	  return mmapemu_obuffer;
	}
      else DPRINTF ("esddsp: /dev/dsp mmap (unsupported, try -m option)...\n");
    }
  return (void *)-1;
}

void *
mmap64 (void  *start,  size_t length, int prot, int flags, int fd, off64_t offset)
{
  static void *(*func) (void *, size_t, int, int, int, off64_t) = NULL;
  
  if (!func)
    func = (void * (*) (void *, size_t, int, int, int, off64_t)) dlsym (REAL_LIBC, "mmap64");

  if(fd != sndfd || sndfd == -1)
    return (*func)(start,length,prot,flags,fd,offset);
  else
    {
      DPRINTF ("esddsp: mmap64 - start = %x, length = %d, prot = %d\n",
	       start, length, prot);
      DPRINTF ("      flags = %d, fd = %d, offset = %lld\n",flags, fd,offset);
      if(mmapemu)
	{
	  mmapemu_osize = length;
	  mmapemu_obuffer = malloc(length);
	  mmapemu_ocount.ptr = mmapemu_ocount.blocks = mmapemu_ocount.bytes = 0;
	  return mmapemu_obuffer;
	}
      else DPRINTF ("esddsp: /dev/dsp mmap64 (unsupported, try -m option)...\n");
    }
  return (void *)-1;
}

int
munmap (void *start, size_t length)
{
  static int (*func) (void *, size_t) = NULL;

  if (!func)
    func = (int (*) (void *, size_t)) dlsym (REAL_LIBC, "munmap");

  if(start != mmapemu_obuffer || mmapemu_obuffer == 0)
    return (*func)(start,length);

  DPRINTF ("esddsp: /dev/dsp munmap...\n");
  mmapemu_obuffer = NULL;
  free(start);

  return 0;
}

#ifdef MULTIPLE_X11AMP

#include <socketbits.h>
#include <sys/param.h>
#include <sys/un.h>

#define ENVSET "X11AMPNUM"

int
unlink (const char *filename)
{
  static int (*func) (const char *) = NULL;
  const char *num;

  if (!func)
    func = (int (*) (const char *)) dlsym (REAL_LIBC, "unlink");

  if (!strcmp (filename, "/tmp/X11Amp_CTRL") && (num = getenv (ENVSET)))
    {
      char buf[PATH_MAX] = "/tmp/X11Amp_CTRL";
      strncat (buf, num, (PATH_MAX - strlen(buf) - 1));
      return (*func) (buf); 
    }
  else
    return (*func) (filename);
}

typedef int (*sa_func_t) (int, struct sockaddr *, int);

static int
sockaddr_mangle (sa_func_t func, int fd, struct sockaddr *addr, int len)
{
  const char *num;

  if (!strcmp (((struct sockaddr_un *) addr)->sun_path, "/tmp/X11Amp_CTRL")
      && (num = getenv(ENVSET)))
    {
      int ret;
      char buf[PATH_MAX] = "/tmp/X11Amp_CTRL";

      struct sockaddr_un *new_addr = malloc (len);

      strncat (buf, num, (PATH_MAX - strlen(buf) - 1));
      memcpy (new_addr, addr, len);
      strncpy (new_addr->sun_path, buf, strlen(new_addr->sun_path));

      ret = (*func) (fd, (struct sockaddr *) new_addr, len);

      free (new_addr);
      return ret;
    } 
  else
    return (*func) (fd, addr, len);
}

int
bind (int fd, struct sockaddr *addr, int len)
{
  static sa_func_t func = NULL;

  if (!func)
    func = (sa_func_t) dlsym (REAL_LIBC, "bind");
  return sockaddr_mangle (func, fd, addr, len);
}

int
connect (int fd, struct sockaddr *addr, int len)
{
  static sa_func_t func = NULL;

  if (!func)
    func = (sa_func_t) dlsym (REAL_LIBC, "connect");
  return sockaddr_mangle (func, fd, addr, len);
}

#endif /* MULTIPLE_X11AMP */

#else /* __GNUC__ */

void
nogcc (void)
{
  ident = NULL;
}

#endif

