/*
 * GTK implementation of a GUI password/passphrase prompt.
 */

#include <assert.h>
#include <time.h>
#include <stdlib.h>

#include <unistd.h>

#include <gtk/gtk.h>
#include <gdk/gdk.h>
#if !GTK_CHECK_VERSION(3,0,0)
#include <gdk/gdkkeysyms.h>
#endif

#include "defs.h"
#include "unifont.h"
#include "gtkcompat.h"
#include "gtkmisc.h"

#include "putty.h"
#include "ssh.h"
#include "misc.h"

#define N_DRAWING_AREAS 3

struct drawing_area_ctx {
    GtkWidget *area;
#ifndef DRAW_DEFAULT_CAIRO
    GdkColor *cols;
#endif
    int width, height;
    enum { NOT_CURRENT, CURRENT, GREYED_OUT } state;
};

struct askpass_ctx {
    GtkWidget *dialog, *promptlabel;
    struct drawing_area_ctx drawingareas[N_DRAWING_AREAS];
    int active_area;
#if GTK_CHECK_VERSION(2,0,0)
    GtkIMContext *imc;
#endif
#ifndef DRAW_DEFAULT_CAIRO
    GdkColormap *colmap;
    GdkColor cols[3];
#endif
    char *error_message;               /* if we finish without a passphrase */
    strbuf *passphrase;                /* if we finish with one */
#if GTK_CHECK_VERSION(3,20,0)
    GdkSeat *seat;                     /* for gdk_seat_grab */
#elif GTK_CHECK_VERSION(3,0,0)
    GdkDevice *keyboard;               /* for gdk_device_grab */
#endif

    int nattempts;
};

static prng *keypress_prng = NULL;
static void feed_keypress_prng(void *data, int size)
{
    put_data(keypress_prng, data, size);
}
void random_add_noise(NoiseSourceId source, const void *noise, int length)
{
    if (keypress_prng)
        prng_add_entropy(keypress_prng, source, make_ptrlen(noise, length));
}
static void setup_keypress_prng(void)
{
    keypress_prng = prng_new(&ssh_sha256);
    prng_seed_begin(keypress_prng);
    noise_get_heavy(feed_keypress_prng);
    prng_seed_finish(keypress_prng);
}
static void cleanup_keypress_prng(void)
{
    prng_free(keypress_prng);
}
static uint64_t keypress_prng_value(void)
{
    /*
     * Don't actually put the passphrase keystrokes themselves into
     * the PRNG; that doesn't seem like the course of wisdom when
     * that's precisely what the information displayed on the screen
     * is trying _not_ to be correlated to.
     */
    noise_ultralight(NOISE_SOURCE_KEY, 0);
    uint8_t data[8];
    prng_read(keypress_prng, data, 8);
    return GET_64BIT_MSB_FIRST(data);
}
static int choose_new_area(int prev_area)
{
    int reduced = keypress_prng_value() % (N_DRAWING_AREAS - 1);
    return (prev_area + 1 + reduced) % N_DRAWING_AREAS;
}

static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
{
    int new_active = choose_new_area(ctx->active_area);
    ctx->drawingareas[ctx->active_area].state = NOT_CURRENT;
    gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
    ctx->drawingareas[new_active].state = CURRENT;
    gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
    ctx->active_area = new_active;
}

static size_t last_char_start(struct askpass_ctx *ctx)
{
    /*
     * GTK always encodes in UTF-8, so we can do this in a fixed way.
     */
    assert(ctx->passphrase->len > 0);
    size_t i = ctx->passphrase->len - 1;
    while ((unsigned)(ctx->passphrase->u[i] - 0x80) < 0x40) {
        if (i == 0)
            break;
        i--;
    }
    return i;
}

static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str)
{
    put_datapl(ctx->passphrase, ptrlen_from_asciz(str));
    visually_acknowledge_keypress(ctx);
}

static void cancel_askpass(struct askpass_ctx *ctx, const char *msg)
{
    strbuf_free(ctx->passphrase);
    ctx->passphrase = NULL;
    ctx->error_message = dupstr(msg);
    gtk_main_quit();
}

static gboolean askpass_dialog_closed(GtkWidget *widget, GdkEvent *event,
                                      gpointer data)
{
    struct askpass_ctx *ctx = (struct askpass_ctx *)data;
    cancel_askpass(ctx, "passphrase input cancelled");
    /* Don't destroy dialog yet, so gtk_askpass_cleanup() can do its work */
    return true;
}

static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
{
    struct askpass_ctx *ctx = (struct askpass_ctx *)data;

    if (event->keyval == GDK_KEY_Return &&
        event->type == GDK_KEY_PRESS) {
        gtk_main_quit();
    } else if (event->keyval == GDK_KEY_Escape &&
               event->type == GDK_KEY_PRESS) {
        cancel_askpass(ctx, "passphrase input cancelled");
    } else {
#if GTK_CHECK_VERSION(2,0,0)
        if (gtk_im_context_filter_keypress(ctx->imc, event))
            return true;
#endif

        if (event->type == GDK_KEY_PRESS) {
            if (!strcmp(event->string, "\x15")) {
                /* Ctrl-U. Wipe out the whole line */
                strbuf_clear(ctx->passphrase);
                visually_acknowledge_keypress(ctx);
            } else if (!strcmp(event->string, "\x17")) {
                /* Ctrl-W. Delete back to the last space->nonspace
                 * boundary. We interpret 'space' in a really simple
                 * way (mimicking terminal drivers), and don't attempt
                 * to second-guess exciting Unicode space
                 * characters. */
                while (ctx->passphrase->len > 0) {
                    char deleted, prior;
                    size_t newlen = last_char_start(ctx);
                    deleted = ctx->passphrase->s[newlen];
                    strbuf_shrink_to(ctx->passphrase, newlen);
                    prior = (ctx->passphrase->len == 0 ? ' ' :
                             ctx->passphrase->s[ctx->passphrase->len-1]);
                    if (!g_ascii_isspace(deleted) && g_ascii_isspace(prior))
                        break;
                }
                visually_acknowledge_keypress(ctx);
            } else if (event->keyval == GDK_KEY_BackSpace) {
                /* Backspace. Delete one character. */
                if (ctx->passphrase->len > 0)
                    strbuf_shrink_to(ctx->passphrase, last_char_start(ctx));
                visually_acknowledge_keypress(ctx);
#if !GTK_CHECK_VERSION(2,0,0)
            } else if (event->string[0]) {
                add_text_to_passphrase(ctx, event->string);
#endif
            }
        }
    }
    return true;
}

#if GTK_CHECK_VERSION(2,0,0)
static void input_method_commit_event(GtkIMContext *imc, gchar *str,
                                      gpointer data)
{
    struct askpass_ctx *ctx = (struct askpass_ctx *)data;
    add_text_to_passphrase(ctx, str);
}
#endif

static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
                           gpointer data)
{
    struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
    ctx->width = event->width;
    ctx->height = event->height;
    gtk_widget_queue_draw(widget);
    return true;
}

#ifdef DRAW_DEFAULT_CAIRO
static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
{
    double rgbval = (ctx->state == CURRENT ? 0 :
                     ctx->state == NOT_CURRENT ? 1 : 0.5);
    cairo_set_source_rgb(cr, rgbval, rgbval, rgbval);
    cairo_paint(cr);
}
#else
static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
{
    GdkGC *gc = gdk_gc_new(win);
    gdk_gc_set_foreground(gc, &ctx->cols[ctx->state]);
    gdk_draw_rectangle(win, gc, true, 0, 0, ctx->width, ctx->height);
    gdk_gc_unref(gc);
}
#endif

#if GTK_CHECK_VERSION(3,0,0)
static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
{
    struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
    askpass_redraw_cairo(cr, ctx);
    return true;
}
#else
static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
                        gpointer data)
{
    struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;

#ifdef DRAW_DEFAULT_CAIRO
    cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(ctx->area));
    askpass_redraw_cairo(cr, ctx);
    cairo_destroy(cr);
#else
    askpass_redraw_gdk(gtk_widget_get_window(ctx->area), ctx);
#endif

    return true;
}
#endif

static gboolean try_grab_keyboard(gpointer vctx)
{
    struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;
    int i, ret;

#if GTK_CHECK_VERSION(3,20,0)
    /*
     * Grabbing the keyboard in GTK 3.20 requires the new notion of
     * GdkSeat.
     */
    GdkSeat *seat;
    GdkWindow *gdkw = gtk_widget_get_window(ctx->dialog);
    if (!GDK_IS_WINDOW(gdkw) || !gdk_window_is_visible(gdkw))
        goto fail;

    seat = gdk_display_get_default_seat(
        gtk_widget_get_display(ctx->dialog));
    if (!seat)
        goto fail;

    ctx->seat = seat;
    ret = gdk_seat_grab(seat, gdkw, GDK_SEAT_CAPABILITY_KEYBOARD,
                        true, NULL, NULL, NULL, NULL);

    /*
     * For some reason GDK 3.22 hides the GDK window as a side effect
     * of a failed grab. I've no idea why. But if we're going to retry
     * the grab, then we need to unhide it again or else we'll just
     * get GDK_GRAB_NOT_VIEWABLE on every subsequent attempt.
     */
    if (ret != GDK_GRAB_SUCCESS)
        gdk_window_show(gdkw);

#elif GTK_CHECK_VERSION(3,0,0)
    /*
     * And it has to be done differently again prior to GTK 3.20.
     */
    GdkDeviceManager *dm;
    GdkDevice *pointer, *keyboard;

    dm = gdk_display_get_device_manager(
        gtk_widget_get_display(ctx->dialog));
    if (!dm)
        goto fail;

    pointer = gdk_device_manager_get_client_pointer(dm);
    if (!pointer)
        goto fail;
    keyboard = gdk_device_get_associated_device(pointer);
    if (!keyboard)
        goto fail;
    if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
        goto fail;

    ctx->keyboard = keyboard;
    ret = gdk_device_grab(ctx->keyboard,
                          gtk_widget_get_window(ctx->dialog),
                          GDK_OWNERSHIP_NONE,
                          true,
                          GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
                          NULL,
                          GDK_CURRENT_TIME);
#else
    /*
     * It's much simpler in GTK 1 and 2!
     */
    ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
                            false, GDK_CURRENT_TIME);
#endif
    if (ret != GDK_GRAB_SUCCESS)
        goto fail;

    /*
     * Now that we've got the keyboard grab, connect up our keyboard
     * handlers.
     */
#if GTK_CHECK_VERSION(2,0,0)
    g_signal_connect(G_OBJECT(ctx->imc), "commit",
                     G_CALLBACK(input_method_commit_event), ctx);
#endif
    g_signal_connect(G_OBJECT(ctx->dialog), "key_press_event",
                     G_CALLBACK(key_event), ctx);
    g_signal_connect(G_OBJECT(ctx->dialog), "key_release_event",
                     G_CALLBACK(key_event), ctx);
#if GTK_CHECK_VERSION(2,0,0)
    gtk_im_context_set_client_window(ctx->imc,
                                     gtk_widget_get_window(ctx->dialog));
#endif

    /*
     * And repaint the key-acknowledgment drawing areas as not greyed
     * out.
     */
    ctx->active_area = keypress_prng_value() % N_DRAWING_AREAS;
    for (i = 0; i < N_DRAWING_AREAS; i++) {
        ctx->drawingareas[i].state =
            (i == ctx->active_area ? CURRENT : NOT_CURRENT);
        gtk_widget_queue_draw(ctx->drawingareas[i].area);
    }

    return false;

  fail:
    /*
     * If we didn't get the grab, reschedule ourself on a timer to try
     * again later.
     *
     * We have to do this rather than just trying once, because there
     * is at least one important situation in which the grab may fail
     * the first time: any user who is launching an add-key operation
     * off some kind of window manager hotkey will almost by
     * definition be running this script with a keyboard grab already
     * active, namely the one-key grab that the WM (or whatever) uses
     * to detect presses of the hotkey. So at the very least we have
     * to give the user time to release that key.
     */
    if (++ctx->nattempts >= 4) {
        cancel_askpass(ctx, "unable to grab keyboard after 5 seconds");
    } else {
        g_timeout_add(1000/8, try_grab_keyboard, ctx);
    }
    return false;
}

void realize(GtkWidget *widget, gpointer vctx)
{
    struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;

    gtk_grab_add(ctx->dialog);

    /*
     * Schedule the first attempt at the keyboard grab.
     */
    ctx->nattempts = 0;
#if GTK_CHECK_VERSION(3,20,0)
    ctx->seat = NULL;
#elif GTK_CHECK_VERSION(3,0,0)
    ctx->keyboard = NULL;
#endif

    g_idle_add(try_grab_keyboard, ctx);
}

static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
                                     const char *window_title,
                                     const char *prompt_text)
{
    int i;
    GtkBox *action_area;

    ctx->passphrase = strbuf_new_nm();

    /*
     * Create widgets.
     */
    ctx->dialog = our_dialog_new();
    gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title);
    gtk_window_set_position(GTK_WINDOW(ctx->dialog), GTK_WIN_POS_CENTER);
    g_signal_connect(G_OBJECT(ctx->dialog), "delete-event",
                     G_CALLBACK(askpass_dialog_closed), ctx);
    ctx->promptlabel = gtk_label_new(prompt_text);
    align_label_left(GTK_LABEL(ctx->promptlabel));
    gtk_widget_show(ctx->promptlabel);
    gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), true);
#if GTK_CHECK_VERSION(3,0,0)
    gtk_label_set_width_chars(GTK_LABEL(ctx->promptlabel), 48);
#endif
    int margin = string_width("MM");
#if GTK_CHECK_VERSION(3,12,0)
    gtk_widget_set_margin_start(ctx->promptlabel, margin);
    gtk_widget_set_margin_end(ctx->promptlabel, margin);
#else
    gtk_misc_set_padding(GTK_MISC(ctx->promptlabel), margin, 0);
#endif
    our_dialog_add_to_content_area(GTK_WINDOW(ctx->dialog),
                                   ctx->promptlabel, true, true, 0);
#if GTK_CHECK_VERSION(2,0,0)
    ctx->imc = gtk_im_multicontext_new();
#endif
#ifndef DRAW_DEFAULT_CAIRO
    {
        gboolean success[2];
        ctx->colmap = gdk_colormap_get_system();
        ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
        ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
        ctx->cols[2].red = ctx->cols[2].green = ctx->cols[2].blue = 0x8000;
        gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
                                  false, true, success);
        if (!success[0] || !success[1])
            return "unable to allocate colours";
    }
#endif

    action_area = our_dialog_make_action_hbox(GTK_WINDOW(ctx->dialog));

    for (i = 0; i < N_DRAWING_AREAS; i++) {
        ctx->drawingareas[i].area = gtk_drawing_area_new();
#ifndef DRAW_DEFAULT_CAIRO
        ctx->drawingareas[i].cols = ctx->cols;
#endif
        ctx->drawingareas[i].state = GREYED_OUT;
        ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
        /* It would be nice to choose this size in some more
         * context-sensitive way, like measuring the size of some
         * piece of template text. */
        gtk_widget_set_size_request(ctx->drawingareas[i].area, 32, 32);
        gtk_box_pack_end(action_area, ctx->drawingareas[i].area,
                         true, true, 5);
        g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
                         "configure_event",
                         G_CALLBACK(configure_area),
                         &ctx->drawingareas[i]);
#if GTK_CHECK_VERSION(3,0,0)
        g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
                         "draw",
                         G_CALLBACK(draw_area),
                         &ctx->drawingareas[i]);
#else
        g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
                         "expose_event",
                         G_CALLBACK(expose_area),
                         &ctx->drawingareas[i]);
#endif

#if GTK_CHECK_VERSION(3,0,0)
        g_object_set(G_OBJECT(ctx->drawingareas[i].area),
                     "margin-bottom", 8, (const char *)NULL);
#endif

        gtk_widget_show(ctx->drawingareas[i].area);
    }
    ctx->active_area = -1;

    /*
     * Arrange to receive key events. We don't really need to worry
     * from a UI perspective about which widget gets the events, as
     * long as we know which it is so we can catch them. So we'll pick
     * the prompt label at random, and we'll use gtk_grab_add to
     * ensure key events go to it.
     */
    gtk_widget_set_sensitive(ctx->dialog, true);

#if GTK_CHECK_VERSION(2,0,0)
    gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), true);
#endif

    /*
     * Wait for the key-receiving widget to actually be created, in
     * order to call gtk_grab_add on it.
     */
    g_signal_connect(G_OBJECT(ctx->dialog), "realize",
                     G_CALLBACK(realize), ctx);

    /*
     * Show the window.
     */
    gtk_widget_show(ctx->dialog);

    return NULL;
}

static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
{
#if GTK_CHECK_VERSION(3,20,0)
    if (ctx->seat)
        gdk_seat_ungrab(ctx->seat);
#elif GTK_CHECK_VERSION(3,0,0)
    if (ctx->keyboard)
        gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
#else
    gdk_keyboard_ungrab(GDK_CURRENT_TIME);
#endif
    gtk_grab_remove(ctx->promptlabel);

    gtk_widget_destroy(ctx->dialog);
}

static bool setup_gtk(const char *display)
{
    static bool gtk_initialised = false;
    int argc;
    char *real_argv[3];
    char **argv = real_argv;
    bool ret;

    if (gtk_initialised)
        return true;

    argc = 0;
    argv[argc++] = dupstr("dummy");
    argv[argc++] = dupprintf("--display=%s", display);
    argv[argc] = NULL;
    ret = gtk_init_check(&argc, &argv);
    while (argc > 0)
        sfree(argv[--argc]);

    gtk_initialised = ret;
    return ret;
}

const bool buildinfo_gtk_relevant = true;

char *gtk_askpass_main(const char *display, const char *wintitle,
                       const char *prompt, bool *success)
{
    struct askpass_ctx ctx[1];
    const char *err;

    ctx->passphrase = NULL;
    ctx->error_message = NULL;

    /* In case gtk_init hasn't been called yet by the program */
    if (!setup_gtk(display)) {
        *success = false;
        return dupstr("unable to initialise GTK");
    }

    if ((err = gtk_askpass_setup(ctx, wintitle, prompt)) != NULL) {
        *success = false;
        return dupprintf("%s", err);
    }
    setup_keypress_prng();
    gtk_main();
    cleanup_keypress_prng();
    gtk_askpass_cleanup(ctx);

    if (ctx->passphrase) {
        *success = true;
        return strbuf_to_str(ctx->passphrase);
    } else {
        *success = false;
        return ctx->error_message;
    }
}

#ifdef TEST_ASKPASS
void modalfatalbox(const char *p, ...)
{
    va_list ap;
    fprintf(stderr, "FATAL ERROR: ");
    va_start(ap, p);
    vfprintf(stderr, p, ap);
    va_end(ap);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char **argv)
{
    bool success;
    int exitcode;
    char *ret;

    enable_dit(); /* maybe overkill, but we _are_ handling a secret */

    gtk_init(&argc, &argv);

    if (argc != 2) {
        success = false;
        ret = dupprintf("usage: %s <prompt text>", argv[0]);
    } else {
        ret = gtk_askpass_main(NULL, "Enter passphrase", argv[1], &success);
    }

    if (!success) {
        fputs(ret, stderr);
        fputc('\n', stderr);
        exitcode = 1;
    } else {
        fputs(ret, stdout);
        fputc('\n', stdout);
        exitcode = 0;
    }

    smemclr(ret, strlen(ret));
    return exitcode;
}
#endif
