//=========================================================================
//  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.
//=========================================================================

// local includes
#include "pqACE3PJobLoader.h"

#include "plugin/pqACE3PAutoStart.h"
#include "plugin/pqACE3PRemoteParaViewBehavior.h"
#include "smtk/simulation/ace3p/Project.h"
#include "smtk/simulation/ace3p/qt/qtModeSelectDialog.h"
#include "smtk/simulation/ace3p/qt/qtProgressDialog.h"
#include "smtk/simulation/ace3p/qt/qtProjectRuntime.h"

// pq includes
#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqCoreUtilities.h"
#include "pqDataRepresentation.h"
#include "pqFileDialogModel.h"
#include "pqMultiViewWidget.h"
#include "pqObjectBuilder.h"
#include "pqPipelineRepresentation.h"
#include "pqPipelineSource.h"
#include "pqRenderView.h"
#include "pqSMAdaptor.h"
#include "pqScalarsToColors.h"
#include "pqServer.h"
#include "pqServerConfiguration.h"
#include "pqServerManagerModel.h"
#include "pqServerResource.h"
#include "pqTabbedMultiViewWidget.h"
#include "pqView.h"

// vtk includes
#include "vtkDataObject.h"
#include "vtkPVArrayInformation.h"
#include "vtkPVDataInformation.h"
#include "vtkPVDataSetAttributesInformation.h"
#include "vtkSMPVRepresentationProxy.h"
#include "vtkSMParaViewPipelineControllerWithRendering.h"
#include "vtkSMProperty.h"
#include "vtkSMProxy.h"
#include "vtkSMSourceProxy.h"
#include "vtkSMTransferFunctionPresets.h"
#include "vtkSMTransferFunctionProxy.h"
#include "vtkSMViewLayoutProxy.h"

#include "vtk_jsoncpp.h"

// Qt includes
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QModelIndex>
#include <QSet>
#include <QString>
#include <QStringList>
#include <QTimer>
#include <QVariant>

//-----------------------------------------------------------------------------
pqACE3PJobLoader::pqACE3PJobLoader(QObject* parent)
  : Superclass(parent)
{
  m_dialog = new smtk::simulation::ace3p::qtModeSelectDialog(pqCoreUtilities::mainWidget());

  QObject::connect(
    m_dialog,
    &smtk::simulation::ace3p::qtModeSelectDialog::selectedModeFiles,
    this,
    &pqACE3PJobLoader::onModelFilesSelected);
}

//-----------------------------------------------------------------------------
static pqACE3PJobLoader* g_instance = nullptr;

pqACE3PJobLoader* pqACE3PJobLoader::instance(QObject* parent)
{
  if (!g_instance)
  {
    g_instance = new pqACE3PJobLoader(parent);
  }

  if (g_instance->parent() == nullptr && parent)
  {
    g_instance->setParent(parent);
  }

  return g_instance;
}

//-----------------------------------------------------------------------------
pqACE3PJobLoader::~pqACE3PJobLoader()
{
  if (g_instance == this)
  {
    g_instance = nullptr;
  }

  QObject::disconnect(this);
}

//-----------------------------------------------------------------------------
pqView* pqACE3PJobLoader::findView(const pqPipelineSource* source)
{
  // Attempt 1, try to find a view in which the source is already shown.
  if (source)
  {
    foreach (pqView* view, source->getViews())
    {
      pqDataRepresentation* repr = source->getRepresentation(0, view);
      if (repr && repr->isVisible())
      {
        return view;
      }
    }
  }

  // Attempt 2, check all the views and see if one is the right type and not
  // showing anything.
  pqApplicationCore* core = pqApplicationCore::instance();
  pqServerManagerModel* smModel = core->getServerManagerModel();
  pqServer* server = pqActiveObjects::instance().activeServer();
  // qDebug() << __FILE__ << __LINE__ << server <<  server->isRemote();

  foreach (pqView* view, smModel->findItems<pqView*>(server))
  {
    if (
      view && (view->getViewType() == "RenderView") &&
      (view->getNumberOfVisibleRepresentations() < 1))
    {
      return view;
    }
  }

#if 1
  // This logic is needed for some reason.
  // It finds the the current layout and makes it the "active frame".
  // Without it, the *new* layout is not raised to the front of the tabbed widget.
  // Wish I knew why.
  if (server->isRemote())
  {
    QList<pqTabbedMultiViewWidget*> wlist =
      pqCoreUtilities::mainWidget()->findChildren<pqTabbedMultiViewWidget*>();
    pqTabbedMultiViewWidget* tmvWidget = wlist[0];
    vtkSMViewLayoutProxy* layout = tmvWidget->layoutProxy();
    tmvWidget->findTab(layout)->makeFrameActive();
  }
#endif

  // Attempt 3, create a render view
  m_progressDialog->setProgressText("Creating render view");
  pqObjectBuilder* builder = core->getObjectBuilder();
  pqView* newView = builder->createView("RenderView", server);
  builder->addToLayout(newView);

  if (server->isRemote())
  {
    // Rename the layout (tab)
    QString name = server->getResource().configuration().name();
    vtkSMViewLayoutProxy* layout = vtkSMViewLayoutProxy::FindLayout(newView->getViewProxy());
    pqProxy* proxy = smModel->findItem<pqProxy*>(layout);
    proxy->rename(name);
  }

  pqActiveObjects::instance().setActiveView(newView);
  return newView;
}

//-----------------------------------------------------------------------------
pqPipelineSource* pqACE3PJobLoader::findPipelineSource(const char* SMName)
{
  pqApplicationCore* core = pqApplicationCore::instance();
  pqServerManagerModel* smModel = core->getServerManagerModel();
  pqServer* server = pqActiveObjects::instance().activeServer();
  // qDebug() << __FILE__ << __LINE__ << std::boolalpha <<  server->isRemote();
  QList<pqPipelineSource*> sources = smModel->findItems<pqPipelineSource*>(server);
  foreach (pqPipelineSource* s, sources)
  {
    if (strcmp(s->getProxy()->GetXMLName(), SMName) == 0)
    {
      return s;
    }
  }

  return nullptr;
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::onRequestLoadJob(const QString& jobId, bool remote)
{
  // Set the field name based on the analysis code
  //   * "efield" for EM types
  //   * empty string for others (to be set after mode files selected)
  static const QSet<QString> EMTypes = { "Omega3P", "S3P", "T3P", "Track3P" };

  auto project = smtk::simulation::ace3p::qtProjectRuntime::instance()->ace3pProject();
  int jobIndex = project->jobsManifest()->findIndex(jobId.toStdString());
  std::string analysis;
  project->getJobRecordField(jobIndex, "analysis", analysis);
  m_fieldName = EMTypes.contains(QString::fromStdString(analysis)) ? "efield" : "";

  if (!remote)
  {
    // Make sure built-in server is active
    pqServer* builtinServer = pqACE3PAutoStart::builtinServer();
    pqActiveObjects::instance().setActiveServer(builtinServer);
    pqCoreUtilities::processEvents();

    this->openModeSelectDialog(jobId, builtinServer);
    return;
  }

  // Get the machine for this job
  std::string _machine;
  project->getJobRecordField(jobIndex, "machine", _machine);
  QString machine = QString::fromStdString(_machine);

  // Check behavior for remote server
  auto* behavior = pqACE3PRemoteParaViewBehavior::instance();
  pqServer* server = behavior->remoteServer(machine);
  if (server == nullptr)
  {
    // Call method to connect to remote server
    QObject::connect(
      behavior,
      &pqACE3PRemoteParaViewBehavior::serverAdded,
      [this, jobId, behavior](pqServer* server) {
        // qDebug() << __FILE__ << __LINE__ << server->isRemote();
        if (server->isRemote())
        {
          // qDebug() << __FILE__ << __LINE__ << (server == pqActiveObjects::instance().activeServer());
          QObject::disconnect(behavior, &pqACE3PRemoteParaViewBehavior::serverAdded, this, nullptr);
          this->openModeSelectDialog(jobId, server);
        }
      });
    behavior->startRemoteServer(machine);
  }
  else
  {
    pqActiveObjects::instance().setActiveServer(server);
    pqCoreUtilities::processEvents();
    this->openModeSelectDialog(jobId, server);
  }
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::openModeSelectDialog(const QString& jobId, pqServer* server)
{
  auto project = smtk::simulation::ace3p::qtProjectRuntime::instance()->ace3pProject();
  int jobIndex = project->jobsManifest()->findIndex(jobId.toStdString());
  bool isLocal = ((server == nullptr) || !server->isRemote());
  std::string analysis;
  project->getJobRecordField(jobIndex, "analysis", analysis);

  // Need paths to mesh file and results directory
  std::string meshFileLocation;
  std::string modesDirectory;
  if (isLocal)
  {
    std::string local_job_folder;
    std::string mesh_filename;
    std::string results_subfolder;
    project->getJobRecordField(jobIndex, "local_job_folder", local_job_folder);
    project->getJobRecordField(jobIndex, "runtime_mesh_filename", mesh_filename);
    project->getJobRecordField(jobIndex, "results_subfolder", results_subfolder);

    meshFileLocation = project->assetsDirectory() + "/" + mesh_filename;
    modesDirectory = local_job_folder + "/download/" + results_subfolder;

    if (analysis == "Track3P")
    {
      std::string particlesDirectory = modesDirectory + "/PARTICLES";
      m_particlesDirectory = QString::fromStdString(particlesDirectory);
    }
  }
  else // (remote)
  {
    std::string runtime_job_folder;
    std::string mesh_filename;
    std::string results_subfolder;
    project->getJobRecordField(jobIndex, "runtime_job_folder", runtime_job_folder);
    project->getJobRecordField(jobIndex, "runtime_mesh_filename", mesh_filename);
    project->getJobRecordField(jobIndex, "results_subfolder", results_subfolder);

    meshFileLocation = runtime_job_folder + "/" + mesh_filename;
    if (analysis == "Track3P")
    {
      // Track3P mode files are in the input folder
      project->getJobRecordField(jobIndex, "runtime_input_folder", modesDirectory);

      // Track3P particle files are in results/PARTICLES folder
      std::string particlesDirectory = runtime_job_folder + "/" + results_subfolder + "/PARTICLES";
      m_particlesDirectory = QString::fromStdString(particlesDirectory);
    }
    else
    {
      modesDirectory = runtime_job_folder + "/" + results_subfolder;
    }
  }
  m_meshFileLocation = QString::fromStdString(meshFileLocation);
  m_modesDirectory = QString::fromStdString(modesDirectory);

  // Check that paths exist
  pqFileDialogModel fileModel(server);
  QString fullPath;
  if (!fileModel.fileExists(m_meshFileLocation, fullPath))
  {
    qCritical() << "Mesh file not found:" << m_meshFileLocation;
    return;
  }

  if (!fileModel.dirExists(m_modesDirectory, fullPath))
  {
    qCritical() << "Directory for mode files not found:" << m_modesDirectory;
    return;
  }

  if (!m_particlesDirectory.isEmpty() && (!fileModel.dirExists(m_particlesDirectory, fullPath)))
  {
    qCritical() << "Directory for particle files not found:" << m_particlesDirectory;
    return;
  }

  fileModel.setCurrentPath(m_modesDirectory);

  QStringList modeFiles = this->getModeFileNames(m_modesDirectory, fileModel);
  m_dialog->initTable(modeFiles);
  m_dialog->show();
}

//-----------------------------------------------------------------------------
static void destroyPortConsumers(pqOutputPort* port)
{
  foreach (pqPipelineSource* consumer, port->getConsumers())
  {
    pqACE3PJobLoader::destroyPipelineSourceAndConsumers(consumer);
  }
}

void pqACE3PJobLoader::destroyPipelineSourceAndConsumers(pqPipelineSource* source)
{
  if (!source)
  {
    return;
  }

  foreach (pqOutputPort* port, source->getOutputPorts())
  {
    destroyPortConsumers(port);
  }

  pqApplicationCore* core = pqApplicationCore::instance();
  pqObjectBuilder* builder = core->getObjectBuilder();
  builder->destroy(source);
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::onModelFilesSelected(const QStringList& modeFiles)
{
  // Make sure mode-select dialog is closed.
  // Because if user clicks "OK" twice, modelbuilder crashes somewhere in pqPropertiesPanel.
  this->m_dialog->hide();
  pqCoreUtilities::processEvents();

  // In its place, display progress dialog
  // Initialize the progress dialog
  if (m_progressDialog == nullptr)
  {
    m_progressDialog = new qtProgressDialog(pqCoreUtilities::mainWidget(), 0, 0, "Progress");
    m_progressDialog->setModal(true);
    m_progressDialog->setCancelButtonVisible(false);
    m_progressDialog->setMessageBoxVisible(true, false);
    m_progressDialog->setAutoClose(true);
    m_progressDialog->setMinDuration(3);
    m_progressDialog->setAutoCloseDelay(5);
  }
  m_progressDialog->clearProgressMessages();
  m_progressDialog->show();
  m_progressDialog->raise();
  m_progressDialog->setLabelText("Setting up rendering pipeline");
  m_progressDialog->setProgressText("Mode files selected.");

  pqCoreUtilities::processEvents();

  // Check m_fieldName
  if (m_fieldName.isEmpty() && !modeFiles.empty())
  {
    // Use prefix of first mode file
    QString modeFile = modeFiles[0];
    int position = modeFile.lastIndexOf('/') + 1;
    int n = modeFile.length() - position - 4;
    m_fieldName = modeFile.mid(position, n);
    if ((m_fieldName == "Strain") || (m_fieldName == "Stress"))
    {
      m_fieldName = QString("%1Diagonal").arg(m_fieldName);
    }
  }

  QString text = QString("Using field name: %1").arg(m_fieldName);
  m_progressDialog->setInfoText(text);
  qInfo() << "Using field name:" << m_fieldName;

  // Delay start of pipeline setup so that progress dialog has cycles first.
  // (parts of pipeline setup block the UI)
  QTimer::singleShot(100, [this, modeFiles]() { this->setupPipeline(modeFiles); });
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::setupPipeline(const QStringList& modeFiles)
{
  pqApplicationCore* core = pqApplicationCore::instance();
  pqObjectBuilder* builder = core->getObjectBuilder();
  vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;

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

  // find the last pipeline source if one exists already in the pipeline
  QList<pqPipelineSource*> sources = smModel->findItems<pqPipelineSource*>(server);
  pqPipelineSource* oldSource = nullptr;
  foreach (pqPipelineSource* s, sources)
  {
    if (strcmp(s->getProxy()->GetXMLName(), "SLACReader") == 0)
    {
      oldSource = s;
    }
  }

  // get the first view the old source is in and delete the old source from the pipeline
  pqView* meshView = nullptr;
  if (oldSource)
  {
    QList<pqView*> views = oldSource->getViews();
    if (views.size() > 0)
    {
      meshView = views[0];

      // Make scalar bars turned off
      QList<pqDataRepresentation*> reps = oldSource->getRepresentations(meshView);
      if (!reps.isEmpty())
      {
        vtkSMPVRepresentationProxy::SetScalarBarVisibility(
          reps[0]->getProxy(), meshView->getProxy(), false);
      }
    }
    destroyPipelineSourceAndConsumers(oldSource);
  }

  QStringList meshFiles;
  meshFiles << m_meshFileLocation;
  {
    m_progressDialog->setInfoText("Setting up SLACReader for mesh and files.");
    pqPipelineSource* meshReader =
      builder->createReader("sources", "SLACReader", meshFiles, server);

    vtkSMSourceProxy* meshReaderProxy = vtkSMSourceProxy::SafeDownCast(meshReader->getProxy());

    // Set up mode (if any).
    pqSMAdaptor::setFileListProperty(meshReaderProxy->GetProperty("ModeFileName"), modeFiles);

    pqPipelineSource* particlesReader = nullptr;
    if (!m_particlesDirectory.isEmpty())
    {
      pqCoreUtilities::processEvents();
      QString msg("Getting particle file list.");
      qInfo() << msg;
      m_progressDialog->setInfoText(msg);
      pqCoreUtilities::processEvents();

      pqFileDialogModel fileModel(server);
      fileModel.setCurrentPath(m_particlesDirectory);
      pqCoreUtilities::processEvents();

      QStringList particlesFiles;
      this->getParticlesFileNames(fileModel, particlesFiles);
      if (!particlesFiles.isEmpty())
      {
        // Instantiate particle reader
        particlesReader =
          builder->createReader("sources", "SLACParticleReader", particlesFiles, server);
      }
    }

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

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

    // ensures that the FrequencyScale and PhaseShift have correct default
    // values.
    meshReaderProxy->GetProperty("FrequencyScale")
      ->Copy(meshReaderProxy->GetProperty("FrequencyScaleInfo"));
    meshReaderProxy->GetProperty("PhaseShift")
      ->Copy(meshReaderProxy->GetProperty("PhaseShiftInfo"));

    // if a meshview is not already available, get a new one
    if (!meshView)
    {
      meshView = findView(meshReader);
    }

    // cast to a render view
    pqRenderView* meshRenderView = qobject_cast<pqRenderView*>(meshView);

    // Make representations.
    m_progressDialog->setProgressText("Making representations.");
    controller->HideAll(meshRenderView->getViewProxy());
    controller->Show(meshReaderProxy, 0, meshRenderView->getViewProxy());
    controller->Show(meshReaderProxy, 1, meshRenderView->getViewProxy());

    if (particlesReader != nullptr)
    {
      // Make representations.
      controller->Show(particlesReader->getSourceProxy(), 0, meshRenderView->getViewProxy());
      // controller->SetVisibility(particlesReader->getSourceProxy(), 0, meshView->getViewProxy(),
      //   manager->actionShowParticles()->isChecked()); // keeping what old code was doing.
      controller->SetVisibility(
        particlesReader->getSourceProxy(), 0, meshRenderView->getViewProxy(), true);

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

    meshRenderView->resetCamera();
    m_progressDialog->setProgressText("Initializing visualization options.");
    this->initVizOptions(meshReader, meshRenderView);
    // We have already made the representations and pushed everything to the
    // server manager.  Thus, there is no state left to be modified.
    meshReader->setModifiedState(pqProxy::UNMODIFIED);
  }

  m_progressDialog->setProgressText("Pipeline ready.");
  m_progressDialog->progressFinished();
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::initVizOptions(pqPipelineSource* meshReader, pqView* meshView)
{
  std::string name = m_fieldName.toStdString();

  // Get the (downcasted) representation.
  pqDataRepresentation* _repr = meshReader->getRepresentation(0, meshView);
  pqPipelineRepresentation* repr = qobject_cast<pqPipelineRepresentation*>(_repr);
  if (!repr)
  {
    QString msg("Did not find representation object.");
    qCritical() << msg;
    m_progressDialog->setErrorText(msg);
    m_progressDialog->progressFinished();
    return;
  }

  // Get information about the field we are supposed to be showing.
  vtkPVDataInformation* dataInfo = repr->getInputDataInformation();
  vtkPVDataSetAttributesInformation* pointInfo = dataInfo->GetPointDataInformation();
  vtkPVArrayInformation* arrayInfo = pointInfo->GetArrayInformation(name.c_str());
  if (!arrayInfo)
  {
    QString msg("Did not find array information.");
    qCritical() << msg;
    m_progressDialog->setErrorText(msg);
    m_progressDialog->progressFinished();
    return;
  }

  // Set the field to color by.
  vtkSMPVRepresentationProxy::SetScalarColoring(
    repr->getProxy(), name.c_str(), vtkDataObject::POINT);

  pqScalarsToColors* lut = repr->getLookupTable();
  vtkSMProxy* lutProxy = lut->getProxy();

  if (m_fieldName == "efield")
  {
    // For efield data, adjust the color map to be rainbow (matching SLACTools plugin)
    pqSMAdaptor::setEnumerationProperty(lutProxy->GetProperty("ColorSpace"), "HSV");

    QVariant opvar = pqSMAdaptor::getMultipleElementProperty(lutProxy->GetProperty("Opacity"));

    // Control points are 4-tuples comprising scalar value + RGB
    QList<QVariant> RGBPoints;
    RGBPoints << 0.0 << 0.0 << 0.0 << 1.0;
    RGBPoints << 1.0 << 1.0 << 0.0 << 0.0;
    pqSMAdaptor::setMultipleElementProperty(lutProxy->GetProperty("RGBPoints"), RGBPoints);

    // NaN color is a 3-tuple RGB.
    QList<QVariant> NanColor;
    NanColor << 0.5 << 0.5 << 0.5;
    pqSMAdaptor::setMultipleElementProperty(lutProxy->GetProperty("NanColor"), NanColor);
  }
  else
  {
    // For non-efield data, use paraview default color map (Cool to Warm)
    vtkSMTransferFunctionPresets* presets = vtkSMTransferFunctionPresets::GetInstance();

    const char* defaultPreset = "Cool to Warm";
    int idx = -1;
    const Json::Value& preset = presets->GetFirstPresetWithName("Cool to Warm", idx);
    if (idx < 0)
    {
      QString msg = QString("Did not find \"%1\" color preset.").arg(defaultPreset);
      m_progressDialog->setWarningText(msg);
      qWarning() << msg;
      return;
    }
    vtkSMTransferFunctionProxy::ApplyPreset(lutProxy, preset, false);
  }

  // Show scalar bar for non-efield data
  bool showScalarBar = m_fieldName != "efield";
  vtkSMPVRepresentationProxy::SetScalarBarVisibility(
    repr->getProxy(), meshView->getProxy(), showScalarBar);

  // Set the range of the scalars to the current range of the field.
  double range[2];
  arrayInfo->GetComponentRange(-1, range);
  lut->setScalarRange(range[0], range[1]);

  lutProxy->UpdateVTKObjects();

  meshView->render();
}

//-----------------------------------------------------------------------------
QStringList pqACE3PJobLoader::getModeFileNames(
  const QString& directory,
  pqFileDialogModel& fileModel)
{
  QStringList ret;

  // Traverse file model for files ending ".mod"
  const QModelIndex rootIndex;
  for (int row = 0; row < fileModel.rowCount(rootIndex); ++row)
  {
    // Model column 0 is filename (pqFileDialogModel.cxx)
    QModelIndex index = fileModel.index(row, 0, rootIndex);
    QString filename = fileModel.data(index, Qt::DisplayRole).toString();
    if (filename.endsWith(".mod"))
    {
      QString path = fileModel.absoluteFilePath(filename);
      ret << path;
      // qDebug() << "Row" << row << path;
    }
  }

  return ret;
}

//-----------------------------------------------------------------------------
void pqACE3PJobLoader::getParticlesFileNames(
  pqFileDialogModel& fileModel,
  QStringList& particlesFiles) const
{
  QString msg("Getting particle files");
  qInfo() << msg;
  m_progressDialog->setInfoText(msg);
  pqCoreUtilities::processEvents();
  particlesFiles.clear();

  // There should be 1 top-level row with file group "partpath_ts..ncdf"
  int rootNumRows = fileModel.rowCount(QModelIndex());
  if (rootNumRows != 1)
  {
    QString msg = QString("Error: Particles file model should have 1 row not %1").arg(rootNumRows);
    qCritical() << msg;
    m_progressDialog->setErrorText(msg);
    m_progressDialog->progressFinished();
    return;
  }

  // First row *should* be partpath_ts..ncdf
  QModelIndex parentIndex = fileModel.index(0, 0, QModelIndex());
  QString parentName = fileModel.data(parentIndex, Qt::DisplayRole).toString();
  if (parentName != "partpath_ts..ncdf")
  {
    QString msg =
      QString("Error: Expected root of particles files model to be partpath_ts..ncdf, not %1")
        .arg(parentName);
    qCritical() << msg;
    m_progressDialog->setErrorText(msg);
    m_progressDialog->progressFinished();
    return;
  }

  int numRows = fileModel.rowCount(parentIndex);
  QString prefix = m_particlesDirectory + "/";
  for (int row = 0; row < numRows; ++row)
  {
    if ((row % 10) == 0)
    {
      pqCoreUtilities::processEvents();
    }
    QModelIndex modelIndex = fileModel.index(row, 0, parentIndex);
    QString filename = fileModel.data(modelIndex, Qt::DisplayRole).toString();
    QString path = fileModel.absoluteFilePath(filename);
    // qDebug() << " " << path;
    particlesFiles.push_back(path);
  }

  msg = QString("Found %1 particles files").arg(particlesFiles.size());
  qInfo() << msg;
  m_progressDialog->setProgressText(msg);
}
