#include "OptimizerBase.h"
#include <iostream>
#include <filesystem>
#include <stdio.h>
#include <stdlib.h>

namespace mdd{
	bool OptimizerBase::Permutation::operator== (const Permutation& ind) {
		return (dna == ind.dna);
	}

	bool OptimizerBase::Permutation::same(Permutation const& a, Permutation const& b) {
		bool check1 = (a.dna == b.dna);
		bool check2 = true;
		if (a.dna.size() != b.dna.size())
		{
			check2 = false;
		}
		else {
			for (size_t i = 0; i < a.dna.size(); i++)
			{
				if (a.dna[i].size() != b.dna[i].size())
				{
					check2 = false;
				}
				else
				{
					for (size_t j = 0; j < a.dna[i].size(); j++)
					{
						if (a.dna[i][j] != b.dna[i][j])
						{
							check2 = false;
						}
					}
				}

			}
		}
		if (check1 != check2)
		{
			int test = 0;
		}
		return check1;
	}

	bool OptimizerBase::Permutation::smaller(Permutation const& a, Permutation const& b) {
		return (a.dna < b.dna);
	}


	bool OptimizerBase::connect(std::shared_ptr<IModule> module) {
		_module = module;
		return true;
	}

	OptimizerBase::opt_state OptimizerBase::updateOutputs()
	{
		opt_state ret;
		ret.module_state = _module->update();
		if (ret.module_state != state::STATE_ERROR)
		{
			for (size_t i = 0; i < _output_vals.size(); ++i)
			{
				_output_vals[i] = _outputs[i]->getValue()[0];
				//std::cout << "get: " << i << ": " << _output_vals[i] << std::endl;
			}
			ret.opt_value = _func_expr.value();
			//std::cout << "get: " << "opt" << ": " << ret.opt_value << std::endl;
		}

		return ret;
	}

	OptimizerBase::OptimizerBase(const std::string& base_config) {
		json jparse = json::parse(base_config);
		if (jparse.contains("configure"))
		{
			_base_config = base_config;
		}
		else {
			_base_config["configure"] = jparse;
		}
		
		json jstate;
		jstate["value"] = "stop";
		jstate["options"].push_back("run");
		jstate["options"].push_back("pause");
		jstate["options"].push_back("stop");

		json jevalue;
		jevalue["value"] = "";

		json jsub;
		jsub["state"] = jstate;
		jsub["evaluate"] = jevalue;
		_base_config["optimizer"] = jsub;
	}

	void OptimizerBase::updateLayout()
	{
		if (_module != nullptr)
		{
			_inputs = _module->getOptimizableInputs();
			_outputs = _module->getOptimizableOutputs();
		}
		_output_vals.clear();
		if (_inputs.empty())
		{
			std::cout << "ERROR: No optimizable inputs detected!" << std::endl;
		}
		if (_outputs.empty())
		{
			std::cout << "ERROR: No optimizable outputs detected!" << std::endl;
		}
		for (auto& out : _outputs)
		{
			_output_vals.push_back(out->getValue()[0]);
		}
	}
	
	bool OptimizerBase::setEvaluation(std::string func)
	{
		updateLayout();
		exprtk::symbol_table<double> symbol_table;
		for (size_t i = 0; i < _output_vals.size(); ++i)
		{
			symbol_table.add_variable("out" + std::to_string(i), _output_vals[i]);
		}
		symbol_table.add_constants();
	
		_func_expr.register_symbol_table(symbol_table);
	
		exprtk::parser<double> parser;
		return parser.compile(func, _func_expr);
	}
	
	bool OptimizerBase::configure(const json& config) {
		json jconfig = config;
		auto jit = jconfig.find("optimizer");
		if (jit != jconfig.end())
		{
			auto jstate_it = jit->find("state");
			if (jstate_it != jit->end())
			{
				std::string key = (*jstate_it)["value"].get<std::string>();
				if (key == "run")
				{
					if (_base_config["optimizer"]["state"]["value"] == "stop")
					{
						reset();
					}
					
					_base_config["optimizer"]["state"]["value"] = "run";
					updateLayout();
					_thread_ptr = std::make_shared<std::thread>(&OptimizerBase::update,this);
				}
				else if (key == "pause") {
					_base_config["optimizer"]["state"]["value"] = "pause";
				}
				else {//stop
					_base_config["optimizer"]["state"]["value"] = "stop";
				}
			}
			jstate_it = jit->find("evaluate");
			if (jstate_it != jit->end()) {
				updateLayout();
				setEvaluation((*jstate_it)["value"].get<std::string>());
				_base_config["optimizer"]["evaluate"]["value"] = (*jstate_it)["value"].get<std::string>();
			}

		}
		jit = jconfig.find("configure");
		if (jit != jconfig.end())
		{
			return configureChild(jit.value());
		}
		return false;
	}

	bool OptimizerBase::configureChild(const json& config) {
		return true;
	}

	const json& OptimizerBase::getConfiguration() {
		return _base_config;
	}
	//virtual std::string getGeneratorID() = 0;
	void OptimizerBase::load(const json& j) {
		configure(j);
		auto it = j.find("db");
		if (it != j.end())
		{
			createDB(it->get<std::string>());
		}
		else {
			createDB("C:/Users/Willi/Documents/projects/mdd/lib/test/db/optimization.db");
		}
		
		loadDB(j);
	}
	json OptimizerBase::dump() {
		json ret = _base_config;
		ret["ID"] = getIdentifier();
		ret["type"] = type;
		ret["key"] = key;

		ret["db"] = "C:/Users/Willi/Documents/projects/mdd/lib/test/db/optimization.db";

		ret["permutations"] = dumpPermutations();
		ret["steps"] = dumpSteps();
		return ret;
	}

	json OptimizerBase::getIdentifier() {
		json jID;
		/*
		jID["name"] = _name;
		jID["id"] = getAppendix();*/
		jID["type"] = type;
		jID["key"] = key;
		//jID["prefix"] = std::vector<std::string>();
		return jID;
	}

	void OptimizerBase::attachCallback(std::function<json(const json&)> callback) {
		_callback = callback;
	}

	void OptimizerBase::callback_add_step(const std::vector<Permutation>& pers){
		json ret;
		ret["step"] = step;
		_mutex.lock();
		for (int i = 0; i < pers.size(); ++i)
		{
			auto ind = Permutation_to_JSON(pers[i]);
			addPermutation(ind);
			addStep(step, pers[i].dna);
			ret["permutations"].push_back(ind);
		}
		_mutex.unlock();
		json jcall;
		jcall["operation"] = "add";
		jcall["args"]["subject"] = getIdentifier();
		jcall["args"]["object"] = ret;
		_callback(jcall);
	}

	void OptimizerBase::callback_evaluate_opt(const Permutation& per) {
		json ret;
		ret["step"] = step;
		
		json jind = Permutation_to_JSON(per);
		jind["processor"] = _module->dump();
		changePermutation(jind);
		ret["permutation"] = jind;
		json jcall;
		jcall["operation"] = "change";
		jcall["args"]["subject"] = getIdentifier();
		jcall["args"]["object"] = ret;
		_callback(jcall);
	}

	void OptimizerBase::callback_autosave(){
		_mutex.lock();
		_base_config["optimizer"]["state"]["value"] = "pause";
		json jcall;
		jcall["operation"] = "autosave";
		_callback(jcall);
		_base_config["optimizer"]["state"]["value"] = "run";
		_mutex.unlock();
	}

	void OptimizerBase::callback_reset() {
		json ret;
		ret["step"] = step;
		
		json jcall;
		jcall["operation"] = "reset";
		jcall["args"]["subject"] = getIdentifier();
		jcall["args"]["object"] = ret;
		_callback(jcall);
	}

	void OptimizerBase::reset(){
		step = -1;
		callback_reset();
	}

	bool OptimizerBase::reachAbort(const std::vector<Permutation>& pers) {
		return true;
	}

	OptimizerBase::~OptimizerBase() {
		if (_thread_ptr != nullptr)
		{
			_thread_ptr->join();
		}
	}

	int sql_callback(void* data, int argc, char** argv, char** azColName) {
		static_cast<OptimizerBase*>(data)->sql_callback(argc, argv, azColName);
		return 0;
	}

	bool OptimizerBase::exec_sql(const std::string& sql) {
		char* zErrMsg = 0;
		int rc;
		sqlite3* _db;

		// Open database
		//if (std::filesystem::exists(_db_path)) {
		//	sqlite3_open(_db_path.c_str(), &_db);
		//	return true;
		//}
		rc = sqlite3_open(_db_path.c_str(), &_db);

		if (rc) {
			fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(_db));
			return false;
		}
		else {
			//fprintf(stdout, "Opened database successfully\n");
		}


		// Execute SQL statement
		rc = sqlite3_exec(_db, sql.c_str(), mdd::sql_callback, this, &zErrMsg);

		if (rc != SQLITE_OK) {
			fprintf(stderr, "SQL error: %s\n", zErrMsg);
			sqlite3_free(zErrMsg);
			sqlite3_close(_db);
			return false;
		}

		sqlite3_close(_db);

		return true;
	}

	json OptimizerBase::exec_sql_with_callback(const std::string& sql) {
		char* zErrMsg = 0;
		int rc;
		sqlite3* _db;
		_jcall = json::basic_json();

		// Open database
		rc = sqlite3_open(_db_path.c_str(), &_db);

		if (rc) {
			fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(_db));
		}
		else {
			//fprintf(stdout, "Opened database successfully\n");
		}
		
		// Execute SQL statement
		rc = sqlite3_exec(_db, sql.c_str(), mdd::sql_callback, this, &zErrMsg);

		if (rc != SQLITE_OK) {
			fprintf(stderr, "SQL error: %s\n", zErrMsg);
			sqlite3_free(zErrMsg);
			sqlite3_close(_db);
			return json::basic_json();
		}
		sqlite3_close(_db);

		return _jcall;
	}

	int OptimizerBase::sql_callback(int argc, char** argv, char** azColName) {
		json jelem;
		for (size_t i = 0; i < argc; i++)
		{
			jelem[azColName[i]] = std::string((argv[i] ? argv[i] : "NULL"));
		}
		_jcall.push_back(jelem);
		//printf(jelem.dump().c_str());
		///TODO test
		return 0;
	}

	json OptimizerBase::Permutation_to_JSON(const Permutation& per) {
		json ret;
		ret["dna"] = per.dna;
		ret["fitness"] = per.fitness;
		ret["status"] = per.status;
		ret["time"] = per.time;
		return ret;
	}

	OptimizerBase::Permutation OptimizerBase::JSON_to_Permutation(const json& j) {
		Permutation per;
		json jelem;
		if (j.is_array())
		{
			jelem = *j.begin();
		}
		else {
			jelem = j;
		}
		auto ittest = jelem.find("dna");
		if (ittest == jelem.end())
		{
			int test = 0;
		}
		json jval = json::parse(jelem["dna"].get<std::string>());
		per.dna = jval.get<std::vector<std::vector<double>>>();

		jval = json::parse(jelem["fitness"].get<std::string>());
		per.fitness = jval.get<double>();
		auto it = jelem.find("status");
		if (it != jelem.end())
		{
			jval = json::parse(it->get<std::string>());
			per.status = jval.get<state>();
		}
		it = jelem.find("time");
		if (it != jelem.end())
		{
			jval = json::parse(it->get<std::string>());
			per.time = jval.get<double>();
		}
		
		return per;
	}

	bool OptimizerBase::createDB(const std::string& db_path) {
		_db_path = db_path;
		if (std::filesystem::exists(_db_path)) {
			clearDB();
		}
		sqlite3* _db;
		int rc;
		char* sql;

		bool ret;

		// Create SQL statement
		sql = "CREATE TABLE PERMUTATIONS("  \
			"dna		TEXT	NOT NULL	PRIMARY KEY," \
			"fitness	FLOAT," \
			"time		FLOAT," \
			"status		TEXT," \
			"processor	TEXT" \
			");";

		ret = exec_sql(sql);

		// Create SQL statement
		sql = "CREATE TABLE STEPS("  \
			"step		INTEGER		NOT NULL," \
			"dna		TEXT		NOT NULL," \
			"CONSTRAINT PK_PERMUTATIONS PRIMARY KEY (step, dna)"
			");";

		ret = ret && exec_sql(sql);
		return ret;
	}

	bool OptimizerBase::clearDB() {

		std::string sql;
		sql = "DELETE FROM PERMUTATIONS;";
		exec_sql(sql);
		sql = "DELETE FROM STEPS;";
		return exec_sql(sql);
	}

	bool OptimizerBase::loadDB(const json& j) {
		bool ret = true;
		bool found = false;
		clearDB();
		auto it = j.find("permutations");
		if (it != j.end())
		{
			found = true;
			ret = ret && loadPermutationDB(*it);
		}

		it = j.find("steps");
		if (it != j.end())
		{
			found = found && true;
			ret = ret && loadStepDB(*it);
		}
		step = -1;
		if (found)
		{
			std::vector<Permutation> bests;
			do
			{
				++step;
				bests = findBest(step);
				if (bests.empty())
				{
					--step;
					break;
				}
			} while (!reachAbort(bests));
		}

		return ret && found;
	}

	bool OptimizerBase::loadStepDB(const json& j) {
		bool ret = true;
		for (size_t i = 0; i < j.size(); i++)
		{
			json jstep = j.at(i);
			ret = ret && addStep(json::parse(jstep["step"].get<std::string>()).get<int>(), json::parse(jstep["dna"].get<std::string>()));
		}
		return ret;
	}

	bool OptimizerBase::loadPermutationDB(const json& j){
		bool ret = true;
		for (size_t i = 0; i < j.size(); i++)
		{
			ret = ret && addPermutation(j.at(i));
		}
		return ret;
	}
	

	json OptimizerBase::dumpPermutations() {
		std::string sql = "SELECT * FROM PERMUTATIONS;";
		json ret = exec_sql_with_callback(sql);
		return ret;
	}

	json OptimizerBase::dumpSteps() {
		std::string sql = "SELECT * FROM STEPS;";
		json ret = exec_sql_with_callback(sql);
		return ret;
	}

	bool OptimizerBase::addPermutation(const json& jper) {
		// Create SQL statement
		std::string sval = "'";
		if (jper["dna"].is_string())
		{
			sval += jper["dna"].get<std::string>();
		}
		else {
			sval += jper["dna"].dump();
		}
		sval += "', " + jper["fitness"].dump() + ", " + jper["time"].dump() + ", " + jper["status"].dump();
		auto it = jper.find("processor");
		if (it != jper.end())
		{
			if (it->is_string())
			{
				sval += ", '" + it->get<std::string>() + "'";
			}
			else {
				sval += ", '" + it->dump() + "'";
			}
		}
		else
		{
			sval += ", NULL";
		}
		
		std::string sql;
		sql =	"INSERT INTO PERMUTATIONS (dna, fitness, time, status, processor) "\
				"VALUES (" + sval + "); ";

		return exec_sql(sql);
	}

	bool OptimizerBase::addStep(int step, json dna) {
		std::string  sql = "INSERT INTO STEPS (step, dna) "\
			"VALUES (" + std::to_string(step) + ", '" + dna.dump() + "');";

		return exec_sql(sql);
	}

	bool OptimizerBase::changePermutation(const json& jper) {
		//std::to_string((char)(per.status))
		std::string sval = "fitness = " + jper["fitness"].dump() + ", time = " + jper["time"].dump() + ", status = " + jper["status"].dump();
		auto it = jper.find("processor");
		if (it != jper.end())
		{
			sval += ", processor = '" + it->dump() + "' ";
		}
		std::string sql;
		sql =	"UPDATE PERMUTATIONS "\
				"SET " + sval + " "+ 
				"WHERE dna = '" + jper["dna"].dump() + "';";

		return exec_sql(sql);
	}

	std::shared_ptr<OptimizerBase::Permutation> OptimizerBase::findPermutation(std::vector<std::vector<double>> dna) {
		json jdna;
		jdna = dna;
		std::string sql =	"SELECT * FROM PERMUTATIONS "\
							"WHERE dna = '" + jdna.dump() + "' AND status = " + std::to_string((char)(state::CHANGED)) + "; ";
		json ret;
		ret = exec_sql_with_callback(sql);
		if (ret != nullptr)
		{
			return std::make_shared<Permutation>(JSON_to_Permutation(ret));
		}
		return ret;
	}

	std::shared_ptr<OptimizerBase::Permutation> OptimizerBase::getPermutation(int step, std::vector<std::vector<double>> dna) {
		json jdna;
		jdna = dna;
		std::string sql =	"SELECT p.dna, p.fitness, p.time, p.status FROM PERMUTATIONS AS p "\
							"LEFT JOIN STEPS AS s ON s.dna = p.dna "\
							"WHERE s.step = " + std::to_string(step) + " AND s.dna = '" + jdna.dump() + "';";
		json ret = exec_sql_with_callback(sql);
		if (ret != nullptr)
		{
			return std::make_shared<Permutation>(JSON_to_Permutation(ret));
		}
		return nullptr;
	}

	json OptimizerBase::getPermutations() {
		std::string sql;
		sql = "SELECT s.step, p.dna, p.fitness, p.time, p.status, p.processor FROM PERMUTATIONS AS p "\
			"LEFT JOIN STEPS AS s ON s.dna = p.dna; ";
		json ret = exec_sql_with_callback(sql);

		return ret;
	}

	std::vector<OptimizerBase::Permutation> OptimizerBase::findBest(int step) {
		std::string sql = "SELECT * FROM PERMUTATIONS "\
			"WHERE dna = ("\
			"SELECT p.dna FROM PERMUTATIONS AS p "\
			"LEFT JOIN STEPS AS s ON s.dna = p.dna "\
			"WHERE s.step = " + std::to_string(step) +" AND ROUND(p.fitness,3) = ( "\
			"SELECT MIN(ROUND(p.fitness,3)) FROM PERMUTATIONS AS p "\
			"LEFT JOIN STEPS AS s ON s.dna = p.dna "\
			"WHERE s.step = " + std::to_string(step)+"));";
		json ret = exec_sql_with_callback(sql);
		std::vector<Permutation> vec;
		if (ret.is_array())
		{
			for (size_t i = 0; i < ret.size(); i++)
			{
				vec.push_back(JSON_to_Permutation(ret[i]));
			}
			
		}
		return vec;
	}
}