/*
 *	A Simple ALSA Volume Control via OSD
 *
 *	(c) 2012 Martin Mares <mj@ucw.cz>
 */

#undef DEBUG

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <alsa/asoundlib.h>

#include "util.h"
#include "osd.h"

static char *alsa_device = "default";
static char *mixer_control = "Master";
static int adjust_by;
static int want_mute = -1;

static snd_mixer_t *mixer;
static snd_mixer_elem_t *elem;

static void init_mixer(void)
{
  int err;

  if (err = snd_mixer_open(&mixer, 0))
    die("snd_mixer_open failed: error %d", err);

  if (err = snd_mixer_attach(mixer, alsa_device))
    die("snd_mixer_attach failed: error %d", err);

  if (err = snd_mixer_selem_register(mixer, NULL, NULL))
    die("snd_mixer_selem_register failed: error %d", err);

  if (err = snd_mixer_load(mixer))
    die("snd_mixer_load: error %d", err);

  for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem))
    {
      const char *name = snd_mixer_selem_get_name(elem);
      int index = snd_mixer_selem_get_index(elem);
      if (!strcmp(name, mixer_control) || index)
	{
	  if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_SIMPLE)
	    die("Unable to handle non-simple mixer controls");
	  if (!snd_mixer_selem_is_active(elem))
	    die("Selected mixer control is not active");
	  DBG("Found mixer control %s[%d]\n", name, index);
	  return;
	}
    }

  die("Unable to find mixer control %s", mixer_control);
}

static int get_mute(void)
{
  int mute_on = 0, mute_off = 0;

  if (snd_mixer_selem_has_playback_switch(elem))
    {
      for (snd_mixer_selem_channel_id_t ch=0; ch < SND_MIXER_SCHN_LAST; ch++)
	{
	  int val;
	  if (!snd_mixer_selem_get_playback_switch(elem, ch, &val))
	    {
	      if (val)
		mute_off++;
	      else
		mute_on++;
	    }
	}
    }
  DBG("Mute: on=%d off=%d\n", mute_on, mute_off);
  return !!mute_on;
}

static long get_volume(long *pmin, long *pmax)
{
  long min, max, curr=0;
  if (!snd_mixer_selem_has_playback_volume(elem) ||
      snd_mixer_selem_get_playback_volume_range(elem, &min, &max))
    {
      *pmin = *pmax = 0;
      return 0;
    }

  for (snd_mixer_selem_channel_id_t ch=0; ch < SND_MIXER_SCHN_LAST; ch++)
    {
      long val;
      if (!snd_mixer_selem_get_playback_volume(elem, ch, &val))
	{
	  if (val > curr)
	    curr = val;
	}
    }

  DBG("Volume: min=%ld max=%ld curr=%ld\n", min, max, curr);
  *pmin = min;
  *pmax = max;
  return curr;
}

static int vol_to_perc(long curr, long min, long max)
{
  return (100LL * (curr-min) + (max-min)/2) / (max-min);
}

static long perc_to_vol(int perc, long min, long max)
{
  return ((long long) perc * (max-min) + 50) / 100;
}

static void show_mixer(void)
{
  long min, max;
  long curr = get_volume(&min, &max);
  int muted = get_mute();

  struct osd_msg *msg = osd_new_msg();
  osd_add_line(msg, "min-duration", "0");
  char buf[256];
  snprintf(buf, sizeof(buf), "%s volume", mixer_control);
  osd_add_line(msg, NULL, buf);
  if (muted)
    osd_add_line(msg, NULL, "[mute]");
  else if (min < max)
    {
      snprintf(buf, sizeof(buf), "%d", vol_to_perc(curr, min, max));
      osd_add_line(msg, "slider", buf);
    }
  osd_send(msg);
}


static void set_mute(void)
{
  if (want_mute < 0)
    return;

  if (want_mute == 2)
    want_mute = !get_mute();

  if (snd_mixer_selem_has_playback_switch(elem))
    {
      for (snd_mixer_selem_channel_id_t ch=0; ch < SND_MIXER_SCHN_LAST; ch++)
	snd_mixer_selem_set_playback_switch(elem, ch, !want_mute);
    }
}

static void set_volume(void)
{
  if (!adjust_by)
    return;

  long min, max;
  long curr = get_volume(&min, &max);
  int perc = vol_to_perc(curr, min, max);

  DBG("Volume: have %d %ld\n", perc, curr);
  perc += adjust_by;
  if (perc < 0)
    perc = 0;
  if (perc > 100)
    perc = 100;
  curr = perc_to_vol(perc, min, max);
  curr = min + (((long long) perc * (max-min) + 50) / 100);
  DBG("Volume: want %d %ld\n", perc, curr);

  for (snd_mixer_selem_channel_id_t ch=0; ch < SND_MIXER_SCHN_LAST; ch++)
    snd_mixer_selem_set_playback_volume(elem, ch, curr);
}

static void NONRET
usage(void)
{
  fprintf(stderr, "\
Usage: osd-alsa <options>\n\
\n\
Options:\n\
-a, --adjust=<percent>	Adjust the control by a given amount\n\
-D, --device=<name>	ALSA device (default: `default')\n\
-m, --mixer=<name>	Name of mixer control (default: `Master')\n\
-0, --mute		Mute the control\n\
-t, --toggle		Mute/unmute the control\n\
-1, --unmute		Unmute the control\n\
");
  exit(1);
}

static const char short_opts[] = "01a:c:D:t";

static const struct option long_opts[] = {
  { "adjust",		required_argument,	NULL,	'a' },
  { "device",		required_argument,	NULL,	'D' },
  { "mixer",		required_argument,	NULL,	'm' },
  { "mute",		no_argument,		NULL,	'0' },
  { "toggle",		no_argument,		NULL,	't' },
  { "unmute",		no_argument,		NULL,	'1' },
  { NULL,		0,			NULL,	0   },
};

int main(int argc, char **argv)
{
  int opt;
  while ((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) >= 0)
    switch (opt)
      {
      case '0':
	want_mute = 1;
	break;
      case '1':
	want_mute = 0;
	break;
      case 'a':
	adjust_by = atoi(optarg);
	break;
      case 'D':
	alsa_device = optarg;
	break;
      case 'm':
	mixer_control = optarg;
	break;
      case 't':
	want_mute = 2;
	break;
      default:
	usage();
      }
  if (optind < argc)
    usage();

  init_mixer();
  osd_init();
  set_mute();
  set_volume();
  show_mixer();
  return 0;
}