//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================
#include "pqWindTunnelControlsView.h"
#include "ui_pqWindTunnelControlsView.h"

#include "smtk/simulation/windtunnel/qt/qtControlsViewConfig.h"
#include "smtk/simulation/windtunnel/qt/qtOpenFoamRunner.h"
#include "smtk/simulation/windtunnel/qt/qtSessionData.h"

// SMTK includes
#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/DirectoryItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKResource.h"
#include "smtk/extension/qt/qtOperationDialog.h"
#include "smtk/operation/Manager.h"
#include "smtk/operation/Operation.h"
#include "smtk/resource/Manager.h"
#include "smtk/resource/Resource.h"
#include "smtk/view/Configuration.h"
#include "smtk/view/Manager.h"

// ParaView includes
#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqDataRepresentation.h"
#include "pqObjectBuilder.h"
#include "pqPipelineSource.h"
#include "pqReloadFilesReaction.h"
#include "pqRenderView.h"
#include "pqSMAdaptor.h"
#include "pqServer.h"
#include "pqServerManagerModel.h"
#include "pqView.h"

#include "vtkSMParaViewPipelineControllerWithRendering.h"
#include "vtkSMProxy.h"
#include "vtkSMSourceProxy.h"

// VTK includes
#include "vtkNew.h"

#include <QComboBox>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFrame>
#include <QMessageBox>
#include <QSharedPointer>
#include <QSizePolicy>
#include <QSpinBox>
#include <QStringList>

#include <set>
#include <string>

namespace
{
const int OP_SUCCEEDED = static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED);
}

// --------------------------------------------------------------------
QMap<QString, pqPipelineSource*> pqWindTunnelControlsView::s_pipelineSourceMap;

void pqWindTunnelControlsView::clearSourceMap()
{
  s_pipelineSourceMap.clear();
}

// --------------------------------------------------------------------
class pqWindTunnelControlsView::Internal : public Ui::pqWindTunnelControlsView
{
  //Q_OBJECT
public:
  QWidget* m_widget = nullptr;
  smtk::simulation::windtunnel::qtControlsViewConfig* m_config = nullptr;
  bool m_dataModified = false; // indicates if foam data needs to be reloaded

  void initialize(const smtk::view::ConfigurationPtr config, QWidget* widget);
};

void pqWindTunnelControlsView::Internal::initialize(
  const smtk::view::ConfigurationPtr config,
  QWidget* widget)
{
  this->setupUi(widget);
  m_widget = widget;

  // Set widgets to retain geometry when hidden
  QSizePolicy policy = m_display->sizePolicy();
  policy.setRetainSizeWhenHidden(true);
  m_display->setSizePolicy(policy);
  m_opacity->setSizePolicy(policy);
  m_showLog->setSizePolicy(policy);

  // Ingest the view configuration
  m_config = new smtk::simulation::windtunnel::qtControlsViewConfig(config);
  bool opVisible = !m_config->operationType().empty();
  m_showLog->setVisible(opVisible);
  m_lastRunLabel->setVisible(opVisible);
  m_run->setVisible(opVisible);

  const QString& foamApp = m_config->foamApplication();
  if (foamApp.isEmpty())
  {
    m_lastRunLabel->setText("");
    m_showLog->setVisible(false);
  }
  else
  {
    QString label = QString("Run %1").arg(foamApp);
    m_run->setText(label);
  }

  const QString& defaultRepr = m_config->defaultRepresentation();
  int comboIndex = m_representation->findText(defaultRepr);
  if (comboIndex >= 0)
  {
    m_representation->setCurrentIndex(comboIndex);
  }
}

// --------------------------------------------------------------------
pqWindTunnelControlsView::qtBaseView* createViewWidget(const smtk::view::Information& info)
{
  pqWindTunnelControlsView* view = new pqWindTunnelControlsView(info);
  return view;
}

pqWindTunnelControlsView::pqWindTunnelControlsView(const smtk::view::Information& info)
  : qtBaseView(info)
{
  m_internal = new Internal;
  this->createWidget();

  QObject::connect(
    m_internal->m_run, &QPushButton::clicked, this, &pqWindTunnelControlsView::onRunClicked);

  QObject::connect(m_internal->m_showLog, &QPushButton::clicked, [this]() {
    auto* runner = smtk::simulation::windtunnel::qtOpenFoamRunner::instance();
    runner->showLogFile(m_internal->m_config->logfilePath());
  });

  // If connected to model resource, find pqPipelineSource
  const QString& role = m_internal->m_config->resourceRole();
  if (!role.isEmpty())
  {
    // There might be a race condition with the pipeline source being created
    auto* source = this->locateSMTKResource(role);
    //qDebug() << "pqWindTunnelControlsView:" << __LINE__ << "source" << source;
  }

  auto* runner = smtk::simulation::windtunnel::qtOpenFoamRunner::instance();
  QObject::connect(runner, &smtk::simulation::windtunnel::qtOpenFoamRunner::started, [this]() {
    m_internal->m_run->setEnabled(false);
  });

  QObject::connect(
    runner,
    &smtk::simulation::windtunnel::qtOpenFoamRunner::finished,
    this,
    &pqWindTunnelControlsView::onProcessFinished);

  QObject::connect(
    m_internal->m_display,
    &QPushButton::clicked,
    this,
    &pqWindTunnelControlsView::onDisplayClicked);

  QObject::connect(
    m_internal->m_representation,
    QOverload<int>::of(&QComboBox::currentIndexChanged),
    this,
    &pqWindTunnelControlsView::onRepresentationChanged);

  QObject::connect(
    m_internal->m_opacity,
    QOverload<int>::of(&QSpinBox::valueChanged),
    this,
    &pqWindTunnelControlsView::onOpacityChanged);

  QObject::connect(
    m_internal->m_hideOthers,
    &QPushButton::clicked,
    this,
    &pqWindTunnelControlsView::onHideOthersClicked);
}

pqWindTunnelControlsView::~pqWindTunnelControlsView()
{
  delete m_internal;
}

bool pqWindTunnelControlsView::isEmpty() const
{
  auto* sessionData = smtk::simulation::windtunnel::qtSessionData::instance();

  // View only usable when there is a container engine
  if (sessionData->containerEngine().isEmpty())
  {
    return true;
  }

  // View is only usable when project is loaded
  auto project = sessionData->project();
  if (project == nullptr)
  {
    return true;
  }

  const QString& cat = m_internal->m_config->category();
  if (!cat.isEmpty())
  {
    std::string _cat = cat.toStdString();

    // Check against current categories
    auto attResourceSet = project->resources().findByRole<smtk::attribute::Resource>("attributes");
    auto attResource = *attResourceSet.begin();
    const std::set<std::string>& activeCategories = attResource->activeCategories();
    if (activeCategories.find(_cat) == activeCategories.end())
    {
      return true;
    }
  }

  // else
  return false;
}

void pqWindTunnelControlsView::createWidget()
{
  this->Widget = new QFrame(this->parentWidget());
  m_internal->initialize(this->configuration(), this->Widget);
}

void pqWindTunnelControlsView::updateUI()
{
  // qDebug() << "Entered pqWindTunnelControlsView::updateUI()";
  this->updateLogControls();
  this->updateGeometryControls();
}

void pqWindTunnelControlsView::onRunClicked()
{
  auto* sessionData = smtk::simulation::windtunnel::qtSessionData::instance();
  auto opManager = sessionData->operationManager();

  auto op = opManager->create(m_internal->m_config->operationType());
  if (op == nullptr)
  {
    QString opType = m_internal->m_config->operationType().c_str();
    QString message = QString("Internal Error: failed to create operation %1.").arg(opType);
    QMessageBox::critical(this->Widget, "Error", message);
    return;
  }

  auto project = sessionData->project();
  if (project == nullptr)
  {
    QString message =
      "The operation cannot run because there is no project loaded into the system.";
    QMessageBox::critical(this->Widget, "No Project Loaded", message);
    return;
  }

  op->parameters()->associate(project);

  // Disable operation's run option so that we can run from here
  auto runItem = op->parameters()->find("run");
  if (runItem != nullptr)
  {
    runItem->setIsEnabled(false);
  }

  // Check for option to use operation dialog
  if (m_internal->m_config->useOperationDialog())
  {
    this->runOperationDialog(op);
    return;
  }

  // Run direct (no dialog)
  auto result = op->operate();
  this->onOperationExecuted(result);
}

void pqWindTunnelControlsView::runOperationDialog(smtk::operation::OperationPtr op)
{
  auto sessionData = smtk::simulation::windtunnel::qtSessionData::instance();
  auto project = sessionData->project();
  auto resourceManager = std::dynamic_pointer_cast<smtk::resource::Resource>(project)->manager();
  auto viewManager = sessionData->viewManager();

  // Construct a modal dialog for the operation spec
  QSharedPointer<smtk::extension::qtOperationDialog> dialog =
    QSharedPointer<smtk::extension::qtOperationDialog>(
      new smtk::extension::qtOperationDialog(op, resourceManager, viewManager, this->widget()));
  dialog->setObjectName("Operation Dialog");
  dialog->setWindowTitle("Specify Operation Properties");
  QObject::connect(
    dialog.get(),
    &smtk::extension::qtOperationDialog::operationExecuted,
    this,
    &pqWindTunnelControlsView::onOperationExecuted);
  dialog->exec();
}

void pqWindTunnelControlsView::onOperationExecuted(smtk::attribute::AttributePtr result)
{
  int outcome = result->findInt("outcome")->value();
  if (outcome != OP_SUCCEEDED)
  {
    QString message = QString("Error generating input files; outcome %1").arg(outcome);
    QMessageBox::critical(this->Widget, "Error", message);
    return;
  }
  // m_internal->m_config->dump();

  // Remove pipeline source if geometry is a project resource
  const QString& key = m_internal->m_config->resourceRole();
  if (!key.isEmpty())
  {
    s_pipelineSourceMap.remove(key);
  }

  // Run foam application next
  QString foamApp = m_internal->m_config->foamApplication();
  if (foamApp.isEmpty())
  {
    this->updateGeometryControls();
    return;
  }

  // Set up async run
  const QString& caseDirectory = m_internal->m_config->caseDirectory();
  QStringList args = m_internal->m_config->foamArguments();
  auto* runner = smtk::simulation::windtunnel::qtOpenFoamRunner::instance();
  runner->start(foamApp, caseDirectory, args);
  // TODO Hide geometry
}

void pqWindTunnelControlsView::onProcessFinished(qint64 pid, int exitcode)
{
  m_internal->m_run->setEnabled(true);

  const QString& foamfilePath = m_internal->m_config->foamfilePath();
  if (exitcode == 0 && !foamfilePath.isEmpty())
  {
    // Create or touch foamfile
    QFile file(foamfilePath);
    file.open(QFile::WriteOnly);
    file.close();

    // If the foamfile is already in memory, set modified flag
    auto iter = s_pipelineSourceMap.find(foamfilePath);
    if (iter != s_pipelineSourceMap.end())
    {
      m_internal->m_dataModified = true;
    }
  }

  this->updateUI();
}

void pqWindTunnelControlsView::onDisplayClicked()
{
  // Only applies if active view is a render view
  pqView* view = pqActiveObjects::instance().activeView();
  pqRenderView* renderView = dynamic_cast<pqRenderView*>(view);
  if (renderView == nullptr)
  {
    qCritical() << "No active view or is not a render view";
    return;
  }

  // Look for the pipeline source
  pqPipelineSource* source = this->getPipelineSource();
  if (source == nullptr)
  {
    // Is this a foam file that hasn't been loaded yet?
    const QString& foamfilePath = m_internal->m_config->foamfilePath();
    if (!foamfilePath.isEmpty())
    {
      // Get the source (load if needed)
      bool isNew = false;
      source = this->getPipelineSource(foamfilePath, isNew);
    }
  }

  if (source == nullptr)
  {
    qCritical() << "Internal error: failed to find or load pipeline source.";
    return;
  }

  pqDataRepresentation* representation = source->getRepresentation(view);
  if (representation == nullptr)
  {
    qWarning() << "No representation for pipeline source.";
    return;
  }

  QString displayText = m_internal->m_display->text();
  if (displayText.startsWith("Hide"))
  {
    representation->setVisible(false);
    view->render();
    m_internal->m_display->setText("Show Data");
    return;
  }

  // (else)
  if (m_internal->m_dataModified)
  {
    auto* proxy = vtkSMSourceProxy::SafeDownCast(source->getProxy());
    pqReloadFilesReaction::reload(proxy);
    m_internal->m_dataModified = false;
  }

  vtkSMProxy* reprProxy = representation->getProxy();

  // Apply representation and opacity settings
  QString repType = m_internal->m_representation->currentText();
  pqSMAdaptor::setEnumerationProperty(reprProxy->GetProperty("Representation"), repType);

  double opacity = static_cast<double>(m_internal->m_opacity->value()) / 100.0;
  pqSMAdaptor::setElementProperty(reprProxy->GetProperty("Opacity"), opacity);

  representation->setVisible(true);
  view->render();
  m_internal->m_display->setText("Hide Data");
}

void pqWindTunnelControlsView::onRepresentationChanged()
{
  pqDataRepresentation* representation = this->getActiveRepresentation();
  if (representation == nullptr)
  {
    qWarning() << "Internal error: representation is null" << __FILE__ << __LINE__;
    return;
  }

  vtkSMProxy* reprProxy = representation->getProxy();
  QString repType = m_internal->m_representation->currentText();
  pqSMAdaptor::setEnumerationProperty(reprProxy->GetProperty("Representation"), repType);
  representation->getInput()->getSourceProxy()->UpdateVTKObjects();
  if (representation->isVisible())
  {
    // Toggle visibility otherwise display doesn't update (dont know why)
    representation->setVisible(false);
    representation->setVisible(true);
    representation->getView()->render();
  }
}

void pqWindTunnelControlsView::onOpacityChanged(int i)
{
  pqDataRepresentation* representation = this->getActiveRepresentation();
  if (representation == nullptr)
  {
    qWarning() << "Internal error: representation is null" << __FILE__ << __LINE__;
    return;
  }

  vtkSMProxy* reprProxy = representation->getProxy();
  double opacity = static_cast<double>(i) / 100.0;
  pqSMAdaptor::setElementProperty(reprProxy->GetProperty("Opacity"), opacity);
  representation->getInput()->getSourceProxy()->UpdateVTKObjects();
  if (representation->isVisible())
  {
    // Toggle visibility otherwise display doesn't update (dont know why)
    representation->setVisible(false);
    representation->setVisible(true);
    representation->getView()->render();
  }
}

void pqWindTunnelControlsView::onHideOthersClicked()
{
  // Only applies if active view is a render view
  pqView* view = pqActiveObjects::instance().activeView();
  pqRenderView* renderView = dynamic_cast<pqRenderView*>(view);
  if (renderView == nullptr)
  {
    qCritical() << "No active view or is not a render view";
    return;
  }

  // Get this source (could be nullptr)
  pqPipelineSource* thisSource = this->getPipelineSource();

  // Traverse sources and hide others
  auto iter = s_pipelineSourceMap.begin();
  for (; iter != s_pipelineSourceMap.end(); ++iter)
  {
    auto* source = iter.value();
    if (source == thisSource)
    {
      continue;
    }

    auto* rep = source->getRepresentation(view);
    if (rep != nullptr)
    {
      rep->setVisible(false);
    }
  } // end (for)

  view->render();
}

void pqWindTunnelControlsView::updateLogControls()
{
  // m_internal->m_config->dump();
  const QString& foamApp = m_internal->m_config->foamApplication();
  if (foamApp.isEmpty())
  {
    return; // no foamApp to run so no logs
  }

  m_internal->m_lastRunLabel->setText("");
  m_internal->m_showLog->setVisible(false);

  const QString& logfilePath = m_internal->m_config->logfilePath();
  QFileInfo fileInfo(logfilePath);
  if (!fileInfo.exists())
  {
    return;
  }

  m_internal->m_showLog->setVisible(true);

  QDateTime dt = fileInfo.lastModified();
  QString dtString = dt.toString("ddd dd-MMM-yyyy hh:mm");
  QString label = QString("Last run: %1").arg(dtString);
  m_internal->m_lastRunLabel->setText(label);
}

void pqWindTunnelControlsView::updateGeometryControls()
{
  // Check whether or not to show geometry controls
  bool showButtons = false;
  pqPipelineSource* source = nullptr;

  const QString& foamfilePath = m_internal->m_config->foamfilePath();
  const QString& resourceRole = m_internal->m_config->resourceRole();
  if (!foamfilePath.isEmpty())
  {
    if (QFileInfo::exists(foamfilePath))
    {
      showButtons = true;
      auto iter = s_pipelineSourceMap.find(foamfilePath);
      if (iter == s_pipelineSourceMap.end())
      {
        // Data not yet loaded
        m_internal->m_display->setText("Load Data");
      }
      else if (m_internal->m_dataModified)
      {
        // Data needs to be loaded
        m_internal->m_display->setText("Reload Data");
      }
      else
      {
        source = iter.value();
      }
    }
  }
  else if (!resourceRole.isEmpty())
  {
    // Check map first
    auto iter = s_pipelineSourceMap.find(resourceRole);
    if (iter == s_pipelineSourceMap.end())
    {
      source = this->locateSMTKResource(resourceRole);
    }
    else
    {
      source = iter.value();
    }
    showButtons = source != nullptr;
  }

  // Is source currently visible?
  pqView* view = pqActiveObjects::instance().activeView();
  pqRenderView* renderView = dynamic_cast<pqRenderView*>(view);
  if (renderView == nullptr)
  {
    showButtons = false;
  }
  else if (source != nullptr)
  {
    auto* representation = source->getRepresentation(view);
    QString text = representation->isVisible() ? "Hide Data" : "Show Data";
    m_internal->m_display->setText(text);
  }

  // Finally, set button visibility
  m_internal->m_display->setVisible(showButtons);
  m_internal->m_representation->setVisible(showButtons);
  m_internal->m_opacity->setVisible(showButtons);
  m_internal->m_hideOthers->setVisible(showButtons);
}

pqPipelineSource* pqWindTunnelControlsView::locateSMTKResource(const QString& resourceRole)
{
  // Find the project resource
  auto* sessionData = smtk::simulation::windtunnel::qtSessionData::instance();
  auto project = sessionData->project();
  if (project == nullptr)
  {
    return nullptr;
  }

  std::set<smtk::resource::ResourcePtr> resourceSet =
    project->resources().findByRole(resourceRole.toStdString());
  if (resourceSet.empty())
  {
    return nullptr;
  }
  auto iter = resourceSet.begin();
  smtk::resource::ResourcePtr projectResource = *iter;

  // Find pipeline source for this resource
  pqServer* server = pqActiveObjects::instance().activeServer();
  pqServerManagerModel* smModel = pqApplicationCore::instance()->getServerManagerModel();
  QList<pqSMTKResource*> sourceList = smModel->findItems<pqSMTKResource*>(server);
  Q_FOREACH (pqSMTKResource* source, sourceList)
  {
    if (source->getResource() == projectResource)
    {
      // Add resource to the map
      s_pipelineSourceMap.insert(resourceRole, source);
      return source;
    }
  } // for (each pqSMTKResource)

  return nullptr; // not found
}

pqDataRepresentation* pqWindTunnelControlsView::getActiveRepresentation() const
{
  // Only applies if active view is a render view
  pqView* view = pqActiveObjects::instance().activeView();
  pqRenderView* renderView = dynamic_cast<pqRenderView*>(view);
  if (renderView == nullptr)
  {
    return nullptr;
  }

  pqPipelineSource* source = this->getPipelineSource();
  if (source == nullptr)
  {
    return nullptr;
  }

  return source->getRepresentation(view);
}

pqPipelineSource* pqWindTunnelControlsView::getPipelineSource() const
{
  QString key;
  const QString& foamfilePath = m_internal->m_config->foamfilePath();
  const QString& role = m_internal->m_config->resourceRole();
  if (!foamfilePath.isEmpty())
  {
    key = foamfilePath;
  }
  else if (!role.isEmpty())
  {
    key = role;
  }
  else
  {
    return nullptr;
  }

  auto iter = s_pipelineSourceMap.find(key);
  return iter == s_pipelineSourceMap.end() ? nullptr : iter.value();
}

pqPipelineSource* pqWindTunnelControlsView::getPipelineSource(const QString& foamfile, bool& isNew)
{
  if (foamfile.isEmpty())
  {
    qWarning() << "Foam File is empty" << __FILE__ << __LINE__;
    return nullptr;
  }

  auto iter = s_pipelineSourceMap.find(foamfile);
  if (iter != s_pipelineSourceMap.end())
  {
    return iter.value();
  }

  // Create pqPipelineSource
  pqApplicationCore* core = pqApplicationCore::instance();
  pqObjectBuilder* builder = core->getObjectBuilder();

  pqServer* server = pqActiveObjects::instance().activeServer();
  if (server == nullptr)
  {
    qWarning() << __FILE__ << __LINE__ << "active server is null";
    return nullptr;
  }
  pqServerManagerModel* smModel = core->getServerManagerModel();

  QStringList files;
  files << foamfile;
  pqPipelineSource* source = builder->createReader("sources", "OpenFOAMReader", files, server);
  vtkSMSourceProxy* readerProxy = source->getSourceProxy();
  isNew = true;

  // Push changes to server so that when the representation gets updated,
  // it uses the property values we set.
  readerProxy->UpdateVTKObjects();

  // ensures that new timestep range, if any gets fetched from the server.
  readerProxy->UpdatePipelineInformation();

  // Create representation for the active view if it's a render view
  pqView* view = pqActiveObjects::instance().activeView();
  pqRenderView* renderView = dynamic_cast<pqRenderView*>(view);
  if (renderView == nullptr)
  {
    qCritical() << "No active view or is not a render view";
    return source;
  }

  // Make representations.
  vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;
  // controller->HideAll(renderView->getViewProxy());
  controller->Show(readerProxy, 0, renderView->getViewProxy());

  renderView->resetCamera();

  // We have already made the representations and pushed everything to the
  // server manager.  Thus, there is no state left to be modified.
  source->setModifiedState(pqProxy::UNMODIFIED);

  s_pipelineSourceMap.insert(foamfile, source);
  return source;
}
