//============================================================================
// Copyright 2009-2020 ECMWF.
// This software is licensed under the terms of the Apache Licence version 2.0
// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
// In applying this licence, ECMWF does not waive the privileges and immunities
// granted to it by virtue of its status as an intergovernmental organisation
// nor does it submit to any jurisdiction.
//============================================================================

#include "ServerComQueue.hpp"

#include "ClientInvoker.hpp"
#include "ServerComThread.hpp"
#include "ServerHandler.hpp"
#include "UIDebug.hpp"
#include "UiLogS.hpp"
#include "UserMessage.hpp"

#include "Log.hpp"

#include <QApplication>

#define _UI_SERVERCOMQUEUE_DEBUG

// This class manages the tasks to be sent to the ServerComThread, which controls
// the communication with the ClientInvoker. The ClientInvoker is hidden from the
// ServerHandler. The ServerHandler defines a task and sends it to
// ServerComQueue, which then passes it on to the ClientInvoker. When a task is finished
// ServerComQueue notifies the ServerHandler about it.

ServerComQueue::ServerComQueue(ServerHandler *server,ClientInvoker *client) :
	QObject(server),
	server_(server),
	client_(client),
    comThread_(nullptr),
	timeout_(5),
    ctStartTimeout_(1000),
    ctStartWaitTimeout_(500), //wait() is a blocking call, so it should be short
    startTimeoutTryCnt_(0),
    ctMaxStartTimeoutTryCnt_(4),
    state_(NoState), //the queue is enabled but not running
    taskStarted_(false),
    taskIsBeingFinished_(false),
    taskIsBeingFailed_(false)
{
	timer_=new QTimer(this);
	timer_->setInterval(timeout_);

	connect(timer_,SIGNAL(timeout()),
			this,SLOT(slotRun()));

    if(client_)
        createThread();
}

// the deletion is inititated by logout()
ServerComQueue::~ServerComQueue()
{
    UI_ASSERT(state_ == DisabledState, "state=" << state_);
    Q_ASSERT(comThread_ == nullptr);
}


void ServerComQueue::createThread()
{
    assert(client_);

    if(comThread_)
        delete comThread_;

    //We create a ServerComThread here. At this point the thread is not doing anything.
    comThread_=new ServerComThread(server_,client_);

    //When the ServerComThread starts it emits a signal that
    //is connected to the queue.
    connect(comThread_, SIGNAL(started()),
            this, SLOT(slotTaskStarted()));

    //When the ServerComThread finishes it emits a signal that
    //is connected to the queue.
    connect(comThread_, SIGNAL(finished()),
            this, SLOT(slotTaskFinished()));

    //When there is an error in ServerComThread it emits the
    //failed() signal that is connected to the queue.
    connect(comThread_, SIGNAL(failed(std::string)),
            this, SLOT(slotTaskFailed(std::string)));

    //The ServerComThread is observing the actual server and its nodes. When there is a change it
    //emits a signal to notify the ServerHandler about it.
    connect(comThread_,SIGNAL(nodeChanged(const Node*, std::vector<ecf::Aspect::Type>)),
                 server_,SLOT(slotNodeChanged(const Node*, std::vector<ecf::Aspect::Type>)));

    connect(comThread_,SIGNAL(defsChanged(std::vector<ecf::Aspect::Type>)),
                 server_,SLOT(slotDefsChanged(std::vector<ecf::Aspect::Type>)));

    connect(comThread_,SIGNAL(rescanNeed()),
                 server_,SLOT(slotRescanNeed()));
}


void ServerComQueue::enable()
{
	state_=NoState;
	start();
}

void ServerComQueue::disable()
{
	if(state_ == DisabledState)
		return;

    UiLogS(server_).dbg() << "ComQueue::disable -->";

	state_=DisabledState;

	//Remove all tasks
	tasks_.clear();

	//Stop the timer
    stopTimer();

    if (comThread_) {

        //If the comthread is running we need to wait
        //until it finishes its task.
        comThread_->wait();

        UiLogS(server_).dbg() << " queue is disabled";
    }

	//Clear the current task
	if(current_)
		current_.reset();

    taskStarted_=false;
}


//This is a special mode to reload the whole ClientInvoker
bool ServerComQueue::prepareReset()
{
	if(state_ == DisabledState || state_ == ResetState || state_ ==  SuspendedState)
		return false;

	//Stop the timer
    stopTimer();

	//Remove all tasks
	tasks_.clear();

    taskStarted_=false;

	state_=ResetState;

	//If the comthread is running we need to wait
	//until it finishes its task.
    if(comThread_ && comThread_->wait(5000))
    {
        //The thread cannot be running
        assert(comThread_->isRunning() == false);
        //assert(taskIsBeingFinished_==false);
        //assert(taskIsBeingFailed_==false);
        //assert(!current_);
        return true;
    }

    return false;
}

//This is a special mode to reload the whole ClientInvoker. Must be called after prepareReset returned true;
void ServerComQueue::reset()
{
	assert(state_ == ResetState);

    if (comThread_) {

        //The thread cannot be running
        assert(comThread_->isRunning() == false);

        //We send a Reset command to the thread!! This is the only task that is allowed
        //during the reset!!
        VTask_ptr task=VTask::create(VTask::ResetTask);
        tasks_.push_back(task);

        //TODO: why do we not run it directly
        //We start the timer with a shorter interval
        timer_->start(100);
    }
}

void ServerComQueue::endReset()
{
	if(state_ == ResetState)
	{
		state_=SuspendedState;
		start();
	}
}


//When the queue is started:
// -it is ready to accept tasks
// -its timer is running
void ServerComQueue::start()
{
    if(!client_ || !comThread_)
        return;

    if(state_ != DisabledState && state_ != ResetState)
	{
        UiLogS(server_).dbg() << "ComQueue::start -->";

        //assert(taskIsBeingFinished_==false);
        //assert(taskIsBeingFailed_==false);
        //assert(!current_);

        startTimeoutTryCnt_=0;
        taskStarted_=false;

		//If the comthread is running we need to wait
		//until it finishes its task.
		comThread_->wait();

		state_=RunningState;

        UiLogS(server_).dbg() << "  thread finished";

		//Starts the timer
		timer_->start(timeout_);

        UiLogS(server_).dbg() << "  timer started";
        UiLogS(server_).dbg() << "<-- ComQueue::start";
	}
}

//The queue contents remains the same but the timer is stopped. Until start() is
//called nothing will be submitted to the queue.
void ServerComQueue::suspend(bool wait)
{
	if(state_ != DisabledState && state_ != ResetState &&
	   state_ != SuspendedState)
	{
		state_=SuspendedState;
        stopTimer();
        if(comThread_  && wait)
		{
			comThread_->wait();
		}

        //assert(taskIsBeingFinished_==false);
        //assert(taskIsBeingFailed_==false);
        //assert(!current_);
	}
}

void ServerComQueue::stopTimer()
{
    timer_->stop();
    startTimeoutTryCnt_=0;
}

bool ServerComQueue::hasTask(VTask::Type t) const
{
	for(const auto & task : tasks_)
	{
		if(task && task->type() == t && task->status() != VTask::CANCELLED &&
		   task->status() != VTask::ABORTED )
			return true;

	}
	return false;
}

bool ServerComQueue::isNextTask(VTask::Type t) const
{
	return (!tasks_.empty() && tasks_.back()->type() == t);
}


void ServerComQueue::addTask(VTask_ptr task)
{
	if(!task)
		return;

	if(isNextTask(VTask::ZombieListTask) && tasks_.back()->type() == task->type())
		return;

	if(state_ == DisabledState || state_ == ResetState ||
	  (task && task->type() ==VTask::ResetTask) )
		return;

	tasks_.push_back(task);
	if(state_ != SuspendedState)
	{
		if(!timer_->isActive())
		{
			//we immediately execute the "current" task
			slotRun();
			//and only start the timer after it
			timer_->start(timeout_);
		}
	}
}

void ServerComQueue::addNewsTask()
{
    addNewsTask("","");
}

void ServerComQueue::addNewsTask(const std::string& key, const std::string& value)
{
    if(state_ == DisabledState || state_ == ResetState)
        return;

    if(isNextTask(VTask::NewsTask))
        return;

    VTask_ptr task=VTask::create(VTask::NewsTask);
    if (!key.empty()) {
        task->param(key, value);
    }

    addTask(task);
    server_->refreshScheduled();
}


void ServerComQueue::addSyncTask()
{
	if(state_ == DisabledState || state_ == ResetState)
		return;

	if(isNextTask(VTask::SyncTask))
		return;

	VTask_ptr task=VTask::create(VTask::SyncTask);
	addTask(task);
}

void ServerComQueue::addSuiteListTask()
{
	if(state_ == DisabledState || state_ == ResetState)
		return;

	if(isNextTask(VTask::SuiteListTask))
		return;

	VTask_ptr task=VTask::create(VTask::SuiteListTask);
	addTask(task);
}

void ServerComQueue::addSuiteAutoRegisterTask()
{
	if(state_ == DisabledState || state_ == ResetState)
		return;

	if(isNextTask(VTask::SuiteAutoRegisterTask))
		return;

	VTask_ptr task=VTask::create(VTask::SuiteAutoRegisterTask);
	addTask(task);
}

void ServerComQueue::addServerVersionTask()
{
    if(state_ == DisabledState || state_ == ResetState)
        return;

    if(isNextTask(VTask::ServerVersionTask))
        return;

    VTask_ptr task=VTask::create(VTask::ServerVersionTask);
    addTask(task);
}

void ServerComQueue::startCurrentTask()
{
    if (comThread_) {
        taskStarted_=false;
        ctStartTime_.start();
        comThread_->task(current_);
    }
}

void ServerComQueue::slotRun()
{
    if (!client_ || !comThread_) {
        return;
    }

    if(state_ == DisabledState ||state_ == SuspendedState )
    {
#ifdef _UI_SERVERCOMQUEUE_DEBUG       
        UI_FUNCTION_LOG_S(server_);
        UiLogS(server_).dbg() << " queue is either disabled or suspended";
#endif
        return;
    }

	if(taskIsBeingFinished_ || taskIsBeingFailed_)
    {
#ifdef _UI_SERVERCOMQUEUE_DEBUG        
        UI_FUNCTION_LOG_S(server_);
        UiLogS(server_).dbg() << " task is either being finished or failed";
#endif
        return;
    }

    if(tasks_.empty() && !current_)
	{
#ifdef _UI_SERVERCOMQUEUE_DEBUG        
        UI_FUNCTION_LOG_S(server_);
        UiLogS(server_).dbg() << " there are no tasks! Stop timer!";
#endif
        timer_->stop();
		return;
	}

#if 0
#ifdef _UI_SERVERCOMQUEUE_DEBUG
    if(tasks_.size() > 0)
    {
        UI_FUNCTION_LOG_S(server_);
        UiLog(server_).dbg() << " number of tasks: "  << tasks_.size();
        for(std::deque<VTask_ptr>::const_iterator it=tasks_.begin(); it != tasks_.end(); it++)
        {
            UiLog(server_).dbg() << "  task: " << (*it)->typeString();
        }
    }
#endif
#endif

    //If a task was sent to the thread but the queue did not get the
    //notification about the thread's start there is a PROBLEM!
    //Note: when the thread starts it emits a signal to the queue
    //and the queue sets taskStarted_ to true. Since this is a queued communication
    //there can be a time lag between the two events. We set a timeout for it!
    //If we pass the timeout we stop the thread and try to resend the task!
    if(current_ && !taskStarted_ && ctStartTime_.elapsed() > ctStartTimeout_)
    {      
        UI_FUNCTION_LOG_S(server_);
        if(startTimeoutTryCnt_ < ctMaxStartTimeoutTryCnt_)
        {
            UiLogS(server_).warn() << " ServerCom thread does not seem to have started within the allocated timeout. \
                          startTimeoutTryCnt_=" << startTimeoutTryCnt_;

            startTimeoutTryCnt_++;
        }
        else
        {
            startTimeoutTryCnt_=0;

            //So if we are here:
            //-the thread did not emit a notification about its start
            //-the elapsed time since we sent the task to the thread past the timeout.
            //-since the timeout was passed slotRun() has been was called at least startTimeoutTryCnt_ times

            bool running=comThread_->isRunning();

            //Problem 1:
            //-the thread is running
            if(running)
            {
                UiLogS(server_).warn() << " It seems that the ServerCom thread started but it is in a bad state. Try to run task again.";
            }

            //Problem 2:
            //-the thread is not running
            else
            {
                UiLogS(server_).warn() << " It seems that the ServerCom thread could not start. Try to run task again.";
            }

            if(comThread_->wait(ctStartWaitTimeout_) == false)
            {              
                UiLogS(server_).warn() << "  Calling wait() on the ServerCom thread failed.";

                //The thread started to run in the meantime. We check its status again at the next slotRun call.
                if(!running && comThread_->isRunning())
                {
                    UiLogS(server_).warn() << "  It seems that in the meantime the thread started to run.\
                                   We will check its state again";
                    return;
                }

                //Otherwise there is nothing to do!!
                //We exit here because when we tried to call terminate() it just hung!!
                UI_ASSERT(0,"Cannot stop ServerCom thread, which is in a bad state");
                exit(1);
#if 0
                comThread_->terminate();
                if(comThread_->wait(taskTimeout_) == false)
                {
                    UiLog(server_).err() << "  Calling wait() after terminate() on the ServerCom thread failed";
                    UiLog(server_).dbg() << "Delete the current ComThread and create a new one.";
                    createThread();
                    UI_ASSERT(0,"Cannot stop ServerCom thread that is in a bad state");

                }
                else
                {
                    UiLog(server_).dbg() << "  Terminating ServerCom thread succeeded.";
                }
#endif
            }

            UiLogS(server_).warn() << "  Calling wait() on the ServerCom thread succeeded.";

            Q_ASSERT(comThread_->isRunning() == false);

            if(current_->status() != VTask::CANCELLED &&
               current_->status() != VTask::ABORTED )
            {
                startCurrentTask();
                return;
            }
            else
            {
#ifdef _UI_SERVERCOMQUEUE_DEBUG
                UiLogS(server_).dbg() << "  current_ aborted or cancelled. Reset current_ !";
#endif
                current_.reset();
            }
        }
    }

	if(current_)
	{
#ifdef _UI_SERVERCOMQUEUE_DEBUG
        //UiLog(server_).dbg() << " still processing reply from previous task";
#endif
        return;
	}

	if(comThread_->isRunning())
	{
#ifdef _UI_SERVERCOMQUEUE_DEBUG
        //UiLog(server_).dbg() << " thread is active";
#endif
		return;
	}

	//We search for the first non-cancelled/aborted task
	while(!tasks_.empty())
	{
		current_=tasks_.front();
		tasks_.pop_front();
		if(current_->status() != VTask::CANCELLED &&
		   current_->status() != VTask::ABORTED )
		{
			break;
		}
        current_.reset();
	}

	if(!current_)
	{
		timer_->stop();
		return;
	}

#ifdef _UI_SERVERCOMQUEUE_DEBUG  
    UiLogS(server_).dbg() << " run task: " <<  current_->typeString();
#endif

    //Send it to the thread
    startCurrentTask();
}

//This slot is called when ComThread finishes its task. At this point the
//thread is not running so it is safe to access the ClientInvoker!
void ServerComQueue::slotTaskStarted()
{
    taskStarted_=true;
}

//This slot is called when ComThread finishes its task. At this point the
//thread is not running so it is safe to access the ClientInvoker!
void ServerComQueue::slotTaskFinished()
{
    if(!client_ || !comThread_) {
        return;
    }

    taskStarted_=false;
    taskIsBeingFinished_=true;
    startTimeoutTryCnt_=0;

    UiLogS(server_).dbg() << "ComQueue::slotTaskFinished -->";

	//We need to leave the load mode
	endReset();

	//If the current task is empty there must have been an error that was
	//handled by the sloTaskFailed slot.
	if(current_)
	{
#ifdef _UI_SERVERCOMQUEUE_DEBUG
        UiLogS(server_).dbg() << " reset current_";
#endif

        VTask_ptr task=current_;
		current_.reset();

		//We notify the server that the task has finished and the results can be accessed.
		server_->clientTaskFinished(task,client_->server_reply());
	}

	taskIsBeingFinished_=false;
}

//This slot is called when the task failed in the ComThread. Right after this signal is emitted
//the thread will finish and and emits the finished() signal that is connected
//to the slotTaskFinished slot.
void ServerComQueue::slotTaskFailed(std::string msg)
{
    if(!client_ || !comThread_) {
        return;
    }

    taskStarted_=false;
    taskIsBeingFailed_=true;
    startTimeoutTryCnt_=0;

    UiLogS(server_).dbg() << "ComQueue::slotTaskFailed -->";
#ifdef _UI_SERVERCOMQUEUE_DEBUG
    if(current_)
        UiLogS(server_).dbg() << " current_ exists";
    else
        UiLogS(server_).dbg() << " current_ is null";
#endif

    //We need to leave the load mode
	endReset();

#ifdef _UI_SERVERCOMQUEUE_DEBUG
    if(current_)
        UiLogS(server_).dbg() << " current_ exists";
    else
        UiLogS(server_).dbg() << " current_ is null";
#endif

#ifdef _UI_SERVERCOMQUEUE_DEBUG
    UiLogS(server_).dbg() << " reset current_";
#endif
    assert(current_);
    VTask_ptr task=current_;
	current_.reset();

	//We notify the server that the task has failed
    server_->clientTaskFailed(task,msg,client_->server_reply());

	taskIsBeingFailed_=false;
}

// Only called from the ServerHandler on delete or recreating the client!!!
bool ServerComQueue::logout() {
    state_=DisabledState;

    //Stop the timer
    timer_->stop();

    //Empty the tasks
    tasks_.clear();

    // The app is not quitting
    if (qApp->property("inQuit").toString() != "1") {

        if (comThread_) {
            //Disconnects all the signals from the thread
            comThread_->disconnect();

            //If the comthread is running we need to wait
            //until it finishes its task.
            if (comThread_->wait(500)) {
                startLogout();
                return false;
            //if it is still running we check it status regularly and
            //start the logout when the thread finishes.
            } else {
                UiLogS(server_).warn() << "Waiting for ComThread to stop and start logout!";
                QTimer::singleShot(5000, this, SLOT(slotLogoutCheck()));
            }
        } else {
            if (client_) {
                delete client_;
                client_=nullptr;
            }
            deleteLater();
            return true;
        }
    // the app is quitting
    } else {
        if (comThread_) {
            //Disconnects all the signals from the thread
            comThread_->disconnect();

            //If the comthread is running we need to wait
            //until it finishes its task. Otherwise we do not perform
            //logout and leave it to the system to clean up.
            if (comThread_->wait(1000)) {
                //we perform the logout without running the thread
                comThread_->blockingLogout();
                comThread_->deleteLater();
                comThread_ = nullptr;
                deleteLater();
                return true;
            }
        } else {
            deleteLater();
            if (client_) {
                delete client_;
                client_=nullptr;
            }
            return true;
        }
    }
   return false;
}

void ServerComQueue::slotLogoutCheck()
{
    Q_ASSERT(state_ == DisabledState);
    //If the comthread is running we need to wait
    //until it finishes its task.

    logoutTryNo_++;
    //Otherwise there is nothing to do!!
    if (logoutTryNo_ > maxLogoutTryNo_) {
        UserMessage::message(UserMessage::ERROR, true,
             "Could not start logout for server " + server_->fullLongName() +
             " because the ServerCom thread cannot be stopped (timeout =" + std::to_string(logoutInterval_*maxLogoutTryNo_/1000) +
             "s has passed). The SeverCom thread must be in a bad state." \
             "EcFlowUI will terminate!");
        exit(1);
    }

    UiLogS(server_).warn() << "Checking ComThread status to start logout. Tryno=" << logoutTryNo_;

    if (!comThread_->isRunning()) {
        startLogout();
    } else {
        QTimer::singleShot(logoutInterval_, this, SLOT(slotLogoutCheck()));
    }
}

void ServerComQueue::startLogout()
{
    Q_ASSERT(state_ == DisabledState);
    Q_ASSERT(comThread_->isRunning() == false);
    connect(comThread_, SIGNAL(logoutDone()),
        this, SLOT(slotLogoutDone()));

    VTask_ptr task=VTask::create(VTask::LogOutTask);
    comThread_->task(task);
}

void ServerComQueue::slotLogoutDone()
{
    Q_ASSERT(state_ == DisabledState);
    if(comThread_) {
        Q_ASSERT(comThread_->isRunning() == false);
        comThread_->deleteLater();
        comThread_ = nullptr;
    }
    // notify the server that the queue and the thread is being deleted
    if (client_) {
        delete client_;
        client_ = nullptr;
    }
    server_->queueLoggedOut();
    deleteLater();
}
