/* the Music Player Daemon (MPD)
 * (c)2003-2004 by Warren Dukes (shank@mercury.chem.pitt.edu)
 * This project's homepage is: http://www.musicpd.org
 *
 * 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 of the License, 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 "volume.h"

#include "command.h"
#include "conf.h"
#include "log.h"
#include "player.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#ifndef NO_OSS_MIXER
#include <sys/soundcard.h>
#endif
#ifdef HAVE_ALSA
#include <alsa/asoundlib.h>
#endif

#define VOLUME_MIXER_TYPE_SOFTWARE	0
#define VOLUME_MIXER_TYPE_OSS		1
#define VOLUME_MIXER_TYPE_ALSA		2

#define VOLUME_MIXER_SOFTWARE_DEFAULT	""
#define VOLUME_MIXER_OSS_DEFAULT	"/dev/mixer"
#define VOLUME_MIXER_ALSA_DEFAULT	"default"

int volume_mixerType = VOLUME_MIXER_TYPE_SOFTWARE;
char * volume_mixerDevice;

#ifndef NO_OSS_MIXER
int volume_ossFd;
int volume_ossControl = SOUND_MIXER_VOLUME;
#endif

#ifdef HAVE_ALSA
snd_mixer_t * volume_alsaMixerHandle = NULL;
snd_mixer_elem_t * volume_alsaElem;
long volume_alsaMin;
long volume_alsaMax;
#endif

#ifndef NO_OSS_MIXER
int prepOssMixer(char * device) {
	int devmask = 0;

	if((volume_ossFd = open(device,O_RDONLY))<0) {
		ERROR("unable to open oss mixer \"%s\"\n",device);
		return -1;
	}

	if(getConf()[CONF_MIXER_CONTROL]) {
		char * labels[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_LABELS;
		char * dup;
		int i,j;

		if(ioctl(volume_ossFd,SOUND_MIXER_READ_DEVMASK,&devmask)<0) {
			ERROR("errors getting read_devmask for oss mixer\n");
			close(volume_ossFd);
			return -1;
		}

		for(i=0;i<SOUND_MIXER_NRDEVICES;i++) {
			dup = strdup(labels[i]);
			/* eliminate spaces at the end */
			j = strlen(dup)-1;
			while(j>=0 && dup[j]==' ') dup[j--] = '\0';
			if(strcmp(dup,getConf()[CONF_MIXER_CONTROL])==0) {
				free(dup);
				break;
			}
			free(dup);
		}

		if(i>=SOUND_MIXER_NRDEVICES) {
			ERROR("mixer control \"%s\" not found\n",
					getConf()[CONF_MIXER_CONTROL]);
			close(volume_ossFd);
			return -1;
		}
		else if(!( ( 1 << i ) & devmask )) {
			ERROR("mixer control \"%s\" not usable\n",
					getConf()[CONF_MIXER_CONTROL]);
			close(volume_ossFd);
			return -1;
		}

		volume_ossControl = i;
	}

	return 0;
}

void closeOssMixer() {
	close(volume_ossFd);
}

int getOssVolumeLevel() {
	int left, right, level;

	if(ioctl(volume_ossFd,MIXER_READ(volume_ossControl),&level) < 0) {
		ERROR("unable to read volume\n");
		return -1;
	}

	left = level & 0xff;
	right = (level & 0xff00) >> 8;

	if(left!=right) {
		ERROR("volume for left and right is not the same, \"%i\" and "
			"\"%i\"\n",left,right);
	}

	return left;
}

int changeOssVolumeLevel(FILE * fp, int change, int rel) {
	int current;
	int new;
	int level;

	if (rel) {
		if((current = getOssVolumeLevel()) < 0) {
			myfprintf(fp,"%s problem getting current volume\n",
					COMMAND_RESPOND_ERROR);
			return -1;
		}

		new = current+change;
	}
	else new = change;

	if(new<0) new = 0;
	else if(new>100) new = 100;

	level = (new << 8) + new;

	if(ioctl(volume_ossFd,MIXER_WRITE(volume_ossControl),&level) < 0) {
		myfprintf(fp,"%s problems setting volume\n",COMMAND_RESPOND_ERROR);
		return -1;
	}

	return 0;
}
#endif

#ifdef HAVE_ALSA
int prepAlsaMixer(char * card) {
	int err;
	snd_mixer_elem_t * elem;

	if((err = snd_mixer_open(&volume_alsaMixerHandle,0))<0) {
		ERROR("problems opening alsa mixer: %s\n",snd_strerror(err));
		return -1;
	}
	
	if((err = snd_mixer_attach(volume_alsaMixerHandle,card))<0) {
		snd_mixer_close(volume_alsaMixerHandle);
		ERROR("problems problems attaching alsa mixer: %s\n",
			snd_strerror(err));
		return -1;
	}
	
	if((err = snd_mixer_selem_register(volume_alsaMixerHandle,NULL,NULL))<0) {
		snd_mixer_close(volume_alsaMixerHandle);
		ERROR("problems snd_mixer_selem_register'ing: %s\n",
			snd_strerror(err));
		return -1;
	}
	
	if((err = snd_mixer_load(volume_alsaMixerHandle))<0) {
		snd_mixer_close(volume_alsaMixerHandle);
		ERROR("problems snd_mixer_selem_register'ing: %s\n",
			snd_strerror(err));
		return -1;
	}

	elem = snd_mixer_first_elem(volume_alsaMixerHandle);
	if(getConf()[CONF_MIXER_CONTROL]) {
		while(elem) {
			if(snd_mixer_elem_get_type(elem)
					==SND_MIXER_ELEM_SIMPLE) 
			{
				if(strcmp(getConf()[CONF_MIXER_CONTROL],
						snd_mixer_selem_get_name(elem))
						==0)
				{
					break;
				}
			}
			elem = snd_mixer_elem_next(elem);
		}

		if(elem) {
			volume_alsaElem = elem;
			snd_mixer_selem_get_playback_volume_range(
					volume_alsaElem,
					&volume_alsaMin,&volume_alsaMax);
			return 0;
		}
	}
	else {
		volume_alsaElem = elem;
		if(snd_mixer_elem_get_type(volume_alsaElem)
				==SND_MIXER_ELEM_SIMPLE) 
		{
			snd_mixer_selem_get_playback_volume_range(
					volume_alsaElem,
					&volume_alsaMin,&volume_alsaMax);
			return 0;
		}
	}

	snd_mixer_close(volume_alsaMixerHandle);
	return -1;
}

void closeAlsaMixer() {
	snd_mixer_close(volume_alsaMixerHandle);
}

int getAlsaVolumeLevel() {
	int ret;
	long level;
	long max = volume_alsaMax;
	long min = volume_alsaMin;
	int err;

	if((err = snd_mixer_selem_get_playback_volume(volume_alsaElem,
		SND_MIXER_SCHN_FRONT_LEFT,&level))<0) {
		ERROR("problems getting alsa volume: %s\n",snd_strerror(err));
		return -1;
	}

	snd_mixer_selem_get_playback_volume(volume_alsaElem,
		SND_MIXER_SCHN_FRONT_LEFT,&level);
	ret =  (int)(100*(((float)(level-min))/(max-min))+0.5);

	return ret;
}

int changeAlsaVolumeLevel(FILE * fp, int change, int rel) {
	float vol;
	long level;
	long max = volume_alsaMax;
	long min = volume_alsaMin;
	int err;

	if((err = snd_mixer_selem_get_playback_volume(volume_alsaElem,
		SND_MIXER_SCHN_FRONT_LEFT,&level))<0) {
		myfprintf(fp,"%s problems getting volume\n",
			COMMAND_RESPOND_ERROR);
		ERROR("problems getting alsa volume: %s\n",snd_strerror(err));
		return -1;
	}

	if (rel) {
		vol =  100.0*(((float)(level-min))/(max-min));
		vol+=change;
	}
	else
		vol = change;

	level = (long)(((vol/100.0)*(max-min)+min)+0.5);
	level = level>max?max:level;
	level = level<min?min:level;

	if((err = snd_mixer_selem_set_playback_volume_all(
				volume_alsaElem,level))<0) {
		myfprintf(fp,"%s problems setting volume\n",
			COMMAND_RESPOND_ERROR);
		ERROR("problems setting alsa volume: %s\n",snd_strerror(err));
		return -1;
	}

	return 0;
}
#endif

int prepMixer(char * device) {
	switch(volume_mixerType) {
#ifdef HAVE_ALSA
	case VOLUME_MIXER_TYPE_ALSA:
		return prepAlsaMixer(device);
#endif
#ifndef NO_OSS_MIXER
	case VOLUME_MIXER_TYPE_OSS:
		return prepOssMixer(device);
#endif
	}

	return 0;
}

void finishVolume() {
	switch(volume_mixerType) {
#ifdef HAVE_ALSA
	case VOLUME_MIXER_TYPE_ALSA:
		closeAlsaMixer();
		break;
#endif
#ifndef NO_OSS_MIXER
	case VOLUME_MIXER_TYPE_OSS:
		closeOssMixer();
		break;
#endif
	}
}

void initVolume() {
	if(0);
#ifdef HAVE_ALSA
	else if(strcmp((getConf())[CONF_MIXER_TYPE],VOLUME_MIXER_ALSA)==0) {
		volume_mixerType = VOLUME_MIXER_TYPE_ALSA;
		volume_mixerDevice = VOLUME_MIXER_ALSA_DEFAULT;
	}
#endif
#ifndef NO_OSS_MIXER
	else if(strcmp((getConf())[CONF_MIXER_TYPE],VOLUME_MIXER_OSS)==0) {
		volume_mixerType = VOLUME_MIXER_TYPE_OSS;
		volume_mixerDevice = VOLUME_MIXER_OSS_DEFAULT;
	}
#endif
	else if(strcmp((getConf())[CONF_MIXER_TYPE],VOLUME_MIXER_SOFTWARE)==0) {
		volume_mixerType = VOLUME_MIXER_TYPE_SOFTWARE;
		volume_mixerDevice = VOLUME_MIXER_SOFTWARE_DEFAULT;
	}
	else {
		ERROR("unknown mixer type: %s\n",(getConf())[CONF_MIXER_TYPE]);
		exit(-1);
	}
	if(strlen((getConf())[CONF_MIXER_DEVICE])) {
		volume_mixerDevice = (getConf())[CONF_MIXER_DEVICE];
	}
}

void openVolumeDevice() {
	if(prepMixer(volume_mixerDevice)<0) {
		ERROR("using software volume\n");
		volume_mixerType = VOLUME_MIXER_TYPE_SOFTWARE;
	}
}

int getSoftwareVolume() {
	return 50*log((getPlayerSoftwareVolume()*(M_E*M_E-1)/100.0)+1)+0.5;
}

int getVolumeLevel() {
	switch(volume_mixerType) {
#ifdef HAVE_ALSA
	case VOLUME_MIXER_TYPE_ALSA:
		return getAlsaVolumeLevel();
#endif
#ifndef NO_OSS_MIXER
	case VOLUME_MIXER_TYPE_OSS:
		return getOssVolumeLevel();
#endif
	case VOLUME_MIXER_TYPE_SOFTWARE:
		return getSoftwareVolume();
	default:
		return -1;
	}
}

int changeSoftwareVolume(FILE * fp, int change, int rel) {
	int new = change;

	if(rel) new+=getSoftwareVolume();

	if(new>100) new = 100;
	else if(new<0) new = 0;

	new = 100.0*(exp(new/50.0)-1)/(M_E*M_E-1)+0.5;

	setPlayerSoftwareVolume(new);

	return 0;
}

int changeVolumeLevel(FILE * fp, int change, int rel) {
	switch(volume_mixerType) {
#ifdef HAVE_ALSA
	case VOLUME_MIXER_TYPE_ALSA:
		return changeAlsaVolumeLevel(fp,change,rel);
#endif
#ifndef NO_OSS_MIXER
	case VOLUME_MIXER_TYPE_OSS:
		return changeOssVolumeLevel(fp,change,rel);
#endif
	case VOLUME_MIXER_TYPE_SOFTWARE:
		return changeSoftwareVolume(fp,change,rel);
	default:
		myfprintf(fp,"%s no volume support!\n",COMMAND_RESPOND_ERROR);
		return -1;
	}
}