/*
    Copyright (C) 2012 Bartosz Broda, Jacek Bochenek, Łukasz Biłeńkij, 
    Damian Jaworski, Wojciech Mazur, Dominik Piasecki, Maciej Piasecki, 
    Radosław Ramocki
    
    This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option)
any later version.

    This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. 

    See the LICENSE and COPYING files for more details.
*/

#include "tuplesserver.h"
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>

#include <boost/foreach.hpp>
#define foreach BOOST_FOREACH

#include "tprocessor.h"

namespace tuplesd{

TuplesServer::TuplesServer(const options &opts)
	: opts_(opts), processor_(opts.tuplesproc)
{
	processing_id_ = -1;
}

TuplesServer::~TuplesServer()
{
	CleanUp();
}
bool TuplesServer::CleanUp()
{
	if(opts_.print_text)
		std::cout << "Cleaning up...\n";

	Connect(); // try to connect
	std::stringstream idss, ss;
	idss << opts_.ip << ":" << opts_.port_no;
	std::string idss_str = idss.str();
	ss << "DELETE from matrix_daemons WHERE id ='" << idss_str << "';";
	mysqlpp::Query query = connection_.query();
	mysqlpp::SimpleResult result = query.execute(ss.str());

	if(!result)
	{
		std::cerr << "Cannot remove this instance of tuplesd from database: "
				  << idss_str << "\nProblem: " << query.error() << std::endl;

		return false;
	}

	RejectLastRequst();

	if(opts_.print_text)
		std::cout << "Cleaning up... successful" << std::endl;



	return true;

}
void TuplesServer::Connect()
{
	if(! connection_.connected() || (! connection_.ping() ) )
	{
		if(opts_.print_text)
			std::cout << "--------- Connecting ---------" << std::endl;
		if(! connection_.connect(opts_.mysql_db.c_str(),
								 opts_.mysql_host.c_str(),
								 opts_.mysql_user.c_str(),
								 opts_.mysql_pass.c_str(),
								 opts_.mysql_port) )
		{
			std::cerr << "Cannot connect to db" << std::endl;
			std::cerr << opts_.mysql_db << " " << opts_.mysql_port
					  << " " << opts_.mysql_host << " "
					  << opts_.mysql_user << " "
					  << opts_.mysql_pass << "\n";
			exit(-1);
		}
		if(opts_.print_text)
			std::cout << "Conecttion successful.\n";
	}
}

void TuplesServer::Run()
{
	if(opts_.print_text)
		std::cout << "Starting main event loop..." << std::endl;
	// add tuplesd to db (working state), check if it wasn't added before
	UpdateMyStatus(false);
	while(true)
	{
		// process requests
		ProcessRequests();
		// listen for processing
		Listen();
	}
}
void TuplesServer::ProcessRequests()
{
	UpdateMyStatus(false);
	while(true)	{
		if (opts_.print_text)
			std::cout << "Processing requests..." << std::endl;

		// LOCK
		mysqlpp::Query query = connection_.query();
		mysqlpp::SimpleResult result = query.execute("LOCK TABLES matrices WRITE;");
		bool no_work = false;
		if(result){
			if (opts_.print_text)
				std::cout << "Locked matrices..." << std::endl;


			request rq = SearchForWork();

			if( rq.matrix_id >= 0 ){// if there is: change status of the request


				std::stringstream ss;
				ss << "UPDATE matrices SET state = 'PROCESSING' "
					  << "WHERE matrix_id = "  << rq.matrix_id << ";";
				mysqlpp::Query query = connection_.query(ss.str());
				query.execute();

			}
			else
				no_work = true;

			result = query.execute("UNLOCK TABLES;");
			if (opts_.print_text)
			{
				if(result)
					std::cout << "Unlocked..." << std::endl;
				else
					std::cout << "Problem with unlocking db: " << result.info() << std::endl;
			}


			if(no_work)
			{
				if (opts_.print_text)
					std::cout << "Nothing to process" << std::endl;
				return;
			}
			else{
				if (opts_.print_text)
					std::cout << "Will process matrix " << rq.name 	<< "For user"
							  << rq.user_id << std::endl;

				try{
					processing_id_ = rq.matrix_id;
					std::string tpath = DeterminePath(rq.matrix_id, opts_.tuples_dir, true);
					std::string mdir = DeterminePath(rq.matrix_id, opts_.matrix_dir, false);
					processor_.ProcessTuples(tpath, mdir , rq.name);

					std::stringstream ss;
					ss << "UPDATE matrices SET state = 'READY' WHERE matrix_id = "
					   << rq.matrix_id  << ";";
					mysqlpp::Query query2 = connection_.query(ss.str());
					query2.execute();
					processing_id_ = -1;
				}catch(ProcessorError &e){
					RejectLastRequst(e.what());
				}catch(...){
					RejectLastRequst();
				}


			}

		}else{
			std::cerr << "Could not lock matrices table. "
					  << result.info() << std::endl;
		}
	}
}

request TuplesServer::SearchForWork()
{
	request rq;
	std::string q = "SELECT matrix_id, name, user_id FROM matrices WHERE state = 'QUEUED' ORDER BY matrix_id LIMIT 1;";
	mysqlpp::Query query = connection_.query(q);

	if (mysqlpp::StoreQueryResult res = query.store()){
		if(res.num_rows() > 0){
			rq.user_id = boost::lexical_cast<long> (res[0]["user_id"]);
			rq.matrix_id = boost::lexical_cast<long> (res[0]["matrix_id"]);
			rq.name = std::string(res[0]["name"]);
			return rq;

		}
	}

	return rq;
}

std::string TuplesServer::DeterminePath(long matrix_id, const std::string& startdir,
										bool attach_matrixname)
{
	std::stringstream q;
	q << "SELECT user_id, name FROM matrices WHERE matrix_id = "
		 << matrix_id << " LIMIT 1;";


	mysqlpp::Query query = connection_.query(q.str());
	if (mysqlpp::StoreQueryResult res = query.store()){
		if(res.num_rows() > 0){
			std::string matrix_name = std::string(res[0]["name"]);
			std::string user_id= std::string(res[0]["user_id"]);
			boost::filesystem::path p(startdir);
			p /= user_id;
			if(attach_matrixname)
				p /= matrix_name;
			return p.string();
		}
	}

	return std::string();

}

void TuplesServer::Listen()
{
	// set this tuplesd db state  to ready
	UpdateMyStatus(true);
	using namespace std;
	using boost::asio::ip::tcp;
	if (opts_.print_text)
		cout << "Starting tuplesd server. Listening on port "
			 << opts_.port_no << "." << endl;

	boost::asio::io_service io_service;

	tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(),
													 opts_.port_no));
	for (;;)
	{
		string message;
		boost::array<char, 4096> buf;
		boost::system::error_code error;

		tcp::socket socket(io_service);
		acceptor.accept(socket);

		size_t len = socket.read_some(boost::asio::buffer(buf), error);
		if (len == 0)
			continue;

		message = buf.data();
		if (opts_.print_text)
			std::cout << "Got message by tcp/ip: " << message << std::endl;

		std::string accept_and_queue = "2";
		boost::asio::write(socket, boost::asio::buffer(accept_and_queue),
						   boost::asio::transfer_all(), error);
		socket.close();
		Connect();
		return;
	}

}

void TuplesServer::UpdateMyStatus(bool ready)
{
	Connect();
	std::stringstream idss;
	idss << opts_.ip << ":" << opts_.port_no;
	mysqlpp::Query query = connection_.query();
	// lock write
	mysqlpp::SimpleResult result =
			query.execute("LOCK TABLES matrix_daemons WRITE;");
	if(result)
	{
		// check if I need to update my info
		std::stringstream ss;
		ss << "SELECT * FROM matrix_daemons WHERE id = '" << idss.str() << "';" ;
		mysqlpp::StoreQueryResult res = query.store(ss.str());
		if(res.num_rows() != 0)
		{
			// if update only: do it now!
			std::stringstream ss;
			ss << "UPDATE matrix_daemons SET ready = '" << (ready?"1":"0") << "' WHERE id ='" << idss.str() << "';";
			mysqlpp::SimpleResult result = query.execute(ss.str());
			if(!result)
			{
				std::cerr << "Cannot update matrix_daemons state!" << std::endl;
				result = query.execute("UNLOCK TABLES;");
				std::stringstream ss;
				ss << "DELETE from matrix_daemons WHERE id ='" << idss.str() << "';";
				result = query.execute(ss.str());
				exit(-1);
			}
		}
		else
		{
			// else : insert
			std::stringstream ss;
			ss << "INSERT INTO matrix_daemons() VALUES('" << idss.str() << "', '" << (ready?"1":"0") << "');";
			mysqlpp::SimpleResult result = query.execute(ss.str());
			if(!result)
			{
				std::cerr << "Cannot add tuplesd daemon to a table!"
						  << std::endl;
				result = query.execute("UNLOCK TABLES;");
				std::stringstream ss;
				ss << "DELETE from matrix_daemon WHERE id ='"
				   << idss.str() << "';";
				result = query.execute(ss.str());
				exit(-1);
			}
		}
	}
	else
	{
		std::cerr << "Cannot lock table matrix_daemon. " << result.info() << std::endl;
		std::stringstream ss;
		ss << "DELETE from matrix_daemon WHERE id ='"
		   << idss.str() << "';";
		result = query.execute(ss.str());
		exit(-1);
	}
	result = query.execute("UNLOCK TABLES;");
}
void TuplesServer::RejectLastRequst(const std::string &problem)
{
	if(processing_id_ > 0){
		if(opts_.print_text)
			std::cout << "Trying to reject last matrix processed" << std::endl;
		std::stringstream ss;
		ss << "UPDATE matrices SET state = 'REJECTED', "
			  << "system_comment=\""<< problem <<"\" "
			  << "WHERE matrix_id = " << processing_id_
			  << ";";
		mysqlpp::Query query2 = connection_.query(ss.str());
		query2.execute();
		processing_id_ = -1;
	}
}
} // ns tuplesd
