// #include <cstdlib>
#include <fstream>
#include <iostream>
#include <cstring>

#include "../../src/core/util/smarthops/TraceReader.hpp"
#include "BasicAlgorithm.hpp"
#include "SmartAlgorithm.hpp"
#include "SimpleAlgorithm.hpp"
#include "ResultCollector.hpp"

#include "../../src/core/util/gzstream/gzstream.h"

#include "util/CommandLine.hpp"
#include "util/Logger.hpp"

#define STDOUT_CMD_STRING "-"

using namespace fail;

typedef enum {
	ALGO_SIMPLE,
	ALGO_SMART,
} algorithm_e;

bool g_use_weights;
bool g_use_watchpoints;
bool g_use_checkpoints;

unsigned int g_cp_thresh;
unsigned int g_cost_cp;
unsigned int g_rollback_thresh;

std::ofstream g_cp_ofstream;

Logger LOG("hop-calculator", false);

int main(int argc, char *argv[])
{
	// Manually fill the command line option parser
	CommandLine &cmd = CommandLine::Inst();

	cmd.addOption("", "", Arg::None, "USAGE: compute-hops [options]");
	CommandLine::option_handle HELP = cmd.addOption("h", "help", Arg::None, "-h,--help \tPrint usage and exit");

	CommandLine::option_handle ALGORITHM =
		cmd.addOption("a", "algorithm", Arg::Required,
			"-a,--algorithm \tHop algorithm (\"simple\"/\"smart\", default: \"none\")");

	CommandLine::option_handle OUTPUT_MODE =
		cmd.addOption("m", "output-mode", Arg::Required,
			"-m,--output-mode \tOutput mode (\"results\"/\"costs\"/\"statistics\", default: \"results\")");

	CommandLine::option_handle TRACE_FILE =
		cmd.addOption("i", "input-file", Arg::Required,
			"-i,--input-file \tInput trace file path");

	CommandLine::option_handle OUTPUT_FILE =
		cmd.addOption("o", "output-file", Arg::Required,
			"-o,--output-file \tOutput file, default: stdout");

	CommandLine::option_handle OUTPUT_FILE_PROTOBUF =
		cmd.addOption("b", "protobuf-output", Arg::None,
			"-b,--protobuf-output \tOutput file will be created in protobuf (HopChain) format");

	CommandLine::option_handle USE_COSTS =
		cmd.addOption("c", "use-costs", Arg::None,
			"-c,--use-costs \tUse hop costs for calculations of Smart-Hopping algorithm");

	CommandLine::option_handle USE_WATCHPOINTS =
		cmd.addOption("w", "use-watchpoints", Arg::None,
			"-w,--use-watchpoints \tUse watchpoints as additional hop candidates");

	CommandLine::option_handle USE_CHECKPOINTS =
		cmd.addOption("", "use-checkpoints", Arg::None,
			"--use-checkpoints \tUse checkpoints to cap costs in case of long hop chains");

	CommandLine::option_handle CP_THRESH =
		cmd.addOption("", "cp-costs-threshold", Arg::Required,
			"--cp-costs-threshold \tIf costs of a hop-chain reach this threshold, a checkpoint will be created");

	CommandLine::option_handle CP_COSTS =
		cmd.addOption("", "cp-costs", Arg::Required,
			"--cp-costs \tThe costs for reloading a checkpoint into the system");

	CommandLine::option_handle CP_ROLLBACK_THRESH =
		cmd.addOption("", "cp-rollback-threshold", Arg::Required,
			"--cp-rollback-threshold \tMinial number of hops to roll back current solution beyond checkpoint. Must be smaller than ((cp_cost_thresh - cp_costs) / 2)");

	CommandLine::option_handle CHECKPOINT_OUTPUT_FILE =
		cmd.addOption("", "cp-output", Arg::Required,
			"--cp-output \tCheckpoint output file");

	for (int i = 1; i < argc; ++i) {
		cmd.add_args(argv[i]);
	}
	if (!cmd.parse()) {
		std::cerr << "Error parsing arguments." << std::endl;
		return 1;
	}

	if (cmd[HELP]) {
		cmd.printUsage();
		if (cmd[HELP]) {
			exit(0);
		} else {
			exit(1);
		}
	}

	if (cmd[USE_CHECKPOINTS]) {
		g_use_checkpoints = true;
	} else {
		g_use_checkpoints = false;
	}

	if (cmd[USE_WATCHPOINTS]) {
		g_use_watchpoints = true;
	} else {
		g_use_watchpoints = false;
	}

	if (cmd[USE_COSTS]) {
		g_use_weights = true;
	} else {
		g_use_weights = false;
	}

	algorithm_e algorithm;

	if (cmd[ALGORITHM].count() > 0) {
		std::string alg(cmd[ALGORITHM].first()->arg);
		if (alg == "simple") {
			algorithm = ALGO_SIMPLE;
		} else if (alg == "smart") {
			algorithm = ALGO_SMART;
		} else {
			exit(-1);
		}
	} else {
		// default
		algorithm = ALGO_SMART;
	}

	output_mode_e output_mode;

	if (cmd[OUTPUT_MODE].count() > 0) {
		std::string outp(cmd[OUTPUT_MODE].first()->arg);
		if (outp == "results") {
			output_mode = OUTPUT_RESULT;
		} else if (outp == "costs") {
			output_mode = OUTPUT_COSTS;
		} else if (outp == "statistics") {
			output_mode = OUTPUT_STATISTICS;
		} else {
			LOG << "Unkown output mode: " << outp << std::endl;
			exit(-1);
		}
	} else {
		// default
		output_mode = OUTPUT_RESULT;
	}

	fail::TraceReader trace;

	if (cmd[TRACE_FILE].count() > 0) {
		const char *filename = cmd[TRACE_FILE].first()->arg;
		if (!trace.openTraceFile(filename, 0)) {
			LOG << "Unable to open and parse input trace file " << filename << std::endl;
			return 1;
		}
	} else {
		LOG << "Input trace file path not defined" << std::endl;
		exit(-1);
	}

	std::ostream *outFile;

	if (cmd[OUTPUT_FILE].count() > 0) {
		std::string filename(cmd[OUTPUT_FILE].first()->arg);

		if (filename.compare(STDOUT_CMD_STRING) == 0) {
			outFile = &std::cout;
		} else {
			std::ofstream *of = new std::ofstream(filename.c_str());
			if (!of->is_open()) {
				LOG << "Unable to open output file " << filename << std::endl;
				exit(-1);
			}
			outFile = of;
		}
	} else {
		outFile = &std::cout;
	}

	if (cmd[OUTPUT_FILE_PROTOBUF] && outFile == &std::cout) {
		LOG << "If protobuf output format is selected, output file must be defined" << std::endl;
		exit(-1);
	}

	if (cmd[USE_COSTS] && !(algorithm == ALGO_SMART)) {
		LOG << "Using costs for calculations is only possible with Smart-Hopping algorithm" << std::endl;
		exit(-1);
	}

	if (cmd[CP_THRESH]) {
		if (cmd[CP_THRESH].count() != 1) {
			LOG << "Could not parse cp-costs-threshold" << std::endl;
			exit(-1);
		}
		g_cp_thresh = strtoul(cmd[CP_THRESH].first()->arg, NULL, 0);
	}

	if (cmd[CP_COSTS]) {
		if (cmd[CP_COSTS].count() != 1) {
			LOG << "Could not parse cp-costs" << std::endl;
			exit(-1);
		}
		g_cost_cp = strtoul(cmd[CP_COSTS].first()->arg, NULL, 0);
	}

	if (cmd[CP_ROLLBACK_THRESH]) {
		if (cmd[CP_ROLLBACK_THRESH].count()!=1) {
			LOG << "Could not parse cp-rollback-threshold" << std::endl;
			exit(-1);
		}
//		if (strcmp(argv[9],"max")==0) {
//			g_rollback_thresh = (g_cp_thresh - g_cost_cp - 5)/2;
//		}
		g_rollback_thresh = strtoul(cmd[CP_ROLLBACK_THRESH].first()->arg, NULL, 0);
	}

	if (cmd[USE_CHECKPOINTS] && !(cmd[CHECKPOINT_OUTPUT_FILE].count() > 0)) {
		LOG << "If checkpointing is enabled, --cp-output must be defined" << std::endl;
		exit(-1);
	}

	if (cmd[CHECKPOINT_OUTPUT_FILE].count() > 0) {
		std::string filename(cmd[CHECKPOINT_OUTPUT_FILE].first()->arg);
		g_cp_ofstream.open(filename.c_str());
		if (!g_cp_ofstream.is_open()) {
			LOG << "Unable to open cp_out_file " << filename << std::endl;
			exit(-1);
		}
	}

	if (cmd[USE_CHECKPOINTS] && (!cmd[CP_THRESH] ||
			!cmd[CP_COSTS]|| !cmd[CP_ROLLBACK_THRESH])) {
		LOG << "If using checkpointing is enabled, also cp-costs-threshold, "
				"cp-costs and cp-rollback-threshold must be defined" << std::endl;
		exit(-1);
	}

	if (!cmd[USE_CHECKPOINTS] && (cmd[CP_THRESH].count() ||
			(cmd[CP_COSTS].count() > 0) || (cmd[CP_ROLLBACK_THRESH].count() > 0))) {
		LOG << "If using checkpointing is disabled, cp-costs-threshold, "
				"cp-costs and cp-rollback-threshold must not be defined" << std::endl;
		exit(-1);
	}

	if (g_use_checkpoints && !(g_cp_thresh > g_cost_cp)) {
		LOG << "cp_cost_thresh needs to be bigger than cp_costs" << std::endl;
		exit(1);
	}

	if (g_use_checkpoints && !(g_rollback_thresh < ((g_cp_thresh - g_cost_cp) / 2))) {
		LOG << "cp_rollback_hop_thresh needs to be smaller than "
				"((cp_cost_thresh - cp_costs) / 2)" << std::endl;
		exit(1);
	}


	ogzstream zipstream;

	ResultCollector rc(*outFile, output_mode);

	if (cmd[OUTPUT_FILE_PROTOBUF]) {
		zipstream.open(cmd[OUTPUT_FILE].first()->arg);
		rc.setProtoOStream(new ProtoOStream(&zipstream));
	}

	BasicAlgorithm *algo;

	switch (algorithm) {
	case ALGO_SMART:
		algo = new SmartAlgorithm(&rc);
		break;
	case ALGO_SIMPLE:
		algo = new SimpleAlgorithm(&rc);
		break;
	default:
		break;
	}

	rc.startTimer();
	algo->calculateAllHops(trace);
	rc.stopTimer();

	rc.finish();

	// ToDo: close output file if not stdout
	if (outFile != &std::cout) {
		((std::ofstream*)outFile)->close();
		delete (std::ofstream*)outFile;
	}

	if (g_use_checkpoints) {
		g_cp_ofstream.close();
	}

	return 0;
}
