/* Distributed under the Apache License, Version 2.0.
See accompanying NOTICE file for details.*/
#include "QPulse.h"
#include <QThread>
#include <QPointer>
#include <QCoreApplication>
#include <QScrollBar>
#include <QMutex>
#include <QMessageBox>

#include "PulsePhysiologyEngine.h"
#include "PulseScenario.h"
#include "PulseConfiguration.h"
#include "scenario/SEScenario.h"
#include "engine/SEAdvanceTime.h"
#include "engine/SEEngineTracker.h"
#include "engine/SEPatientConfiguration.h"
#include "engine/SEConditionManager.h"
#include "patient/SEPatient.h"
#include "properties/SEScalar0To1.h"
#include "properties/SEScalarArea.h"
#include "properties/SEScalarFrequency.h"
#include "properties/SEScalarLength.h"
#include "properties/SEScalarMass.h"
#include "properties/SEScalarMassPerVolume.h"
#include "properties/SEScalarPower.h"
#include "properties/SEScalarPressure.h"
#include "properties/SEScalarTime.h"
#include "properties/SEScalarVolume.h"
#include "utils/TimingProfile.h"
#include <thread>

class LoggerForward2Qt : public LoggerForward
{
public:
  LoggerForward2Qt() {}
  virtual ~LoggerForward2Qt() {}
  virtual void ForwardDebug(const std::string& msg, const std::string& origin) { msgs.push_back(msg); }
  virtual void ForwardInfo(const std::string& msg, const std::string& origin)
  { 
    for (std::string str : IgnoreActions)
    {
      if (msg.find(str) != str.npos)
        return;
    }
    msgs.push_back(msg);
  }
  virtual void ForwardWarning(const std::string& msg, const std::string& origin) { msgs.push_back(msg); }
  virtual void ForwardError(const std::string& msg, const std::string& origin)   { msgs.push_back("[ERROR] " + msg); }
  virtual void ForwardFatal(const std::string& msg, const std::string& origin)   { msgs.push_back("[ERROR] " + msg); }

  std::vector<std::string> IgnoreActions;
  std::vector<std::string> msgs;
};

class QPulse::Controls
{
public:
  Controls(QThread& thread, QTextEdit& log) : Thread(thread), LogBox(log)
  {
    Pulse = CreatePulseEngine();
    Pulse->GetLogger()->SetLogFile("PulseExplorer.log");
    Pulse->GetLogger()->SetForward(&Log2Qt);
    Scenario = new PulseScenario(Pulse->GetLogger());
  }
  virtual ~Controls()
  {
    delete Scenario;
  }

  std::unique_ptr<PhysiologyEngine>              Pulse;
  LoggerForward2Qt                               Log2Qt;
  QThread&                                       Thread;
  QTextEdit&                                     LogBox;
  QMutex                                         Mutex;
  TimingProfile                                  Timer;
  ExplorerMode                                   Mode = ExplorerMode::None;
  bool                                           Running=false;
  bool                                           Paused=false;
  bool                                           SaveState=false;
  bool                                           RunInRealtime=true;
  bool                                           Advancing=false;
  double                                         AdvanceStep_s;
  PulseScenario*                                 Scenario;
  std::string                                    EngineStateFile="";
  std::vector<PulseListener*>                    Listeners;
  std::map<size_t, std::vector<SEAction const*>> Actions;// Key is sim time in seconds to process the action vector
};

QPulse::QPulse(QThread& thread, QTextEdit& log) : QObject(), SEAdvanceHandler(true)
{
  m_Controls = new Controls(thread,log);
  m_Controls->Pulse->SetAdvanceHandler(this);

  connect(this, SIGNAL(AtSteadyState()), SLOT(AtSteadyStateUpdateUI()));
  connect(this, SIGNAL(Advanced()), SLOT(PhysiologyUpdateUI()));
  connect(this, SIGNAL(EngineError()), SLOT(EngineErrorUI()));
}

QPulse::~QPulse()
{
  Stop();
  delete m_Controls;
}

QTextEdit& QPulse::GetLogBox()
{
  return m_Controls->LogBox;
}
void QPulse::ScrollLogBox()
{
  QScrollBar *sb = m_Controls->LogBox.verticalScrollBar();
  sb->setValue(sb->maximum());
  m_Controls->LogBox.update();
}

void QPulse::IgnoreAction(const std::string& name)
{
  m_Controls->Log2Qt.IgnoreActions.push_back(name);
}

PhysiologyEngine& QPulse::GetEngine()
{
  return *m_Controls->Pulse;
}

double QPulse::GetTimeStep_s()
{
  return m_Controls->AdvanceStep_s;
}

void QPulse::Start()
{
  Worker* worker = new Worker(*this);
  worker->moveToThread(&m_Controls->Thread);
  connect(&m_Controls->Thread, SIGNAL(started()), worker, SLOT(Work()));
  connect(&m_Controls->Thread, SIGNAL(finished()), worker, SLOT(deleteLater()));
  m_Controls->Thread.start();
}
void Worker::Work()
{
  _qpulse.AdvanceTime();
}

void QPulse::Stop()
{
  if (m_Controls->Thread.isRunning())
  {
    m_Controls->Running = false;
    while (m_Controls->Advancing)
      std::this_thread::sleep_for(std::chrono::seconds(1));
    m_Controls->Thread.quit();
  }
}

bool QPulse::IsRunning()
{
  return m_Controls->Running;
}

void QPulse::Clear()
{
  Stop();
  m_Controls->EngineStateFile = "";
  m_Controls->Running = false;
  m_Controls->Paused = false;
  m_Controls->SaveState = false;
  m_Controls->RunInRealtime = true;
  m_Controls->Log2Qt.IgnoreActions.clear();
  while(m_Controls->Advancing)
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
  m_Controls->Scenario->Clear();
  m_Controls->Pulse->GetEngineTracker()->Clear();
}

void QPulse::Reset()
{
  Stop();
  m_Controls->Running = false;
  m_Controls->Paused = false;
  m_Controls->SaveState = false;
  m_Controls->RunInRealtime = true;
  while (m_Controls->Advancing)
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
}

ExplorerMode QPulse::GetMode() { return m_Controls->Mode; }
void QPulse::SetMode(ExplorerMode m) { m_Controls->Mode = m; }

bool QPulse::PlayPause()
{
  if (m_Controls->Thread.isRunning())
    m_Controls->Paused = !m_Controls->Paused;
  return m_Controls->Paused;
}

void QPulse::Save()
{
  m_Controls->SaveState = true;
}

bool QPulse::ToggleRealtime()
{
  if (m_Controls->Thread.isRunning())
    m_Controls->RunInRealtime = !m_Controls->RunInRealtime;
  return m_Controls->RunInRealtime;
}


void QPulse::RegisterListener(PulseListener* l)
{
  if (l == nullptr)
    return;
  auto itr = std::find(m_Controls->Listeners.begin(), m_Controls->Listeners.end(), l);
  if (itr == m_Controls->Listeners.end())
    m_Controls->Listeners.push_back(l);
}

void QPulse::RemoveListener(PulseListener* l)
{
  auto itr = std::find(m_Controls->Listeners.begin(), m_Controls->Listeners.end(), l);
  if (itr != m_Controls->Listeners.end())
    m_Controls->Listeners.erase(itr);
}

PulseScenario& QPulse::GetScenario()
{
  return *m_Controls->Scenario;
}

SEPatientConfiguration& QPulse::GetPatientConfiguration()
{
  return m_Controls->Scenario->GetPatientConfiguration();
}

PulseConfiguration& QPulse::GetPulseConfiguration()
{
  return m_Controls->Scenario->GetConfiguration();
}

void QPulse::AddScenarioAction(ushort time_s, SEAction const& a)
{
  m_Controls->Actions[time_s].push_back(&a);
}

void QPulse::AdvanceTime()
{
  std::stringstream ss;
  double count_s = 0;
  m_Controls->Advancing = true;
  m_Controls->Timer.Start("ui");
  if (m_Controls->EngineStateFile.empty())
  {
    bool stable = m_Controls->Pulse->InitializeEngine(GetPatientConfiguration());
    if (!stable)
    {
      m_Controls->Running = false;
      m_Controls->Advancing = false;
      emit EngineError();
      return;
    }
  }
  m_Controls->Actions.clear();
  // Get the time step after we initialize, it can be changed by the configuration
  m_Controls->AdvanceStep_s = m_Controls->Pulse->GetTimeStep(TimeUnit::s);
  // Notify listeners we are starting (we stabilized if we needed to)
  m_Controls->Mutex.lock();
  for (PulseListener* l : m_Controls->Listeners)
    l->AtSteadyState(*m_Controls->Pulse);
  m_Controls->Mutex.unlock();
  emit AtSteadyState();
  // Give sometime to Qt to process this signal
  // We may want to just set the pause, and at the end
  // of processing the signal, unpause it.. 
  std::this_thread::sleep_for(std::chrono::seconds(1));

  // Set up the main procesing thread
  m_Controls->Running = true;
  // Set up timing variables
  std::chrono::time_point<std::chrono::steady_clock> begin;
  std::chrono::milliseconds   timePerFrame{ int(m_Controls->AdvanceStep_s*1000) };
  std::chrono::duration<double> dt(m_Controls->AdvanceStep_s);
  // Process actions every second
  ushort next_s = 0;
  while (m_Controls->Running)
  {
    begin = std::chrono::steady_clock::now();
    if (m_Controls->SaveState)
    {
      m_Controls->SaveState = false;
      ss.str("");
      ss << "./states/" << m_Controls->Pulse->GetPatient().GetName() << "@" << m_Controls->Pulse->GetSimulationTime(TimeUnit::s) << "s.json";
      m_Controls->Pulse->SerializeToFile(ss.str());
      m_Controls->Pulse->GetLogger()->Info("Saved state : "+ss.str());
    }
    if (m_Controls->Paused)
    {
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    else
    {
      
      try
      {
        ushort now_s = std::floor(m_Controls->Pulse->GetSimulationTime(TimeUnit::s));
        if (now_s == next_s)
        {
          auto actions = m_Controls->Actions.find(next_s);
          if (actions != m_Controls->Actions.end())
          {
            for (SEAction const* a : actions->second)
            {
              m_Controls->Pulse->ProcessAction(*a);
            }
            m_Controls->Actions.erase(next_s);
          }
          next_s++;
        }
        m_Controls->Pulse->AdvanceModelTime(dt.count(), TimeUnit::s);
      }
      catch (CommonDataModelException ex)
      {
        m_Controls->Running = false;
        m_Controls->Pulse->GetLogger()->Info("Pulse Error : " + std::string(ex.what()));
      }

      auto stepDuration = std::chrono::steady_clock::now() - begin;
      if (m_Controls->RunInRealtime && stepDuration < timePerFrame)
      {
        auto nap = timePerFrame - stepDuration;
        std::this_thread::sleep_for(nap);
        dt = std::chrono::steady_clock::now() - begin;
      }
      else // TODO (AB) Things are running slower than we want... what should we do?
      {
        dt = timePerFrame;
      }
    }
  }
  m_Controls->Advancing = false;
}

void QPulse::OnAdvance(double time_s)
{
  if (m_Controls->Running)
  {
    m_Controls->Mutex.lock();
    for (PulseListener* l : m_Controls->Listeners)
      l->ProcessPhysiology(*m_Controls->Pulse);
    m_Controls->Mutex.unlock();
  }
  if (m_Controls->Timer.GetElapsedTime_s("ui") > 0.1)
  {
    emit Advanced();// Only update the UI every 0.1 seconds
    m_Controls->Timer.Start("ui");// Reset our timer
  }
}

void QPulse::AtSteadyStateUpdateUI()
{
  m_Controls->Mutex.lock();
  for (PulseListener* l : m_Controls->Listeners)
    l->AtSteadyStateUpdateUI();
  m_Controls->Mutex.unlock();
}
void QPulse::PhysiologyUpdateUI()
{
  m_Controls->Mutex.lock();
  for (std::string msg : m_Controls->Log2Qt.msgs)
    m_Controls->LogBox.append(QString(msg.c_str()));
  m_Controls->Log2Qt.msgs.clear();
  if (m_Controls->Running)
  {
    for (PulseListener* l : m_Controls->Listeners)
      l->PhysiologyUpdateUI();
  }
  m_Controls->Mutex.unlock();
}
void QPulse::EngineErrorUI()
{
  m_Controls->Mutex.lock();
  for (std::string msg : m_Controls->Log2Qt.msgs)
    m_Controls->LogBox.append(QString(msg.c_str()));
  m_Controls->Log2Qt.msgs.clear();

  for (PulseListener* l : m_Controls->Listeners)
    l->EngineErrorUI();
  m_Controls->Mutex.unlock();
}

std::string QPulse::GetEngineStateFilename()
{
  return m_Controls->EngineStateFile;
}
void QPulse::SetEngineStateFilename(std::string const& stateFilename)
{
  m_Controls->EngineStateFile = stateFilename;
}
