/* rhash_main.c - main() and other top-level functions
 *
 * rhash is a small utility written in C that computes various message
 * digests of files. The message digests include CRC32, MD5, SHA1, TTH,
 * ED2K, GOST and many other.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h> /* free() */
#include <signal.h>
#include <locale.h>
#include <assert.h>

#include "rhash_main.h"
#include "calc_sums.h"
#include "common_func.h"
#include "file_mask.h"
#include "find_file.h"
#include "hash_print.h"
#include "hash_update.h"
#include "parse_cmdline.h"
#include "output.h"
#include "win_utils.h"
#include "librhash/rhash.h"


struct rhash_t rhash_data;

/**
 * Check if the file must be skipped. Returns 1 if the file path
 * is the same as the output or the log file path.
 *
 * @param file the file to check
 * @param mask the mask of accepted files
 * @return 1 if the file should be skipped, 0 otherwise
 */
static int must_skip_file(file_t* file)
{
	const rsh_tchar* path = FILE_TPATH(file);

	/* check if the file path is the same as the output or the log file path */
	return (opt.output && are_paths_equal(path, opt.output)) ||
		(opt.log && are_paths_equal(path, opt.log));
}

/**
 * Callback function to process files while recursively traversing a directory.
 * It hashes, checks or updates a file according to the current work mode.
 *
 * @param file the file to process
 * @param preprocess non-zero when preprocessing files, zero for actual processing.
 */
static int find_file_callback(file_t* file, int preprocess)
{
	int res = 0;
	assert(!FILE_ISDIR(file));
	assert(opt.search_data);

	if (rhash_data.interrupted) {
		opt.search_data->options |= FIND_CANCEL;
		return 0;
	}

	if (preprocess) {
		if (!file_mask_match(opt.files_accept, file->path) ||
			(opt.files_exclude && file_mask_match(opt.files_exclude, file->path)) ||
			must_skip_file(file)) {
			return 0;
		}

		if (opt.fmt & FMT_SFV) {
			print_sfv_header_line(rhash_data.out, file, 0);
		}

		rhash_data.batch_size += file->size;
	} else {
		int not_root = !(file->mode & FILE_IFROOT);

		if (not_root) {
			if ((opt.mode & (MODE_CHECK | MODE_UPDATE)) != 0) {
				/* check and update modes use the crc_accept list */
				if (!file_mask_match(opt.crc_accept, file->path)) {
					return 0;
				}
			} else {
				if (!file_mask_match(opt.files_accept, file->path) ||
					(opt.files_exclude && file_mask_match(opt.files_exclude, file->path))) {
					return 0;
				}
			}
		}
		if (must_skip_file(file)) return 0;

		if (opt.mode & (MODE_CHECK | MODE_CHECK_EMBEDDED)) {
			res = check_hash_file(file, not_root);
		} else {
			if (opt.mode & MODE_UPDATE) {
				res = update_hash_file(file);
			} else {
				/* default mode: calculate hash */
				const char* print_path = file->path;
				if (print_path[0] == '.' && IS_PATH_SEPARATOR(print_path[1])) print_path += 2;
				res = calculate_and_print_sums(rhash_data.out, file, print_path);
				if (rhash_data.interrupted) return 0;
				rhash_data.processed++;
			}
		}
	}
	if (res < 0) rhash_data.error_flag = 1;
	return 1;
}

/* previous SIGINT handler */
void (*prev_sigint_handler)(int) = NULL;

/**
 * Handler for the SIGINT signal, sent when user press Ctrl+C.
 * The handler prints message and exits the program.
 *
 * @param signum the processed signal identifier SIGINT
 */
static void ctrl_c_handler(int signum)
{
	(void)signum;
	rhash_data.interrupted = 1;
	if (rhash_data.rctx) {
		rhash_cancel(rhash_data.rctx);
	}
}

#define MAX_TEMPLATE_SIZE 65536

/**
 * Load printf-template from file, specified by options or config.
 */
static int load_printf_template(void)
{
	FILE* fd = fopen(opt.template_file, "rb");
	char buffer[8192];
	size_t len;
	int error = 0;

	if (!fd) {
		log_file_error(opt.template_file);
		return 0;
	}

	rhash_data.template_text = rsh_str_new();

	while (!feof(fd)) {
		len = fread(buffer, 1, 8192, fd);
		if (ferror(fd)) break;

		rsh_str_append_n(rhash_data.template_text, buffer, len);
		if (rhash_data.template_text->len >= MAX_TEMPLATE_SIZE) {
			log_msg(_("%s: template file is too big\n"), opt.template_file);
			error = 1;
		}
	}

	if (ferror(fd)) {
		log_file_error(opt.template_file);
		error = 1;
	}

	fclose(fd);
	rhash_data.printf_str = rhash_data.template_text->str;
	return !error;
}

/**
 * Free data allocated by an rhash_t object
 *
 * @param ptr pointer to rhash_t object
 */
void rhash_destroy(struct rhash_t* ptr)
{
	free_print_list(ptr->print_list);
	rsh_str_free(ptr->template_text);
	if (ptr->rctx) rhash_free(ptr->rctx);
	if (ptr->out) fclose(ptr->out);
	if (ptr->log) fclose(ptr->log);
#ifdef _WIN32
	if (ptr->program_dir) free(ptr->program_dir);
#endif
}

static void free_allocated_data(void)
{
	options_destroy(&opt);
	rhash_destroy(&rhash_data);
}

static void i18n_initialize(void)
{
	setlocale(LC_ALL, ""); /* set locale according to the environment */

#ifdef USE_GETTEXT
	bindtextdomain(TEXT_DOMAIN, LOCALEDIR); /* set the text message domain */
	IF_WINDOWS(setup_locale_dir());
	textdomain(TEXT_DOMAIN);
#endif /* USE_GETTEXT */
}

/**
 * RHash program entry point.
 *
 * @param argc number of program arguments including the program path
 * @param argv program arguments
 * @return the program exit code, zero on success and 1 on error
 */
int main(int argc, char *argv[])
{
	timedelta_t timer;
	int exit_code;
	int sfv;

	memset(&rhash_data, 0, sizeof(rhash_data));
	memset(&opt, 0, sizeof(opt));
	rhash_data.out = stdout; /* set initial output streams */
	rhash_data.log = stderr; /* can be altered by options later */
	rsh_install_exit_handler(free_allocated_data);

	IF_WINDOWS(init_program_dir());
	init_hash_info_table();

	i18n_initialize(); /* initialize locale and translation */

	read_options(argc, argv); /* load config and parse command line options */
	prev_sigint_handler = signal(SIGINT, ctrl_c_handler); /* install SIGINT handler */
	rhash_library_init();
	setup_percents();

	/* in benchmark mode just run benchmark and exit */
	if (opt.mode & MODE_BENCHMARK) {
		unsigned flags = (opt.flags & OPT_BENCH_RAW ? BENCHMARK_CPB | BENCHMARK_RAW : BENCHMARK_CPB);
		if ((opt.flags & OPT_BENCH_RAW) == 0) {
			rsh_fprintf(rhash_data.out, _("%s v%s benchmarking...\n"), PROGRAM_NAME, get_version_string());
		}
		run_benchmark(opt.sum_flags, flags);
		exit_code = (rhash_data.interrupted ? 3 : 0);
		rsh_exit(exit_code);
	}

	if (!opt.has_files) {
		if (argc > 1) {
			log_warning(_("no files/directories were specified at command line\n"));
		}

		/* print short usage help */
		log_msg(_("Usage: %s [OPTION...] <FILE>...\n\n"
			"Run `%s --help' for more help.\n"), CMD_FILENAME, CMD_FILENAME);
		rsh_exit(0);
	}
	assert(opt.search_data != 0);

	/* setup printf formatting string */
	rhash_data.printf_str = opt.printf_str;

	if (opt.template_file) {
		if (!load_printf_template()) rsh_exit(2);
	} else if (!rhash_data.printf_str && !(opt.mode & (MODE_CHECK | MODE_CHECK_EMBEDDED))) {
		/* initialize printf output format according to '--<hashname>' options */
		init_printf_format( (rhash_data.template_text = rsh_str_new()) );
		rhash_data.printf_str = rhash_data.template_text->str;

		if (opt.flags & OPT_VERBOSE) {
			char* str = rsh_strdup(rhash_data.printf_str);
			log_msg(_("Format string is: %s\n"), str_trim(str));
			free(str);
		}
	}

	if (rhash_data.printf_str) {
		rhash_data.print_list = parse_print_string(rhash_data.printf_str, &opt.sum_flags);
	}

	opt.search_data->options = FIND_SKIP_DIRS;
	opt.search_data->options |= (opt.flags & OPT_FOLLOW ? FIND_FOLLOW_SYMLINKS : 0);
	opt.search_data->call_back = find_file_callback;

	if ((sfv = (opt.fmt == FMT_SFV && !opt.mode))) {
		print_sfv_banner(rhash_data.out);
	}

	/* preprocess files */
	if (sfv || opt.bt_batch_file) {
		/* note: errors are not reported on preprocessing */
		opt.search_data->call_back_data = 1;
		scan_files(opt.search_data);

		fflush(rhash_data.out);
	}

	/* measure total processing time */
	rsh_timer_start(&timer);
	rhash_data.processed = 0;

	/* process files */
	opt.search_data->options |= FIND_LOG_ERRORS;
	opt.search_data->call_back_data = 0;
	scan_files(opt.search_data);

	if ((opt.mode & MODE_CHECK_EMBEDDED) && rhash_data.processed > 1) {
		print_check_stats();
	}

	if (!rhash_data.interrupted) {
		if (opt.bt_batch_file && rhash_data.rctx) {
			rhash_final(rhash_data.rctx, 0);
			save_torrent_to(opt.bt_batch_file, rhash_data.rctx);
		}

		if ((opt.flags & OPT_SPEED) &&
			!(opt.mode & (MODE_CHECK | MODE_UPDATE)) &&
			rhash_data.processed > 1)
		{
			double time = rsh_timer_stop(&timer);
			print_time_stats(time, rhash_data.total_size, 1);
		}
	} else {
		/* check if interruption was not reported yet */
		if (rhash_data.interrupted == 1) report_interrupted();
	}

	exit_code = (rhash_data.error_flag ? 1 :
		opt.search_data->errors_count ? 2 :
		rhash_data.interrupted ? 3 : 0);
	rsh_exit(exit_code);
	return 0; /* unreachable statement */
}
