/* GStreamer OSS Mixer implementation
 * Copyright (C) 2003 Ronald Bultje <rbultje@ronald.bitfreak.net>
 *
 * gstossmixer.c: mixer interface implementation for OSS
 *
 * 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.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>

#include "gstossmixer.h"

#define MASK_BIT_IS_SET(mask, bit) \
  (mask & (1 << bit))

static void	gst_ossmixer_track_class_init (GstOssMixerTrackClass *klass);
static void	gst_ossmixer_track_init    (GstOssMixerTrack *track);

static gboolean	gst_ossmixer_supported	   (GstImplementsInterface *iface,
					    GType           iface_type);
static const GList *
		gst_ossmixer_list_tracks   (GstMixer       *ossmixer);

static void	gst_ossmixer_set_volume	   (GstMixer       *ossmixer,
					    GstMixerTrack  *track,
					    gint           *volumes);
static void	gst_ossmixer_get_volume	   (GstMixer       *ossmixer,
					    GstMixerTrack  *track,
					    gint           *volumes);

static void	gst_ossmixer_set_record	   (GstMixer       *ossmixer,
					    GstMixerTrack  *track,
					    gboolean        record);
static void	gst_ossmixer_set_mute	   (GstMixer       *ossmixer,
					    GstMixerTrack  *track,
					    gboolean        mute);

static const gchar **labels = NULL;
static GstMixerTrackClass *parent_class = NULL;

/* three functions: firstly, OSS has the nasty habit of inserting
 * spaces in the labels, we want to get rid of them. Secondly,
 * i18n is impossible with OSS' way of providing us with mixer
 * labels, so we make a 'given' list of i18n'ed labels. Since
 * i18n doesn't actually work, we fake it (FIXME). Thirdly, I
 * personally don't like the "1337" names that OSS gives to their
 * labels ("Vol", "Mic", "Rec"), I'd rather see full names. */
#define _(s) s

static void
fill_labels (void)
{
  gint i, pos;
  gchar *origs[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_LABELS;
  struct {
    gchar *given, *wanted;
  } cases[] = {
    /* Note: this list is simply ripped from soundcard.h. For
     * some people, some values might be missing (3D surround,
     * etc.) - feel free to add them. That's the reason why
     * I'm doing this in such a horribly complicated way. */
    { "Vol  ",    _("Volume")     },
    { "Bass ",    _("Bass")       },
    { "Trebl",    _("Treble")     },
    { "Synth",    _("Synth")      },
    { "Pcm  ",    _("PCM")        },
    { "Spkr ",    _("Speaker")    },
    { "Line ",    _("Line-in")    },
    { "Mic  ",    _("Microphone") },
    { "CD   ",    _("CD")         },
    { "Mix  ",    _("Mixer")      },
    { "Pcm2 ",    _("PCM-2")      },
    { "Rec  ",    _("Record")     },
    { "IGain",    _("In-gain")    },
    { "OGain",    _("Out-gain")   },
    { "Line1",    _("Line-1")     },
    { "Line2",    _("Line-2")     },
    { "Line3",    _("Line-3")     },
    { "Digital1", _("Digital-1")  },
    { "Digital2", _("Digital-2")  },
    { "Digital3", _("Digital-3")  },
    { "PhoneIn",  _("Phone-in")   },
    { "PhoneOut", _("Phone-out")  },
    { "Video",    _("Video")      },
    { "Radio",    _("Radio")      },
    { "Monitor",  _("Monitor")    },
    { NULL, NULL }
  };

  labels = g_malloc (sizeof (gchar *) * SOUND_MIXER_NRDEVICES);

  for (i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
    for (pos = 0; cases[pos].given != NULL; pos++) {
      if (!strcmp (cases[pos].given, origs[i])) {
        labels[i] = g_strdup (cases[pos].wanted);
        break;
      }
    }
    if (cases[pos].given == NULL)
      labels[i] = g_strdup (origs[i]);
  }
}

GType
gst_ossmixer_track_get_type (void)
{
  static GType gst_ossmixer_track_type = 0;

  if (!gst_ossmixer_track_type) {
    static const GTypeInfo ossmixer_track_info = {
      sizeof (GstOssMixerTrackClass),
      NULL,
      NULL,
      (GClassInitFunc) gst_ossmixer_track_class_init,
      NULL,
      NULL,
      sizeof (GstOssMixerTrack),
      0,
      (GInstanceInitFunc) gst_ossmixer_track_init,
      NULL
    };

    gst_ossmixer_track_type =
	g_type_register_static (GST_TYPE_MIXER_TRACK,
				"GstOssMixerTrack",
				&ossmixer_track_info, 0);
  }

  return gst_ossmixer_track_type;
}

static void
gst_ossmixer_track_class_init (GstOssMixerTrackClass *klass)
{
  parent_class = g_type_class_ref (GST_TYPE_MIXER_TRACK);
}

static void
gst_ossmixer_track_init (GstOssMixerTrack *track)
{
  track->lvol = track->rvol = 0;
  track->track_num = 0;
}

GstMixerTrack *
gst_ossmixer_track_new (GstOssElement *oss,
                        gint track_num,
                        gint max_chans,
                        gint flags)
{
  GstOssMixerTrack *osstrack;
  GstMixerTrack *track;
  gint volume;

  if (!labels)
    fill_labels ();

  osstrack = g_object_new (GST_TYPE_OSSMIXER_TRACK, NULL);
  track = GST_MIXER_TRACK (osstrack);
  track->label = g_strdup (labels[track_num]);
  track->num_channels = max_chans;
  track->flags = flags;
  track->min_volume = 0;
  track->max_volume = 100;
  osstrack->track_num = track_num;

  /* volume */
  if (ioctl(oss->mixer_fd, MIXER_READ (osstrack->track_num), &volume) < 0) {
    g_warning("Error getting device (%d) volume: %s",
	      osstrack->track_num, strerror(errno));
    volume = 0;
  }
  osstrack->lvol = (volume & 0xff);
  if (track->num_channels == 2) {
    osstrack->rvol = ((volume >> 8) & 0xff);
  }

  return track;
}

void
gst_oss_interface_init (GstImplementsInterfaceClass *klass)
{
  /* default virtual functions */
  klass->supported = gst_ossmixer_supported;
}

void
gst_ossmixer_interface_init (GstMixerClass *klass)
{
  GST_MIXER_TYPE (klass) = GST_MIXER_HARDWARE;
  
  /* default virtual functions */
  klass->list_tracks = gst_ossmixer_list_tracks;
  klass->set_volume = gst_ossmixer_set_volume;
  klass->get_volume = gst_ossmixer_get_volume;
  klass->set_mute = gst_ossmixer_set_mute;
  klass->set_record = gst_ossmixer_set_record;
}

static gboolean
gst_ossmixer_supported (GstImplementsInterface *iface,
			GType                   iface_type)
{
  g_assert (iface_type == GST_TYPE_MIXER);

  return (GST_OSSELEMENT (iface)->mixer_fd != -1);
}

static gboolean
gst_ossmixer_contains_track (GstOssElement    *oss,
			     GstOssMixerTrack *osstrack)
{
  const GList *item;

  for (item = oss->tracklist; item != NULL; item = item->next)
    if (item->data == osstrack)
      return TRUE;

  return FALSE;
}

static const GList *
gst_ossmixer_list_tracks (GstMixer *mixer)
{
  return (const GList *) GST_OSSELEMENT (mixer)->tracklist;
}

static void
gst_ossmixer_get_volume (GstMixer      *mixer,
			 GstMixerTrack *track,
			 gint          *volumes)
{
  gint volume;
  GstOssElement *oss = GST_OSSELEMENT (mixer);
  GstOssMixerTrack *osstrack = GST_OSSMIXER_TRACK (track);

  /* assert that we're opened and that we're using a known item */
  g_return_if_fail (oss->mixer_fd != -1);
  g_return_if_fail (gst_ossmixer_contains_track (oss, osstrack));

  if (track->flags & GST_MIXER_TRACK_MUTE) {
    volumes[0] = osstrack->lvol;
    if (track->num_channels == 2) {
      volumes[1] = osstrack->rvol;
    }
  } else {
    /* get */
    if (ioctl(oss->mixer_fd, MIXER_READ (osstrack->track_num), &volume) < 0) {
      g_warning("Error getting recording device (%d) volume: %s",
	        osstrack->track_num, strerror(errno));
      volume = 0;
    }

    osstrack->lvol = volumes[0] = (volume & 0xff);
    if (track->num_channels == 2) {
      osstrack->rvol = volumes[1] = ((volume >> 8) & 0xff);
    }
  }
}

static void
gst_ossmixer_set_volume (GstMixer      *mixer,
			 GstMixerTrack *track,
			 gint          *volumes)
{
  gint volume;
  GstOssElement *oss = GST_OSSELEMENT (mixer);
  GstOssMixerTrack *osstrack = GST_OSSMIXER_TRACK (track);

  /* assert that we're opened and that we're using a known item */
  g_return_if_fail (oss->mixer_fd != -1);
  g_return_if_fail (gst_ossmixer_contains_track (oss, osstrack));

  /* prepare the value for ioctl() */
  if (!(track->flags & GST_MIXER_TRACK_MUTE)) {
    volume = (volumes[0] & 0xff);
    if (track->num_channels == 2) {
      volume |= ((volumes[1] & 0xff) << 8);
    }

    /* set */
    if (ioctl(oss->mixer_fd, MIXER_WRITE (osstrack->track_num), &volume) < 0) {
      g_warning("Error setting recording device (%d) volume (0x%x): %s",
	        osstrack->track_num, volume, strerror(errno));
      return;
    }
  }

  osstrack->lvol = volumes[0];
  if (track->num_channels == 2) {
    osstrack->rvol = volumes[1];
  }
}

static void
gst_ossmixer_set_mute (GstMixer      *mixer,
		       GstMixerTrack *track,
		       gboolean       mute)
{
  int volume;
  GstOssElement *oss = GST_OSSELEMENT (mixer);
  GstOssMixerTrack *osstrack = GST_OSSMIXER_TRACK (track);

  /* assert that we're opened and that we're using a known item */
  g_return_if_fail (oss->mixer_fd != -1);
  g_return_if_fail (gst_ossmixer_contains_track (oss, osstrack));

  if (mute) {
    volume = 0;
  } else {
    volume = (osstrack->lvol & 0xff);
    if (MASK_BIT_IS_SET (oss->stereomask, osstrack->track_num)) {
      volume |= ((osstrack->rvol & 0xff) << 8);
    }
  }

  if (ioctl(oss->mixer_fd, MIXER_WRITE(osstrack->track_num), &volume) < 0) {
    g_warning("Error setting mixer recording device volume (0x%x): %s",
	      volume, strerror(errno));
    return;
  }

  if (mute) {
    track->flags |= GST_MIXER_TRACK_MUTE;
  } else {
    track->flags &= ~GST_MIXER_TRACK_MUTE;
  }
}

static void
gst_ossmixer_set_record (GstMixer      *mixer,
			 GstMixerTrack *track,
			 gboolean       record)
{
  GstOssElement *oss = GST_OSSELEMENT (mixer);
  GstOssMixerTrack *osstrack = GST_OSSMIXER_TRACK (track);

  /* assert that we're opened and that we're using a known item */
  g_return_if_fail (oss->mixer_fd != -1);
  g_return_if_fail (gst_ossmixer_contains_track (oss, osstrack));

  /* if there's nothing to do... */
  if ((record && GST_MIXER_TRACK_HAS_FLAG (track, GST_MIXER_TRACK_RECORD)) ||
      (!record && !GST_MIXER_TRACK_HAS_FLAG (track, GST_MIXER_TRACK_RECORD)))
    return;

  /* if we're exclusive, then we need to unset the current one(s) */
  if (oss->mixcaps & SOUND_CAP_EXCL_INPUT) {
    GList *track;
    for (track = oss->tracklist; track != NULL; track = track->next) {
      GstMixerTrack *turn = (GstMixerTrack *) track->data;
      turn->flags &= ~GST_MIXER_TRACK_RECORD;
    }
    oss->recdevs = 0;
  }

  /* set new record bit, if needed */
  if (record) {
    oss->recdevs |= (1 << osstrack->track_num);
  } else {
    oss->recdevs &= ~(1 << osstrack->track_num);
  }

  /* set it to the device */
  if (ioctl(oss->mixer_fd, SOUND_MIXER_WRITE_RECSRC, &oss->recdevs) < 0) {
    g_warning("Error setting mixer recording devices (0x%x): %s",
	      oss->recdevs, strerror(errno));
    return;
  }

  if (record) {
    track->flags |= GST_MIXER_TRACK_RECORD;
  } else {
    track->flags &= ~GST_MIXER_TRACK_RECORD;
  }
}

void
gst_ossmixer_build_list (GstOssElement *oss)
{
  gint i, devmask, master = -1;
  const GList *pads = gst_element_get_pad_list (GST_ELEMENT (oss));
  GstPadDirection dir = GST_PAD_UNKNOWN;
#ifdef SOUND_MIXER_INFO
  struct mixer_info minfo;
#endif

  g_return_if_fail (oss->mixer_fd == -1);

  oss->mixer_fd = open (oss->mixer_dev, O_RDWR);
  if (oss->mixer_fd == -1) {
    /* this is valid. OSS devices don't need to expose a mixer */
    GST_DEBUG ("Failed to open mixer device %s, mixing disabled: %s",
	       oss->mixer_dev, strerror (errno));
    return;
  }

  /* get direction */
  if (pads && g_list_length ((GList *) pads) == 1)
    dir = GST_PAD_DIRECTION (GST_PAD (pads->data));

  /* get masks */
  if (ioctl (oss->mixer_fd, SOUND_MIXER_READ_RECMASK, &oss->recmask) < 0 ||
      ioctl (oss->mixer_fd, SOUND_MIXER_READ_RECSRC, &oss->recdevs) < 0 ||
      ioctl (oss->mixer_fd, SOUND_MIXER_READ_STEREODEVS, &oss->stereomask) < 0 ||
      ioctl (oss->mixer_fd, SOUND_MIXER_READ_DEVMASK, &devmask) < 0 ||
      ioctl (oss->mixer_fd, SOUND_MIXER_READ_CAPS, &oss->mixcaps) < 0) {
    GST_DEBUG ("Failed to get device masks - disabling mixer");
    close (oss->mixer_fd);
    oss->mixer_fd = -1;
    return;
  }

  /* get name */
#ifdef SOUND_MIXER_INFO
  if (ioctl (oss->mixer_fd, SOUND_MIXER_INFO, &minfo) == 0) {
    oss->device_name = g_strdup (minfo.name);
  }
#else
  oss->device_name = g_strdup ("Unknown");
#endif

  /* find master volume */
  if (devmask & SOUND_MASK_VOLUME)
    master = SOUND_MIXER_VOLUME;
  else if (devmask & SOUND_MASK_PCM)
    master = SOUND_MIXER_PCM;
  else if (devmask & SOUND_MASK_SPEAKER)
    master = SOUND_MIXER_SPEAKER; /* doubtful... */
  /* else: no master, so we won't set any */

  /* build track list */
  for (i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
    if (devmask & (1 << i)) {
      GstMixerTrack *track;
      gboolean input = FALSE, stereo = FALSE, record = FALSE;

      /* track exists, make up capabilities */
      if (MASK_BIT_IS_SET (oss->stereomask, i))
        stereo = TRUE;
      if (MASK_BIT_IS_SET (oss->recmask, i))
        input = TRUE;
      if (MASK_BIT_IS_SET (oss->recdevs, i))
        record = TRUE;

      /* do we want this in our list? */
      if ((dir == GST_PAD_SRC && input == FALSE) ||
          (dir == GST_PAD_SINK && i != SOUND_MIXER_PCM))
        continue;

      /* add track to list */
      track = gst_ossmixer_track_new (oss, i, stereo ? 2 : 1,
					  (record ? GST_MIXER_TRACK_RECORD : 0) |
					  (input ? GST_MIXER_TRACK_INPUT :
						   GST_MIXER_TRACK_OUTPUT) |
					  ((master != i) ? 0 :
						   GST_MIXER_TRACK_MASTER));
      oss->tracklist = g_list_append (oss->tracklist, track);
    }
  }
}

void
gst_ossmixer_free_list (GstOssElement *oss)
{
  if (oss->mixer_fd == -1)
    return;

  g_list_foreach (oss->tracklist, (GFunc) g_object_unref, NULL);
  g_list_free (oss->tracklist);
  oss->tracklist = NULL;

  if (oss->device_name) {
    g_free (oss->device_name);
    oss->device_name = NULL;
  }

  close (oss->mixer_fd);
  oss->mixer_fd = -1;
}