
#define _GNU_SOURCE

#include <stdlib.h>
#include <newt.h>
#include <termios.h>
#include <linux/kd.h>
#include <sys/poll.h>
#include <string.h>
#include <stdio.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
#include <limits.h>
#include <wchar.h>
#include <dlfcn.h>

#ifdef HAVE_LIBTEXTWRAP
#include <textwrap.h>
#endif

#include <cdebconf/constants.h>
#include <cdebconf/question.h>
#include <cdebconf/frontend.h>
#include <cdebconf/cdebconf_newt.h>

#include "detect_keys.h"
#include "newt-plugin-detect-keyboard.h"

static size_t strwidth(const char *width);
static int strtruncate (char *what, size_t maxsize);

/*
 * This struct may look like overkill; however it might make sense to
 * replace it, and press_key(), with something stateful (along the lines
 * of newt_progress_*).
 */
static struct detect_keys_frontend newt_dkf = {
    press_key: newt_press_key,
    is_key_there: newt_is_key_there,
};

static const char *
wait_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-waiting", "Waiting %d seconds ...");
}

static const char *
present_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-present", "Is there a key labelled '%s' ?");
}

static const char *
present_plain_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-present-plain", "Is there a key labelled '%s' ? Only count labels for the key on its own or with Shift, not with Alt or other modifier keys.");
}

static const char *
delay_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-delay", "30");
}

static const char *
wrong_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-wrong", "Keycode %d was not expected -- ignored.\nNo need to press Shift or other modifier keys.");
}

static const char *
keypress_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/key-press", "Please press one of these keys: %s");
}

static const char *
yes_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/button-yes", "Yes");
}

static const char *
no_text(struct frontend *obj)
{
    return question_get_text(obj, "debconf/button-no", "No");
}

static int
newt_press_key(struct frontend *obj, char *syms, int *codes, int *result)
{
    int width = 80, win_width;
#ifdef HAVE_LIBTEXTWRAP
    int flags = 0;
#else
    int flags = NEWT_FLAG_WRAP;
#endif
    int state = atoi(delay_text(obj))+1; /* timer -- 0=done, -1=error */
    newtComponent form, prompt, prompt2, delay;
    char *request, *dtitle;
    struct termios old,new, old0,new0;
    int oldkbmode;
    unsigned char buf[64];
    int i, len, j, fd;
    struct pollfd pfd = { fd:0, events:POLLIN };
    int ret = DC_NOTOK;
    int last_code = -1;

    newtGetScreenSize(&width, NULL);
    win_width = width-7;
    newtCenteredWindow(win_width, 9, obj->title);
    prompt = newtTextbox(TEXT_PADDING, 2, win_width-2*TEXT_PADDING, 2, flags);
    assert(prompt);
    prompt2 = newtTextbox(TEXT_PADDING, 4, win_width-2*TEXT_PADDING, 2, flags);
    assert(prompt2);
    delay = newtTextbox(TEXT_PADDING, 6, win_width-2*TEXT_PADDING, 2, flags);
    assert(delay);
    form = cdebconf_newt_create_form(NULL);
    assert(form);

    asprintf(&request, keypress_text(obj), syms);
    newtTextboxSetText(prompt, request);
    free(request);

    newtFormAddComponents(form, prompt, prompt2, delay, NULL);

    dtitle=malloc(1000);
    sprintf(dtitle, "Do %s --", syms);
    for(j=0; codes[j] != -1; j++)
	sprintf(dtitle+strlen(dtitle)," %02x",codes[j]);
    free(dtitle);
    
    /* Set the console to emit "standard" Linux keycodes. We don't know
     * what mode console and/or standard input is in at the moment, so
     * take care to save+restore everything.
     *
     * Note that the actual reading is still done through standard
     * input, via bterm.
     */

    fd = open("/dev/console", O_RDWR);
    if (fd == -1)
	fd = 0;

    if (tcgetattr(0, &old0) == -1)
	state = -1;
    if (tcgetattr(0, &new0) == -1)
	state = -1;
    if (tcgetattr(fd, &old) == -1)
	state = -1;
    if (tcgetattr(fd, &new) == -1)
	state = -1;
    if (ioctl(fd, KDGKBMODE, &oldkbmode))
	state = -1;

    new.c_lflag &= ~ (ICANON | ECHO | ISIG);
    new.c_iflag = 0;
    new.c_cc[VMIN] = 0;
    new.c_cc[VTIME] = 0;
    if (state > 0 && tcsetattr(fd, TCSAFLUSH, &new) == -1)
	state = -1;
    if (state > 0 && ioctl(fd, KDSKBMODE, K_MEDIUMRAW))
	state = -1;

    new0.c_lflag &= ~ (ICANON | ECHO | ISIG);
    new0.c_iflag = 0;
    new0.c_cc[VMIN] = 0;
    new0.c_cc[VTIME] = 0;
    if (state > 0 && tcsetattr(0, TCSAFLUSH, &new0) == -1)
	state = -1;

    /* Now do the actual work.
     * state=0: found a keycode that's OK.
     */
    while(state > 0) {
	state -= 1;
	/* Annoy people every ten seconds */
	if(state<10 || !(state%10)) {
	    asprintf(&dtitle, wait_text(obj), state);
	    newtTextboxSetText(delay, dtitle);
	    free(dtitle);
	}
	newtDrawForm(form);
	newtRefresh();

	switch(poll(&pfd,1,1000)) {
	case 0: /* timed out */
	    break;
	case 1:
	    len = read(0, buf, sizeof(buf));
	    if (len<1) break;

	    for(i=0; state && i<len; i++) {
		if (buf[i] & 0x80) /* key release */
		    continue;

		last_code = buf[i];
		for(j=0;state && codes[j] != -1;j++) {
		    if (buf[i] == codes[j]) {
			state = 0;
			*result = buf[i];
			ret = DC_OK;
		    }
		}
	    }
	    if (state && state < atoi(delay_text(obj))/2)
		state = atoi(delay_text(obj))/2;
	    if (state && last_code != -1) {
	    	asprintf(&dtitle, wrong_text(obj), last_code);
	    	newtTextboxSetText(prompt2, dtitle);
	    	free(dtitle);
		last_code = -1;
	    }
	    break;
	default: /* error */
	    state = -1;
	    break;
	}
    }
    ioctl(fd, KDSKBMODE, oldkbmode);
    tcsetattr(fd, 0, &old);
    tcsetattr(0, 0, &old0);
    
    if (fd) close(fd);

    newtFormDestroy(form);
    newtPopWindow();
    return ret;
}

/*
 * Function: newt_is_key_there
 * Input: struct frontend *obj - frontend object
 *        char *syn - symbol to check for
 *        bool plain - if true, check for plain/shift only
 * Output: int - DC_OK, DC_NOTOK
 *         bool *result - user's answer
 * Description: handler which asks whether a key is present
 * Assumptions: none
 */
static int
newt_is_key_there(struct frontend *obj, char *sym, bool plain, bool *result)
{
    newtComponent form, bYes, bNo, textbox, cRet;
    int width = 80, height = 24;
    int win_width, win_height = -1, t_height, t_width;
    int t_width_scroll = 0, t_width_title, t_width_buttons;
    int ret;
#ifdef HAVE_LIBTEXTWRAP
    textwrap_t tw;
    char *wrappedtext;
    int flags = 0;
#else
    int flags = NEWT_FLAG_WRAP;
#endif
    char *full_description;

    asprintf(&full_description, plain ? present_plain_text(obj) : present_text(obj), sym);

    newtGetScreenSize(&width, &height);
    win_width = width-7;
    strtruncate(obj->title, win_width-9);
#ifdef HAVE_LIBTEXTWRAP
    textwrap_init(&tw);
    textwrap_columns(&tw, win_width - 2 - 2*TEXT_PADDING);
    wrappedtext = textwrap(&tw, full_description);
    free(full_description);
    full_description = wrappedtext;
#endif
    if (full_description != NULL) {
      t_height = cdebconf_newt_get_text_height(full_description, win_width);
    }
    else
        t_height = 0;
    if (t_height + 4 <= height-5)
        win_height = t_height + 4;
    else {
        win_height = height-5;
        flags |= NEWT_FLAG_SCROLL;
        t_width_scroll = 2;
    }
    t_height = win_height - 4;
    t_width = cdebconf_newt_get_text_width(full_description);
    t_width_buttons = 2*BUTTON_PADDING;
    t_width_buttons += cdebconf_newt_get_text_width(yes_text(obj)) + 3;
    t_width_buttons += cdebconf_newt_get_text_width(no_text(obj)) + 3;
    if (t_width_buttons > t_width)
        t_width = t_width_buttons;
    if (win_width > t_width + 2*TEXT_PADDING + t_width_scroll)
        win_width = t_width + 2*TEXT_PADDING + t_width_scroll;
    t_width_title = cdebconf_newt_get_text_width(obj->title);
    if (t_width_title > win_width)
        win_width = t_width_title;

    cdebconf_newt_create_window(win_width, win_height, obj->title, NULL);
    form = cdebconf_newt_create_form(NULL);
    textbox = newtTextbox(TEXT_PADDING, 1, t_width, t_height, flags);
    assert(textbox);
    if (full_description != NULL)
        newtTextboxSetText(textbox, full_description);
    free(full_description);

    bYes = newtCompactButton(TEXT_PADDING + BUTTON_PADDING - 1, win_height-2, yes_text(obj));
    bNo = newtCompactButton(win_width - TEXT_PADDING - BUTTON_PADDING - strwidth(no_text(obj)) - 3, win_height-2, no_text(obj));
    newtFormAddComponents(form, textbox, bYes, bNo, NULL);
    
    newtFormSetCurrent(form, bNo);
    cRet = newtRunForm(form);
    if (cRet == bYes) {
        ret = DC_OK;
        *result = true;
    } else if (cRet == bNo) {
        ret = DC_OK;
        *result = false;
    } else
        ret = DC_NOTOK;
    newtFormDestroy(form);
    newtPopWindow();
    return ret;
}


int
cdebconf_newt_handler_detect_keyboard(struct frontend *obj, struct question *q)
{
    char *result, *filename;
    int res;

    filename = q_get_choices(obj, q);
	if (!filename || !*filename)
		return DC_NOTOK;
    res = detect_keys(obj, &newt_dkf, filename, &result);
    if (res != DC_OK)
	return res;
    question_setvalue(q, result);
    return DC_OK;
}

int
strtruncate (char *what, size_t maxsize)
{
    size_t pos;
    int k;
    char *p;
    wchar_t c;

    if (strwidth(what) <= maxsize)
        return 0;

    /*  Replace end of string with ellipsis "..."; as the last character
     *  may have a double width, stops 4 characters before the end
     */
    pos = 0;
    for (p = what; (k = mbtowc (&c, p, MB_LEN_MAX)) > 0 && pos < maxsize-5; p += k)
        pos += wcwidth (c);

    for (k=0; k < 3; k++, p++)
        *p = '.';
    *p = '\0';
    return 1;
}  

size_t
strwidth (const char *what)
{
    size_t res;
    int k;
    const char *p;
    wchar_t c;

    for (res = 0, p = what ; (k = mbtowc (&c, p, MB_LEN_MAX)) > 0 ; p += k)
        res += wcwidth (c);

    return res;
}  
