// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <unistd.h>
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include "log-writes.h"

enum option_indexes {
	NEXT_FLUSH,
	NEXT_FUA,
	START_ENTRY,
	END_MARK,
	LOG,
	REPLAY,
	LIMIT,
	VERBOSE,
	FIND,
	NUM_ENTRIES,
	NO_DISCARD,
	FSCK,
	CHECK,
	START_MARK,
	START_SECTOR,
	END_SECTOR,
};

static struct option long_options[] = {
	{"next-flush", no_argument, NULL, 0},
	{"next-fua", no_argument, NULL, 0},
	{"start-entry", required_argument, NULL, 0},
	{"end-mark", required_argument, NULL, 0},
	{"log", required_argument, NULL, 0},
	{"replay", required_argument, NULL, 0},
	{"limit", required_argument, NULL, 0},
	{"verbose", no_argument, NULL, 'v'},
	{"find", no_argument, NULL, 0},
	{"num-entries", no_argument, NULL, 0},
	{"no-discard", no_argument, NULL, 0},
	{"fsck", required_argument, NULL, 0},
	{"check", required_argument, NULL, 0},
	{"start-mark", required_argument, NULL, 0},
	{"start-sector", required_argument, NULL, 0},
	{"end-sector", required_argument, NULL, 0},
	{ NULL, 0, NULL, 0 },
};

static void usage(void)
{
	fprintf(stderr, "Usage: replay-log --log <logfile> [options]\n");
	fprintf(stderr, "\t--replay <device> - replay onto a specific "
		"device\n");
	fprintf(stderr, "\t--limit <number> - number of entries to replay\n");
	fprintf(stderr, "\t--next-flush - replay to/find the next flush\n");
	fprintf(stderr, "\t--next-fua - replay to/find the next fua\n");
	fprintf(stderr, "\t--start-entry <entry> - start at the given "
		"entry #\n");
	fprintf(stderr, "\t--start-mark <mark> - mark to start from\n");
	fprintf(stderr, "\t--end-mark <mark> - replay to/find the given mark\n");
	fprintf(stderr, "\t--find - put replay-log in find mode, will search "
		"based on the other options\n");
	fprintf(stderr, "\t--number-entries - print the number of entries in "
		"the log\n");
	fprintf(stderr, "\t--no-discard - don't process discard entries\n");
	fprintf(stderr, "\t--fsck - the fsck command to run, must specify "
		"--check\n");
	fprintf(stderr, "\t--check [<number>|flush|fua|discard] when to check "
		"the file system, mush specify --fsck\n");
	fprintf(stderr, "\t--start-sector <sector> - replay ops on region "
		"from <sector> onto <device>\n");
	fprintf(stderr, "\t--end-sector <sector> - replay ops on region "
		"to <sector> onto <device>\n");
	fprintf(stderr, "\t-v or --verbose - print replayed ops\n");
	fprintf(stderr, "\t-vv - print also skipped ops\n");
	exit(1);
}

/*
 * Check if the log entry flag matches one of the stop_flags.
 * If stop_flag has LOG_MARK, then looking also for match of
 * the mark label.
 */
static int should_stop(struct log_write_entry *entry, u64 stop_flags,
		       char *mark)
{
	u64 flags = le64_to_cpu(entry->flags);
	int check_mark = (stop_flags & LOG_MARK_FLAG);
	/* mark data begins after entry header */
	char *buf = entry->data;
	/* entry buffer is padded with at least 1 zero after data_len */
	u64 buflen = le64_to_cpu(entry->data_len) + 1;

	if (flags & stop_flags) {
		if (!check_mark)
			return 1;
		if ((flags & LOG_MARK_FLAG) &&
		    !strncmp(mark, buf, buflen))
			return 1;
	}
	return 0;
}

static int run_fsck(struct log *log, char *fsck_command)
{
	int ret = fsync(log->replayfd);
	if (ret)
		return ret;
	ret = system(fsck_command);
	if (ret >= 0)
		ret = WEXITSTATUS(ret);
	return ret ? -1 : 0;
}

enum log_replay_check_mode {
	CHECK_NUMBER = 1,
	CHECK_FUA = 2,
	CHECK_FLUSH = 3,
	CHECK_DISCARD = 4,
};

static int seek_to_mark(struct log *log, struct log_write_entry *entry,
			char *mark)
{
	int ret;

	while ((ret = log_seek_next_entry(log, entry, 1)) == 0) {
		if (should_stop(entry, LOG_MARK_FLAG, mark))
			break;
	}
	if (ret == 1) {
		fprintf(stderr, "Couldn't find starting mark\n");
		ret = -1;
	}

	return ret;
}

int main(int argc, char **argv)
{
	char *logfile = NULL, *replayfile = NULL, *fsck_command = NULL;
	struct log_write_entry *entry;
	u64 stop_flags = 0;
	u64 start_entry = 0;
	u64 start_sector = 0;
	u64 end_sector = -1ULL;
	u64 run_limit = 0;
	u64 num_entries = 0;
	u64 check_number = 0;
	char *end_mark = NULL, *start_mark = NULL;
	char *tmp = NULL;
	struct log *log;
	int find_mode = 0;
	int c;
	int opt_index;
	int ret;
	int print_num_entries = 0;
	int discard = 1;
	enum log_replay_check_mode check_mode = 0;

	while ((c = getopt_long(argc, argv, "v", long_options,
				&opt_index)) >= 0) {
		switch(c) {
		case 'v':
			log_writes_verbose++;
			continue;
		case '?':
			usage();
		default:
			break;
		}

		switch(opt_index) {
		case NEXT_FLUSH:
			stop_flags |= LOG_FLUSH_FLAG;
			break;
		case NEXT_FUA:
			stop_flags |= LOG_FUA_FLAG;
			break;
		case START_ENTRY:
			start_entry = strtoull(optarg, &tmp, 0);
			if (tmp && *tmp != '\0') {
				fprintf(stderr, "Invalid entry number\n");
				exit(1);
			}
			tmp = NULL;
			break;
		case START_MARK:
			/*
			 * Biggest sectorsize is 4k atm, so limit the mark to 4k
			 * minus the size of the entry.  Say 4097 since we want
			 * an extra slot for \0.
			 */
			start_mark = strndup(optarg, 4097 -
					     sizeof(struct log_write_entry));
			if (!start_mark) {
				fprintf(stderr, "Couldn't allocate memory\n");
				exit(1);
			}
			break;
		case END_MARK:
			/*
			 * Biggest sectorsize is 4k atm, so limit the mark to 4k
			 * minus the size of the entry.  Say 4097 since we want
			 * an extra slot for \0.
			 */
			end_mark = strndup(optarg, 4097 -
					   sizeof(struct log_write_entry));
			if (!end_mark) {
				fprintf(stderr, "Couldn't allocate memory\n");
				exit(1);
			}
			stop_flags |= LOG_MARK_FLAG;
			break;
		case LOG:
			logfile = strdup(optarg);
			if (!logfile) {
				fprintf(stderr, "Couldn't allocate memory\n");
				exit(1);
			}
			break;
		case REPLAY:
			replayfile = strdup(optarg);
			if (!replayfile) {
				fprintf(stderr, "Couldn't allocate memory\n");
				exit(1);
			}
			break;
		case LIMIT:
			run_limit = strtoull(optarg, &tmp, 0);
			if (tmp && *tmp != '\0') {
				fprintf(stderr, "Invalid entry number\n");
				exit(1);
			}
			tmp = NULL;
			break;
		case FIND:
			find_mode = 1;
			break;
		case NUM_ENTRIES:
			print_num_entries = 1;
			break;
		case NO_DISCARD:
			discard = 0;
			break;
		case FSCK:
			fsck_command = strdup(optarg);
			if (!fsck_command) {
				fprintf(stderr, "Couldn't allocate memory\n");
				exit(1);
			}
			break;
		case CHECK:
			if (!strcmp(optarg, "flush")) {
				check_mode = CHECK_FLUSH;
			} else if (!strcmp(optarg, "fua")) {
				check_mode = CHECK_FUA;
			} else if (!strcmp(optarg, "discard")) {
				check_mode = CHECK_DISCARD;
			} else {
				check_mode = CHECK_NUMBER;
				check_number = strtoull(optarg, &tmp, 0);
				if (!check_number || (tmp && *tmp != '\0')) {
					fprintf(stderr,
						"Invalid entry number\n");
					exit(1);
				}
				tmp = NULL;
			}
			break;
		case START_SECTOR:
			start_sector = strtoull(optarg, &tmp, 0);
			if (tmp && *tmp != '\0') {
				fprintf(stderr, "Invalid sector number\n");
				exit(1);
			}
			tmp = NULL;
			break;
		case END_SECTOR:
			end_sector = strtoull(optarg, &tmp, 0);
			if (tmp && *tmp != '\0') {
				fprintf(stderr, "Invalid sector number\n");
				exit(1);
			}
			tmp = NULL;
			break;
		default:
			usage();
		}
	}

	if (!logfile)
		usage();

	log = log_open(logfile, replayfile);
	if (!log)
		exit(1);
	free(logfile);
	free(replayfile);

	if (!discard)
		log->flags |= LOG_IGNORE_DISCARD;

	log->start_sector = start_sector;
	log->end_sector = end_sector;

	entry = malloc(log->sectorsize);
	if (!entry) {
		fprintf(stderr, "Couldn't allocate buffer\n");
		log_free(log);
		exit(1);
	}

	if (start_mark) {
		ret = seek_to_mark(log, entry, start_mark);
		if (ret)
			exit(1);
		free(start_mark);
	} else {
		ret = log_seek_entry(log, start_entry);
		if (ret)
			exit(1);
	}

	if ((fsck_command && !check_mode) || (!fsck_command && check_mode))
		usage();

	/* We just want to find a given entry */
	if (find_mode) {
		while ((ret = log_seek_next_entry(log, entry, 1)) == 0) {
			num_entries++;
			if ((run_limit && num_entries == run_limit) ||
			    should_stop(entry, stop_flags, end_mark)) {
				printf("%llu@%llu\n",
				       (unsigned long long)log->cur_entry - 1,
				       log->cur_pos / log->sectorsize);
				log_free(log);
				return 0;
			}
		}
		log_free(log);
		if (ret < 0)
			return ret;
		fprintf(stderr, "Couldn't find entry\n");
		return 1;
	}

	/* Used for scripts, just print the number of entries in the log */
	if (print_num_entries) {
		printf("%llu\n", (unsigned long long)log->nr_entries);
		log_free(log);
		return 0;
	}

	/* No replay, just spit out the log info. */
	if (!replayfile) {
		printf("Log version=%d, sectorsize=%lu, entries=%llu\n",
		       WRITE_LOG_VERSION, (unsigned long)log->sectorsize,
		       (unsigned long long)log->nr_entries);
		log_free(log);
		return 0;
	}

	while ((ret = log_replay_next_entry(log, entry, 1)) == 0) {
		num_entries++;
		if (fsck_command) {
			if ((check_mode == CHECK_NUMBER) &&
			    !(num_entries % check_number))
				ret = run_fsck(log, fsck_command);
			else if ((check_mode == CHECK_FUA) &&
				 should_stop(entry, LOG_FUA_FLAG, NULL))
				ret = run_fsck(log, fsck_command);
			else if ((check_mode == CHECK_FLUSH) &&
				 should_stop(entry, LOG_FLUSH_FLAG, NULL))
				ret = run_fsck(log, fsck_command);
			else if ((check_mode == CHECK_DISCARD) &&
				 should_stop(entry, LOG_DISCARD_FLAG, NULL))
				ret = run_fsck(log, fsck_command);
			else
				ret = 0;
			if (ret) {
				fprintf(stderr, "Fsck errored out on entry "
					"%llu\n",
					(unsigned long long)log->cur_entry - 1);
				break;
			}
		}

		if ((run_limit && num_entries == run_limit) ||
		    should_stop(entry, stop_flags, end_mark))
			break;
	}
	fsync(log->replayfd);
	log_free(log);
	free(end_mark);
	if (ret < 0)
		exit(1);
	return 0;
}
