/* Distributed under the Apache License, Version 2.0.
   See accompanying NOTICE file for details.*/
#include "VentilatorWidget.h"
#include "ui_Ventilator.h"

#include <QLayout>
#include <QGraphicsLayout>

#include "pulse/cdm/engine/SEDataRequestManager.h"
#include "pulse/cdm/engine/SEEngineTracker.h"
#include "pulse/cdm/properties/SEScalar0To1.h"
#include "pulse/cdm/properties/SEScalarFrequency.h"
#include "pulse/cdm/properties/SEScalarPressure.h"
#include "pulse/cdm/properties/SEScalarTime.h"
#include "pulse/cdm/properties/SEScalarVolume.h"
#include "pulse/cdm/properties/SEScalarVolumePerPressure.h"
#include "pulse/cdm/properties/SEScalarVolumePerTime.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/SEMechanicalVentilator.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorConfiguration.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorContinuousPositiveAirwayPressure.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorHold.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorLeak.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorPressureControl.h"
#include "pulse/cdm/system/equipment/mechanical_ventilator/actions/SEMechanicalVentilatorVolumeControl.h"
#include "pulse/cdm/system/physiology/SERespiratorySystem.h"
#include "pulse/engine/PulseEngine.h"


#include "QwtPulsePlot.h"
#include "LabeledComboBox.h"
#include "NumberWidget.h"
#include "QMutex"
#include "VentilatorHysteresisWidget.h"



struct QVentilatorWidget::Private : public Ui::VentilatorWidget
{
  Private(QVentilatorWidget* parent) : parent(parent)
  {
    setupUi(parent);
    setupInitialValues();
    hysteresis = new QVentilatorHysteresisWidget(parent, Qt::WindowType::Window);
    hysteresis->hide();
    connect(hysteresisButton, &QPushButton::released, [this]()
    {
      if (hysteresis->isVisible()) hysteresis->hide();
      else hysteresis->show();
    });
  }

  QVector<LabeledDialWidget*> dials;
  QVector<NumberWidget*> numbers;
  LabeledDialWidget* slopeWidget = nullptr;
  LabeledDialWidget* tiWidget = nullptr;
  QVentilatorWidget* parent = nullptr;
  QVentilatorHysteresisWidget* hysteresis = nullptr;

  static const int PC = 0;
  static const int VC = 1;
  static const int CPAP = 2;

  struct ActionSet {
    SEMechanicalVentilatorPressureControl pc;
    SEMechanicalVentilatorVolumeControl vc;
    SEMechanicalVentilatorContinuousPositiveAirwayPressure cpap;
  };

  ActionSet initialValues;
  ActionSet currentValues;

  std::vector<std::unique_ptr<SEAction>> actions;

  int modeFromAction = -1;

  QMutex actionMutex;

  void addAction(std::unique_ptr<SEAction> p)
  {
    QMutexLocker lock(&actionMutex);
    actions.push_back(std::move(p));
  }

  void applyActions(PhysiologyEngine& pulse)
  {
    QMutexLocker lock(&actionMutex);
    for (auto& action : actions)
    {
      pulse.ProcessAction(*action);
    }
    actions.clear();
  }

  void setupInitialValues()
  {
    {
      auto& action = initialValues.pc;
      action.SetConnection(eSwitch::On);
      action.GetFractionInspiredOxygen().SetValue(.21);
      action.GetInspiratoryPressure().SetValue(13.0, PressureUnit::cmH2O);
      action.GetInspiratoryPeriod().SetValue(1.0, TimeUnit::s);
      action.GetRespirationRate().SetValue(12, FrequencyUnit::Per_min);
      action.GetPositiveEndExpiredPressure().SetValue(5.0, PressureUnit::cmH2O);
      action.GetSlope().SetValue(0.2, TimeUnit::s);
    }
    {
      auto& action = initialValues.vc;
      action.SetConnection(eSwitch::On);
      action.GetFractionInspiredOxygen().SetValue(0.21);
      action.GetTidalVolume().SetValue(600, VolumeUnit::mL);
      action.GetInspiratoryPeriod().SetValue(1.0, TimeUnit::s);
      action.GetRespirationRate().SetValue(12, FrequencyUnit::Per_min);
      action.GetPositiveEndExpiredPressure().SetValue(5.0, PressureUnit::cmH2O);
      action.GetFlow().SetValue(50, VolumePerTimeUnit::L_Per_min);
    }
    {
      auto& action = initialValues.cpap;
      action.SetConnection(eSwitch::On);
      action.GetFractionInspiredOxygen().SetValue(0.21);
      action.GetPositiveEndExpiredPressure().SetValue(5.0, PressureUnit::cmH2O);
      action.GetDeltaPressureSupport().SetValue(8, PressureUnit::cmH2O);
      action.GetSlope().SetValue(0.2, TimeUnit::s);
    }
  }

  enum UIState {
    Default,
    Off,                // Engine is off
    VentilatorNone,     // Panel State for "None"
    Enabled,
    Disconnected,
    Connected,
    Hold
  };

  void clearSlope()
  {
    if (tiWidget == nullptr) return;
    disconnect(tiWidget, &LabeledDialWidget::valueChanged, parent, &QVentilatorWidget::updateSlope);
    tiWidget = nullptr;
    slopeWidget = nullptr;
  }

  void setupSlope(LabeledDialWidget* ti, LabeledDialWidget* slope)
  {
    clearSlope();
    slopeWidget = slope;
    tiWidget = ti;
    connect(tiWidget, &LabeledDialWidget::valueChanged, parent, &QVentilatorWidget::updateSlope);
  }

  void showDials(int maxIndex)
  {
    for (int i = 0; i < maxIndex && i < dials.size(); ++i)
    {
      dials[i]->show();
    }
  }


  void setMode(QString mode, bool resetValues = false)
  {
    clearSlope();

    for (auto dial : dials)
    {
      dial->hide();
    }

    if (mode == "NONE")
    {
      setUIState(VentilatorNone);
      return;
    }
    else if (mode == "PC" || mode == "PC-CMV" || mode == "PC-AC")
    {
      if (resetValues) currentValues.pc.Copy(initialValues.pc);
      auto& action = currentValues.pc;
      dial_0->setup("FiO2", "",0.21, 1.0, 100, 0.1, &action.GetFractionInspiredOxygen());
      dial_1->setup("Pinsp", "cmH2O", 1, 100, 1, 1, &action.GetInspiratoryPressure());
      dial_2->setup("Ti", "s", 0.1, 60, 10, 0.1, &action.GetInspiratoryPeriod());
      dial_3->setup("RR", "bpm", 10, 60, 1, 1.0, &action.GetRespirationRate());
      dial_4->setup("PEEP", "cmH2O", 0, 50, 1, 1.0, &action.GetPositiveEndExpiredPressure());
      dial_5->setup("Slope", "s", 0, dial_2->getValue(), 10, 0.1, &action.GetSlope());
      setupSlope(dial_2, dial_5);
      showDials(6);
    }
    else if (mode == "VC" || mode == "VC-CMV" || mode == "VC-AC")
    {
      if (resetValues) currentValues.vc.Copy(initialValues.vc);
      auto& action = currentValues.vc;
      dial_0->setup("FiO2", "", 0.21, 1.0, 100, 0.1, &action.GetFractionInspiredOxygen());
      dial_1->setup("VT", "mL", 100, 2000, 1, 1.0, &action.GetTidalVolume());
      dial_2->setup("Ti", "s", 0.1, 60, 10, 0.1, &action.GetInspiratoryPeriod());
      dial_3->setup("RR", "bpm", 10, 60, 1, 1.0, &action.GetRespirationRate());
      dial_4->setup("PEEP", "cmH2O", 0, 50, 1, 1.0, &action.GetPositiveEndExpiredPressure());
      dial_5->setup("Flow", "L/min", 1, 100, 1, 1.0, &action.GetFlow());
      showDials(6);
    }
    else if (mode == "CPAP")
    {
      if (resetValues) currentValues.cpap.Copy(initialValues.cpap);
      auto& action = currentValues.cpap;
      dial_0->setup("FiO2", "", 0.21, 1.0, 100, 0.1, &action.GetFractionInspiredOxygen());
      dial_1->setup("PEEP", "cmH2O", 0, 50, 1, 1.0, &action.GetPositiveEndExpiredPressure());
      dial_2->setup("deltaPsupp", "cmH2O", 1, 100, 1, 1.0, &action.GetDeltaPressureSupport());
      dial_3->setup("Slope", "s", 0, 1, 100, 0.1, &action.GetSlope());
      showDials(4);
    }
    else {
      qWarning() << "Mode " << mode << " not found.";
    }

    setUIState((applyButton->text() == "Connect") ? Disconnected : Connected);
  }

  void setUIState(UIState state)
  {
    switch (state)
    {
    case Default:
      reset();
      setUIState(Off);
      break;
    case Off:
      modeBox->setEnabled(false);
      applyButton->setEnabled(false);
      holdButton->setEnabled(false);
      disconnectButton->setEnabled(false);
      break;
    case Enabled:
      modeBox->setEnabled(true);
      setUIState((applyButton->text() == "Connect") ? Disconnected : Connected);
      break;
    case VentilatorNone:
      modeBox->setEnabled(true);
      applyButton->setEnabled(false);
      holdButton->setEnabled(false);
      disconnectButton->setEnabled(false);
      break;
    case Connected:
      applyButton->setText("Apply");
      applyButton->setEnabled(true);
      holdButton->setEnabled(true);
      disconnectButton->setEnabled(true);
      break;
    case Disconnected:
      applyButton->setText("Connect");
      applyButton->setEnabled(true);
      holdButton->setDisabled(true);
      disconnectButton->setDisabled(true);
      break;
    }
  }

  void reset()
  {
    for (auto number : numbers)
    {
      number->setValue(0);
    }


    modeBox->setCurrentIndex(0);


    for (auto dial : dials)
    {
      dial->hide();
    }

    plot_0->clear();
    plot_0->setYRange(-0.1, 20.);
    min_0->setText("0");
    max_0->setText("20");
    plot_0->setMaxPoints(50 * 15);    // 15 Seconds
    plot_0->setScaleMode(PulsePainterPlot::ScaleMode::Expanding);
    plot_0->setUpdateMode(PulsePainterPlot::UpdateMode::InPlace);
    plot_1->clear();
    plot_1->setYRange(-30, 30);
    min_1->setText("-30");
    max_1->setText("30");
    plot_1->setMaxPoints(50 * 15);    // 15 Seconds
    plot_1->setScaleMode(PulsePainterPlot::ScaleMode::Expanding);
    plot_1->setUpdateMode(PulsePainterPlot::UpdateMode::InPlace);
    plot_2->clear();
    plot_2->setYRange(-100.0, 500);
    plot_2->setMaxPoints(50 * 15);    // 15 Seconds
    plot_2->setScaleMode(PulsePainterPlot::ScaleMode::Expanding);
    plot_2->setUpdateMode(PulsePainterPlot::UpdateMode::InPlace);
    min_2->setText("-100");
    max_2->setText("500");

    currentValues.pc.Copy(initialValues.pc);
    currentValues.vc.Copy(initialValues.vc);
    currentValues.cpap.Copy(initialValues.cpap);
  }

  void resetDials()
  {
    setMode(modeBox->currentText(), true);
  }
};

QVentilatorWidget::QVentilatorWidget(QWidget *parent, Qt::WindowFlags flags) :
  QWidget(parent, flags),
  d(new Private(this))
{
  d->dials = {
    d->dial_0,
    d->dial_1,
    d->dial_2,
    d->dial_3,
    d->dial_4,
    d->dial_5
  };

  d->numbers = {
    d->number_0, d->number_1, d->number_2, d->number_3, d->number_4,
    d->number_5, d->number_6, d->number_7
  };

  setupNumbers();

  connect(d->modeBox, &QComboBox::currentTextChanged, this, &QVentilatorWidget::setMode);

  connect(d->applyButton, &QPushButton::clicked, this, &QVentilatorWidget::applyAction);
  connect(d->disconnectButton, &QPushButton::clicked, this, &QVentilatorWidget::disconnectAction);
  connect(d->holdButton, &QPushButton::pressed, this, &QVentilatorWidget::addHold);
  connect(d->holdButton, &QPushButton::released, this, &QVentilatorWidget::removeHold);
  connect(d->resetButton, &QPushButton::released, this, &QVentilatorWidget::resetDials);

  Reset();
}

QVentilatorWidget::~QVentilatorWidget()
{
  delete d;
}

void QVentilatorWidget::Reset()
{
  d->setUIState(QVentilatorWidget::Private::Default);
}

void QVentilatorWidget::AtSteadyState(PhysiologyEngine& pulse)
{
  // Create Data Requests here ...

  auto& dm = pulse.GetEngineTracker()->GetDataRequestManager();

  // PulsePhysiology Name, Unit, UI Abbreviation
  // HS would like to set up things this way but that still has a few issues
//   static std::vector<std::tuple<std::string, CCompoundUnit, QString>> items =
//   {
//     {"PeakInspiratoryPressure", PressureUnit::cmH2O, "PIP"},
//     {"MeanAirwayPressure", PressureUnit::cmH2O, "Pmean"},
//     {"RespirationRate", FrequencyUnit::Per_min, "RR"},
//     {"TidalVolume", VolumeUnit::mL, "VT"},
//     {"TotalPulmonaryVentilation", VolumePerTimeUnit::L_Per_min, "MVe"},
//     {"EndTidalCarbonDioxidePressure", PressureUnit::mmHg, "etCO2"},
//     {"PulmonaryCompliance", {}, "cDyn"},
//   };
//
//   int i = 0;
//   for (auto& item : items)
//   {
//     const std::string& physName = std::get<0>(item);
//     const CCompoundUnit& unit = std::get<1>(item);
//     const QString& label = std::get<2>(item);
//
//     if (physName.empty()) continue;
//
//     d->numbers[i]->setDataRequest(&dm.CreatePhysiologyDataRequest(physName, unit));
//
//     d->numbers[i]->setName(label);
//     d->numbers[i]->setUnit(QString(unit.GetString().c_str()));
//     ++i;
//   }
  d->hysteresis->AtSteadyState(pulse);
}

void QVentilatorWidget::ProcessPhysiology(PhysiologyEngine& pulse)
{
  // This is called from a thread, you should NOT update UI here
  // This is where we pull data from pulse, and push any actions to it

  d->hysteresis->ProcessPhysiology(pulse);

  auto ventilator = pulse.GetMechanicalVentilator();

  d->number_0->setWithoutUpdate(ventilator->GetPeakInspiratoryPressure(PressureUnit::cmH2O));
  d->number_1->setWithoutUpdate(ventilator->GetTidalVolume(VolumeUnit::mL));
  d->number_2->setWithoutUpdate(ventilator->GetMeanAirwayPressure(PressureUnit::cmH2O));
  d->number_3->setWithoutUpdate(ventilator->GetTotalPulmonaryVentilation(VolumePerTimeUnit::L_Per_min));
  d->number_4->setWithoutUpdate(ventilator->GetRespirationRate(FrequencyUnit::Per_min));
  d->number_5->setWithoutUpdate(ventilator->GetEndTidalCarbonDioxidePressure(PressureUnit::mmHg));
  d->number_6->setWithoutUpdate(ventilator->GetDynamicPulmonaryCompliance(VolumePerPressureUnit::L_Per_cmH2O));
  d->number_7->setWithoutUpdate(ventilator->GetInspiratoryExpiratoryRatio());

  double time_s = pulse.GetSimulationTime(TimeUnit::s);
  d->plot_0->append(time_s, ventilator->GetAirwayPressure(PressureUnit::cmH2O));
  d->plot_1->append(time_s, ventilator->GetInspiratoryFlow(VolumePerTimeUnit::L_Per_min));
  d->plot_2->append(time_s, ventilator->GetTotalLungVolume(VolumeUnit::mL));

  d->applyActions(pulse);
}

void QVentilatorWidget::AtSteadyStateUpdateUI()
{
  d->hysteresis->AtSteadyStateUpdateUI();

}

void QVentilatorWidget::PhysiologyUpdateUI(const std::vector<SEAction const*>& actions)
{
  // This is called from a slot, you can update UI here
  // This is where we take the pulse data we pulled and push it to a UI widget
  for (auto number : d->numbers)
  {
    number->updateValue();
  }

  // Only the last/newest action in the set is relevant
  auto itEnd = actions.crend();
  auto it = actions.crbegin();
  while (it != itEnd)
  {
    if (auto typedAction = dynamic_cast<const SEMechanicalVentilatorPressureControl*>(*it))
    {
      d->currentValues.pc.Copy(*typedAction);
      QString currentText = (typedAction->GetMode() == eMechanicalVentilator_PressureControlMode::AssistedControl) ?
          "PC-AC" : "PC-CMV";
      d->modeBox->setCurrentText(currentText);
      break;
    }
    if (auto typedAction = dynamic_cast<const SEMechanicalVentilatorVolumeControl*>(*it))
    {
      d->currentValues.vc.Copy(*typedAction);
      QString currentText = (typedAction->GetMode() == eMechanicalVentilator_VolumeControlMode::AssistedControl) ?
          "VC-AC" : "VC-CMV";
      d->modeBox->setCurrentText("VC-CMV");
      break;
    }
    if (auto typedAction = dynamic_cast<const SEMechanicalVentilatorContinuousPositiveAirwayPressure*>(*it))
    {
      d->currentValues.cpap.Copy(*typedAction);
      d->modeBox->setCurrentText("CPAP");
      break;
    }
    ++it;
  }

  // update plot axis labels
  {
    auto range = d->plot_0->getYRange();
    d->min_0->setText(QString::number(range.first, 'f', 0));
    d->max_0->setText(QString::number(range.second, 'f', 0));
  }
  {
    auto range = d->plot_1->getYRange();
    d->min_1->setText(QString::number(range.first, 'f', 0));
    d->max_1->setText(QString::number(range.second, 'f', 0));
  }
  {
    auto range = d->plot_2->getYRange();
    d->min_2->setText(QString::number(range.first, 'f', 0));
    d->max_2->setText(QString::number(range.second, 'f', 0));
  }

  d->hysteresis->PhysiologyUpdateUI(actions);
}

void QVentilatorWidget::EngineErrorUI()
{

}

void QVentilatorWidget::StartEngine()
{
  d->setUIState(QVentilatorWidget::Private::VentilatorNone);
}

void QVentilatorWidget::updateSlope(double value)
{
  d->slopeWidget->setMaximumValue(value);
}

void QVentilatorWidget::applyAction()
{
  QString mode = d->modeBox->currentText();
  SEScalarTime t;
  t.Invalidate();

  if (mode == "NONE")
  {
    return;
  }
  else if (mode == "PC-CMV" || mode == "PC-AC")
  {
    auto action = std::make_unique<SEMechanicalVentilatorPressureControl>();
    action->Copy(d->currentValues.pc);
    action->SetConnection(eSwitch::On);
    if (mode == "PC-AC")
    {
      action->SetMode(eMechanicalVentilator_PressureControlMode::AssistedControl);
    }
    else
    {
      action->SetMode(eMechanicalVentilator_PressureControlMode::ContinuousMandatoryVentilation);
    }
    emit(UpdateAction(*action, t));
    d->addAction(std::move(action));
  }
  else if (mode == "VC-CMV" || mode == "VC-AC")
  {
    auto action = std::make_unique<SEMechanicalVentilatorVolumeControl>();
    action->Copy(d->currentValues.vc);
    action->SetConnection(eSwitch::On);
    // See setMode for dial assignments
    if (mode == "VC-AC")
    {
      action->SetMode(eMechanicalVentilator_VolumeControlMode::AssistedControl);
    }
    else
    {
      action->SetMode(eMechanicalVentilator_VolumeControlMode::ContinuousMandatoryVentilation);
    }
    emit(UpdateAction(*action, t));

    d->addAction(std::move(action));
  }
  else if (mode == "CPAP")
  {
    auto action = std::make_unique<SEMechanicalVentilatorContinuousPositiveAirwayPressure>();
    action->SetConnection(eSwitch::On);
    action->Copy(d->currentValues.cpap);
    emit(UpdateAction(*action, t));

    d->addAction(std::move(action));
  }
  else {
    qWarning() << "Mode " << mode << " not found.";
  }

  d->setUIState(Private::Connected);

}

void QVentilatorWidget::disconnectAction()
{
  auto action = std::make_unique<SEMechanicalVentilatorConfiguration>();
  action->GetSettings().SetConnection(eSwitch::Off);
  SEScalarTime t;
  t.Invalidate();
  emit(UpdateAction(*action, t));
  d->addAction(std::move(action));

  d->setUIState(Private::Disconnected);
}

void QVentilatorWidget::addHold()
{
  /// If the hold button is enabled
  /// both of these other buttons are enabled as well, therefore
  /// we don't need to cache the state
  d->applyButton->setDisabled(true);
  d->disconnectButton->setDisabled(true);

  auto action = std::make_unique<SEMechanicalVentilatorHold>();
  action->SetState(eSwitch::On);
  d->addAction(std::move(action));
}

void QVentilatorWidget::removeHold()
{
  d->applyButton->setDisabled(false);
  d->disconnectButton->setDisabled(false);

  auto action = std::make_unique<SEMechanicalVentilatorHold>();
  action->SetState(eSwitch::Off);
  d->addAction(std::move(action));
}


void QVentilatorWidget::setMode(QString val)
{
  d->setMode(val);
}

void QVentilatorWidget::resetDials()
{
  d->resetDials();
}

void QVentilatorWidget::setupNumbers()
{
  d->number_0->setup("PIP", "cmH20");
  d->number_1->setup("VT", "mL");
  d->number_2->setup("Pmean", "cmH20");
  d->number_3->setup("MVe", "L/min");
  d->number_4->setup("RR", "/min");
  d->number_5->setup("etCO2", "mmHg"); // , 2
  d->number_6->setup("cDyn", "L/cmH2O", 2);
  d->number_7->setup("I:E", "", 2);
}

void QVentilatorWidget::lockForHold()
{
  d->applyButton->setDisabled(true);
}

void QVentilatorWidget::unlock()
{
  d->applyButton->setDisabled(false);
}
