/*
 * isoloir-keyfilter — filtre clavier pour la borne Isoloir.
 *
 * S'appuie sur le pilote Interception (oblitum, https://github.com/oblitum/Interception).
 *
 * Principe (par defaut, AUTO) : le clavier "natif" de la borne (clavier INTEGRE du
 * portable, non-USB) reste TOTALEMENT libre ; tout autre clavier — c.-a-d. tout
 * clavier USB externe, celui laisse au participant — est restreint aux seules
 * touches alphanumeriques. Aucun apprentissage n'est necessaire : la detection se
 * fait sur le type de peripherique (un clavier USB expose "VID_" dans son hardware
 * id ; le clavier integre non). C'est un fonctionnement "refus par defaut" : un
 * clavier externe inconnu est automatiquement verrouille.
 *
 * Cas particulier (poste FIXE ou l'operateur utilise aussi un clavier USB) : il n'y
 * a pas de clavier integre a distinguer. On peut alors EPINGLER explicitement le
 * clavier de confiance (voir --learn) : son hardware id est ecrit dans un fichier
 * de config, et il devient le seul clavier tout-autorise ; tous les autres sont
 * filtres.
 *
 * Filtrage au niveau SCANCODE (touche physique), donc valable AZERTY comme QWERTY.
 * Maj (0x2A/0x36) DOIT passer : sur AZERTY les chiffres = Maj + rangee du haut, et
 * les lettres accentuees sont sur cette meme rangee. Ctrl/Alt/Win/Tab etant bloques,
 * aucune combinaison d'evasion n'est possible depuis un clavier filtre.
 *
 * Modes :
 *   isoloir-keyfilter.exe                 filtrage resident, detection AUTO.
 *   isoloir-keyfilter.exe <cfg>           filtrage resident ; si <cfg> contient un
 *                                         hardware id, ce clavier est le clavier de
 *                                         confiance (epinglage), sinon AUTO.
 *   isoloir-keyfilter.exe --list          diagnostic : pressez une touche sur chaque
 *                                         clavier, affiche id + classement.
 *   isoloir-keyfilter.exe --learn <cfg>   epingle un clavier de confiance : pressez
 *                                         une touche sur le clavier a garder LIBRE ;
 *                                         son id est ecrit dans <cfg>.
 *
 * Compilation (Windows, SDK Interception a cote) : voir README.txt / build.ps1.
 */

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <wchar.h>
#include "interception.h"

#define HWID_BYTES 1024                 /* buffer hardware id, en octets            */
#define HWID_WCHARS (HWID_BYTES / 2)    /* idem en nombre de wchar_t                */
#define MAX_DEVICE 32                   /* borne du cache par device (kbd 1..10)    */

/* Scancodes autorises (touches physiques, hors touches etendues E0/E1). */
static int scancode_allowed(unsigned short code)
{
    if (code >= 0x02 && code <= 0x0D) return 1; /* rangee chiffres : 1 2 .. 0 - =    */
    if (code == 0x0E) return 1;                 /* Retour arriere                    */
    if (code >= 0x10 && code <= 0x1B) return 1; /* rangee haut (AZERTY A Z E R .. ^$) */
    if (code >= 0x1E && code <= 0x28) return 1; /* rangee milieu (AZERTY Q S D .. M*) */
    if (code >= 0x2B && code <= 0x35) return 1; /* rangee bas (AZERTY W X C .. ! < >) */
    if (code == 0x2A || code == 0x36) return 1; /* Maj gauche / droite               */
    if (code == 0x39) return 1;                 /* Espace                            */
    if (code == 0x3A) return 1;                 /* Verr. Maj (CapsLock)              */
    if (code == 0x45) return 1;                 /* Verr. Num (NumLock)               */
    if (code >= 0x47 && code <= 0x53) return 1; /* Pavé numérique (AZERTY 7 8 9 / * - +) */

    return 0;
}

/* Copie ASCII d'un hardware id (les ids sont ASCII : "HID\VID_046D&PID_C31C&..."). */
static void wide_to_ascii(const wchar_t *w, char *out, size_t outsz)
{
    size_t i = 0;
    for (; w[i] && i + 1 < outsz; ++i) {
        wchar_t c = w[i];
        out[i] = (c > 0 && c < 128) ? (char)c : '?';
    }
    out[i] = '\0';
}

/* Recherche de sous-chaine insensible a la casse. */
static int contains_ci(const char *hay, const char *needle)
{
    size_t nlen = strlen(needle);
    if (nlen == 0) return 0;
    for (const char *p = hay; *p; ++p) {
        size_t i = 0;
        while (i < nlen && p[i] &&
               tolower((unsigned char)p[i]) == tolower((unsigned char)needle[i])) {
            i++;
        }
        if (i == nlen) return 1;
    }
    return 0;
}

static void rstrip(char *s)
{
    size_t n = strlen(s);
    while (n && (s[n-1] == '\n' || s[n-1] == '\r' || s[n-1] == ' ' || s[n-1] == '\t')) {
        s[--n] = '\0';
    }
}

/*
 * Un clavier est "de confiance" (toutes touches autorisees) si :
 *  - un id de confiance a ete epingle (target non vide) et l'id du device le contient ;
 *  - sinon (mode AUTO) : seul le clavier VRAIMENT INTEGRE l'est. Tout clavier expose
 *    comme peripherique externe est filtre :
 *      "VID_" -> USB (id de type HID\VID_xxxx&PID_xxxx),
 *      "HID"  -> tout peripherique HID (USB ou Bluetooth),
 *      "BTH"  -> Bluetooth (enumerateurs BTHENUM / BTHLEDEVICE...).
 *    Le clavier integre (PS/2, ACPI...) ne contient aucune de ces chaines.
 *
 * Si le clavier integre est malgre tout expose en HID et se retrouve filtre a tort,
 * epinglez-le explicitement avec --learn (l'epinglage a priorite sur cette regle).
 */
static int device_trusted(const char *ascii, const char *target)
{
    if (target[0] != '\0') return contains_ci(ascii, target);
    if (contains_ci(ascii, "VID_")) return 0;
    if (contains_ci(ascii, "HID"))  return 0;
    if (contains_ci(ascii, "BTH"))  return 0;
    return 1;
}

/* Recupere l'id ASCII d'un device dans out. */
static void device_hwid(InterceptionContext ctx, InterceptionDevice device,
                        char *out, size_t outsz)
{
    wchar_t hwid[HWID_WCHARS];
    unsigned int n = interception_get_hardware_id(ctx, device, hwid, sizeof(hwid));
    if (n == 0) { out[0] = '\0'; return; }
    wide_to_ascii(hwid, out, outsz);
}

static int run_filter(const char *cfg_path)
{
    char target[HWID_BYTES];
    target[0] = '\0';
    if (cfg_path) {
        FILE *f = fopen(cfg_path, "r");
        if (f) {
            if (fgets(target, sizeof(target), f)) rstrip(target);
            fclose(f);
        }
    }

    InterceptionContext ctx = interception_create_context();
    if (!ctx) {
        fprintf(stderr, "Contexte Interception KO. Pilote installe et redemarrage effectue ?\n");
        return 1;
    }
    /* KEY_ALL : intercepter TOUTES les frappes, y compris les touches etendues
     * E0/E1 (Windows, Impr. Ecran, fleches...). Un masque DOWN|UP ne les capture
     * pas et le pilote les laisserait passer sans nous les transmettre. */
    interception_set_filter(ctx, interception_is_keyboard, INTERCEPTION_FILTER_KEY_ALL);

    if (target[0]) printf("Filtrage actif. Clavier de confiance (epingle) : %s\n", target);
    else printf("Filtrage actif. Mode AUTO : seul le clavier integre (non-USB) est libre.\n");
    fflush(stdout);

    /* Cache par device : -1 inconnu, 0 filtre, 1 de confiance. */
    signed char trusted[MAX_DEVICE];
    memset(trusted, -1, sizeof(trusted));

    InterceptionDevice device;
    InterceptionKeyStroke stroke;
    char ascii[HWID_BYTES];

    while ((device = interception_wait(ctx)) != 0) {
        if (interception_receive(ctx, device, (InterceptionStroke *)&stroke, 1) <= 0) continue;

        int is_trusted = 1;
        if (device >= 0 && device < MAX_DEVICE) {
            if (trusted[device] < 0) {
                device_hwid(ctx, device, ascii, sizeof(ascii));
                trusted[device] = device_trusted(ascii, target) ? 1 : 0;
            }
            is_trusted = trusted[device];
        }

        if (!is_trusted) {
            /* Clavier externe/filtre : on bloque TOUTES les touches etendues (E0/E1)
             * -- fleches, Windows, Impr. Ecran, Ctrl/Alt droits, pave nav... -- et
             * toute touche hors whitelist alphanumerique. */
            int extended = (stroke.state & (INTERCEPTION_KEY_E0 | INTERCEPTION_KEY_E1)) != 0;
            if (extended || !scancode_allowed(stroke.code)) {
                continue; /* non renvoyee => avalee */
            }
        }

        interception_send(ctx, device, (InterceptionStroke *)&stroke, 1);
    }

    interception_destroy_context(ctx);
    return 0;
}

static int run_list(void)
{
    InterceptionContext ctx = interception_create_context();
    if (!ctx) {
        fprintf(stderr, "Contexte Interception KO. Pilote installe et redemarrage effectue ?\n");
        return 1;
    }
    interception_set_filter(ctx, interception_is_keyboard, INTERCEPTION_FILTER_KEY_ALL);

    printf("Diagnostic. Pressez une touche sur CHAQUE clavier (Echap pour quitter).\n");
    printf("En mode AUTO : seul le clavier integre est LIBRE ; USB/Bluetooth -> FILTRE.\n\n");
    fflush(stdout);

    signed char seen[MAX_DEVICE];
    memset(seen, 0, sizeof(seen));

    InterceptionDevice device;
    InterceptionKeyStroke stroke;
    char ascii[HWID_BYTES];

    while ((device = interception_wait(ctx)) != 0) {
        if (interception_receive(ctx, device, (InterceptionStroke *)&stroke, 1) <= 0) continue;
        interception_send(ctx, device, (InterceptionStroke *)&stroke, 1); /* ne rien bloquer */

        if (stroke.code == 0x01) break; /* Echap */

        if (device >= 0 && device < MAX_DEVICE && !seen[device]) {
            seen[device] = 1;
            device_hwid(ctx, device, ascii, sizeof(ascii));
            int trust = device_trusted(ascii, "");
            printf("device %d | %s | id=%s\n",
                   device, trust ? "LIBRE (auto)" : "FILTRE (auto)",
                   ascii[0] ? ascii : "(inconnu)");
            fflush(stdout);
        }
    }

    interception_destroy_context(ctx);
    return 0;
}

static int run_learn(const char *cfg_path)
{
    InterceptionContext ctx = interception_create_context();
    if (!ctx) {
        fprintf(stderr, "Contexte Interception KO. Pilote installe et redemarrage effectue ?\n");
        return 1;
    }
    /* KEY_ALL : intercepter TOUTES les frappes, y compris les touches etendues
     * E0/E1 (Windows, Impr. Ecran, fleches...). Un masque DOWN|UP ne les capture
     * pas et le pilote les laisserait passer sans nous les transmettre. */
    interception_set_filter(ctx, interception_is_keyboard, INTERCEPTION_FILTER_KEY_ALL);

    printf("Epinglage : pressez une touche sur le clavier a garder LIBRE (operateur)...\n");
    fflush(stdout);

    InterceptionDevice device;
    InterceptionKeyStroke stroke;
    char ascii[HWID_BYTES];
    int rc = 1;

    while ((device = interception_wait(ctx)) != 0) {
        if (interception_receive(ctx, device, (InterceptionStroke *)&stroke, 1) <= 0) continue;
        interception_send(ctx, device, (InterceptionStroke *)&stroke, 1); /* ne rien bloquer */

        if (stroke.state & INTERCEPTION_KEY_UP) continue; /* attendre un appui */

        device_hwid(ctx, device, ascii, sizeof(ascii));
        if (ascii[0] == '\0') {
            fprintf(stderr, "Hardware id introuvable pour ce clavier.\n");
            break;
        }

        printf("Hardware id detecte :\n  %s\n", ascii);
        FILE *f = fopen(cfg_path, "w");
        if (!f) { fprintf(stderr, "Ecriture impossible : %s\n", cfg_path); break; }
        fprintf(f, "%s\n", ascii);
        fclose(f);
        printf("Clavier de confiance epingle dans %s\n", cfg_path);
        rc = 0;
        break;
    }

    interception_destroy_context(ctx);
    return rc;
}

int main(int argc, char **argv)
{
    if (argc == 1) return run_filter(NULL);                       /* AUTO */
    if (argc == 2 && strcmp(argv[1], "--list") == 0) return run_list();
    if (argc == 3 && strcmp(argv[1], "--learn") == 0) return run_learn(argv[2]);
    if (argc == 2 && argv[1][0] != '-') return run_filter(argv[1]); /* cfg (epinglage) ou AUTO */

    fprintf(stderr,
            "Usage :\n"
            "  %s                 filtrage (detection auto du clavier integre)\n"
            "  %s <cfg>           filtrage (clavier de confiance epingle si <cfg> renseigne)\n"
            "  %s --list          diagnostic des claviers connectes\n"
            "  %s --learn <cfg>   epingle le clavier a garder libre\n",
            argv[0], argv[0], argv[0], argv[0]);
    return 2;
}
