/*
 * mixer_widget.c - mixer widget and keys handling
 * Copyright (c) 1998,1999 Tim Janik
 *                         Jaroslav Kysela <perex@perex.cz>
 * Copyright (c) 2009      Clemens Ladisch <clemens@ladisch.de>
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "aconfig.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <alsa/asoundlib.h>
#include "gettext_curses.h"
#include "version.h"
#include "utils.h"
#include "die.h"
#include "mem.h"
#include "colors.h"
#include "widget.h"
#include "textbox.h"
#include "proc_files.h"
#include "card_select.h"
#include "volume_mapping.h"
#include "mixer_controls.h"
#include "mixer_display.h"
#include "mixer_widget.h"

snd_mixer_t *mixer;
char *mixer_device_name;
bool unplugged;

struct widget mixer_widget;

enum view_mode view_mode;

int focus_control_index;
snd_mixer_selem_id_t *current_selem_id;
unsigned int current_control_flags;

bool controls_changed;

enum channel_mask {
	LEFT = 1,
	RIGHT = 2,
};

static int elem_callback(snd_mixer_elem_t *elem, unsigned int mask)
{
	if (mask & (SND_CTL_EVENT_MASK_REMOVE |
		    SND_CTL_EVENT_MASK_INFO |
		    SND_CTL_EVENT_MASK_VALUE))
		controls_changed = TRUE;
	return 0;
}

static int mixer_callback(snd_mixer_t *mixer, unsigned int mask, snd_mixer_elem_t *elem)
{
	if (mask & SND_CTL_EVENT_MASK_ADD) {
		snd_mixer_elem_set_callback(elem, elem_callback);
		controls_changed = TRUE;
	}
	return 0;
}

void create_mixer_object(struct snd_mixer_selem_regopt *selem_regopt)
{
	int err;

	err = snd_mixer_open(&mixer, 0);
	if (err < 0)
		fatal_alsa_error(_("cannot open mixer"), err);

	mixer_device_name = cstrdup(selem_regopt->device);
	err = snd_mixer_selem_register(mixer, selem_regopt, NULL);
	if (err < 0)
		fatal_alsa_error(_("cannot open mixer"), err);

	snd_mixer_set_callback(mixer, mixer_callback);

	err = snd_mixer_load(mixer);
	if (err < 0)
		fatal_alsa_error(_("cannot load mixer controls"), err);

	err = snd_mixer_selem_id_malloc(&current_selem_id);
	if (err < 0)
		fatal_error("out of memory");
}

static void set_view_mode(enum view_mode m)
{
	view_mode = m;
	create_controls();
}

static void close_hctl(void)
{
	free_controls();
	if (mixer_device_name) {
		snd_mixer_detach(mixer, mixer_device_name);
		free(mixer_device_name);
		mixer_device_name = NULL;
	}
}

static void check_unplugged(void)
{
	snd_hctl_t *hctl;
	snd_ctl_t *ctl;
	unsigned int state;
	int err;

	unplugged = FALSE;
	if (mixer_device_name) {
		err = snd_mixer_get_hctl(mixer, mixer_device_name, &hctl);
		if (err >= 0) {
			ctl = snd_hctl_ctl(hctl);
			/* just any random function that does an ioctl() */
			err = snd_ctl_get_power_state(ctl, &state);
			if (err == -ENODEV)
				unplugged = TRUE;
		}
	}
}

void close_mixer_device(void)
{
	check_unplugged();
	close_hctl();

	display_card_info();
	set_view_mode(view_mode);
}

bool select_card_by_name(const char *device_name)
{
	int err;
	bool opened;
	char *msg;

	close_hctl();
	unplugged = FALSE;

	opened = FALSE;
	if (device_name) {
		err = snd_mixer_attach(mixer, device_name);
		if (err >= 0)
			opened = TRUE;
		else {
			msg = casprintf(_("Cannot open mixer device '%s'."), device_name);
			show_alsa_error(msg, err);
			free(msg);
		}
	}
	if (opened) {
		mixer_device_name = cstrdup(device_name);

		err = snd_mixer_load(mixer);
		if (err < 0)
			fatal_alsa_error(_("cannot load mixer controls"), err);
	}

	display_card_info();
	set_view_mode(view_mode);
	return opened;
}

static void show_help(void)
{
	const char *help[] = {
		_("Esc     Exit"),
		_("F1 ? H  Help"),
		_("F2 /    System information"),
		_("F3      Show playback controls"),
		_("F4      Show capture controls"),
		_("F5      Show all controls"),
		_("Tab     Toggle view mode (F3/F4/F5)"),
		_("F6 S    Select sound card"),
		_("L       Redraw screen"),
		"",
		_("Left    Move to the previous control"),
		_("Right   Move to the next control"),
		"",
		_("Up/Down    Change volume"),
		_("+ -        Change volume"),
		_("Page Up/Dn Change volume in big steps"),
		_("End        Set volume to 0%"),
		_("0-9        Set volume to 0%-90%"),
		_("Q W E      Increase left/both/right volumes"),
		/* TRANSLATORS: or Y instead of Z */
		_("Z X C      Decrease left/both/right volumes"),
		_("B          Balance left and right volumes"),
		"",
		_("M          Toggle mute"),
		/* TRANSLATORS: or , . */
		_("< >        Toggle left/right mute"),
		"",
		_("Space      Toggle capture"),
		/* TRANSLATORS: or Insert Delete */
		_("; '        Toggle left/right capture"),
		"",
		_("Authors:"),
		_("  Tim Janik"),
		_("  Jaroslav Kysela <perex@perex.cz>"),
		_("  Clemens Ladisch <clemens@ladisch.de>"),
	};
	show_text(help, ARRAY_SIZE(help), _("Help"));
}

void refocus_control(void)
{
	if (focus_control_index < controls_count) {
		snd_mixer_selem_get_id(controls[focus_control_index].elem, current_selem_id);
		current_control_flags = controls[focus_control_index].flags;
	}

	display_controls();
}

static struct control *get_focus_control(unsigned int type)
{
	if (focus_control_index >= 0 &&
	    focus_control_index < controls_count &&
	    (controls[focus_control_index].flags & IS_ACTIVE) &&
	    (controls[focus_control_index].flags & type))
		return &controls[focus_control_index];
	else
		return NULL;
}

static void change_enum_to_percent(struct control *control, int value)
{
	unsigned int i;
	unsigned int index;
	unsigned int new_index;
	int items;
	int err;

	i = ffs(control->enum_channel_bits) - 1;
	err = snd_mixer_selem_get_enum_item(control->elem, i, &index);
	if (err < 0)
		return;
	new_index = index;
	if (value == 0) {
		new_index = 0;
	} else if (value == 100) {
		items = snd_mixer_selem_get_enum_items(control->elem);
		if (items < 1)
			return;
		new_index = items - 1;
	}
	if (new_index == index)
		return;
	for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i)
		if (control->enum_channel_bits & (1 << i))
			snd_mixer_selem_set_enum_item(control->elem, i, new_index);
}

static void change_enum_relative(struct control *control, int delta)
{
	int items;
	unsigned int i;
	unsigned int index;
	int new_index;
	int err;

	items = snd_mixer_selem_get_enum_items(control->elem);
	if (items < 1)
		return;
	err = snd_mixer_selem_get_enum_item(control->elem, 0, &index);
	if (err < 0)
		return;
	new_index = (int)index + delta;
	if (new_index < 0)
		new_index = 0;
	else if (new_index >= items)
		new_index = items - 1;
	if (new_index == index)
		return;
	for (i = 0; i <= SND_MIXER_SCHN_LAST; ++i)
		if (control->enum_channel_bits & (1 << i))
			snd_mixer_selem_set_enum_item(control->elem, i, new_index);
}

static void change_volume_to_percent(struct control *control, int value, unsigned int channels)
{
	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int);

	if (!(control->flags & HAS_VOLUME_1))
		channels = LEFT;
	if (control->flags & TYPE_PVOLUME)
		set_func = set_normalized_playback_volume;
	else
		set_func = set_normalized_capture_volume;
	if (channels & LEFT)
		set_func(control->elem, control->volume_channels[0], value / 100.0, 0);
	if (channels & RIGHT)
		set_func(control->elem, control->volume_channels[1], value / 100.0, 0);
}

static double clamp_volume(double v)
{
	if (v < 0)
		return 0;
	if (v > 1)
		return 1;
	return v;
}

static void change_volume_relative(struct control *control, int delta, unsigned int channels)
{
	double (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t);
	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, double, int);
	double left, right;
	int dir;

	if (!(control->flags & HAS_VOLUME_1))
		channels = LEFT;
	if (control->flags & TYPE_PVOLUME) {
		get_func = get_normalized_playback_volume;
		set_func = set_normalized_playback_volume;
	} else {
		get_func = get_normalized_capture_volume;
		set_func = set_normalized_capture_volume;
	}
	if (channels & LEFT)
		left = get_func(control->elem, control->volume_channels[0]);
	if (channels & RIGHT)
		right = get_func(control->elem, control->volume_channels[1]);
	dir = delta > 0 ? 1 : -1;
	if (channels & LEFT) {
		left = clamp_volume(left + delta / 100.0);
		set_func(control->elem, control->volume_channels[0], left, dir);
	}
	if (channels & RIGHT) {
		right = clamp_volume(right + delta / 100.0);
		set_func(control->elem, control->volume_channels[1], right, dir);
	}
}

static void change_control_to_percent(int value, unsigned int channels)
{
	struct control *control;

	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM);
	if (!control)
		return;
	if (control->flags & TYPE_ENUM)
		change_enum_to_percent(control, value);
	else
		change_volume_to_percent(control, value, channels);
	display_controls();
}

static void change_control_relative(int delta, unsigned int channels)
{
	struct control *control;

	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME | TYPE_ENUM);
	if (!control)
		return;
	if (control->flags & TYPE_ENUM)
		change_enum_relative(control, delta);
	else
		change_volume_relative(control, delta, channels);
	display_controls();
}

static void toggle_switches(unsigned int type, unsigned int channels)
{
	struct control *control;
	unsigned int switch_1_mask;
	int (*get_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int *);
	int (*set_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, int);
	snd_mixer_selem_channel_id_t channel_ids[2];
	int left, right;
	int err;

	control = get_focus_control(type);
	if (!control)
		return;
	if (type == TYPE_PSWITCH) {
		switch_1_mask = HAS_PSWITCH_1;
		get_func = snd_mixer_selem_get_playback_switch;
		set_func = snd_mixer_selem_set_playback_switch;
		channel_ids[0] = control->pswitch_channels[0];
		channel_ids[1] = control->pswitch_channels[1];
	} else {
		switch_1_mask = HAS_CSWITCH_1;
		get_func = snd_mixer_selem_get_capture_switch;
		set_func = snd_mixer_selem_set_capture_switch;
		channel_ids[0] = control->cswitch_channels[0];
		channel_ids[1] = control->cswitch_channels[1];
	}
	if (!(control->flags & switch_1_mask))
		channels = LEFT;
	if (channels & LEFT) {
		err = get_func(control->elem, channel_ids[0], &left);
		if (err < 0)
			return;
	}
	if (channels & RIGHT) {
		err = get_func(control->elem, channel_ids[1], &right);
		if (err < 0)
			return;
	}
	if (channels & LEFT)
		set_func(control->elem, channel_ids[0], !left);
	if (channels & RIGHT)
		set_func(control->elem, channel_ids[1], !right);
	display_controls();
}

static void toggle_mute(unsigned int channels)
{
	toggle_switches(TYPE_PSWITCH, channels);
}

static void toggle_capture(unsigned int channels)
{
	toggle_switches(TYPE_CSWITCH, channels);
}

static void balance_volumes(void)
{
	struct control *control;
	double left, right;
	int err;

	control = get_focus_control(TYPE_PVOLUME | TYPE_CVOLUME);
	if (!control || !(control->flags & HAS_VOLUME_1))
		return;
	if (control->flags & TYPE_PVOLUME) {
		left = get_normalized_playback_volume(control->elem, control->volume_channels[0]);
		right = get_normalized_playback_volume(control->elem, control->volume_channels[1]);
	} else {
		left = get_normalized_capture_volume(control->elem, control->volume_channels[0]);
		right = get_normalized_capture_volume(control->elem, control->volume_channels[1]);
	}
	left = (left + right) / 2;
	if (control->flags & TYPE_PVOLUME) {
		set_normalized_playback_volume(control->elem, control->volume_channels[0], left, 0);
		set_normalized_playback_volume(control->elem, control->volume_channels[1], left, 0);
	} else {
		set_normalized_capture_volume(control->elem, control->volume_channels[0], left, 0);
		set_normalized_capture_volume(control->elem, control->volume_channels[1], left, 0);
	}
	display_controls();
}

static void on_handle_key(int key)
{
	switch (key) {
	case 27:
	case KEY_CANCEL:
	case KEY_F(10):
		mixer_widget.close();
		break;
	case KEY_F(1):
	case KEY_HELP:
	case 'H':
	case 'h':
	case '?':
		show_help();
		break;
	case KEY_F(2):
	case '/':
		create_proc_files_list();
		break;
	case KEY_F(3):
		set_view_mode(VIEW_MODE_PLAYBACK);
		break;
	case KEY_F(4):
		set_view_mode(VIEW_MODE_CAPTURE);
		break;
	case KEY_F(5):
		set_view_mode(VIEW_MODE_ALL);
		break;
	case '\t':
		set_view_mode((enum view_mode)((view_mode + 1) % VIEW_MODE_COUNT));
		break;
	case KEY_F(6):
	case 'S':
	case 's':
		create_card_select_list();
		break;
	case KEY_REFRESH:
	case 12:
	case 'L':
	case 'l':
		clearok(mixer_widget.window, TRUE);
		display_controls();
		break;
	case KEY_LEFT:
	case 'P':
	case 'p':
		if (focus_control_index > 0) {
			--focus_control_index;
			refocus_control();
		}
		break;
	case KEY_RIGHT:
	case 'N':
	case 'n':
		if (focus_control_index < controls_count - 1) {
			++focus_control_index;
			refocus_control();
		}
		break;
	case KEY_PPAGE:
		change_control_relative(5, LEFT | RIGHT);
		break;
	case KEY_NPAGE:
		change_control_relative(-5, LEFT | RIGHT);
		break;
#if 0
	case KEY_BEG:
	case KEY_HOME:
		change_control_to_percent(100, LEFT | RIGHT);
		break;
#endif
	case KEY_LL:
	case KEY_END:
		change_control_to_percent(0, LEFT | RIGHT);
		break;
	case KEY_UP:
	case '+':
	case 'K':
	case 'k':
	case 'W':
	case 'w':
		change_control_relative(1, LEFT | RIGHT);
		break;
	case KEY_DOWN:
	case '-':
	case 'J':
	case 'j':
	case 'X':
	case 'x':
		change_control_relative(-1, LEFT | RIGHT);
		break;
	case '0': case '1': case '2': case '3': case '4':
	case '5': case '6': case '7': case '8': case '9':
		change_control_to_percent((key - '0') * 10, LEFT | RIGHT);
		break;
	case 'Q':
	case 'q':
		change_control_relative(1, LEFT);
		break;
	case 'Y':
	case 'y':
	case 'Z':
	case 'z':
		change_control_relative(-1, LEFT);
		break;
	case 'E':
	case 'e':
		change_control_relative(1, RIGHT);
		break;
	case 'C':
	case 'c':
		change_control_relative(-1, RIGHT);
		break;
	case 'M':
	case 'm':
		toggle_mute(LEFT | RIGHT);
		break;
	case 'B':
	case 'b':
	case '=':
		balance_volumes();
		break;
	case '<':
	case ',':
		toggle_mute(LEFT);
		break;
	case '>':
	case '.':
		toggle_mute(RIGHT);
		break;
	case ' ':
		toggle_capture(LEFT | RIGHT);
		break;
	case KEY_IC:
	case ';':
		toggle_capture(LEFT);
		break;
	case KEY_DC:
	case '\'':
		toggle_capture(RIGHT);
		break;
	}
}

static void create(void)
{
	static const char title[] = " AlsaMixer v" SND_UTIL_VERSION_STR " ";

	widget_init(&mixer_widget, screen_lines, screen_cols, 0, 0,
		    attr_mixer_frame, WIDGET_BORDER);
	if (screen_cols >= (sizeof(title) - 1) + 2) {
		wattrset(mixer_widget.window, attr_mixer_active);
		mvwaddstr(mixer_widget.window, 0, (screen_cols - (sizeof(title) - 1)) / 2, title);
	}
	init_mixer_layout();
	display_card_info();
	set_view_mode(view_mode);
}

static void on_window_size_changed(void)
{
	create();
}

static void on_close(void)
{
	widget_free(&mixer_widget);
}

void mixer_shutdown(void)
{
	free_controls();
	if (mixer)
		snd_mixer_close(mixer);
	if (current_selem_id)
		snd_mixer_selem_id_free(current_selem_id);
}

struct widget mixer_widget = {
	.handle_key = on_handle_key,
	.window_size_changed = on_window_size_changed,
	.close = on_close,
};

void create_mixer_widget(void)
{
	create();
}
