#include <alsa/asoundlib.h>
#include <poll.h>
#include "e_mod_system.h"
#include "e.h"

struct e_mixer_callback_desc
{
   int (*func)(void *data, E_Mixer_System *self);
   void *data;
   E_Mixer_System *self;
   Ecore_Idler *idler;
   Eina_List *handlers;
};


static int _mixer_callback_add(E_Mixer_System *self, int (*func)(void *data, E_Mixer_System *self), void *data);
static int _mixer_callback_del(E_Mixer_System *self, struct e_mixer_callback_desc *desc);


static Eina_Bool
_cb_dispatch(void *data)
{
   struct e_mixer_callback_desc *desc;
   int r;

   desc = data;
   snd_mixer_handle_events(desc->self);
   r = desc->func(desc->data, desc->self);
   desc->idler = NULL;

   if (!r)
     _mixer_callback_del(desc->self, desc); /* desc is invalid then. */

   return ECORE_CALLBACK_CANCEL;
}

static Eina_Bool
_cb_fd_handler(void *data, Ecore_Fd_Handler *fd_handler)
{
   struct e_mixer_callback_desc *desc;

   desc = data;

   if (ecore_main_fd_handler_active_get(fd_handler, ECORE_FD_ERROR))
     {
	desc->handlers = eina_list_remove(desc->handlers, fd_handler);
	if (!desc->handlers)
	  {
	     E_Mixer_System *s;
	     int (*f)(void *, E_Mixer_System *);
	     void *d;

	     s = desc->self;
	     f = desc->func;
	     d = desc->data;
	     _mixer_callback_del(s, desc);
	     _mixer_callback_add(s, f, d);
	  }
	return ECORE_CALLBACK_CANCEL;
     }

   if (!desc->idler)
     desc->idler = ecore_idler_add(_cb_dispatch, desc);
   return ECORE_CALLBACK_RENEW;
}

static int
_mixer_callback_add(E_Mixer_System *self, int (*func)(void *data, E_Mixer_System *self), void *data)
{
   struct e_mixer_callback_desc *desc;
   struct pollfd *pfds;
   int len;

   len = snd_mixer_poll_descriptors_count(self);
   if (len <= 0)
     return 0;

   desc = malloc(sizeof(struct e_mixer_callback_desc));
   if (!desc)
     return 0;

   desc->func = func;
   desc->data = data;
   desc->self = self;
   desc->idler = NULL;
   desc->handlers = NULL;

   pfds = alloca(len * sizeof(struct pollfd));
   len = snd_mixer_poll_descriptors(self, pfds, len);
   if (len <= 0)
     {
	free(desc);
	return 0;
     }

   while (len > 0)
     {
	Ecore_Fd_Handler *fd_handler;

	len--;
	fd_handler = ecore_main_fd_handler_add(
					       pfds[len].fd, ECORE_FD_READ, _cb_fd_handler, desc, NULL, NULL);
	desc->handlers = eina_list_prepend(desc->handlers, fd_handler);
     }

   snd_mixer_set_callback_private(self, desc);

   return 1;
}

static int
_mixer_callback_del(E_Mixer_System *self, struct e_mixer_callback_desc *desc)
{
   Ecore_Fd_Handler *handler;

   EINA_LIST_FREE(desc->handlers, handler)
     ecore_main_fd_handler_del(handler);

   snd_mixer_set_callback_private(self, NULL);

   free(desc);

   return 1;
}

static int
_mixer_callback_replace(E_Mixer_System *self __UNUSED__, struct e_mixer_callback_desc *desc, int (*func)(void *data, E_Mixer_System *self), void *data)
{
   desc->func = func;
   desc->data = data;

   return 1;
}

E_Mixer_System *
e_mixer_system_new(const char *name)
{
   snd_mixer_t *handle;
   int err;

   if (!name)
     return NULL;

   err = snd_mixer_open(&handle, 0);
   if (err < 0)
     goto error_open;

   err = snd_mixer_attach(handle, name);
   if (err < 0)
     goto error_load;

   err = snd_mixer_selem_register(handle, NULL, NULL);
   if (err < 0)
     goto error_load;

   err = snd_mixer_load(handle);
   if (err < 0)
     goto error_load;

   return handle;

 error_load:
   snd_mixer_close(handle);
 error_open:
   fprintf(stderr, "MIXER: Cannot get hardware info: %s\n", snd_strerror(err));
   return NULL;
}

void
e_mixer_system_del(E_Mixer_System *self)
{
   struct e_mixer_callback_desc *desc;

   if (!self)
     return;

   desc = snd_mixer_get_callback_private(self);
   if (desc)
     _mixer_callback_del(self, desc);

   snd_mixer_close(self);
}

int
e_mixer_system_callback_set(E_Mixer_System *self, int (*func)(void *data, E_Mixer_System *self), void *data)
{
   struct e_mixer_callback_desc *desc;

   if (!self)
     return 0;

   desc = snd_mixer_get_callback_private(self);
   if (!desc)
     {
	if (func)
	  return _mixer_callback_add(self, func, data);
	return 1;
     }
   else
     {
	if (func)
	  return _mixer_callback_replace(self, desc, func, data);
	else
	  return _mixer_callback_del(self, desc);
     }
}

Eina_List *
e_mixer_system_get_cards(void)
{
   int err, card_num;
   Eina_List *cards;

   cards = NULL;
   card_num = -1;
   while (((err = snd_card_next(&card_num)) == 0) && (card_num >= 0))
     {
	snd_ctl_t *control;
	char buf[256];

	snprintf(buf, sizeof(buf), "hw:%d", card_num);

	if (snd_ctl_open(&control, buf, 0) < 0)
	  break;
	snd_ctl_close(control);
	cards = eina_list_append(cards, eina_stringshare_add(buf));
     }

   if (err < 0)
     fprintf(stderr, "MIXER: Cannot get available card number: %s\n",
	     snd_strerror(err));

   return cards;
}

void
e_mixer_system_free_cards(Eina_List *cards)
{
   const char *card;

   EINA_LIST_FREE(cards, card)
     eina_stringshare_del(card);
}

const char *
e_mixer_system_get_default_card(void)
{
   static const char buf[] = "hw:0";
   snd_ctl_t *control;

   if (snd_ctl_open(&control, buf, 0) < 0)
     return NULL;
   snd_ctl_close(control);
   return eina_stringshare_add(buf);
}

const char *
e_mixer_system_get_card_name(const char *card)
{
   snd_ctl_card_info_t *hw_info;
   const char *name;
   snd_ctl_t *control;
   int err;

   if (!card)
     return NULL;

   snd_ctl_card_info_alloca(&hw_info);

   err = snd_ctl_open(&control, card, 0);
   if (err < 0)
     return NULL;

   err = snd_ctl_card_info(control, hw_info);
   if (err < 0)
     {
	fprintf(stderr, "MIXER: Cannot get hardware info: %s: %s\n", card,
		snd_strerror(err));
	snd_ctl_close(control);
	return NULL;
     }

   snd_ctl_close(control);
   name = snd_ctl_card_info_get_name(hw_info);
   if (!name)
     {
	fprintf(stderr, "MIXER: Cannot get hardware name: %s\n", card);
	return NULL;
     }

   return eina_stringshare_add(name);
}

Eina_List *
e_mixer_system_get_channels(E_Mixer_System *self)
{
   Eina_List *channels;
   snd_mixer_elem_t *elem;

   if (!self)
     return NULL;

   channels = NULL;

   elem = snd_mixer_first_elem(self);
   for (; elem; elem = snd_mixer_elem_next(elem))
     {
	if ((!snd_mixer_selem_is_active(elem)) ||
            (!snd_mixer_selem_has_playback_volume(elem)))
	  continue;

	channels = eina_list_append(channels, elem);
     }

   return channels;
}

void
e_mixer_system_free_channels(Eina_List *channels)
{
   eina_list_free(channels);
}

Eina_List *
e_mixer_system_get_channels_names(E_Mixer_System *self)
{
   Eina_List *channels;
   snd_mixer_elem_t *elem;
   snd_mixer_selem_id_t *sid;

   if (!self)
     return NULL;

   channels = NULL;
   snd_mixer_selem_id_alloca(&sid);

   elem = snd_mixer_first_elem(self);
   for (; elem; elem = snd_mixer_elem_next(elem))
     {
	const char *name;
	if ((!snd_mixer_selem_is_active(elem)) ||
            (!snd_mixer_selem_has_playback_volume(elem)))
	  continue;

	snd_mixer_selem_get_id(elem, sid);
	name = snd_mixer_selem_id_get_name(sid);
	if (name)
	  channels = eina_list_append(channels, eina_stringshare_add(name));
     }

   return channels;
}

void
e_mixer_system_free_channels_names(Eina_List *channels_names)
{
   const char *channel;

   EINA_LIST_FREE(channels_names, channel)
     eina_stringshare_del(channel);
}

const char *
e_mixer_system_get_default_channel_name(E_Mixer_System *self)
{
   snd_mixer_elem_t *elem;
   snd_mixer_selem_id_t *sid;

   if (!self)
     return NULL;

   snd_mixer_selem_id_alloca(&sid);

   elem = snd_mixer_first_elem(self);
   for (; elem; elem = snd_mixer_elem_next(elem))
     {
	const char *name;
	if ((!snd_mixer_selem_is_active(elem)) ||
            (!snd_mixer_selem_has_playback_volume(elem)))
	  continue;

	snd_mixer_selem_get_id(elem, sid);
	name = snd_mixer_selem_id_get_name(sid);
	if (name)
	  return eina_stringshare_add(name);
     }

   return NULL;
}

E_Mixer_Channel *
e_mixer_system_get_channel_by_name(E_Mixer_System *self, const char *name)
{
   snd_mixer_elem_t *elem;
   snd_mixer_selem_id_t *sid;

   if ((!self) || (!name))
     return NULL;

   snd_mixer_selem_id_alloca(&sid);

   elem = snd_mixer_first_elem(self);
   for (; elem; elem = snd_mixer_elem_next(elem))
     {
	const char *n;
	if ((!snd_mixer_selem_is_active(elem)) ||
            (!snd_mixer_selem_has_playback_volume(elem)))
	  continue;

	snd_mixer_selem_get_id(elem, sid);
	n = snd_mixer_selem_id_get_name(sid);
	if (n && (strcmp(n, name) == 0))
	  return elem;
     }

   return NULL;
}

void
e_mixer_system_channel_del(E_Mixer_Channel *channel __UNUSED__)
{
}

const char *
e_mixer_system_get_channel_name(E_Mixer_System *self, E_Mixer_Channel *channel)
{
   snd_mixer_selem_id_t *sid;
   const char *name;

   if ((!self) || (!channel))
     return NULL;

   snd_mixer_selem_id_alloca(&sid);
   snd_mixer_selem_get_id(channel, sid);
   name = eina_stringshare_add(snd_mixer_selem_id_get_name(sid));

   return name;
}

int
e_mixer_system_get_volume(E_Mixer_System *self, E_Mixer_Channel *channel, int *left, int *right)
{
   long lvol, rvol, range, min, max;

   if ((!self) || (!channel) || (!left) || (!right))
     return 0;

   snd_mixer_handle_events(self);
   snd_mixer_selem_get_playback_volume_range(channel, &min, &max);
   range = max - min;
   if (range < 1)
     return 0;

   if (snd_mixer_selem_has_playback_channel(channel, 0))
     snd_mixer_selem_get_playback_volume(channel, 0, &lvol);
   else
     lvol = min;

   if (snd_mixer_selem_has_playback_channel(channel, 1))
     snd_mixer_selem_get_playback_volume(channel, 1, &rvol);
   else
     rvol = min;

   if (snd_mixer_selem_is_playback_mono(channel) ||
       snd_mixer_selem_has_playback_volume_joined(channel))
     rvol = lvol;

   *left = rint((double)(lvol - min) * 100 / (double)range);
   *right = rint((double)(rvol - min) * 100 / (double)range);

   return 1;
}

int
e_mixer_system_set_volume(E_Mixer_System *self, E_Mixer_Channel *channel, int left, int right)
{
   long range, min, max, div;
   int mode;

   if ((!self) || (!channel))
     return 0;

   snd_mixer_handle_events(self);
   snd_mixer_selem_get_playback_volume_range(channel, &min, &max);
   div = 100 + min;
   if (div == 0)
     {
	div = 1; /* no zero-division */
	min++;
     }

   range = max - min;
   if (range < 1)
     return 0;

   mode = 0;
   if (left >= 0)
     {
	left = (((range * left) + (range / 2)) / div) - min;
	mode |= 1;
     }

   if (right >= 0)
     {
	right = (((range * right) + (range / 2)) / div) - min;
	mode |= 2;
     }

   if (mode & 1)
     snd_mixer_selem_set_playback_volume(channel, 0, left);

   if ((!snd_mixer_selem_is_playback_mono(channel)) &&
       (!snd_mixer_selem_has_playback_volume_joined(channel)) &&
       (mode & 2))
     {
	if (snd_mixer_selem_has_playback_channel(channel, 1))
	  snd_mixer_selem_set_playback_volume(channel, 1, right);
     }

   return 1;
}

int
e_mixer_system_can_mute(E_Mixer_System *self, E_Mixer_Channel *channel)
{
   if ((!self) || (!channel))
     return 0;

   snd_mixer_handle_events(self);
   return (snd_mixer_selem_has_playback_switch(channel) ||
           snd_mixer_selem_has_playback_switch_joined(channel));
}

int
e_mixer_system_get_mute(E_Mixer_System *self, E_Mixer_Channel *channel, int *mute)
{
   if ((!self) || (!channel) || (!mute))
     return 0;

   snd_mixer_handle_events(self);
   if (snd_mixer_selem_has_playback_switch(channel) ||
       snd_mixer_selem_has_playback_switch_joined(channel))
     {
	int m;

	/* XXX: not checking for return, always returns 0 even if it worked.
	 * alsamixer also don't check it. Bug?
	 */
	snd_mixer_selem_get_playback_switch(channel, 0, &m);
	*mute = !m;
     }
   else
     *mute = 0;

   return 1;
}

int
e_mixer_system_set_mute(E_Mixer_System *self, E_Mixer_Channel *channel, int mute)
{
   if ((!self) || (!channel))
     return 0;

   snd_mixer_handle_events(self);
   if (snd_mixer_selem_has_playback_switch(channel) ||
       snd_mixer_selem_has_playback_switch_joined(channel))
     return snd_mixer_selem_set_playback_switch_all(channel, !mute);
   else
     return 0;
}

int
e_mixer_system_get_state(E_Mixer_System *self, E_Mixer_Channel *channel, E_Mixer_Channel_State *state)
{
   int r;

   if (!state)
     return 0;

   r = e_mixer_system_get_mute(self, channel, &state->mute);
   r &= e_mixer_system_get_volume(self, channel, &state->left, &state->right);
   return r;
}

int
e_mixer_system_set_state(E_Mixer_System *self, E_Mixer_Channel *channel, const E_Mixer_Channel_State *state)
{
   int r;

   if (!state)
     return 0;

   r = e_mixer_system_set_mute(self, channel, state->mute);
   r &= e_mixer_system_set_volume(self, channel, state->left, state->right);
   return r;
}

int
e_mixer_system_has_capture(E_Mixer_System *self, E_Mixer_Channel *channel)
{
   if ((!self) || (!channel))
     return 0;

   return snd_mixer_selem_has_capture_switch(channel);
}
