/******************************************************************************/
/* SlunkCrypt, by LoRd_MuldeR <MuldeR2@GMX.de>                                */
/* This work has been released under the CC0 1.0 Universal license!           */
/******************************************************************************/

/* Internal */
#include "utils.h"
#include "crypt.h"
#include "pwgen.h"
#include "selftest.h"

/* Library */
#include <slunkcrypt.h>

/* CRT */
#include <string.h>
#include <time.h>
#include <inttypes.h>
#include <ctype.h>
#include <signal.h>

// ==========================================================================
// Constants
// ==========================================================================

#define MODE_HELP 0
#define MODE_VERS 1
#define MODE_ENCR 2
#define MODE_DECR 3
#define MODE_PASS 4
#define MODE_TEST 5

static const size_t RCMD_PWDLEN_LENGTH = 12U;
static const size_t DFLT_PWDLEN_LENGTH = 24U;

static const CHR* const ENV_PASSWORD = T("SLUNK_PASSPHRASE");
static const CHR* const ENV_KEEPFILE = T("SLUNK_KEEP_INCOMPLETE");
static const CHR* const ENV_NTHREADS = T("SLUNK_THREADS");

static const CHR* const PREFIX_PASS = T("pass:");
static const CHR* const PREFIX_FILE = T("file:");

// ==========================================================================
// Auxiliary functions
// ==========================================================================

#define PW_FROM_ENV (!(argc > 4))

static int parse_slunk_mode(const CHR* const command)
{
	if ((!STRICMP(command, T("-h"))) || (!STRICMP(command, T("/?"))) || (!STRICMP(command, T("--help"))))
	{
		return MODE_HELP;
	}
	else if ((!STRICMP(command, T("-v"))) || (!STRICMP(command, T("--version"))))
	{
		return MODE_VERS;
	}
	else if ((!STRICMP(command, T("-e"))) || (!STRICMP(command, T("--encrypt"))))
	{
		return MODE_ENCR;
	}
	else if ((!STRICMP(command, T("-d"))) || (!STRICMP(command, T("--decrypt"))))
	{
		return MODE_DECR;
	}
	else if ((!STRICMP(command, T("-p"))) || (!STRICMP(command, T("--make-pw"))))
	{
		return MODE_PASS;
	}
	else if ((!STRICMP(command, T("-t"))) || (!STRICMP(command, T("--self-test"))))
	{
		return MODE_TEST;
	}
	else
	{
		return -1; /*invalid command*/
	}
}

static void print_manpage(const CHR *const program)
{
	FPUTS(T("====================================================================\n"), stderr);
	FPUTS(T("This software has been released under the CC0 1.0 Universal license:\n"), stderr);
	FPUTS(T("https://creativecommons.org/publicdomain/zero/1.0/legalcode\n"), stderr);
	FPUTS(T("====================================================================\n\n"), stderr);
	FPUTS(T("Usage:\n"), stderr);
	FPRINTF(stderr, T("  %") T(PRISTR) T(" --encrypt [pass:<pass>|file:<file>] <input.txt> <output.enc>\n"), program);
	FPRINTF(stderr, T("  %") T(PRISTR) T(" --decrypt [pass:<pass>|file:<file>] <input.enc> <output.txt>\n"), program);
	FPRINTF(stderr, T("  %") T(PRISTR) T(" --make-pw [<length>]\n\n"), program);
	FPRINTF(stderr, T("Optionally, reads passphrase from the %") T(PRISTR) T(" environment variable.\n\n"), ENV_PASSWORD);
}

static char *copy_passphrase(const CHR *const passphrase)
{
	if ((!passphrase) || (!passphrase[0U]))
	{
		FPUTS(T("Error: The passphrase input string must not be empty!\n\n"), stderr);
		return NULL;
	}

	char *const buffer = CHR_to_utf8(passphrase);
	if (!buffer)
	{
		FPUTS(T("Error: Failed to allocate the string buffer!\n\n"), stderr);
	}

	return buffer;
}

static uint32_t environ_get_uint(const CHR *const name)
{
	const CHR *const value = GETENV(name);
	if (value)
	{
		return (uint32_t) STRTOUL(value);
	}
	return 0U;
}

static int environ_get_flag(const CHR *const name)
{
	return (environ_get_uint(name) != 0);
}

static void check_excess_arguments(const int argc, int maximum)
{
	if (argc > maximum)
	{
		FPUTS(T("Warning: Excess command-line argument(s) will be ignored!\n\n"), stderr);
		fflush(stderr);
	}
}

static void sigint_handler(const int sig)
{
	if (sig == SIGINT)
	{
		g_slunkcrypt_abort_flag = 1;
	}
}

// ==========================================================================
// Main function
// ==========================================================================

int MAIN(const int argc, CHR *const argv[])
{
	int result = EXIT_FAILURE;
	const CHR *input_file = NULL, *output_file = NULL;
	char *passphrase_buffer = NULL;

	init_terminal();
	setup_signal_handler(SIGINT, sigint_handler);

	FPRINTF(stderr, T("SlunkCrypt Utility (%") T(PRIstr) T("-%") T(PRIstr) T("), by LoRd_MuldeR <MuldeR2@GMX.de>\n"), OS_TYPE_NAME, CPU_ARCH);
	FPRINTF(stderr, T("Using libSlunkCrypt-%") T(PRIstr) T(" v%u.%u.%u [%") T(PRIstr) T("]\n\n"),
		SLUNKCRYPT_HAVE_THREADS ? "MT" : "ST", SLUNKCRYPT_VERSION_MAJOR, SLUNKCRYPT_VERSION_MINOR, SLUNKCRYPT_VERSION_PATCH, SLUNKCRYPT_BUILD);

	fflush(stderr);

	/* ----------------------------------------------------- */
	/* Parse arguments                                       */
	/* ----------------------------------------------------- */

	if ((argc < 1) || (!argv[0]))
	{
		FPUTS(T("Error: Argument array is empty. The program was called incorrectly!\n\n"), stderr);
		goto clean_up;
	}

	if (argc < 2)
	{
		FPRINTF(stderr, T("Error: Nothing to do. Please type '%") T(PRISTR) T(" --help' for details!\n\n"), get_file_name(argv[0U]));
		goto clean_up;
	}

	const int slunk_mode = parse_slunk_mode(argv[1U]);
	switch (slunk_mode)
	{
	case MODE_HELP:
		print_manpage(get_file_name(argv[0U]));
	case MODE_VERS:
		result = EXIT_SUCCESS;
		goto clean_up;
	case MODE_ENCR:
	case MODE_DECR:
		break; /*fallthrough*/
	case MODE_PASS:
		check_excess_arguments(argc, 3);
		result = generate_passphrase((argc > 2) ? STRTOUL(argv[2U]) : DFLT_PWDLEN_LENGTH);
		goto clean_up;
	case MODE_TEST:
		check_excess_arguments(argc, 2);
		result = run_selftest_routine(environ_get_uint(ENV_NTHREADS));
		goto clean_up;
	default:
		FPRINTF(stderr, T("Error: The specified command \"%") T(PRISTR) T("\" is unknown!\n\n"), argv[1U]);
		goto clean_up;
	}

	if (argc < 4)
	{
		FPRINTF(stderr, T("Error: Required argument is missing. Please type '%") T(PRISTR) T(" --help' for details!\n\n"), get_file_name(argv[0U]));
		goto clean_up;
	}

	check_excess_arguments(argc, 5);

	const CHR *const passphrase = PW_FROM_ENV ? GETENV(ENV_PASSWORD) : argv[2U];
	if ((!passphrase) || (!passphrase[0U]))
	{
		FPUTS(T("Error: The passphrase must be specified, directly or indirectly!\n\n"), stderr);
		goto clean_up;
	}

	if ((!PW_FROM_ENV) && STRICMP(passphrase, T("-")))
	{
		if ((!STARTS_WITH(passphrase, PREFIX_PASS)) && (!STARTS_WITH(passphrase, PREFIX_FILE)))
		{
			FPRINTF(stderr, T("Error: The passphrase must start with a '%") T(PRISTR) T("' or '%") T(PRISTR) T("' prefix!\n\n"), PREFIX_PASS, PREFIX_FILE);
			goto clean_up;
		}
	}

	input_file = argv[PW_FROM_ENV ? 2U : 3U];
	if (!input_file[0U])
	{
		FPUTS(T("Error: The specified input file name must not be empty!\n\n"), stderr);
		goto clean_up;
	}

	output_file = argv[PW_FROM_ENV ? 3U : 4U];
	if (!output_file[0U])
	{
		FPUTS(T("Error: The specified output file name must not be empty!\n\n"), stderr);
		goto clean_up;
	}

	if (same_file(input_file, output_file) > 0)
	{
		FPUTS(T("Error: The input and output files must not be the same!\n\n"), stderr);
		goto clean_up;
	}

	/* ----------------------------------------------------- */
	/* Initialize passphrase                                 */
	/* ----------------------------------------------------- */

	if (!(passphrase_buffer = PW_FROM_ENV ? copy_passphrase(passphrase) :
			(STARTS_WITH(passphrase, PREFIX_PASS) ? copy_passphrase(passphrase + STRLEN(PREFIX_PASS)) :
			(STARTS_WITH(passphrase, PREFIX_FILE) ? read_passphrase(passphrase + STRLEN(PREFIX_FILE)) : read_passphrase(T("-"))))))
	{
		goto clean_up;
	}

	slunkcrypt_bzero((CHR*)passphrase, STRLEN(passphrase) * sizeof(CHR));

	const size_t passphrase_len = strlen(passphrase_buffer);
	if (passphrase_len < SLUNKCRYPT_PWDLEN_MIN)
	{
		FPRINTF(stderr, T("Error: Passphrase must be at least %u characters in length!\n\n"), (unsigned)SLUNKCRYPT_PWDLEN_MIN);
		goto clean_up;
	}
	else if (passphrase_len > SLUNKCRYPT_PWDLEN_MAX)
	{
		FPRINTF(stderr, T("Error: Passphrase must be at most %u characters in length!\n\n"),  (unsigned)SLUNKCRYPT_PWDLEN_MAX);
		goto clean_up;
	}

	if (slunk_mode == MODE_ENCR)
	{
		if (passphrase_len < RCMD_PWDLEN_LENGTH)
		{
			FPRINTF(stderr, T("Warning: Using a *short* passphrase; a length of %u characters or more is recommended!\n\n"), (unsigned)RCMD_PWDLEN_LENGTH);
		}
		else if (weak_passphrase(passphrase_buffer))
		{
			FPUTS(T("Warning: Using a *weak* passphrase; a mix of upper-case letters, lower-case letters, digits and other characters is recommended!\n\n"), stderr);
		}
	}

	fflush(stderr);

	/* ----------------------------------------------------- */
	/* Encrypt or decrypt                                    */
	/* ----------------------------------------------------- */

	const uint64_t clk_start = clock_read();
	const crypt_options_t options = { environ_get_flag(ENV_KEEPFILE), environ_get_uint(ENV_NTHREADS) };

	switch (slunk_mode)
	{
	case MODE_ENCR:
		result = encrypt(passphrase_buffer, input_file, output_file, &options);
		break;
	case MODE_DECR:
		result = decrypt(passphrase_buffer, input_file, output_file, &options);
		break;
	default:
		FPUTS(T("Unexpected mode encountered!\n\n"), stderr);
	}

	if (!g_slunkcrypt_abort_flag)
	{
		FPRINTF(stderr, T("--------\n\nOperation completed after %.1f seconds.\n\n"), (clock_read() - clk_start) / ((double)clock_freq()));
	}

	/* ----------------------------------------------------- */
	/* Final clean-up                                        */
	/* ----------------------------------------------------- */

clean_up:

	fflush(stderr);

	if (passphrase_buffer)
	{
		slunkcrypt_bzero(passphrase_buffer, strlen(passphrase_buffer));
		free(passphrase_buffer);
	}

	return result;
}

#if defined(_WIN32) && defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR)
void __wgetmainargs(int*, wchar_t***, wchar_t***, int, int*);
int main()
{
	wchar_t** enpv, ** argv;
	int argc, si = 0;
	__wgetmainargs(&argc, &argv, &enpv, 1, &si);
	return wmain(argc, argv);
}
#endif
