/* 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 "cdm/CommonDataModel.h"
#include "PulsePhysiologyEngine.h"
#include "PulseScenario.h"
#include "PulseConfiguration.h"
#include "cdm/scenario/SEScenario.h"
#include "cdm/engine/SEAdvanceTime.h"
#include "cdm/engine/SEEngineTracker.h"
#include "cdm/engine/SEPatientConfiguration.h"
#include "cdm/engine/SEConditionManager.h"
#include "cdm/patient/SEPatient.h"
#include "cdm/properties/SEScalar0To1.h"
#include "cdm/properties/SEScalarArea.h"
#include "cdm/properties/SEScalarFrequency.h"
#include "cdm/properties/SEScalarLength.h"
#include "cdm/properties/SEScalarMass.h"
#include "cdm/properties/SEScalarMassPerVolume.h"
#include "cdm/properties/SEScalarPower.h"
#include "cdm/properties/SEScalarPressure.h"
#include "cdm/properties/SEScalarTime.h"
#include "cdm/properties/SEScalarVolume.h"
#include "cdm/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(msg); }
  virtual void ForwardFatal(const std::string& msg, const std::string& origin)   { msgs.push_back(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("PulseExplorer.log");
    Pulse->GetLogger()->SetForward(&Log2Qt);
    Scenario = new PulseScenario(Pulse->GetSubstanceManager());
  }
  virtual ~Controls()
  {
    delete Scenario;
  }

  std::unique_ptr<PhysiologyEngine>      Pulse;
  LoggerForward2Qt                       Log2Qt;
  QThread&                               Thread;
  QTextEdit&                             LogBox;
  QMutex                                 Mutex;
  TimingProfile                          Timer;
  bool                                   Stabilize=false;
  bool                                   Running=false;
  bool                                   Paused=false;
  bool                                   SaveState=false;
  bool                                   RunInRealtime=true;
  bool                                   Advancing=false;
  double                                 AdvanceStep_s;
  PulseScenario*                         Scenario;
  std::vector<PulseListener*>            Listeners;
};

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()));
}

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;
}

SEEngineTracker& QPulse::GetEngineTracker()
{
  return *m_Controls->Pulse->GetEngineTracker();
}

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::Reset()
{
  Stop();
  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();
}

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);
}

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

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

void QPulse::StabilizeEngine(bool activate)
{
  m_Controls->Stabilize = activate;
}

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

void QPulse::AdvanceTime()
{
  std::stringstream ss;
  double count_s = 0;
  long long sleep_ms;
  m_Controls->Advancing = true;
  m_Controls->AdvanceStep_s = m_Controls->Pulse->GetTimeStep(TimeUnit::s);
  m_Controls->Timer.Start("ui");
  if (m_Controls->Stabilize)
  {
    bool stable = m_Controls->Pulse->InitializeEngine(GetPatientConfiguration(), &GetPulseConfiguration());
    if (!stable)
    {
      QMessageBox msgBox;
      msgBox.setWindowTitle("Error!");
      QString err = "Unable to stabilize engine";
      msgBox.setText(err);
      msgBox.exec();
      m_Controls->Running = false;
      m_Controls->Advancing = false;
      return;
    }
  }
  // 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
  size_t aIdx = 0;
  const SEAdvanceTime* adv=nullptr;
  const std::vector<const SEAction*>* actions = nullptr;
  actions = &m_Controls->Scenario->GetActions();
  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<float> dt(m_Controls->AdvanceStep_s);
  while (m_Controls->Running)
  {
    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(), SerializationFormat::JSON);
      m_Controls->Pulse->GetLogger()->Info("Saved state : "+ss.str());
    }
    if (m_Controls->Paused)
    {
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    else
    {
      begin = std::chrono::steady_clock::now();
      try
      {
        if(actions==nullptr || actions->empty())
          m_Controls->Pulse->AdvanceModelTime(dt.count(), TimeUnit::s);
        else
        {
          if (adv != nullptr)
          {
            count_s += dt.count();
            m_Controls->Pulse->AdvanceModelTime(dt.count(), TimeUnit::s);
            if (count_s >= adv->GetTime(TimeUnit::s))
            {
              count_s = 0;
              adv = nullptr;
            }
          }
          if (adv == nullptr)
          {
            for (; aIdx < actions->size();)
            {
              const SEAction* a = (*actions)[aIdx++];
              adv = dynamic_cast<const SEAdvanceTime*>(a);
              if (adv != nullptr)
                break;
              m_Controls->Pulse->ProcessAction(*a);
            }
            if (aIdx == actions->size())
            {
              adv = nullptr;
              actions = nullptr;
              m_Controls->Pulse->GetLogger()->Info("Scenario Complete");
            }
            continue;
          }
        }
      }
      catch (CommonDataModelException ex) {}

      auto stepDuration = std::chrono::steady_clock::now() - begin;
      if (m_Controls->RunInRealtime && stepDuration < timePerFrame)
      {
        std::this_thread::sleep_for(timePerFrame - stepDuration);
        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, const PhysiologyEngine& engine)
{
  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::EngineToPatientConfiguration()
{
  GetPatientConfiguration().GetPatient().Copy(m_Controls->Pulse->GetPatient());
  GetPatientConfiguration().GetConditions().Copy(m_Controls->Pulse->GetConditionManager());
}