#include "include/common.h"
#include "include/misc.h"
#include "include/mixer-alsa.h"

#include <alsa/asoundlib.h>

static bool get_mixer_state(void);

struct mixer_element {
    const char *name;		/* name of channel */
    const char *sname;		/* short name of the channel */
    snd_mixer_elem_t *element;  /* Mixer element pointer */
    long min;                   /* Min volume */
    long max;                   /* Max volume */
    int dev;			/* channel device number */
    int prev_dev_lr_volume;	/* last known left/right volume
				 * (in device format) */
    float volume;		/* volume, in [0, 1] */
    float balance;		/* balance, in [-1, 1] */
    bool has_playback;
    bool has_playback_switch;
    bool has_capture;
    bool has_capture_switch;
    bool is_recording;		/* is it recording? */
    bool is_stereo;		/* capable of stereo? */
    bool is_muted;		/* is it muted? */
    int (*get_volume)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *);
    int (*set_volume)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long);
};

static snd_mixer_t *mixer;
static struct mixer_element *element;
static int cur_element = 0;
static int n_elements;
static bool needs_update = true;

static int elem_callback(__attribute__((unused)) snd_mixer_elem_t *elem,
                         __attribute__((unused)) unsigned int mask)
{
    needs_update = true;
    return 0;
}

static int mixer_callback(__attribute__((unused)) snd_mixer_t *ctl,
                          unsigned int mask,
			  snd_mixer_elem_t *elem)
{
    if (mask & SND_CTL_EVENT_MASK_ADD) {
        snd_mixer_elem_set_callback(elem, elem_callback);
        needs_update = true;
    }
    return 0;
}

static bool is_exclude(const char *short_name,
                       const char *exclude[])
{
    for (int i = 0; exclude[i] != NULL; i++) {
        if (!strcmp(short_name, exclude[i])) {
            return true;
        }
    }
    return false;
}

void mixer_alsa_init(const char *mixer_device, bool verbose, const char * exclude[])
{
    int err;
    static struct snd_mixer_selem_regopt selem_regopt = {
        .ver = 1,
        .abstract = SND_MIXER_SABSTRACT_NONE,
    };
    selem_regopt.device = mixer_device;
    if (verbose) {
        printf("Sound card: %s\n", mixer_device);
        puts("Supported elements:");
    }

    if ((err = snd_mixer_open(&mixer, 0)) < 0) {
        fprintf(stderr, "snd_mixer_open error");
        mixer = NULL;
        return;
    }

    if ((err = snd_mixer_selem_register(mixer, &selem_regopt, NULL)) < 0) {
        fprintf(stderr, "snd_mixer_selem_register error");
        snd_mixer_close(mixer);
        mixer = NULL;
        return;
    }

    snd_mixer_set_callback(mixer, mixer_callback);

    if ((err = snd_mixer_load(mixer)) < 0) {
        fprintf(stderr, "snd_mixer_load error");
        snd_mixer_close(mixer);
        mixer = NULL;
        return;
    }

    int all_elements = snd_mixer_get_count(mixer);
    element = (struct mixer_element *)malloc(all_elements *
                                             sizeof(struct mixer_element));
    memset(element, 0, all_elements * sizeof(struct mixer_element));
    snd_mixer_elem_t *elem = snd_mixer_first_elem(mixer);

    int f = 0;
    for (int e = 0; e < all_elements; e++) {
        element[f].element = elem;
        element[f].name = snd_mixer_selem_get_name(elem);

        if (is_exclude(element[f].name, exclude)) {
            if (verbose)
                printf("  x: '%s' - disabled\n", element[f].name);
            elem = snd_mixer_elem_next(elem);
            continue;
        }

        element[f].sname = element[f].name;

        element[f].has_capture = snd_mixer_selem_has_capture_volume(elem);
        element[f].has_capture &= snd_mixer_selem_has_capture_channel(elem, SND_MIXER_SCHN_FRONT_LEFT);

        element[f].has_playback = snd_mixer_selem_has_playback_volume(elem);
        element[f].has_playback &= snd_mixer_selem_has_playback_channel(elem,SND_MIXER_SCHN_FRONT_LEFT);

        if (element[f].has_playback) {
            snd_mixer_selem_get_playback_volume_range(elem, &(element[f].min), &(element[f].max));
            element[f].is_stereo = !snd_mixer_selem_is_playback_mono(elem);
            element[f].is_stereo &= snd_mixer_selem_has_playback_channel(elem,SND_MIXER_SCHN_FRONT_RIGHT);
            element[f].get_volume = snd_mixer_selem_get_playback_volume;
            element[f].set_volume = snd_mixer_selem_set_playback_volume;
            element[f].has_playback_switch = snd_mixer_selem_has_playback_switch(elem);
        }
        else if (element[f].has_capture) {
            snd_mixer_selem_get_capture_volume_range(elem, &(element[f].min), &(element[f].max));
            element[f].is_stereo = !snd_mixer_selem_is_capture_mono(element[f].element);
            element[f].is_stereo &= snd_mixer_selem_has_capture_channel(elem,SND_MIXER_SCHN_FRONT_RIGHT);
            element[f].get_volume = snd_mixer_selem_get_capture_volume;
            element[f].set_volume = snd_mixer_selem_set_capture_volume;
            element[f].has_capture_switch = snd_mixer_selem_has_capture_switch(elem);
        } else {
            elem = snd_mixer_elem_next(elem);
            continue;
        }

        if (verbose) {
            printf("  %d: '%s'%s%s%s%s %s (%ld - %ld)\n",
                   f,
                   element[f].name,
                   element[f].has_playback? " pvolume" : "",
                   element[f].has_playback_switch? " pswitch" : "",
                   element[f].has_capture? " cvolume" : "",
                   element[f].has_capture_switch? " cswitch" : "",
                   element[f].is_stereo? "Stereo" : "Mono",
                   element[f].min, element[f].max);
        }

        elem = snd_mixer_elem_next(elem);
        f++;
    }
    n_elements = f;
    get_mixer_state();
}

static bool element_is_muted(int e)
{
    if (!element[e].has_playback_switch)
        return false;

    snd_mixer_elem_t *elem = element[e].element;
    int left_on;
    snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_FRONT_LEFT, &left_on);
    if (left_on)
        return false;
    if (element[e].is_stereo) {
        int right_on;
        snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_FRONT_RIGHT, &right_on);
        if (right_on)
            return false;
    }
    return true;
}

static bool element_is_recording(int e)
{
    if (!element[e].has_capture_switch)
        return false;

    snd_mixer_elem_t *elem = element[e].element;
    int left_on;
    snd_mixer_selem_get_capture_switch(elem, SND_MIXER_SCHN_FRONT_LEFT, &left_on);
    if (left_on)
        return true;
    if (element[e].is_stereo) {
        int right_on;
        snd_mixer_selem_get_capture_switch(elem, SND_MIXER_SCHN_FRONT_RIGHT, &right_on);
        if (right_on)
            return true;
    }
    return false;
}

static bool get_mixer_state(void)
{
    if (!needs_update)
        return false;
    needs_update = false;

    for (int e = 0; e < n_elements; e++) {
        snd_mixer_elem_t *elem = element[e].element;
        long left, right;

        element[e].get_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &left);
        float fleft = 1.0 * (left-element[e].min) / (element[e].max-element[e].min);
        if (element[e].is_stereo) {
            element[e].get_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, &right);
            float fright = 1.0 * (right-element[e].min) /
                (element[e].max-element[e].min);
            lr_to_vb(fleft, fright, &(element[e].volume), &(element[e].balance));
        } else {
            element[e].volume = fleft;
            element[e].balance = 0.0;
        }

        element[e].is_muted = element_is_muted(e);
        element[e].is_recording = element_is_recording(e);

        //printf("Channel %s, has_vol: %d, stereo: %d, muted: %d, max: %ld, min: %ld, left: %ld, right: %ld, vol: %f, bal: %f\n", element[e].name, element[e].has_playback, element[e].is_stereo, element[e].is_muted, element[e].max, element[e].min, left, element[e].is_stereo?right:-1, element[e].volume, element[e].balance);
    }
    return true;
}

static void set_mixer_state(void)
{
    float left, right;
    long dev_left_volume, dev_right_volume;
    if (!element[cur_element].has_playback && !element[cur_element].has_capture)
        return;

    bool muted = element_is_muted(cur_element);
    if (muted != element[cur_element].is_muted) {
        snd_mixer_selem_set_playback_switch_all(element[cur_element].element,
                                                element[cur_element].is_muted?
                                                0:1);
    }

    bool recording = element_is_recording(cur_element);
    if (recording != element[cur_element].is_recording) {
        snd_mixer_selem_set_capture_switch_all(element[cur_element].element,
                                               element[cur_element].is_recording?
                                               1:0);
    }

    if (element[cur_element].is_stereo)
        vb_to_lr(element[cur_element].volume,
                 element[cur_element].balance, &left, &right);
    else
        left = element[cur_element].volume;

    long range = element[cur_element].max - element[cur_element].min;
    dev_left_volume = element[cur_element].min + (long) (range * left);
    element[cur_element].set_volume(element[cur_element].element,
                                    SND_MIXER_SCHN_FRONT_LEFT,
                                    dev_left_volume);
    if (element[cur_element].is_stereo) {
        dev_right_volume = element[cur_element].min + (long) (range * right);
        element[cur_element].set_volume(element[cur_element].element,
                                        SND_MIXER_SCHN_FRONT_RIGHT,
                                        dev_right_volume);
    }
}

bool mixer_alsa_is_changed(void)
{
    return get_mixer_state();
}

int mixer_alsa_get_channel_count(void)
{
    return n_elements;
}

int mixer_alsa_get_channel(void)
{
    return cur_element;
}

const char *mixer_alsa_get_channel_name(void)
{
    return element[cur_element].name;
}

const char *mixer_alsa_get_short_name(void)
{
    return element[cur_element].sname;
}

void mixer_alsa_set_channel(int element)
{
    assert((element >= 0) && (element < n_elements));

    cur_element = element;
    get_mixer_state();
}

void mixer_alsa_set_channel_rel(int delta_element)
{
    cur_element = (cur_element + delta_element) % n_elements;
    if (cur_element < 0)
	cur_element += n_elements;
    get_mixer_state();
}

float mixer_alsa_get_volume(void)
{
    get_mixer_state();
    return element[cur_element].volume;
}

void mixer_alsa_set_volume(float volume)
{
    assert((volume >= 0.0) && (volume <= 1.0));
    element[cur_element].volume = volume;
    set_mixer_state();
}

void mixer_alsa_set_volume_rel(float delta_volume)
{
    element[cur_element].volume += delta_volume;
    element[cur_element].volume = CLAMP(element[cur_element].volume, 0.0, 1.0);
    set_mixer_state();
}

float mixer_alsa_get_balance(void)
{
    get_mixer_state();
    return element[cur_element].balance;
}

void mixer_alsa_set_balance(float balance)
{
    assert((balance >= -1.0) && (balance <= 1.0));
    if (element[cur_element].is_stereo) {
	element[cur_element].balance = balance;
	set_mixer_state();
    }
}

void mixer_alsa_set_balance_rel(float delta_balance)
{
    if (element[cur_element].is_stereo) {
	element[cur_element].balance += delta_balance;
	element[cur_element].balance =
	    CLAMP(element[cur_element].balance, -1.0, 1.0);
	set_mixer_state();
    }
}

void mixer_alsa_toggle_mute(void)
{
    if (element[cur_element].has_playback_switch) {
        element[cur_element].is_muted ^= 1;
        set_mixer_state();
    }
}

void mixer_alsa_toggle_rec(void)
{
    if (element[cur_element].has_capture_switch) {
        element[cur_element].is_recording ^= 1;
        set_mixer_state();
    }
}

bool mixer_alsa_is_muted(void)
{
    return element[cur_element].is_muted;
}

bool mixer_alsa_is_stereo(void)
{
    return element[cur_element].is_stereo;
}

bool mixer_alsa_is_rec(void)
{
    return element[cur_element].is_recording;
}

bool mixer_alsa_can_rec(void)
{
    return element[cur_element].has_capture;
}

void mixer_alsa_tick(void)
{
    snd_mixer_handle_events(mixer);
}
