/** @file
 * Map velocity values of NOTEON events between ALSA MIDI sequencer i/o ports.
 */

#include <getopt.h>
#include <math.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>

#include "alsa.h"

#define NAME "avelmap"
#define LOGS(fmt) (void)fprintf(stderr, NAME ": " fmt "\n")
#define LOGV(fmt, ...) (void)fprintf(stderr, NAME ": " fmt "\n", __VA_ARGS__)


/** Precomputed or loaded tiny mapping of velocity values. */
static unsigned char VELOCITY_MAP[128];


/** Interrupt has been received, so shutdown on next occasion. */
static volatile int SHUTDOWN = 0;
static void signal_handler(int sig) { SHUTDOWN = sig; }


/** Schedule shutdown upon INT, QUIT, or TERM. */
static int register_handlers() {
    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, &signal_handler);
    signal(SIGQUIT, &signal_handler);
    signal(SIGTERM, &signal_handler);
    return 0;
}


/** Fill in the velocity mapping for cheap repeated lookup lateron. */
static void precompute_map(double power, double factor, double offset) {
    for (size_t velocity = 0; velocity < sizeof(VELOCITY_MAP) / sizeof(*VELOCITY_MAP); ++velocity) {
        VELOCITY_MAP[velocity] = (unsigned char)round(
            fmax(0.0, fmin(127.0, pow((double)velocity / 127.0, power) * 127.0 * factor + offset)));
    }
}


/** Apply the desired change on the given sequencer event. */
static snd_seq_event_t* convert(snd_seq_event_t* ev, void* ctx) {
    const unsigned char* velocity_map = (unsigned char*)ctx;
    if (ev->type == SND_SEQ_EVENT_NOTEON && ev->data.note.velocity > 0 && ev->data.note.velocity < 128) {
        ev->data.note.velocity = velocity_map[ev->data.note.velocity];
        if (ev->data.note.velocity == 0) {
            ev->type = SND_SEQ_EVENT_NOTEOFF;
        }
    }
    return ev;
}


static int usage() {
    LOGS(
        "usage: [-h] [-d] [-p power] [-f factor] [-c offset]\n\n"
        "map velocity values of NOTEON events between ALSA MIDI sequencer i/o ports:\n"
        "velocity = (((velocity / 127)^power * 127) * factor) + offset\n\n"
        "optional arguments:\n"
        "  -h         show this help and exit\n"
        "  -d         debug dump computed velocity mapping: ]0, 127] -> [0, 127]\n"
        "  -p power   velocity power function, for example 0.5 or 2 (default: 1.0)\n"
        "  -f factor  linear scaling factor (default: 1.0)\n"
        "  -c offset  constant offset (default: 0.0)");
    return EXIT_FAILURE;
}


int main(int argc, char* const* argv) {
    unsigned debug = 0;
    double power = 1.0;
    double factor = 1.0;
    double offset = 0.0;

    int opt;
    while ((opt = getopt(argc, argv, "hdp:f:c:")) != -1) {
        switch (opt) {
            case 'd':
                debug++;
                break;
            case 'p':
                power = atof(optarg);
                break;
            case 'f':
                factor = atof(optarg);
                break;
            case 'c':
                offset = atof(optarg);
                break;
            case 'h':
            default:
                return usage();
        }
    }
    if (optind != argc) {
        return usage();
    }

    LOGV("applying power %f, factor %f, offset %f", power, factor, offset);
    precompute_map(power, factor, offset);
    if (debug) {
        for (int i = 1; i < 128; ++i) {
            LOGV("%3d -> %3d", i, VELOCITY_MAP[i]);
        }
    }

    if (register_handlers() != 0) {
        return EXIT_FAILURE;
    }

    alsa_seq_client_t client;
    if (client_open(&client, NAME) != 0) {
        LOGS("cannot open alsa sequencer client");
        return EXIT_FAILURE;
    }

    const int npfd = snd_seq_poll_descriptors_count(client.handle, POLLIN);
    struct pollfd* const pfd = (struct pollfd*)calloc((size_t)npfd, sizeof(struct pollfd));
    (void)snd_seq_poll_descriptors(client.handle, pfd, (unsigned)npfd, POLLIN);

    LOGV("running as client %d", client.client_id);
    while (!SHUTDOWN) {
        if (poll(pfd, (unsigned long)npfd, 1000) > 0) {
            client_input_loop(&client, &convert, VELOCITY_MAP);
        }
    }
    LOGS("shutting down");

    client_close(&client);
    free(pfd);

    return EXIT_SUCCESS;
}