/** @file
 * Map CONTROLLER or PGMCHANGE events between ALSA MIDI sequencer i/o ports.
 */

#include <alsa/asoundlib.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>

#include "alsa.h"


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

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))


typedef enum { NONE = 0, CHANNEL, PARAM, VALUE } wildcard_t;

typedef struct {
    snd_seq_event_type_t type;
    snd_seq_ev_ctrl_t data;
    wildcard_t wildcard;
} event_t;

typedef struct {
    event_t mod;
    event_t src;
    event_t dst;
} mapping_t;

typedef struct {
    size_t num;
    const mapping_t* curr_mod;
    mapping_t* rules;
} ruleset_t;


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


/** Check whether the given sequencer event should be processed. */
static int match(const snd_seq_event_t* ev, const event_t* src) {
    if (ev->type != src->type) return 0;
    if (src->wildcard != CHANNEL && ev->data.control.channel != src->data.channel) return 0;
    if (src->wildcard != PARAM && ev->data.control.param != src->data.param) return 0;
    if (src->wildcard != VALUE && ev->data.control.value != src->data.value) return 0;
    return 1;
}


/** Extract the source wildcard value. An int should cover all possible values. */
static int get(wildcard_t src, const snd_seq_ev_ctrl_t* ev) {
    if (src == CHANNEL)
        return (int)ev->channel;
    else if (src == PARAM)
        return (int)ev->param;
    else if (src == VALUE)
        return (int)ev->value;
    else
        return 0;
}


/** Clamp the source value to the target value range. */
static int clamp(wildcard_t dst, int value) {
    if (dst == CHANNEL)
        return MIN(MAX(value, 0), UCHAR_MAX);
    else if (dst == PARAM)
        return MIN(MAX(value, 0), INT_MAX); // UINT_MAX
    else if (dst == VALUE)
        return MIN(MAX(value, INT_MIN), INT_MAX);
    else
        return 0;
}


/** Apply the data transformation according to the configured mapping destination. */
static void map_data(const mapping_t* mapping, snd_seq_ev_ctrl_t* ev) {
    int mapped = clamp(mapping->dst.wildcard, get(mapping->src.wildcard, ev));
    ev->channel = (mapping->dst.wildcard == CHANNEL) ? (unsigned char)mapped : mapping->dst.data.channel;
    ev->param = (mapping->dst.wildcard == PARAM) ? (unsigned)mapped : mapping->dst.data.param;
    ev->value = (mapping->dst.wildcard == VALUE) ? (int)mapped : mapping->dst.data.value;
}


/** Rewrite the event according to the matching rule. */
static void map_event(const mapping_t* mapping, snd_seq_event_t* ev) {
    ev->type = mapping->dst.type;
    map_data(mapping, &ev->data.control);
}


/** Main sequencer callback to apply the transformation if a rule matches. */
static snd_seq_event_t* convert(snd_seq_event_t* ev, void* ctx) {
    ruleset_t* ruleset = (ruleset_t*)ctx;

    if (ev->type != SND_SEQ_EVENT_CONTROLLER && ev->type != SND_SEQ_EVENT_PGMCHANGE) {
        return ev;
    }

    const mapping_t* escape_mod = NULL;
    if (ruleset->curr_mod != NULL) {
        if (match(ev, &ruleset->curr_mod->mod)) {
            ev->type = SND_SEQ_EVENT_NONE;
            return ev;
        } else if (match(ev, &ruleset->curr_mod->src)) {
            map_event(ruleset->curr_mod, ev);
            return ev;
        } else {
            escape_mod = ruleset->curr_mod;
            ruleset->curr_mod = NULL;
        }
    }

    for (size_t i = 0; i < ruleset->num; ++i) {
        const mapping_t* rule = &ruleset->rules[i];
        if (escape_mod != NULL && memcmp(&escape_mod->mod, &rule->mod, sizeof(event_t)) == 0 && match(ev, &rule->src)) {
            ruleset->curr_mod = rule;
            map_event(rule, ev);
            return ev;
        } else if (match(ev, &rule->mod)) {
            ruleset->curr_mod = rule;
            ev->type = SND_SEQ_EVENT_NONE;
            return ev;
        } else if (rule->mod.type != SND_SEQ_EVENT_NONE) {
            continue;
        } else if (match(ev, &rule->src)) {
            map_event(rule, ev);
            return ev;
        }
    }

    return ev;
}


/** Parse a source/destination definition of the form: 'CTL|PGM[channel|*,param|*,value|*]' */
static int parse_event(const char* s, event_t* e) {
    memset(e, 0, sizeof(event_t));
    if (s == NULL) {
        e->type = SND_SEQ_EVENT_NONE;
        return 0;
    }
    while (*s == ' ') ++s;

    if (strncmp(s, "CTL", 3) == 0) {
        e->type = SND_SEQ_EVENT_CONTROLLER;
    } else if (strncmp(s, "PGM", 3) == 0) {
        e->type = SND_SEQ_EVENT_PGMCHANGE;
    } else {
        return -1;
    }

    if (sscanf(s + 3, "[%hhu,%u,%d]", &e->data.channel, &e->data.param, &e->data.value) == 3) {
        e->wildcard = NONE;
    } else if (sscanf(s + 3, "[*,%u,%d]", &e->data.param, &e->data.value) == 2) {
        e->wildcard = CHANNEL;
    } else if (sscanf(s + 3, "[%hhu,*,%d]", &e->data.channel, &e->data.value) == 2) {
        e->wildcard = PARAM;
    } else if (sscanf(s + 3, "[%hhu,%u,*]", &e->data.channel, &e->data.param) == 2) {
        e->wildcard = VALUE;
    } else {
        return -1;
    }

    return 0;
}


/** Parse a 'source -> destination' pair (mapping).*/
static int parse_mapping(const char* s, mapping_t* mapping) {
    const char* mod = strstr(s, "+");
    const char* sep = strstr(s, "->");
    if (mod != NULL && sep != NULL && parse_event(s, &mapping->mod) == 0 && parse_event(mod + 1, &mapping->src) == 0 && parse_event(sep + 2, &mapping->dst) == 0) {
        return 0;
    } else if (mod == NULL && sep != NULL && parse_event(NULL, &mapping->mod) == 0 && parse_event(s, &mapping->src) == 0 && parse_event(sep + 2, &mapping->dst) == 0) {
        return 0;
    } else {
        return -1;
    }
}


/** Append a newly parsed mapping to the ruleset. */
static int push_mapping(ruleset_t* ruleset, const char* arg) {
    if (*arg == '\0' || *arg == '#' || *arg == '\r' || *arg == '\n') {
        return 0;
    }
    ruleset->rules = realloc(ruleset->rules, sizeof(mapping_t) * (ruleset->num + 1));
    if (parse_mapping(arg, &ruleset->rules[ruleset->num]) == 0) {
        ruleset->num++;
        return 0;
    }
    return -1;
}


int main(int argc, char* const* argv) {
    ruleset_t ruleset = {0, NULL, NULL};
    for (int i = 1; i < argc; ++i) {
        if (push_mapping(&ruleset, argv[i]) != 0) {
            free(ruleset.rules);
            LOGS(
                "List of event mappings expected.\n\n"
                "Map CONTROLLER or PGMCHANGE events between ALSA MIDI sequencer i/o ports.\n"
                "A wildcard can be used in place of a channel, param, or value - for example:\n"
                "'CTL[0,77,*] -> PGM[9,0,*]'\n"
                "Modifier events let the configured rules apply until matched otherwise, such as:\n"
                "'CTL[*,99,0] + PGM[9,0,*] -> CTL[0,77,127]'");
            return EXIT_FAILURE;
        }
    }
    LOGV("got %zu mappings", ruleset.num);

    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, &ruleset);
        }
    }
    LOGS("shutting down");

    client_close(&client);
    free(pfd);
    free(ruleset.rules);

    return EXIT_SUCCESS;
}