/* $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;
}