/*
 * Copyright © 2014 Ran Benita <ran234@gmail.com>
 * SPDX-License-Identifier: MIT
 */

#include "config.h"

#include <assert.h>
#include <getopt.h>
#include <signal.h>
#include <spawn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "xkbcommon/xkbcommon.h"
#include "xkbcommon/xkbcommon-keysyms.h"
#include "xkbcommon/xkbcommon-names.h"
#include "xkbcommon/xkbcommon-x11.h"

#include "evdev-scancodes.h"
#include "test.h"
#include "tools/tools-common.h"
#include "utils.h"
#include "utils-text.h"
#include "xvfb-wrapper.h"

/* Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB
 * keycode set (where ESC is 9). */
#define EVDEV_OFFSET 8

enum update_files {
    NO_UPDATE = 0,
    UPDATE_USING_TEST_INPUT,
    UPDATE_USING_TEST_OUTPUT
};

static enum update_files update_test_files = NO_UPDATE;


static char *
prepare_keymap(const char *original, bool strip_all_comments)
{
    if (strip_all_comments) {
        return strip_lines(original, strlen(original), "//");
    } else {
        char *tmp = uncomment(original, strlen(original), "//");
        char *ret = strip_lines(tmp, strlen(tmp), "//");
        free(tmp);
        return ret;
    }
}

/*
 * Reset keymap to `empty` on the server.
 * It seems that xkbcomp does not fully set the keymap on the server and
 * the conflicting leftovers may raise errors.
 */
static int
reset_keymap(const char* display)
{
    char *envp[] = { NULL };
    const char* setxkbmap_argv[] = {
        "setxkbmap", "-display", display,
        "-geometry", "pc",
        "-keycodes", "evdev",
        "-compat", "basic",
        "-types", "basic+numpad", /* Avoid extra types */
        "-symbols", "us",
        // "-v", "10",
        NULL
    };

    /* Launch xkbcomp */
    pid_t setxkbmap_pid;
    int ret = posix_spawnp(&setxkbmap_pid, "setxkbmap", NULL, NULL,
                           (char* const*)setxkbmap_argv, envp);
    if (ret != 0) {
        fprintf(stderr,
                "[ERROR] Cannot run setxkbmap. posix_spawnp error %d: %s\n",
                ret, strerror(ret));
        if (ret == ENOENT) {
            fprintf(stderr,
                    "[ERROR] setxkbmap may be missing. "
                    "Please install the corresponding package, "
                    "e.g. \"setxkbmap\" or \"x11-xkb-utils\".\n");
        }
        return TEST_SETUP_FAILURE;
    }
    int status;
    ret = waitpid(setxkbmap_pid, &status, 0);
    return (ret < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
        ? TEST_SETUP_FAILURE
        : EXIT_SUCCESS;
}

// TODO: unused for now
// /* Use xkbcomp to load a keymap from a file */
// static int
// run_xkbcomp_file(const char* display, xcb_connection_t *conn, int32_t device_id,
//                  const char *xkb_path)
// {
//     /* Prepare xkbcomp parameters */
//     char *envp[] = { NULL };
//     const char* xkbcomp_argv[] = {"xkbcomp", "-I", xkb_path, display, NULL};
//
//     /* Launch xkbcomp */
//     pid_t xkbcomp_pid;
//     int ret = posix_spawnp(&xkbcomp_pid, "xkbcomp", NULL, NULL,
//                            (char* const*)xkbcomp_argv, envp);
//     if (ret != 0) {
//         fprintf(stderr,
//                 "[ERROR] Cannot run xkbcomp. posix_spawnp error %d: %s\n",
//                 ret, strerror(ret));
//         if (ret == ENOENT) {
//             fprintf(stderr,
//                     "[ERROR] xkbcomp may be missing. "
//                     "Please install the corresponding package, "
//                     "e.g. \"xkbcomp\" or \"x11-xkb-utils\".\n");
//         }
//         return TEST_SETUP_FAILURE;
//     }
//
//     /* Wait for xkbcomp to complete */
//     int status;
//     ret = waitpid(xkbcomp_pid, &status, 0);
//     return (ret < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
//         ? TEST_SETUP_FAILURE
//         : EXIT_SUCCESS;
// }

enum {
    PIPE_READ  = 0,
    PIPE_WRITE = 1
};

/* Use xkbcomp to load a keymap from a string */
static int
run_xkbcomp_str(const char* display, const char *include_path, const char *keymap)
{
    int ret = EXIT_FAILURE;
    assert(keymap);

    /* Prepare xkbcomp parameters */
    char *envp[] = { NULL };
    char include_path_arg[PATH_MAX+2] = "-I";
    const char *xkbcomp_argv[] = {
        "xkbcomp", "-I" /* reset include path*/, include_path_arg,
        "-opt", "g", "-w", "10", "-" /* stdin */, display, NULL
    };
    if (include_path) {
        ret = snprintf(include_path_arg, ARRAY_SIZE(include_path_arg),
                       "-I%s", include_path);
        if (ret < 0 || ret >= (int)ARRAY_SIZE(include_path_arg)) {
            return TEST_SETUP_FAILURE;
        }
    }

    /* Prepare input */
    int stdin_pipe[2];
    posix_spawn_file_actions_t action;
    if (pipe(stdin_pipe) == -1) {
        perror("pipe");
        ret = TEST_SETUP_FAILURE;
        goto pipe_error;
    }
    if (posix_spawn_file_actions_init(&action)) {
        perror("spawn_file_actions_init error");
        goto posix_spawn_file_actions_init_error;
    }

    /* Make spawned process close unused write-end of pipe, else it will not
     * receive EOF when write-end of the pipe is closed below and it will result
     * in a deadlock. */
    if (posix_spawn_file_actions_addclose(&action, stdin_pipe[PIPE_WRITE]))
        goto posix_spawn_file_actions_error;
    /* Make spawned process replace stdin with read end of the pipe */
    if (posix_spawn_file_actions_adddup2(&action, stdin_pipe[PIPE_READ], STDIN_FILENO))
        goto posix_spawn_file_actions_error;

    /* Launch xkbcomp */
    pid_t xkbcomp_pid;
    ret = posix_spawnp(&xkbcomp_pid, "xkbcomp", &action, NULL,
                       (char* const*)xkbcomp_argv, envp);
    if (ret != 0) {
        fprintf(stderr,
                "[ERROR] Cannot run xkbcomp. posix_spawnp error %d: %s\n",
                ret, strerror(ret));
        if (ret == ENOENT) {
            fprintf(stderr,
                    "[ERROR] xkbcomp may be missing. "
                    "Please install the corresponding package, "
                    "e.g. \"xkbcomp\" or \"x11-xkb-utils\".\n");
        }
        goto posix_spawn_file_actions_init_error;
    }
    /* Close unused read-end of pipe */
    close(stdin_pipe[PIPE_READ]);
    const ssize_t count = write(stdin_pipe[PIPE_WRITE], keymap, strlen(keymap));
    /* Close write-end of the pipe, to emit EOF */
    close(stdin_pipe[PIPE_WRITE]);
    if (count == -1) {
        perror("Cannot write keymap to stdin");
        kill(xkbcomp_pid, SIGTERM);
    }

    /* Wait for xkbcomp to complete */
    int status;
    ret = waitpid(xkbcomp_pid, &status, 0);
    ret = (ret < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
        ? TEST_SETUP_FAILURE
        : EXIT_SUCCESS;
    goto cleanup;

posix_spawn_file_actions_error:
    perror("posix_spawn_file_actions_* error");
posix_spawn_file_actions_init_error:
    close(stdin_pipe[PIPE_WRITE]);
    close(stdin_pipe[PIPE_READ]);
    ret = TEST_SETUP_FAILURE;
cleanup:
    posix_spawn_file_actions_destroy(&action);
pipe_error:
    return ret;
}

// TODO: implement tests to ensure compatibility with xkbcomp
// struct compile_buffer_private {
//     const char* display;
//     xcb_connection_t *conn;
//     int32_t device_id;
// };
//
// static struct xkb_keymap *
// compile_buffer(struct xkb_context *ctx, const char *buf, size_t len,
//                void *private)
// {
//     const struct compile_buffer_private *config = private;
//     assert(buf[len - 1] == '\0');
//
//     char *include_path = test_get_path("");
//     int ret = run_xkbcomp_str(config->display, config->conn,
//                               config->device_id, include_path, buf);
//     free(include_path);
//
//     if (ret != EXIT_SUCCESS)
//         return NULL;
//
//     return xkb_x11_keymap_new_from_device(ctx, config->conn, config->device_id,
//                                           XKB_KEYMAP_COMPILE_NO_FLAGS);
// }

static int
test_keymap_roundtrip(struct xkb_context *ctx,
                      const char* display, xcb_connection_t *conn,
                      int32_t device_id, bool print_keymap, bool tweak,
                      enum xkb_keymap_serialize_flags serialize_flags,
                      const char *keymap_path)
{
    /* Get raw keymap */
    FILE *file = NULL;
    if (isempty(keymap_path) || strcmp(keymap_path, "-") == 0) {
        /* Read stdin */
        file = tools_read_stdin();
        if (!file)
            return TEST_SETUP_FAILURE;
    } else {
        /* Read file from path */
        file = fopen(keymap_path, "rb");
        if (!file) {
            perror("Unable to read file");
            return TEST_SETUP_FAILURE;
        }
    }
    char *original = read_file(keymap_path, file);
    fclose(file);
    if (!original)
        return TEST_SETUP_FAILURE;

    /* Pre-process keymap string */
    char* expected = prepare_keymap(original, tweak);
    free(original);
    if (!expected) {
        return TEST_SETUP_FAILURE;
    }

    /* Prepare X server */
    int ret = reset_keymap(display);
    if (ret != EXIT_SUCCESS) {
#ifdef __APPLE__
        /* Brew may not provide setxkbmap */
#else
        goto xkbcomp_error;
#endif
    }

    /* Load keymap into X server */
    ret = run_xkbcomp_str(display, NULL, expected);
    if (ret != EXIT_SUCCESS)
        goto xkbcomp_error;

    /* Get keymap from X server */
    struct xkb_keymap *keymap =
        xkb_x11_keymap_new_from_device(ctx, conn, device_id,
                                       XKB_KEYMAP_COMPILE_NO_FLAGS);
    assert(keymap);
    if (!keymap) {
        ret = EXIT_FAILURE;
        fprintf(stderr, "ERROR: Failed to get keymap from X server.\n");
        goto xkbcomp_error;
    }

    /* Dump keymap and compare to expected */
    char *got = xkb_keymap_get_as_string2(keymap, XKB_KEYMAP_USE_ORIGINAL_FORMAT,
                                          serialize_flags);
    if (!got) {
        ret = EXIT_FAILURE;
        fprintf(stderr, "ERROR: Failed to dump keymap.\n");
    } else {
        if (print_keymap)
            fprintf(stdout, "%s\n", got);
        if (!streq(got, expected)) {
            ret = EXIT_FAILURE;
            fprintf(stderr,
                    "ERROR: roundtrip failed. "
                    "Lengths difference: got %zu, expected %zu.\n",
                    strlen(got), strlen(expected));
        } else {
            ret = EXIT_SUCCESS;
            fprintf(stderr, "Roundtrip succeed.\n");
        }
        free(got);
    }

    xkb_keymap_unref(keymap);
xkbcomp_error:
    free(expected);
    return ret;
}

static int
init_x11_connection(const char *display, xcb_connection_t **conn_rtrn,
                    int32_t *device_id_rtrn)
{
    xcb_connection_t *conn = xcb_connect(display, NULL);
    if (xcb_connection_has_error(conn)) {
        return TEST_SETUP_FAILURE;
    }

    int ret = xkb_x11_setup_xkb_extension(conn,
                                          XKB_X11_MIN_MAJOR_XKB_VERSION,
                                          XKB_X11_MIN_MINOR_XKB_VERSION,
                                          XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS,
                                          NULL, NULL, NULL, NULL);
    if (!ret)
        goto err_xcb;

    int32_t device_id = xkb_x11_get_core_keyboard_device_id(conn);
    if (device_id == -1)
        goto err_xcb;

    *conn_rtrn = conn;
    *device_id_rtrn = device_id;
    return EXIT_SUCCESS;

err_xcb:
    xcb_disconnect(conn);
    return TEST_SETUP_FAILURE;
}

X11_TEST(test_basic)
{
    if (update_test_files != NO_UPDATE)
        return EXIT_SUCCESS;

    xcb_connection_t *conn = NULL;
    int32_t device_id = 0;
    int ret = init_x11_connection(display, &conn, &device_id);
    if (ret != EXIT_SUCCESS) {
        ret = TEST_SETUP_FAILURE;
        goto err_xcb;
    }

    struct xkb_context *ctx = test_get_context(CONTEXT_NO_FLAG);
    assert(ctx);

    static const struct {
        const char *path;
        enum xkb_keymap_serialize_flags serialize_flags;
    } keymaps[] = {
        {
            .path = "keymaps/host-no-pretty.xkb",
            .serialize_flags = TEST_KEYMAP_SERIALIZE_FLAGS
                             & ~XKB_KEYMAP_SERIALIZE_PRETTY
        },
        /* This last keymap will be used for the next tests */
        {
            .path = "keymaps/host.xkb",
            .serialize_flags = TEST_KEYMAP_SERIALIZE_FLAGS
        },
    };
    for (size_t k = 0; k < ARRAY_SIZE(keymaps); k++) {
        fprintf(stderr, "------\n*** %s: #%zu ***\n", __func__, k);
        char *keymap_path = test_get_path(keymaps[k].path);
        assert(keymap_path);

        ret = test_keymap_roundtrip(ctx, display, conn, device_id,
                                    false, false, keymaps[k].serialize_flags,
                                    keymap_path);
        assert(ret == EXIT_SUCCESS);

        free(keymap_path);
    }

    struct xkb_keymap *keymap =
        xkb_x11_keymap_new_from_device(ctx, conn, device_id,
                                       XKB_KEYMAP_COMPILE_NO_FLAGS);

    /* Check capitalization transformation */
    struct xkb_state *state =
        xkb_x11_state_new_from_device(keymap, conn, device_id);
    assert(state);
    xkb_keysym_t sym;
    sym = xkb_state_key_get_one_sym(state, KEY_A + EVDEV_OFFSET);
    assert(sym == XKB_KEY_a);
    sym = xkb_state_key_get_one_sym(state, KEY_LEFT + EVDEV_OFFSET);
    assert(sym == XKB_KEY_Left);
    const xkb_mod_index_t caps_idx =
        xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_CAPS);
    assert(caps_idx != XKB_MOD_INVALID);
    const xkb_mod_mask_t caps = UINT32_C(1) << caps_idx;
    xkb_state_update_mask(state, 0, 0, caps, 0, 0, 0);
    sym = xkb_state_key_get_one_sym(state, KEY_A + EVDEV_OFFSET);
    assert(sym == XKB_KEY_A);
    sym = xkb_state_key_get_one_sym(state, KEY_LEFT + EVDEV_OFFSET);
    assert(sym == XKB_KEY_Left);
    xkb_state_unref(state);

    xkb_keymap_unref(keymap);
    xkb_context_unref(ctx);

    ret = EXIT_SUCCESS;

err_xcb:
    if (conn != NULL)
        xcb_disconnect(conn);
    return ret;
}

struct xkbcomp_roundtrip_data {
    const char *path;
    bool tweak_comments;
    enum xkb_keymap_serialize_flags serialize_flags;
};

static int
xkbcomp_roundtrip(const char *display, void *private) {
    const struct xkbcomp_roundtrip_data *priv = private;

    xcb_connection_t *conn = NULL;
    int32_t device_id = 0;
    int ret = init_x11_connection(display, &conn, &device_id);
    if (ret != EXIT_SUCCESS) {
        ret = TEST_SETUP_FAILURE;
        goto error_xcb;
    }

    struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    if (!ctx) {
        ret = EXIT_FAILURE;
        goto error_context;
    }

    ret = test_keymap_roundtrip(ctx, display, conn, device_id, true,
                                priv->tweak_comments, priv->serialize_flags,
                                priv->path);

    xkb_context_unref(ctx);
error_context:
error_xcb:
    xcb_disconnect(conn);
    return ret;
}

static void
usage(FILE *fp, char *progname)
{
    fprintf(fp,
            "Usage: %s [--update] [--update-obtained] "
            "[--keymap KEYMAP_FILE] [--tweak] [--no-pretty] [--help]\n",
            progname);
}

int
main(int argc, char **argv) {
    test_init();

    enum options {
        OPT_UPDATE_GOLDEN_TEST_WITH_OUTPUT,
        OPT_UPDATE_GOLDEN_TEST_WITH_INPUT,
        OPT_FILE,
        OPT_TWEAK_COMMENTS,
        OPT_NO_PRETTY,
    };
    static struct option opts[] = {
        {"help",            no_argument,       0, 'h'},
        {"update-obtained", no_argument,       0, OPT_UPDATE_GOLDEN_TEST_WITH_OUTPUT},
        {"update",          no_argument,       0, OPT_UPDATE_GOLDEN_TEST_WITH_INPUT},
        {"keymap",          required_argument, 0, OPT_FILE},
        {"tweak",           no_argument,       0, OPT_TWEAK_COMMENTS},
        {"no-pretty",       no_argument,       0, OPT_NO_PRETTY},
        {0, 0, 0, 0},
    };

    bool tweak_comments = false;
    const char *path = NULL;
    enum xkb_keymap_serialize_flags serialize_flags =
        (enum xkb_keymap_serialize_flags) TEST_KEYMAP_SERIALIZE_FLAGS;

    while (1) {
        int opt;
        int option_index = 0;

        opt = getopt_long(argc, argv, "h", opts, &option_index);
        if (opt == -1)
            break;

        switch (opt) {
        case OPT_UPDATE_GOLDEN_TEST_WITH_OUTPUT:
            update_test_files = UPDATE_USING_TEST_OUTPUT;
            break;
        case OPT_UPDATE_GOLDEN_TEST_WITH_INPUT:
            update_test_files = UPDATE_USING_TEST_INPUT;
            break;
        case OPT_FILE:
            path = optarg;
            break;
        case OPT_TWEAK_COMMENTS:
            tweak_comments = optarg;
            break;
        case OPT_NO_PRETTY:
            serialize_flags &= ~XKB_KEYMAP_SERIALIZE_PRETTY;
            break;
        case 'h':
            usage(stdout, argv[0]);
            return EXIT_SUCCESS;
        default:
            usage(stderr, argv[0]);
            return EXIT_INVALID_USAGE;
        }
    }

    if (update_test_files != NO_UPDATE && path != NULL) {
        fprintf(stderr,
                "ERROR: --update* and --keymap are mutually exclusive.\n");
        usage(stderr, argv[0]);
        return EXIT_INVALID_USAGE;
    }

    if (path == NULL) {
        return x11_tests_run();
    } else {
        struct xkbcomp_roundtrip_data priv = {
            .path = path,
            .tweak_comments = tweak_comments,
            .serialize_flags = serialize_flags
        };
        return xvfb_wrapper(xkbcomp_roundtrip, &priv);
    }
}
