/**
 * Copyright (C) 2006 Philipp Hahn <pmhahn@users.sourceforge.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <math.h>
#include <alsa/asoundlib.h>
#include "utils.h"
#include "mixer.h"

static char *card, *channel;
static int muted = 0;
static int mutecount = 0;
static snd_mixer_t *handle = NULL;
static snd_mixer_elem_t *elem = NULL;

static long alsa_min, alsa_max, alsa_vol;

static void alsa_open_mixer( int verbose )
{
    int err;
    static snd_mixer_selem_id_t *sid = NULL;
    if ((err = snd_mixer_open (&handle, 0)) < 0) {
	if (verbose)
	    fprintf(stderr, "mixer: open error: %s\n", snd_strerror(err));
        return;
    }
    if ((err = snd_mixer_attach (handle, card)) < 0) {
	if (verbose)
	    fprintf(stderr, "mixer: attach error: %s\n", snd_strerror(err));
        goto error;
    }
    if ((err = snd_mixer_selem_register (handle, NULL, NULL)) < 0) {
	if (verbose)
	    fprintf(stderr, "mixer: register error: %s\n", snd_strerror(err));
        goto error;
    }
    if ((err = snd_mixer_load (handle)) < 0) {
	if (verbose)
	    fprintf(stderr, "mixer: load error: %s\n", snd_strerror(err));
        goto error;
    }
    snd_mixer_selem_id_malloc(&sid);
    if (sid == NULL)
        goto error;
    snd_mixer_selem_id_set_name(sid, channel);
    if (!(elem = snd_mixer_find_selem(handle, sid))) {
	if (verbose)
	    fprintf(stderr, "mixer: unable to find mixer for channel %s\n",
		    channel);
        goto error;
    }
    if (!snd_mixer_selem_has_playback_volume(elem)) {
	if (verbose)
	    fprintf(stderr, "mixer: no playback\n");
        goto error;
    }
    snd_mixer_selem_get_playback_volume_range(elem, &alsa_min, &alsa_max);
    if ((alsa_max - alsa_min) <= 0) {
	if (verbose)
	    fprintf(stderr, "mixer: no valid playback range\n");
        goto error;
    }
    snd_mixer_selem_id_free(sid);
    return;

error:
    if (sid)
        snd_mixer_selem_id_free(sid);
    if (handle) {
        snd_mixer_close(handle);
        handle = NULL;
    }
    return;
}

/* Volume saved to file */
static int alsa_get_unmute_volume( void )
{
    long val;
    assert (elem);

    if (snd_mixer_selem_is_playback_mono(elem)) {
        snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_MONO, &val);
        return val;
    } else {
        int c, n = 0;
        long sum = 0;
        for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
            if (snd_mixer_selem_has_playback_channel(elem, c)) {
                snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &val);
                sum += val;
                n++;
            }
        }
        if (! n) {
            return 0;
        }

        val = sum / n;
        sum = (long)((double)(alsa_vol * (alsa_max - alsa_min)) / 100. + 0.5);

        if (sum != val) {
           alsa_vol = (long)(((val * 100.) / (alsa_max - alsa_min)) + 0.5);
        }
        return alsa_vol;
    }
}

static int alsa_get_volume( void )
{
    if (muted)
        return 0;
    else
        return alsa_get_unmute_volume();
}

static int alsa_set_volume( int percentdiff )
{
    long volume;

    alsa_get_volume();

    alsa_vol += percentdiff;
    if( alsa_vol > 100 ) alsa_vol = 100;
    if( alsa_vol < 0 ) alsa_vol = 0;

    volume = (long)((alsa_vol * (alsa_max - alsa_min) / 100.) + 0.5);

    snd_mixer_selem_set_playback_volume_all(elem, volume + alsa_min);
    snd_mixer_selem_set_playback_switch_all(elem, 1);
    muted = 0;
    mutecount = 0;

    return alsa_vol;
}

static void alsa_mute( int mute )
{
    /**
     * Make sure that if multiple users mute the card,
     * we only honour the last one.
     */
    if( !mute && mutecount ) mutecount--;
    if( mutecount ) return;

    if( mute ) {
        mutecount++;
        muted = 1;
        if (snd_mixer_selem_has_playback_switch(elem))
            snd_mixer_selem_set_playback_switch_all(elem, 0);
        else
            fprintf(stderr, "mixer: mute not implemented\n");
    } else {
        muted = 0;
        if (snd_mixer_selem_has_playback_switch(elem))
            snd_mixer_selem_set_playback_switch_all(elem, 1);
        else
            fprintf(stderr, "mixer: mute not implemented\n");
    }
}

static int alsa_ismute( void )
{
    return muted;
}

static int alsa_set_device( const char *devname, int verbose )
{
    int i;

    if (card) free(card);
    card = strdup( devname );
    if( !card ) return -1;

    i = strcspn( card, "/" );
    if( i == strlen( card ) ) {
        channel = "Master";
    } else {
        card[i] = 0;
        channel = card + i + 1;
    }
    alsa_open_mixer( verbose );
    if (!handle) {
	if( verbose ) {
	    fprintf( stderr, "mixer: Can't open mixer %s (channel %s), "
		     "mixer volume and mute unavailable.\n", card, channel );
	}
        return -1;
    }
    return 0;
}

static void alsa_set_state( int ismuted, int unmute_volume )
{
    /**
     * 1. we come back unmuted: Don't touch anything
     * 2. we don't have a saved volume: Don't touch anything
     * 3. we come back muted and we have a saved volume:
     *    - if tvtime muted it, unmute to old volume
     *    - if user did it, remember that we're muted and old volume
     */
    if( alsa_get_volume() == 0 && unmute_volume > 0 ) {
        snd_mixer_selem_set_playback_volume_all(elem, unmute_volume);
        muted = 1;

        if( !ismuted ) {
            alsa_mute( 0 );
        }
    }
}

static void alsa_close_device( void )
{
    elem = NULL;
    if (handle)
        snd_mixer_close(handle);
    handle = NULL;
    muted = 0;
    mutecount = 0;
}

struct mixer alsa_mixer = {
    .set_device = alsa_set_device,
    .set_state = alsa_set_state,
    .get_volume = alsa_get_volume,
    .get_unmute_volume = alsa_get_unmute_volume,
    .set_volume = alsa_set_volume,
    .mute = alsa_mute,
    .ismute = alsa_ismute,
    .close_device = alsa_close_device,
};
// vim: ts=4 sw=4 et foldmethod=marker
