#include "k5-int.h"
#if (!defined(_WIN32) || (defined(_WIN32) && defined(__CYGWIN32__))) && !defined(macintosh)
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <limits.h>
/* Is vxworks broken w.r.t. termios? --tlyu */
#ifdef __vxworks
#define ECHO_PASSWORD
#endif

#include <termios.h>

static void	catch_signals(void);
static void	restore_signals(void);
static krb5_sigtype	intrfunc(int sig);

static krb5_error_code	setup_tty(FILE*, int);
static krb5_error_code	restore_tty(FILE*);

#ifdef POSIX_SIGNALS
static struct sigaction osigint;
#else
static krb5_sigtype (*osigint)();
#endif

static volatile int got_int;

krb5_error_code KRB5_CALLCONV
krb5_prompter_posix(
    krb5_context	context,
    void		*data,
    const char		*name,
    const char		*banner,
    int			num_prompts,
    krb5_prompt		prompts[])
{
    int		fd, i, scratchchar;
    FILE	*fp;
    char	*retp;
    krb5_error_code	errcode;

    errcode = KRB5_LIBOS_CANTREADPWD;

    if (name) {
	fputs(name, stdout);
	fputs("\n", stdout);
    }
    if (banner) {
       fputs(banner, stdout);
       fputs("\n", stdout);
    }

    /*
     * Get a non-buffered stream on stdin.
     */
    fp = NULL;
    fd = dup(STDIN_FILENO);
    if (fd < 0)
	return KRB5_LIBOS_CANTREADPWD;
    fp = fdopen(fd, "r");
    if (fp == NULL)
	goto cleanup;
    if (setvbuf(fp, NULL, _IONBF, 0))
	goto cleanup;

    for (i = 0; i < num_prompts; i++) {
	errcode = KRB5_LIBOS_CANTREADPWD;
	/* fgets() takes int, but krb5_data.length is unsigned. */
	if (prompts[i].reply->length > INT_MAX)
	    goto cleanup;

	errcode = setup_tty(fp, prompts[i].hidden);
	if (errcode)
	    break;

	/* put out the prompt */
	(void)fputs(prompts[i].prompt, stdout);
	(void)fputs(": ", stdout);
	(void)fflush(stdout);
	(void)memset(prompts[i].reply->data, 0, prompts[i].reply->length);

	got_int = 0;
	retp = fgets(prompts[i].reply->data, (int)prompts[i].reply->length,
		     fp);
	if (prompts[i].hidden)
	    putchar('\n');
	if (retp == NULL) {
	    if (got_int)
		errcode = KRB5_LIBOS_PWDINTR;
	    else
		errcode = KRB5_LIBOS_CANTREADPWD;
	    restore_tty(fp);
	    break;
	}

	/* replace newline with null */
	retp = strchr(prompts[i].reply->data, '\n');
	if (retp != NULL)
	    *retp = '\0';
	else {
	    /* flush rest of input line */
	    do {
		scratchchar = getc(fp);
	    } while (scratchchar != EOF && scratchchar != '\n');
	}

	errcode = restore_tty(fp);
	if (errcode)
	    break;
	prompts[i].reply->length = strlen(prompts[i].reply->data);
    }
cleanup:
    if (fp != NULL)
	fclose(fp);
    else if (fd >= 0)
	close(fd);

    return errcode;
}

static krb5_sigtype intrfunc(int sig)
{
    got_int = 1;
}

static void
catch_signals(void)
{
#ifdef POSIX_SIGNALS
    struct sigaction sa;

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = intrfunc;
    sigaction(SIGINT, &sa, &osigint);
#else
    osigint = signal(SIGINT, intrfunc);
#endif
}

static void
restore_signals(void)
{
#ifdef POSIX_SIGNALS
    sigaction(SIGINT, &osigint, NULL);
#else
    signal(SIGINT, osigint);
#endif
}

static struct termios saveparm;

static krb5_error_code
setup_tty(FILE *fp, int hidden)
{
    krb5_error_code	ret;
    int			fd;
    struct termios	tparm;

    ret = KRB5_LIBOS_CANTREADPWD;
    catch_signals();
    fd = fileno(fp);
    do {
	if (!isatty(fd)) {
	    ret = 0;
	    break;
	}
	if (tcgetattr(fd, &tparm) < 0)
	    break;
	saveparm = tparm;
#ifndef ECHO_PASSWORD
	if (hidden)
	    tparm.c_lflag &= ~(ECHO|ECHONL);
#endif
	tparm.c_lflag |= ISIG|ICANON;
	if (tcsetattr(STDIN_FILENO, TCSANOW, &tparm) < 0)
	    break;
	ret = 0;
    } while (0);
    /* If we're losing, restore signal handlers. */
    if (ret)
	restore_signals();
    return ret;
}

static krb5_error_code
restore_tty(FILE* fp)
{
    int ret, fd;

    ret = 0;
    fd = fileno(fp);
    if (isatty(fd)) {
	ret = tcsetattr(fd, TCSANOW, &saveparm);
	if (ret < 0)
	    ret = KRB5_LIBOS_CANTREADPWD;
	else
	    ret = 0;
    }
    restore_signals();
    return ret;
}

#else /* non-Cygwin Windows, or Mac */

#if defined(_WIN32)

#include <io.h>

krb5_error_code KRB5_CALLCONV
krb5_prompter_posix(krb5_context context,
		    void *data,
		    const char *name,
		    const char *banner,
		    int num_prompts,
		    krb5_prompt prompts[])
{
    HANDLE		handle;
    DWORD		old_mode, new_mode;
    char		*ptr;
    int			scratchchar;
    krb5_error_code	errcode = 0;
    int			i;

    handle = GetStdHandle(STD_INPUT_HANDLE);
    if (handle == INVALID_HANDLE_VALUE)
	return ENOTTY;
    if (!GetConsoleMode(handle, &old_mode))
	return ENOTTY;

    new_mode = old_mode;
    new_mode |=  ( ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT );
    new_mode &= ~( ENABLE_ECHO_INPUT );

    if (!SetConsoleMode(handle, new_mode))
	return ENOTTY;

    if (!SetConsoleMode(handle, old_mode))
	return ENOTTY;

    if (name) {
	fputs(name, stdout);
	fputs("\n", stdout);
    }

    if (banner) {
       fputs(banner, stdout);
       fputs("\n", stdout);
    }

    for (i = 0; i < num_prompts; i++) {
	if (prompts[i].hidden) {
	    if (!SetConsoleMode(handle, new_mode)) {
		errcode = ENOTTY;
		goto cleanup;
	    }
	}

	fputs(prompts[i].prompt,stdout);
	fputs(": ", stdout);
	fflush(stdout);
	memset(prompts[i].reply->data, 0, prompts[i].reply->length);

	if (fgets(prompts[i].reply->data, prompts[i].reply->length, stdin)
	    == NULL) {
	    if (prompts[i].hidden)
		putchar('\n');
	    errcode = KRB5_LIBOS_CANTREADPWD;
	    goto cleanup;
	}
	if (prompts[i].hidden)
	    putchar('\n');
	/* fgets always null-terminates the returned string */

	/* replace newline with null */
	if ((ptr = strchr(prompts[i].reply->data, '\n')))
	    *ptr = '\0';
	else /* flush rest of input line */
	    do {
		scratchchar = getchar();
	    } while (scratchchar != EOF && scratchchar != '\n');
    
	prompts[i].reply->length = strlen(prompts[i].reply->data);

	if (!SetConsoleMode(handle, old_mode)) {
	    errcode = ENOTTY;
	    goto cleanup;
	}
    }

 cleanup:
    if (errcode) {
	for (i = 0; i < num_prompts; i++) {
	    memset(prompts[i].reply->data, 0, prompts[i].reply->length);
	}
    }
    return errcode;
}

#else /* !_WIN32 */

krb5_error_code KRB5_CALLCONV
krb5_prompter_posix(krb5_context context,
		    void *data,
		    const char *name,
		    const char *banner,
		    int num_prompts,
		    krb5_prompt prompts[])
{
    return(EINVAL);
}
#endif /* !_WIN32 */
#endif /* Windows or Mac */

void
krb5int_set_prompt_types(krb5_context context, krb5_prompt_type *types)
{
    context->prompt_types = types;
}

krb5_prompt_type*
KRB5_CALLCONV
krb5_get_prompt_types(krb5_context context)
{
    return context->prompt_types;
}
