/*******************************************************
 * Copyright (C) 2017-2020 CENOS Ltd vg@cenos-platform.com
 *
 * This file is part of CENOS.
 *
 * CENOS can not be copied and/or distributed without the express
 * permission of CENOS Ltd
 *******************************************************/
#include <thread> 
#include <future>

#include "nlohmann/json.hpp"
#include "Setup_Worker.h"
#include "JobManager.h"
#include "misc/messageQueue.hpp"
#include "Mesh_Worker.h"

#include "SalomeModule_Worker.h"
#include "FreeCadModule_Worker.h"
#include "Topology_Worker.h"
#include "Template_Worker.h"

#include "yamlExample.h"

using json = nlohmann::json;

JobManager::JobManager() 
{
	sendingEnabled_ = true;
	stopAllJobs = false;
};

JobManager::~JobManager() 
{
};


void JobManager::addJob(std::string input, websocketpp::connection_hdl hdl)
{
	try
	{
		if (Job::getCommandFromInput(input) == "stopExecution")
		{
			// if stopExecution received, we stop all current jobs
			// some jobs (e.g. runFreecad) does not have stop function implemented and it will not be impacted
			stopAllJobs = true;
			return;
		}
		if (Job::getCommandFromInput(input) == "close")
		{
			stopAllJobs = true;
			terminate = true;
			return;
		}

		auto job = std::make_shared<Job>(input, hdl);

		//if job with such guid already exists, put hdl of existing job 
		// so the answer is sent to initial caller
		std::lock_guard<std::mutex> lck(lck_);
		for (auto& j : runningJobs_)
		{
			if (j->guid() == job->guid())
			{
				auto sub_job = std::make_shared<Job>(input, j->hdl());
				j->addSubJob(sub_job);

				jobQueue_.push_back(sub_job);
				return;
			}
		}

		jobQueue_.push_back(job);
		return;
	}
	catch (messagedata & errorMsg)
	{
		send(hdl, errorMsg.outputData());
	}
	catch (std::exception &e)
	{
		std::string errStr = std::string(e.what());
		messagedata error = messagedata(messagedata::status::error, messagedata::type::response, errStr);
		error.sendTo = "gui";
		send(hdl, error.outputData()); 
	}
}



void JobManager::run()
{
	try
	{
		std::vector<std::future<void>> jobFutures;

		pluginManager.Init();

		while (!terminate)
		{
			// execute all jobs currently in jobQueue
			while (!jobQueue_.empty())
			{
				std::lock_guard<std::mutex> lck(lck_);
				jobFutures.push_back(std::async(std::launch::async,
					&JobManager::executeJob,
					this,
					jobQueue_.front()));
				// add job to running job vector
				runningJobs_.push_back(jobQueue_.front());
				jobQueue_.pop_front();
			}

			Sleep(10);

			// stop jobs if called stopExecution
			if (stopAllJobs)
			{
				for (auto& job : runningJobs_)
				{
					job->changeStateToStopRequested();
				}
				stopAllJobs = false;
			}

			// process finished jobs
			auto it = jobFutures.begin();
			while (it != jobFutures.end())
			{
				if (it->wait_for(std::chrono::milliseconds(1)) == std::future_status::ready)
				{
					std::lock_guard<std::mutex> lck(lck_);
					it->get();
					it = jobFutures.erase(it);
				}
				else
				{
					++it;
				}
			}

			// delete running jobs if finished
			auto jobIt = runningJobs_.begin();
			while (jobIt != runningJobs_.end())
			{
				if ((*jobIt)->isFinished())
				{
					jobIt = runningJobs_.erase(jobIt);
				}
				else
				{
					++jobIt;
				}
			}
		}

		for (auto it = jobFutures.begin(); it != jobFutures.end(); it++) {
			if (it->valid()) {
				it->wait();
			}
		}
	}
	catch (std::exception e)
	{
		std::cout << "Exception caught in JobManager : " << e.what();
		LOG(ERROR) << "Exception caught in JobManager : " << e.what();
	}
	catch (std::string e)
	{
		std::cout << "Exception caught in JobManager : " << e;
		LOG(ERROR) << "Exception caught in JobManager : " << e;
	}
}

bool JobManager::hasJobs()
{
	if (jobQueue_.empty())
		return false;
	else
		return true;
}

void JobManager::disableSending()
{
	sendingEnabled_ = false;
}

void JobManager::executeJob(std::shared_ptr<Job> job)
{
	websocketpp::connection_hdl hdl = job->hdl();
	std::string command = job->command();
	std::string guid = job->guid();
	job->changeStateToRunning();
	//start first job from queue
	LOG(INFO) << "Executing job " << command;

	MessageQueue_ messageQueue = std::make_shared<MessageQueue>();

	messagedata reply;

	try
	{
		
		//run job asynchronously
		std::shared_ptr<Worker> worker = prescribeWorker(job, pluginManager);
		auto jobFuture = std::async(std::launch::async, &Worker::run, worker, messageQueue);
		//while job not finished, send the data from messagaQueue
		while (jobFuture.wait_for(std::chrono::milliseconds(1)) != std::future_status::ready) {
			while (!messageQueue->empty())
			{
				//LOG(INFO) << "log message " + messageQueue.frontData();
				send(hdl, messageQueue->frontData());
				messageQueue->pop();
			}
			Sleep(10);
		}

		// construct reply message
		reply = jobFuture.get();
		reply.sendTo = "gui";
		LOG(INFO) << "Finished job " << command;
	}
	catch (std::string errStr)
	{
		LOG(ERROR) << errStr;
		reply.sendTo = "gui";
		reply = messagedata(messagedata::status::error, messagedata::type::response, errStr, guid);
	}


	//check if job has subjobs and wait for them to finish
	// CP - 1413
	size_t sub_job_count = job->getSubJobs().size();
	if (sub_job_count  >0)
	{
		bool all_subjobs_finished = false;
		while (!all_subjobs_finished)
		{
			int count = 0;
			for (auto j : job->getSubJobs())
			{
				if (j->isFinished())
					count++;
			}
			if (count == sub_job_count)
				all_subjobs_finished = true;
		}
	}


	send(hdl, reply.outputData());
	LOG(INFO) << "sending reply " + reply.outputData();
//	send(hdl, messagedata(messagedata::info_lvl, messagedata::log, "Finished command " + command).outputData());
	job->changeStateToFinished();
}

void JobManager::send(websocketpp::connection_hdl hdl_, std::string data)
{
 	if (sendingEnabled_)
		client_->send(hdl_, data, websocketpp::frame::opcode::text);
}


std::shared_ptr<Worker> JobManager::prescribeWorker(std::shared_ptr<Job>&  j, PluginManager m)
{
	if (j->module() == "mesh")
		//return new Mesh_Worker(std::move(j));
		return std::make_shared<Mesh_Worker>(Mesh_Worker(j));
	if (j->module() == "setup")
		return std::make_shared<Setup_Worker>(Setup_Worker(j, m));
	else if (j->module() == "salome")
		return std::make_shared<SalomeModule_Worker>(SalomeModule_Worker(j));
	else if (j->module() == "freecad")
		return std::make_shared<FreeCadModule_Worker>(FreeCadModule_Worker(j));
	else if (j->module() == "topology")
		return std::make_shared<Topology_Worker>(Topology_Worker(j));
	else if (j->module() == "template")
		return std::make_shared<Template_Worker>(Template_Worker(j));
	else
		return std::make_shared<Worker>(Worker(j));
}


