/***************************************************************************/
/* Copyright (c) 2016 Amir Plivatsky                                       */
/* lg_isatty() is based on code sent to the Cygwin discussion group by     */
/* Corinna Vinschen, Cygwin Project Co-Leader, on 2012.                    */
/* All rights reserved                                                     */
/*                                                                         */
/* Use of the link grammar parsing system is subject to the terms of the   */
/* license set forth in the LICENSE file included with this software.      */
/* This license allows free redistribution and use in source and binary    */
/* forms, with or without modification, subject to certain conditions.     */
/*                                                                         */
/***************************************************************************/

#ifdef _WIN32
#include <windows.h>
#include <wchar.h>
#include <io.h>
#include <shellapi.h>                   // CommandLineToArgvW()
#include <errhandlingapi.h>             // GetLastError()
#else
#include <pwd.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#endif /* _WIN32 */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if !HAVE_ASPRINTF
#include <stdarg.h>
#endif // !HAVE_ASPRINTF

#include "parser-utilities.h"
#include "lg_readline.h"

/**
 * Expand an initial '~' to home directory.
 *
 * @param filename The filename to expand. Its length must be >= 1.
 * @return A newly-allocated filename, expanded if possible. Freed by caller.
 *
 * Note: ~user is not supported here on Windows.
 */
char *expand_homedir(const char *filename)
{
	if (filename[0] != '~') return strdup(filename);

#ifndef _WIN32
	char *user = NULL;
	const char *user_end = &filename[strcspn(filename, "/")];
	if (user_end != &filename[1])
	{
		user = strdup(filename + 1);
		user[user_end - filename - 1] = '\0';
	}
#endif /* _WIN32 */

#ifdef _WIN32
	char *home;

	const char *homepath = getenv("HOMEPATH");
	if ((homepath == NULL) || (homepath[0] == '\0')) return strdup(filename);
	const char *homedrive = getenv("HOMEDRIVE");
	if (homedrive == NULL) homedrive = "";

	home = malloc(strlen(homepath) + strlen(homedrive) + 1);
	strcpy(home, homedrive);
	strcat(home, homepath);
	filename++;
#else
	const char *home;

	if (user == NULL)
	{
		home = getenv("HOME");
		if ((home == NULL) || (home[0] == '\0')) return strdup(filename);
		filename++;
	}
	else
	{
		struct passwd *pwd;
		pwd = getpwnam(user);
		free(user);
		if (pwd == NULL) return strdup(filename);
		home = pwd->pw_dir;
		filename = user_end;
	}
#endif

	size_t filename_len = strlen(filename);
	size_t home_len = strlen(home);

	char *eh_filename = malloc(home_len + filename_len + 1);
	memcpy(eh_filename, home, home_len);
	memcpy(eh_filename + home_len, filename, filename_len + 1);

#ifdef _WIN32
	free(home);
#endif

	return eh_filename;
}

#ifdef _WIN32
#define INPUT_UTF16_SIZE 4096
/**
 * Get a line from the console in UTF-8.
 * This function bypasses the code page conversion and reads Unicode
 * directly from the console.
 * @param inbuf Buffer for the returned line (in UTF-8 encoding).
 * @param inbuf_size The size of this buffer.
 * @return -1: error; 0: EOF; 1: Success.
 */
int get_console_line(char *inbuf, int inbuf_size)
{
	static HANDLE console_handle = NULL;
	wchar_t winbuf[INPUT_UTF16_SIZE];
	static bool eof;

	if (eof) return 0;

	if (NULL == console_handle)
	{
		console_handle = CreateFileA("CONIN$", GENERIC_READ, FILE_SHARE_READ,
		                             NULL, OPEN_EXISTING, 0, NULL);
		if (!console_handle || (INVALID_HANDLE_VALUE == console_handle))
		{
			snprintf(inbuf, inbuf_size, "CreateFileA CONIN$: Error %lu",
			         GetLastError());
			return -1;
		}
	}

	DWORD nchar;
	if (!ReadConsoleW(console_handle, &winbuf, INPUT_UTF16_SIZE-1, &nchar, NULL))
	{
		snprintf(inbuf, inbuf_size, "ReadConsoleW: Error %lu\n", GetLastError());
		return -1;
	}
	winbuf[nchar] = L'\0'; /* nchar is always <= INPUT_UTF16_SIZE-1. */

	nchar = WideCharToMultiByte(CP_UTF8, 0, winbuf, -1, inbuf,
	                            inbuf_size, NULL, NULL);
	if (0 == nchar)
	{
		DWORD err = GetLastError();
		if (err == ERROR_INSUFFICIENT_BUFFER)
			snprintf(inbuf, inbuf_size, "Input line too long (>%d)", inbuf_size-3);
		else
			snprintf(inbuf, inbuf_size, "WideCharToMultiByte CP_UTF8: Error %lu",
			         err);
		return -1;
	}

	/* Make sure we don't have conversion problems, by searching for U+FFFD. */
	const char *invalid_char  = strstr(inbuf, "\xEF\xBF\xBD");
	if (NULL != invalid_char)
	{
		prt_error("Error: Unable to process UTF8 in input string.\n");
		inbuf[0] = '\0'; /* Just ignore the input line. */
	}

	/* ^Z is read as a character. Convert it to an EOF indication. */
	char *ctrl_z_position = strchr(inbuf, '\x1A');
	if (ctrl_z_position != NULL)
	{
		if (ctrl_z_position == inbuf) return 0;
		*ctrl_z_position = '\n';
		eof = true;
		return 1;
	}

	return 1;
}

static int console_input_cp;
static int console_output_cp;
static void restore_console_cp(void)
{
	SetConsoleCP(console_input_cp);
	SetConsoleOutputCP(console_output_cp);
}

static BOOL CtrlHandler(DWORD fdwCtrlType)
{
	if ((CTRL_C_EVENT == fdwCtrlType) || (CTRL_BREAK_EVENT  == fdwCtrlType))
	{
		fprintf(stderr, "Interrupt\n");
		restore_console_cp();
		exit(2);
	}
	return FALSE;
}

/**
 * Set the output conversion attributes for transparency.
 * This way UTF-8 output doesn't pass any conversion.
 */
static void win32_set_utf8_output(void)
{
	if (-1 == _setmode(fileno(stdout), _O_BINARY))
	{
		prt_error("Warning: _setmode(fileno(stdout), _O_BINARY): %s.\n",
			strerror(errno));
	}

	console_input_cp = GetConsoleCP();
	console_output_cp = GetConsoleOutputCP();
	atexit(restore_console_cp);
	if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE))
	{
		prt_error("Warning: Cannot not set code page restore handler.\n");
	}
	/* For file output. It is too late for output pipes.
	 * If output pipe is desired, one can set CP_UTF8 by the
	 * command "chcp 65001" before invoking link-parser. */
	if (!SetConsoleCP(CP_UTF8))
	{
		prt_error("Warning: Cannot set input codepage %d (error %lu).\n",
			CP_UTF8, GetLastError());
	}
	/* For Console output. */
	if (!SetConsoleOutputCP(CP_UTF8))
	{
		prt_error("Warning: Cannot set output codepage %d (error %lu).\n",
			CP_UTF8, GetLastError());
	}
}

#include <winternl.h>
/**
 * isatty() compatibility for running under Cygwin when compiling
 * using the Windows native C library.
 * WIN32 isatty() gives a wrong result (for Cygwin) on Cygwin's
 * pseudo-ttys, because they are implemented as Windows pipes.
 * Here is compatibility layer, originally sent to the Cygwin
 * discussion group by Corinna Vinschen, Cygwin Project Co-Leader.
 * See: https://www.cygwin.com/ml/cygwin/2012-11/msg00214.html
 */
int lg_isatty(int fd)
{
	HANDLE fh;
	long buf[66];  /* NAME_MAX + 1 + sizeof ULONG */
	PFILE_NAME_INFO pfni = (PFILE_NAME_INFO)buf;
	PWCHAR cp;

	/* Fetch the underlying HANDLE. */
	fh = (HANDLE)_get_osfhandle(fd);
	if (!fh || (INVALID_HANDLE_VALUE == fh))
	{
		errno = EBADF;
		return 0;
	}

/* Windows _isatty() is buggy: It returns a nonzero for the NUL device! */
#if 0
	if (_isatty(fd))
		return 1;
#else
	/* This detects a console device reliably. */
	CONSOLE_SCREEN_BUFFER_INFO sbi;
	DWORD mode;
	if (GetConsoleMode(fh, &mode) || GetConsoleScreenBufferInfo(fh, &sbi))
		return 1;
#endif

	/* Must be a pipe. */
	if (GetFileType(fh) != FILE_TYPE_PIPE)
		goto no_tty;

	if (!GetFileInformationByHandleEx(fh, FileNameInfo, pfni, sizeof(buf)))
	{
		printf("GetFileInformationByHandleEx: Error %lu\n", GetLastError());
		goto no_tty;
	}

	/* The filename is not guaranteed to be NUL-terminated. */
	pfni->FileName[pfni->FileNameLength / sizeof (WCHAR)] = L'\0';

	/* Now check the name pattern.  The filename of a Cygwin pseudo tty pipe
	   matches this regex:

			 ^\\(cygwin|msys)-{[a-z\d]}16-pty\d+-(to|from)-master

		%16llx is the hash of the Cygwin installation, (to support multiple
		parallel installations), %d id the pseudo tty number, "to" or "from"
		differs the pipe direction. "from" is a stdin, "to" a stdout-like
		pipe. */
	cp = pfni->FileName;
	if ((!wcsncmp(cp, L"\\cygwin-", 8) && !wcsncmp(cp + 24, L"-pty", 4)) ||
	    (!wcsncmp(cp, L"\\msys-", 6)   && !wcsncmp(cp + 22, L"-pty", 4)))
	{
		cp = wcschr(cp + 26, '-');
		if (!cp)
			goto no_tty;
		if (!wcsncmp(cp, L"-from-master", 12) || !wcsncmp(cp, L"-to-master", 10))
			return 1;
	}
no_tty:
	errno = ENOTTY;
	return 0;
}

/*
 * Clean up Windows argv initialization.
 * Needed for MSVC and MinGW.
 */
static char **utf8_argv;
static int utf8_argc;
static void argv2utf8_free(void)
{
	for (int i = 0; i < utf8_argc; i++)
		free(utf8_argv[i]);
	free(utf8_argv);
}

/**
 * Convert argv from the startup locale to UTF-8.
 */
static char **argv2utf8(int argc)
{
	char **nargv = malloc(argc * sizeof(char *));
	LPWSTR *warglist = CommandLineToArgvW(GetCommandLineW(), &argc);

	if (NULL == warglist) return NULL;

	for (int i = 0; i < argc; i++)
	{
		int n = WideCharToMultiByte(CP_UTF8, 0, warglist[i], -1, NULL, 0, NULL, NULL);

		nargv[i] = malloc(n);
		n = WideCharToMultiByte(CP_UTF8, 0, warglist[i], -1, nargv[i], n, NULL, NULL);
		if (0 == n)
		{
			prt_error("Error: WideCharToMultiByte CP_UTF8 failed: Error %lu.\n",
			         GetLastError());
			return NULL;
		}
	}
	LocalFree(warglist);

	utf8_argv = nargv;
	utf8_argc = argc;
	atexit(argv2utf8_free);

	return nargv;
}

static bool running_under_cygwin;

char **ms_windows_setup(int argc)
{
	/* If compiled with MSVC/MinGW, we still support running under Cygwin.
	 * This is done by checking running_under_cygwin to resolve
	 * incompatibilities. */
	const char *ostype = getenv("OSTYPE");
	if ((NULL != ostype) && (0 == strcmp(ostype, "cygwin")))
		running_under_cygwin = true;

	/* argv encoding is in the current locale. */
	char **argv = argv2utf8(argc);
	if (NULL == argv)
	{
		prt_error("Fatal error: Unable to parse command line\n");
		exit(-1);
	}

	win32_set_utf8_output();

	return argv;
}
#endif /* _WIN32 */

/*
 * @return -1: error; 0: EOF; 1: Success.
 */
static int fgets_with_check(char *inbuf, unsigned int inbuf_size, FILE *fh)
{
	const char *rc = fgets(inbuf, inbuf_size, fh);
	if (rc == NULL)
	{
		/* EOF or error */
		if (!ferror(fh)) return 0;
		snprintf(inbuf, inbuf_size, "fgets(): %s", strerror(errno));
		return -1;
	}

	size_t len = strlen(inbuf);
	if ((len == inbuf_size -1) && inbuf[len -2] != '\n')
	{
		snprintf(inbuf, inbuf_size, "Input line too long (>%u).", inbuf_size-2);
		return -1;
	}

	return 1;
}

static int get_terminal_line(const char *uprompt, char **buf,
                             unsigned int bufsize, FILE *in, FILE *out, bool tty)
{
	int rc;

#ifdef HAVE_EDITLINE
	*buf = lg_readline(uprompt);
	rc = (*buf != NULL);
#else
	fprintf(out, "%s", uprompt);
	fflush(out);
#ifdef _WIN32
	if (!running_under_cygwin && tty)
	{
		rc = get_console_line(*buf, bufsize);
	}
	else
#endif /* _WIN32 */
	{
		rc = fgets_with_check(*buf, bufsize, in);
	}
#endif /* HAVE_EDITLINE */

	return rc;
}

/*
 * Get an input line. Notify errors.
 *
 * @param in: Input file handle.
 * @param out: Output file handle.
 * @param tty \c false: from file; \c true: from terminal.
 * @return \c true if successful, \c false on error.
 */
bool get_line(const char *uprompt, char **buf, unsigned int bufsize,
                     FILE *in, FILE *out, bool tty)
{
	int rc;

	if ((in != stdin) || !tty)
	{
		/* Get input from a file. */
		rc = fgets_with_check(*buf, bufsize, in);
	}
	else
	{
		/* If we are here, the input is from a terminal. */
		rc = get_terminal_line(uprompt, buf, bufsize, in, out, tty);
	}

	if (rc == 0) return false; /* EOF */
	if (rc == -1)
	{
		prt_error("Fatal error: %s\n", *buf);
		return false;
	}

	return true;
}

static unsigned int screen_width = INITIAL_SCREEN_WIDTH;
/**
 * Query the system for the current tty/console width.
 * May be called directly or as SIGWINCH handler (if SIGWINCH exists).
 * Put the found screen width in a global variable \c screen_width;
 * @param sig Used only for type check in case SIGWINCH exists.
 * @return Void.
 */
static void get_screen_width(int sig)
{
	static int isatty_stdout = -1;

	if (isatty_stdout == -1) isatty_stdout = isatty(fileno(stdout));
	if (!isatty_stdout) return;

	int fd = fileno(stdout);

#ifdef _WIN32
	HANDLE console;
	CONSOLE_SCREEN_BUFFER_INFO info;

	/* Create a handle to the console screen. */
	console = (HANDLE)_get_osfhandle(fd);
	if (!console || (console == INVALID_HANDLE_VALUE)) return;

	/* Calculate the size of the console window. */
	if (GetConsoleScreenBufferInfo(console, &info) == 0) return;

	screen_width = info.dwSize.X;
	return;
#else
	struct winsize ws;

	/* If there is no controlling terminal, the fileno will fail. This
	 * seems to happen while building docker images, I don't know why.
	 */
	if (fd < 0) return;

	if (0 != ioctl(fd, TIOCGWINSZ, &ws))
	{
		perror("stdout: ioctl TIOCGWINSZ");
		return;
	}

	/* Set the screen width only if the returned value seems
	 * rational: it's positive and not insanely tiny.
	 */
	if ((10 < ws.ws_col) && (16123 > ws.ws_col))
		screen_width = ws.ws_col;
#endif /* _WIN32 */
}

/**
 * Set the "width" user variable from the global \c screen_size.
 * If SIGWINCH is not defined, poll the screen width beforehand.
 */
void set_screen_width(Command_Options* copts)
{
#if !defined SIGWINCH || defined _WIN32
	get_screen_width(0);
#endif

	copts->screen_width = screen_width;
}

#ifdef INTERRUPT_EXIT
static void interrupt_exit(int n)
{
	exit(128+n);
}
#endif

void initialize_screen_width(Command_Options *copts)
{
#ifdef SIGWINCH
#if HAVE_SIGACTION
	struct sigaction winch_act = { { 0 } };
	winch_act.sa_handler = get_screen_width;
	sigemptyset(&winch_act.sa_mask);
	winch_act.sa_flags = 0;
	if (sigaction(SIGWINCH, &winch_act, NULL) == -1)
		perror("sigaction SIGWINCH");
#else
	if (signal(SIGWINCH, get_screen_width) == SIG_ERR)
		perror("signal SIGWINCH");
#endif /* HAVE_SIGACTION */
#endif /* SIGWINCH */

#ifdef INTERRUPT_EXIT
	(void)signal(SIGINT, interrupt_exit);
	(void)signal(SIGTERM, interrupt_exit);
#endif

	get_screen_width(0);
	set_screen_width(copts);
}

#ifdef __MINGW32__
/*
 * A workaround for printing UTF-8 on the console.
 * These functions are also implemented in the LG library.
 * For the strange story see the comment in utilities.c there.
 */

int __mingw_vfprintf (FILE * __restrict__ stream, const char * __restrict__ fmt, va_list vl)
{
	int n = vsnprintf(NULL, 0, fmt, vl);
	if (0 > n) return n;
	char *buf = malloc(n+1);
	if (NULL == buf) return -1;
	n = vsnprintf(buf, n+1, fmt, vl);
	if (0 > n)
	{
		free(buf);
		return n;
	}

	n = fputs(buf, stdout);
	free(buf);
	return n;
}

int __mingw_vprintf (const char * __restrict__ fmt, va_list vl)
{
	return __mingw_vfprintf(stdout, fmt, vl);
}
#endif /* __MINGW32__ */

#if !HAVE_ASPRINTF
int vasprintf(char ** restrict buf, const char * restrict fmt, va_list vl)
{
	va_list vl_copy;
	va_copy(vl_copy, vl);

	int n = vsnprintf(NULL, 0, fmt, vl);
	if (n < 0) {
		va_end(vl_copy);
		return n;
	}

	*buf = malloc(n + 1);
	if (*buf == NULL) {
		va_end(vl_copy);
		return -1;
	}

	n = vsnprintf(*buf, n + 1, fmt, vl_copy);
	if (n < 0) free(*buf);

	va_end(vl_copy);
	return n;
}

int asprintf(char ** restrict buf, const char * restrict fmt, ...)
{
	va_list args;
	va_start(args, fmt);
	int result = vasprintf(buf, fmt, args);
	va_end(args);
	return result;
}
#endif /* !HAVE_ASPRINTF */
