// File: alsa_plugin.cpp
// Author: Neil Macvicar (blackmogu@vfemail.net)
// Date: 14/12/2002
// Abstract: Implementation of the ALSA Mixer plugin for Kdetv
// Revision Control:
//
// 0.3: 31/07/2003 Changed the way in which mixer volume was set. The original
//                 suggestion was submitted by Dirk Ziegelmeier.
//                 Added a new error reporting define, REPORT and more comments.
//
// 0.2: 26/03/2003 Minor changes to the constructor implemented
//                 New method of creating an alsa mixer plugin added
//
// 0.1: 14/12/2002 Initial Implementation

/*
 * Copyright (C) 2002 Neil Macvicar <blackmogu@vfemail.net>
 *
 * 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.
 *
 * 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; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Steet, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "kdetv_alsa.h"
#include <iostream>
#include <kdebug.h>
#include <stdio.h>
#include <qwidget.h>
#include <qframe.h>
#include <qlayout.h>
#include <qcombobox.h>
#include <qlabel.h>
#include <klocale.h>
#include <kgenericfactory.h>
#include <kmessagebox.h>
#include <kconfig.h>

//-----------------------------------------------------------------------------
// Constructors and destructors
//-----------------------------------------------------------------------------

KdetvALSA::KdetvALSA(Kdetv *ktv, QObject *parent, const char *name)
    : KdetvMixerPlugin(ktv, "alsamixer", parent, name)
{
	const char *sig="[KdetvALSA()]";

	REPORT << "initializing plugin" << endl;

	card_selector = NULL;
	element_selector = NULL;
	active_HCTL.truncate(0);
	active_mixer_element.truncate(0);
	active_mixer = NULL;
	active_element = NULL;
	muted_flag = false;
    volume_left = 0;
    volume_right = 0;

	KdetvALSA::loadConfig();

	// we need to check if this configuration is still available

	REPORT << "plugin initialization completed" << endl;
}

KdetvALSA::~KdetvALSA()
{
	const char *sig="[~KdetvALSA()]";

	REPORT << "unloading plugin" << endl;

	KdetvALSA::detachMixer(active_mixer, active_HCTL.local8Bit());

	REPORT << "unloading complete" << endl;
}

//-----------------------------------------------------------------------------
// Configuration member functions
//-----------------------------------------------------------------------------

void KdetvALSA::saveConfig(void)
{
	const char *sig="[saveConfig()]";

	QMap<int, QString>::const_iterator it;
	QString mixer_element = element_selector->currentText();
	int retval;
	char *card_name;

	REPORT << "saving plugin configuration" << endl;

	// this is called when OK is clicked in the config. Find out the HCTL ID
	// from the human readable card name held in card_selector
	for(it = probed_cards.constBegin() ; it != probed_cards.constEnd() ; ++it) {
		if (snd_card_get_name(it.key(), &card_name))
            continue;
		if(card_selector->currentText() == card_name) break;
	}

	retval = useCardMixerElement(it.data(), mixer_element);

	if(retval != 0) return; // don't bother saving this config, it's invalid

	_cfg->setGroup("ALSA Mixer");
	_cfg->writeEntry("HCTL ID", active_HCTL);
	_cfg->writeEntry("Mixer Element", element_selector->currentText());
	_cfg->sync();

	REPORT << "configuration saved" << endl;
}

void KdetvALSA::loadConfig(void)
{
	const char *sig="[loadConfig()]";
	QString HCTL_id, mixer_element;

	REPORT << "loading pre-saved plugin configuration" << endl;

	_cfg->setGroup("ALSA Mixer");
	HCTL_id = _cfg->readEntry("HCTL ID", "");
	mixer_element = _cfg->readEntry("Mixer Element", "");

	if(KdetvALSA::useCardMixerElement(HCTL_id, mixer_element) != 0) {
		REPORT << "loading pre-saved plugin configuration failed" << endl;
		return;
	}

	// unmute the mixer device if it is indeed muted
	if(KdetvALSA::setMuted(false) != 0) {
		REPORT << "failed to unmute mixer element" << endl;
		return;
	}

	REPORT << "pre-saved plugin configuration loaded" << endl;

	return;
}

//-----------------------------------------------------------------------------
// Atomic member functions
//-----------------------------------------------------------------------------

snd_mixer_t *KdetvALSA::attachMixer(const QString &HCTL_id)
{
	const char *sig="[attachMixer()]";
	snd_mixer_t *tmp_mixer;
	int retcode;

	REPORT << "attempting to attach a mixer to " << HCTL_id << endl;

	if((retcode = snd_mixer_open(&tmp_mixer, 0)) != 0) {
		REPORT << "ERROR: snd_mixer_open failed: " << strerror(-retcode) << endl;
		return(NULL);
	}

	if((retcode = snd_mixer_attach(tmp_mixer, HCTL_id.local8Bit())) != 0) {
		REPORT << "ERROR: snd_mixer_attach failed: " << strerror(-retcode) << endl;
		snd_mixer_close(tmp_mixer);
		return(NULL);
	}

	snd_mixer_selem_register(tmp_mixer, NULL, NULL);

	// load all of the available elements for this card
	if(KdetvALSA::loadMixerElements(tmp_mixer) != 0) {
		KdetvALSA::detachMixer(tmp_mixer, active_HCTL.local8Bit());
		active_HCTL.truncate(0);
		return(NULL);
	}

	REPORT << "mixer attached successfully to " << HCTL_id << endl;

	return(tmp_mixer);
}

int KdetvALSA::detachMixer(snd_mixer_t *mixer, const char *HCTL_id)
{
	const char *sig="[detachMixer()]";
	int retval;

	if( (active_mixer == NULL) || active_HCTL.isEmpty() ) {
	    return 0;
	}

	REPORT << "detaching mixer from " << HCTL_id << endl;

	snd_mixer_free(mixer);

	if((retval = snd_mixer_detach(mixer, HCTL_id)) != 0) {
		REPORT << "ERROR: snd_mixer_detach failed: " << strerror(-retval) << endl;
		return(retval);
	}

	if((retval = snd_mixer_close(mixer)) != 0) {
		REPORT << "ERROR: snd_mixer_close failed: " << strerror(-retval) << endl;
		return(retval);
	}

	REPORT << "mixer detached successfully from " << HCTL_id << endl;

	return 0;
}

int KdetvALSA::loadMixerElements(snd_mixer_t *mixer)
{
	const char *sig="[loadMixerElements()]";
	snd_mixer_elem_t *mixer_element;
	snd_mixer_selem_id_t *sid;
	int retval, element_count = 0;

	REPORT << "discovering mixer elements" << endl;

	// no API documentation for this yet
	snd_mixer_selem_id_alloca(&sid);

	// the data in mixer_elements is no longer pertinent. Erase
	mixer_elements.clear();

	// load the mixer elements
	if((retval = snd_mixer_load(mixer)) != 0) {
		REPORT << "ERROR: snd_mixer_load failed: " << strerror(-retval) << endl;
		return(retval);
	}

	for(mixer_element = snd_mixer_first_elem(mixer) ; mixer_element ;
        mixer_element = snd_mixer_elem_next(mixer_element)) {
		element_count++;
		snd_mixer_selem_get_id(mixer_element, sid);

		// filter out any non-playback mixer elements
		if(!snd_mixer_selem_is_active(mixer_element)) continue;
		if(!snd_mixer_selem_has_playback_volume(mixer_element)) continue;
		if(!snd_mixer_selem_has_playback_switch(mixer_element)) continue;

		mixer_elements.insert(mixer_element, snd_mixer_selem_id_get_name(sid));
		REPORT << " + " << snd_mixer_selem_id_get_name(sid) << endl;
	}

	REPORT << " elements discovered : " << element_count << endl;

	// fail if we don't have mixer elements
	if (mixer_elements.count() == 0) {
        return -1;
	}
	REPORT << " playback elements : " << mixer_elements.count() << endl;

	return(0);
}

int KdetvALSA::probeDevices(void)
{
	const char *sig="[probeDevices()]";
	int card_id = -1;
	int retval;

	REPORT << "querying ALSA driver for soundcards" << endl;

	// clear out our probed_cards QMap
	probed_cards.clear();

	// Repopulate our probed_cards map
	while((retval = snd_card_next(&card_id)) == 0) {
		if(card_id == -1) break;

		// store the card information
		probed_cards.insert(card_id, QString("hw:%1").arg(card_id));
	}

	// check to see if we exited the loop due to an error
	if(retval != 0) {
		REPORT << "ERROR snd_card_next failed: "  << strerror(-retval) << endl;
	}

	REPORT << "ALSA driver reported " << probed_cards.size() << " cards" << endl;

	return(0);
}

//-----------------------------------------------------------------------------
// Compound member functions
//-----------------------------------------------------------------------------

int KdetvALSA::useCardMixerElement(const QString &HCTL_id, QString &mixer_element)
{
	const char *sig="[useCardMixerElement()]";
	QMap<snd_mixer_elem_t *, QString>::const_iterator it;

	REPORT << "request mixer element " << mixer_element << " on " << HCTL_id
           << endl;

	// detach and unload any existing mixer
	if(!active_HCTL.isEmpty() && active_mixer != NULL) {
		if(KdetvALSA::detachMixer(active_mixer, active_HCTL.local8Bit()) != 0)
			return(1);
		active_HCTL.truncate(0);
		active_mixer = NULL;
	}

	// attach a mixer to the HCTL interface
	if((active_mixer = KdetvALSA::attachMixer(HCTL_id)) == NULL)
		return(1);
	active_HCTL = HCTL_id;

	// obtain the active element for this card
	for(it = mixer_elements.constBegin() ; it != mixer_elements.constEnd() ; ++it) {
		if(mixer_element != it.data()) continue;
		break;
	}

	active_element = it.key();
	active_mixer_element = it.data();

	REPORT << "mixer element " << mixer_element << " acquired" << endl;
	return(0);
}

QWidget *KdetvALSA::configWidget(QWidget *parent, const char *name)
{
	const char *sig="[configWidget()]";
	QFrame *config_frame = new QFrame(parent, name);
	QGridLayout *config_grid = new QGridLayout(config_frame, 7, 7);

	// we need two pull-down lists, one for sound cards, and one for mixer
	// elements for the selected card
	QLabel *Label1 = new QLabel(i18n("Available cards:"), config_frame);
	QLabel *Label2 = new QLabel(i18n("Mixer elements:"), config_frame);

	REPORT << "creating configuration screen" << endl;

	// Initialise all pertinent objects.
	card_selector = new QComboBox(config_frame, "Card List");
	element_selector = new QComboBox(config_frame, "Element List");

	mixer_elements.clear();

	config_grid->addMultiCellWidget(Label1, 0, 0, 0, 2);
	config_grid->addMultiCellWidget(Label2, 1, 1, 0, 2);
	config_grid->addMultiCellWidget(card_selector, 0, 0, 3, 7);
	config_grid->addMultiCellWidget(element_selector, 1, 1, 3, 7);

	// scan for available cards
	KdetvALSA::probeDevices();

	// populate the sound card QComboBox with available cards
	if(!probed_cards.empty()) {
		QMap<int, QString>::const_iterator it;
		char *card_name;

		for(it = probed_cards.constBegin() ; it != probed_cards.constEnd() ; ++it) {
			if (snd_card_get_name(it.key(), &card_name))
                continue;
			card_selector->insertItem(card_name);
		}

		for(it = probed_cards.constBegin() ; it != probed_cards.constEnd() ; ++it) {
            if (it.data() == active_HCTL) {
                if (snd_card_get_name(it.key(), &card_name))
                    continue;
                card_selector->setCurrentText(card_name);
				break;
			}
		}

		cardChanged(card_selector->currentText());
		for(int i = 0; i < element_selector->count(); i++) {
            if (element_selector->text(i) == active_mixer_element) {
                element_selector->setCurrentItem(i);
				break;
			}
		}
	}

    if (card_selector->count() == 0) {
        KMessageBox::error(0L,
                           i18n("No mixers found. Check you ALSA library/driver installation."),
                           i18n("No ALSA Mixers Found"));
        delete config_frame;
        return NULL;
    }

	// attach a signal handler for the event of a new card being selected
	connect(card_selector, SIGNAL(activated(const QString &)),
            this, SLOT(cardChanged(const QString &)));

	REPORT << "configuration screen created" << endl;

	return(config_frame);
}

void KdetvALSA::cardChanged(const QString &card_name)
{
	const char *sig="[cardChanged()]";
	snd_mixer_t *mixer;
	QMap<int, QString>::const_iterator it;

	REPORT << "loading mixer elements for " << card_name << endl;

	// clear the element_selector list
	element_selector->clear();

	// obtain the HCTL ID for the given card name
	for(it = probed_cards.constBegin() ; it != probed_cards.constEnd() ; ++it) {
		char *tmp = NULL;

		if(snd_card_get_name(it.key(), &tmp) != 0)
			return;

		if(card_name == tmp)
            break;
	}

	if((mixer = KdetvALSA::attachMixer(it.data())) != NULL) {
		QMap<snd_mixer_elem_t *, QString>::const_iterator it2;

		for(it2 = mixer_elements.constBegin() ; it2 != mixer_elements.constEnd() ; ++it2)
			element_selector->insertItem(it2.data());

		KdetvALSA::detachMixer(mixer, it.data().local8Bit());
	}

	element_selector->setCurrentItem(0);

	REPORT << "elements added to QComboBox" << endl;
	return;
}

//------------------------------------------------------------------------------
// Volume related member functions
//------------------------------------------------------------------------------

bool KdetvALSA::muted(void)
{
	// this returns the current playback status of the mixer _plugin_
	return(muted_flag);
}

int KdetvALSA::setMuted(bool mute)
{
	const char *sig="[setMuted()]";
	int playback_status;

	if(active_element == NULL){
		REPORT << "ERROR: no active mixer element present" << endl;
		return(1);
	}

 	// we mute the channels by toggling the playback switch on the active element
 	snd_mixer_selem_get_playback_switch(active_element,
                                        SND_MIXER_SCHN_FRONT_LEFT, &playback_status);

	// toggle if necessary
	if(!(playback_status ^ mute)) {
		snd_mixer_selem_set_playback_switch_all(active_element, !playback_status);
		muted_flag = mute;
 	}

	REPORT << mixer_elements[active_element] << " on " << active_HCTL
           << " muted=" << muted_flag << endl;
	return 0;
}

int KdetvALSA::setVolume(int LeftVolume, int RightVolume)
{
    const char *sig="[setVolume()]";
	long min, max, vol;

	if(active_element == NULL){
		REPORT << "ERROR: no active mixer element present" << endl;
		return(1);
	}

	// unmute the mixer if it is in a muted state
	if(KdetvALSA::muted())
		KdetvALSA::setMuted(false);

	// discover our playback volume range for converting the *Volume levels
	// from a percantage into the correct value for the soundcard channels
	snd_mixer_selem_get_playback_volume_range(active_element, &min, &max);

	// set the left channel volume level on the soundcard
	vol = ((LeftVolume * (max - min)) / 100) + min;
	snd_mixer_selem_set_playback_volume(active_element,
                                        SND_MIXER_SCHN_FRONT_LEFT, vol);
	volume_left = LeftVolume;

	// set the right channel volume level on the soundcard
	vol = ((RightVolume * (max - min)) / 100) + min;
	snd_mixer_selem_set_playback_volume(active_element,
                                        SND_MIXER_SCHN_FRONT_RIGHT, vol);
	volume_right = RightVolume;

	return(0);
}

int KdetvALSA::volumeLeft(void)
{
	// const char *sig="[volumeLeft()]";
	return(volume_left);
}

int KdetvALSA::volumeRight(void)
{
	// const char *sig="[volumeRight()]";
	return(volume_right);
}

//------------------------------------------------------------------------------

extern "C" {
	KDETV_EXPORT KdetvALSA* create_alsa(Kdetv *ktv)
    {
		return new KdetvALSA(ktv, 0, "ALSA plugin");
	}
}

#include "kdetv_alsa.moc"

