/* $NetBSD: main.c,v 1.7 2024/11/03 10:43:27 rillig Exp $ */
/*-
 * Copyright (c) 2021 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Nia Alarie.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
#include <sys/audioio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <paths.h>
#include <curses.h>
#include <stdlib.h>
#include <err.h>
#include "app.h"
#include "draw.h"
#include "parse.h"

static void process_device_select(struct aiomixer *, unsigned int);
static void open_device(struct aiomixer *, const char *);
static void __dead usage(void);
static int adjust_level(int, int);
static int select_class(struct aiomixer *, unsigned int);
static int select_control(struct aiomixer *, unsigned int);
static void slide_control(struct aiomixer *, struct aiomixer_control *, bool);
static int toggle_set(struct aiomixer *);
static void step_up(struct aiomixer *);
static void step_down(struct aiomixer *);
static int read_key(struct aiomixer *, int);

static void __dead
usage(void)
{
	fputs("aiomixer [-u] [-d device]\n", stderr);
	exit(1);
}

static int
select_class(struct aiomixer *aio, unsigned int n)
{
	struct aiomixer_class *class;
	unsigned i;

	if (n >= aio->numclasses)
		return -1;

	class = &aio->classes[n];
	aio->widgets_resized = true;
	aio->class_scroll_y = 0;
	aio->curcontrol = 0;
	aio->curclass = n;
	for (i = 0; i < class->numcontrols; ++i) {
		class->controls[i].setindex = -1;
		draw_control(aio, &class->controls[i], false);
	}
	draw_classbar(aio);
	return 0;
}

static int
select_control(struct aiomixer *aio, unsigned int n)
{
	struct aiomixer_class *class;
	struct aiomixer_control *lastcontrol;
	struct aiomixer_control *control;

	class = &aio->classes[aio->curclass];

	if (n >= class->numcontrols)
		return -1;

	lastcontrol = &class->controls[aio->curcontrol];
	lastcontrol->setindex = -1;
	draw_control(aio, lastcontrol, false);

	control = &class->controls[n];
	aio->curcontrol = n;
	control->setindex = 0;
	draw_control(aio, control, true);

	if (aio->class_scroll_y > control->widget_y) {
		aio->class_scroll_y = control->widget_y;
		aio->widgets_resized = true;
	}

	if ((control->widget_y + control->height) >
	    ((getmaxy(stdscr) - 4) + aio->class_scroll_y)) {
		aio->class_scroll_y = control->widget_y;
		aio->widgets_resized = true;
	}
	return 0;
}

static int
adjust_level(int level, int delta)
{
	if (level > (AUDIO_MAX_GAIN - delta))
		return AUDIO_MAX_GAIN;

	if (delta < 0 && level < (AUDIO_MIN_GAIN + (-delta)))
		return AUDIO_MIN_GAIN;

	return level + delta;
}

static void
slide_control(struct aiomixer *aio,
    struct aiomixer_control *control, bool right)
{
	struct mixer_devinfo *info = &control->info;
	struct mixer_ctrl value;
	unsigned char *level;
	int i, delta;
	int cur_index = 0;

	if (info->type != AUDIO_MIXER_SET) {
		value.dev = info->index;
		value.type = info->type;
		if (info->type == AUDIO_MIXER_VALUE)
			value.un.value.num_channels = info->un.v.num_channels;

		if (ioctl(aio->fd, AUDIO_MIXER_READ, &value) < 0)
			err(EXIT_FAILURE, "failed to read mixer control");
	}

	switch (info->type) {
	case AUDIO_MIXER_VALUE:
		if (info->un.v.delta != 0) {
			delta = right ? info->un.v.delta : -info->un.v.delta;
		} else {
			/* delta is 0 in qemu with sb(4) */
			delta = right ? 16 : -16;
		}
		/*
		 * work around strange problem where the level can be
		 * increased but not decreased, seen with uaudio(4)
		 */
		if (delta < 16)
			delta *= 2;
		if (aio->channels_unlocked) {
			level = &value.un.value.level[control->setindex];
			*level = (unsigned char)adjust_level(*level, delta);
		} else {
			for (i = 0; i < value.un.value.num_channels; ++i) {
				level = &value.un.value.level[i];
				*level = (unsigned char)adjust_level(*level, delta);
			}
		}
		break;
	case AUDIO_MIXER_ENUM:
		for (i = 0; i < info->un.e.num_mem; ++i) {
			if (info->un.e.member[i].ord == value.un.ord) {
				cur_index = i;
				break;
			}
		}
		if (right) {
			value.un.ord = cur_index < (info->un.e.num_mem - 1) ?
			    info->un.e.member[cur_index + 1].ord :
			    info->un.e.member[0].ord;
		} else {
			value.un.ord = cur_index > 0 ?
			    info->un.e.member[cur_index - 1].ord :
			    info->un.e.member[control->info.un.e.num_mem - 1].ord;
		}
		break;
	case AUDIO_MIXER_SET:
		if (right) {
			control->setindex =
			    control->setindex < (info->un.s.num_mem - 1) ?
				control->setindex + 1 : 0;
		} else {
			control->setindex = control->setindex > 0 ?
			    control->setindex - 1 :
				control->info.un.s.num_mem - 1;
		}
		break;
	}

	if (info->type != AUDIO_MIXER_SET) {
		if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &value) < 0)
			err(EXIT_FAILURE, "failed to adjust mixer control");
	}

	draw_control(aio, control, true);
}

static int
toggle_set(struct aiomixer *aio)
{
	struct mixer_ctrl ctrl;
	struct aiomixer_class *class = &aio->classes[aio->curclass];
	struct aiomixer_control *control = &class->controls[aio->curcontrol];

	ctrl.dev = control->info.index;
	ctrl.type = control->info.type;

	if (control->info.type != AUDIO_MIXER_SET)
		return -1;

	if (ioctl(aio->fd, AUDIO_MIXER_READ, &ctrl) < 0)
		err(EXIT_FAILURE, "failed to read mixer control");

	ctrl.un.mask ^= control->info.un.s.member[control->setindex].mask;

	if (ioctl(aio->fd, AUDIO_MIXER_WRITE, &ctrl) < 0)
		err(EXIT_FAILURE, "failed to read mixer control");

	draw_control(aio, control, true);
	return 0;
}

static void
step_up(struct aiomixer *aio)
{
	struct aiomixer_class *class;
	struct aiomixer_control *control;

	class = &aio->classes[aio->curclass];
	control = &class->controls[aio->curcontrol];

	if (aio->channels_unlocked &&
	    control->info.type == AUDIO_MIXER_VALUE &&
	    control->setindex > 0) {
		control->setindex--;
		draw_control(aio, control, true);
		return;
	}
	select_control(aio, aio->curcontrol - 1);
}

static void
step_down(struct aiomixer *aio)
{
	struct aiomixer_class *class;
	struct aiomixer_control *control;

	class = &aio->classes[aio->curclass];
	control = &class->controls[aio->curcontrol];

	if (aio->channels_unlocked &&
	    control->info.type == AUDIO_MIXER_VALUE &&
	    control->setindex < (control->info.un.v.num_channels - 1)) {
		control->setindex++;
		draw_control(aio, control, true);
		return;
	}

	select_control(aio, (aio->curcontrol + 1) % class->numcontrols);
}

static int
read_key(struct aiomixer *aio, int ch)
{
	struct aiomixer_class *class;
	struct aiomixer_control *control;
	size_t i;

	switch (ch) {
	case KEY_RESIZE:
		class = &aio->classes[aio->curclass];
		resize_widgets(aio);
		draw_header(aio);
		draw_classbar(aio);
		for (i = 0; i < class->numcontrols; ++i) {
			draw_control(aio,
			    &class->controls[i],
			    aio->state == STATE_CONTROL_SELECT ?
				(aio->curcontrol == i) : false);
		}
		break;
	case KEY_LEFT:
	case 'h':
		if (aio->state == STATE_CLASS_SELECT) {
			select_class(aio, aio->curclass > 0 ?
			    aio->curclass - 1 : aio->numclasses - 1);
		} else if (aio->state == STATE_CONTROL_SELECT) {
			class = &aio->classes[aio->curclass];
			slide_control(aio,
			    &class->controls[aio->curcontrol], false);
		}
		break;
	case KEY_RIGHT:
	case 'l':
		if (aio->state == STATE_CLASS_SELECT) {
			select_class(aio,
			    (aio->curclass + 1) % aio->numclasses);
		} else if (aio->state == STATE_CONTROL_SELECT) {
			class = &aio->classes[aio->curclass];
			slide_control(aio,
			    &class->controls[aio->curcontrol], true);
		}
		break;
	case KEY_UP:
	case 'k':
		if (aio->state == STATE_CONTROL_SELECT) {
			if (aio->curcontrol == 0) {
				class = &aio->classes[aio->curclass];
				control = &class->controls[aio->curcontrol];
				control->setindex = -1;
				aio->state = STATE_CLASS_SELECT;
				draw_control(aio, control, false);
			} else {
				step_up(aio);
			}
		}
		break;
	case KEY_DOWN:
	case 'j':
		if (aio->state == STATE_CLASS_SELECT) {
			class = &aio->classes[aio->curclass];
			if (class->numcontrols > 0) {
				aio->state = STATE_CONTROL_SELECT;
				select_control(aio, 0);
			}
		} else if (aio->state == STATE_CONTROL_SELECT) {
			step_down(aio);
		}
		break;
	case '\n':
	case ' ':
		if (aio->state == STATE_CONTROL_SELECT)
			toggle_set(aio);
		break;
	case '1':
		select_class(aio, 0);
		break;
	case '2':
		select_class(aio, 1);
		break;
	case '3':
		select_class(aio, 2);
		break;
	case '4':
		select_class(aio, 3);
		break;
	case '5':
		select_class(aio, 4);
		break;
	case '6':
		select_class(aio, 5);
		break;
	case '7':
		select_class(aio, 6);
		break;
	case '8':
		select_class(aio, 7);
		break;
	case '9':
		select_class(aio, 8);
		break;
	case 'q':
	case '\e':
		if (aio->state == STATE_CONTROL_SELECT) {
			class = &aio->classes[aio->curclass];
			control = &class->controls[aio->curcontrol];
			aio->state = STATE_CLASS_SELECT;
			draw_control(aio, control, false);
			break;
		}
		return 1;
	case 'u':
		aio->channels_unlocked = !aio->channels_unlocked;
		if (aio->state == STATE_CONTROL_SELECT) {
			class = &aio->classes[aio->curclass];
			control = &class->controls[aio->curcontrol];
			if (control->info.type == AUDIO_MIXER_VALUE)
				draw_control(aio, control, true);
		}
		break;
	}

	draw_screen(aio);
	return 0;
}

static void
process_device_select(struct aiomixer *aio, unsigned int num_devices)
{
	unsigned int selected_device = 0;
	char device_path[16];
	int ch;

	draw_mixer_select(num_devices, selected_device);

	while ((ch = getch()) != ERR) {
		switch (ch) {
		case '\n':
			clear();
			(void)snprintf(device_path, sizeof(device_path),
			    "/dev/mixer%d", selected_device);
			open_device(aio, device_path);
			return;
		case KEY_UP:
		case 'k':
			if (selected_device > 0)
				selected_device--;
			else
				selected_device = (num_devices - 1);
			break;
		case KEY_DOWN:
		case 'j':
			if (selected_device < (num_devices - 1))
				selected_device++;
			else
				selected_device = 0;
			break;
		case '1':
			selected_device = 0;
			break;
		case '2':
			selected_device = 1;
			break;
		case '3':
			selected_device = 2;
			break;
		case '4':
			selected_device = 3;
			break;
		case '5':
			selected_device = 4;
			break;
		case '6':
			selected_device = 5;
			break;
		case '7':
			selected_device = 6;
			break;
		case '8':
			selected_device = 7;
			break;
		case '9':
			selected_device = 8;
			break;
		}
		draw_mixer_select(num_devices, selected_device);
	}
}

static void
open_device(struct aiomixer *aio, const char *device)
{
	int ch;

	if ((aio->fd = open(device, O_RDWR)) < 0)
		err(EXIT_FAILURE, "couldn't open mixer device");

	if (ioctl(aio->fd, AUDIO_GETDEV, &aio->mixerdev) < 0)
		err(EXIT_FAILURE, "AUDIO_GETDEV failed");

	aio->state = STATE_CLASS_SELECT;

	aiomixer_parse(aio);

	create_widgets(aio);

	draw_header(aio);
	select_class(aio, 0);
	draw_screen(aio);

	while ((ch = getch()) != ERR) {
		if (read_key(aio, ch) != 0)
			break;
	}
}

static __dead void
on_signal(int dummy)
{
	endwin();
	exit(0);
}

int
main(int argc, char **argv)
{
	const char *mixer_device = NULL;
	struct aiomixer *aio;
	char mixer_path[32];
	unsigned int mixer_count = 0;
	int i, fd;
	int ch;
	char *no_color = getenv("NO_COLOR");

	if ((aio = malloc(sizeof(struct aiomixer))) == NULL) {
		err(EXIT_FAILURE, "malloc failed");
	}

	while ((ch = getopt(argc, argv, "d:u")) != -1) {
		switch (ch) {
		case 'd':
			mixer_device = optarg;
			break;
		case 'u':
			aio->channels_unlocked = true;
			break;
		default:
			usage();
			break;
		}
	}

	argc -= optind;
	argv += optind;

	if (initscr() == NULL)
		err(EXIT_FAILURE, "can't initialize curses");

	(void)signal(SIGHUP, on_signal);
	(void)signal(SIGINT, on_signal);
	(void)signal(SIGTERM, on_signal);

	curs_set(0);
	keypad(stdscr, TRUE);
	cbreak();
	noecho();

	aio->use_colour = true;

	if (!has_colors())
		aio->use_colour = false;

	if (no_color != NULL && no_color[0] != '\0')
		aio->use_colour = false;

	if (aio->use_colour) {
		start_color();
		use_default_colors();
		init_pair(COLOR_CONTROL_SELECTED, COLOR_BLUE, COLOR_BLACK);
		init_pair(COLOR_LEVELS, COLOR_GREEN, COLOR_BLACK);
		init_pair(COLOR_SET_SELECTED, COLOR_BLACK, COLOR_GREEN);
		init_pair(COLOR_ENUM_ON, COLOR_WHITE, COLOR_RED);
		init_pair(COLOR_ENUM_OFF, COLOR_WHITE, COLOR_BLUE);
		init_pair(COLOR_ENUM_MISC, COLOR_BLACK, COLOR_YELLOW);
	}

	if (mixer_device != NULL) {
		open_device(aio, mixer_device);
	} else {
		for (i = 0; i < 16; ++i) {
			(void)snprintf(mixer_path, sizeof(mixer_path),
			    "/dev/mixer%d", i);
			fd = open(mixer_path, O_RDWR);
			if (fd == -1)
				break;
			close(fd);
			mixer_count++;
		}

		if (mixer_count > 1) {
			process_device_select(aio, mixer_count);
		} else {
			open_device(aio, _PATH_MIXER);
		} 
	}

	endwin();
	close(aio->fd);
	free(aio);

	return 0;
}
