// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Sandia Corporation
// SPDX-License-Identifier: BSD-3-Clause

#include "pqCoreTestUtility.h"

#include <QApplication>
#include <QCommonStyle>
#include <QCoreApplication>
#include <QEvent>
#include <QFileInfo>
#include <QImage>
#include <QPixmap>
#include <QWidget>
#include <QtDebug>

#include "QtTestingConfigure.h"

#include "QVTKOpenGLNativeWidget.h"
#include "QVTKOpenGLStereoWidget.h"
#include "pqApplicationCore.h"
#include "pqCollaborationEventPlayer.h"
#include "pqColorButtonEventPlayer.h"
#include "pqColorButtonEventTranslator.h"
#include "pqColorDialogEventPlayer.h"
#include "pqColorDialogEventTranslator.h"
#include "pqConsoleWidgetEventPlayer.h"
#include "pqConsoleWidgetEventTranslator.h"
#include "pqCoreConfiguration.h"
#include "pqEventDispatcher.h"
#include "pqFileDialogEventPlayer.h"
#include "pqFileDialogEventTranslator.h"
#include "pqFileUtilitiesEventPlayer.h"
#include "pqFlatTreeViewEventPlayer.h"
#include "pqFlatTreeViewEventTranslator.h"
#include "pqImageUtil.h"
#include "pqLineEditEventPlayer.h"
#include "pqQVTKWidget.h"
#include "pqQVTKWidgetEventPlayer.h"
#include "pqQVTKWidgetEventTranslator.h"
#include "pqServer.h"
#include "pqServerManagerModel.h"
#include "pqUndoStack.h"
#include "pqView.h"
#include "pqXMLEventObserver.h"
#include "pqXMLEventSource.h"
#include "vtkBMPWriter.h"
#include "vtkErrorCode.h"
#include "vtkImageDifference.h"
#include "vtkImageShiftScale.h"
#include "vtkJPEGWriter.h"
#include "vtkNew.h"
#include "vtkPNGReader.h"
#include "vtkPNGWriter.h"
#include "vtkPNMWriter.h"
#include "vtkPVServerInformation.h"
#include "vtkProcessModule.h"
#include "vtkRemoteWriterHelper.h"
#include "vtkRenderWindow.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMViewLayoutProxy.h"
#include "vtkSMViewProxy.h"
#include "vtkSmartPointer.h"
#include "vtkTIFFWriter.h"
#include "vtkTesting.h"
#include "vtkTrivialProducer.h"
#include "vtkWindowToImageFilter.h"
#include "vtksys/SystemTools.hxx"

#include <cassert>

#ifdef QT_TESTING_WITH_PYTHON
#include "pqPythonEventSourceImage.h"
#endif

const char* pqCoreTestUtility::PQ_COMPAREVIEW_PROPERTY_NAME = "PQ_COMPAREVIEW_PROPERTY_NAME";

static constexpr const char* DASHBOARD_MODE_ENV_VAR = "DASHBOARD_TEST_FROM_CTEST";

template <typename WriterT>
bool saveImage(vtkWindowToImageFilter* Capture, const QFileInfo& File)
{
  WriterT* const writer = WriterT::New();
  writer->SetInputConnection(Capture->GetOutputPort());
  writer->SetFileName(File.filePath().toUtf8().data());
  writer->Write();
  const bool result = writer->GetErrorCode() == vtkErrorCode::NoError;
  writer->Delete();

  return result;
}

/////////////////////////////////////////////////////////////////////////////
// pqCoreTestUtility

pqCoreTestUtility::pqCoreTestUtility(QObject* p)
  : pqTestUtility(p)
{
  // we don't want to the dispatcher to wait during event playback. We will
  // explicitly register timers that need to be timed out.
  pqEventDispatcher::setEventPlaybackDelay(0);

  // add an XML source
  this->addEventSource("xml", new pqXMLEventSource(this));
  this->addEventObserver("xml", new pqXMLEventObserver(this));

#ifdef QT_TESTING_WITH_PYTHON
  this->addEventSource("py", new pqPythonEventSourceImage(this));
#endif

  this->eventTranslator()->addWidgetEventTranslator(new pqQVTKWidgetEventTranslator(this));
  this->eventTranslator()->addWidgetEventTranslator(new pqFlatTreeViewEventTranslator(this));
  this->eventTranslator()->addWidgetEventTranslator(new pqColorButtonEventTranslator(this));
  this->eventTranslator()->addWidgetEventTranslator(new pqColorDialogEventTranslator(this));
  this->eventTranslator()->addWidgetEventTranslator(new pqConsoleWidgetEventTranslator(this));

  this->eventPlayer()->addWidgetEventPlayer(new pqFileUtilitiesEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqLineEditEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqQVTKWidgetEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqFlatTreeViewEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqColorButtonEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqColorDialogEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqCollaborationEventPlayer(this));
  this->eventPlayer()->addWidgetEventPlayer(new pqConsoleWidgetEventPlayer(this));
}

//-----------------------------------------------------------------------------
void pqCoreTestUtility::updatePlayers()
{
  if (vtksys::SystemTools::HasEnv(DASHBOARD_MODE_ENV_VAR))
  {
    // Safe to add multiple times
    this->eventPlayer()->addWidgetEventPlayer(new pqFileDialogEventPlayer(this));
  }
  else
  {
    this->eventPlayer()->removeWidgetEventPlayer("pqFileDialogEventPlayer");
  }
}

//-----------------------------------------------------------------------------
void pqCoreTestUtility::updateTranslators()
{
  if (vtksys::SystemTools::HasEnv(DASHBOARD_MODE_ENV_VAR))
  {
    // Safe to add multiple times
    this->eventTranslator()->addWidgetEventTranslator(new pqFileDialogEventTranslator(this));
  }
  else
  {
    this->eventTranslator()->removeWidgetEventTranslator("pqFileDialogEventTranslator");
  }
}

//-----------------------------------------------------------------------------
pqCoreTestUtility::~pqCoreTestUtility() = default;

//-----------------------------------------------------------------------------
QString pqCoreTestUtility::DataRoot()
{
  return pqCoreTestUtility::cleanPath(
    QString::fromStdString(pqCoreConfiguration::instance()->dataDirectory()));
}

//-----------------------------------------------------------------------------
QString pqCoreTestUtility::BaselineDirectory()
{
  auto dir = QString::fromStdString(pqCoreConfiguration::instance()->baselineDirectory());
  if (dir.isEmpty())
  {
    // Finally use the xml file location if an instance is available
    pqApplicationCore* core = pqApplicationCore::instance();
    if (core != nullptr)
    {
      pqTestUtility* testUtil = core->testUtility();
      dir = QFileInfo(testUtil->filename()).path();
    }
  }

  if (dir.isEmpty())
  {
    // Use current CWD in case none is provided
    dir = ".";
  }

  return pqCoreTestUtility::cleanPath(dir);
}

//-----------------------------------------------------------------------------
QString pqCoreTestUtility::TestDirectory()
{
  return pqCoreTestUtility::cleanPath(
    QString::fromStdString(pqCoreConfiguration::instance()->testDirectory()));
}

//-----------------------------------------------------------------------------
QString pqCoreTestUtility::cleanPath(const QString& arg)
{
  if (arg.isEmpty())
  {
    return arg;
  }
  auto result = QDir::cleanPath(arg);

  // Ensure all slashes face forward ...
  result.replace('\\', '/');

  // Remove any trailing slashes ...
  if (result.size() && result.at(result.size() - 1) == '/')
  {
    result.chop(1);
  }

  // Trim excess whitespace ...
  result = result.trimmed();
  return result;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::SaveScreenshot(vtkRenderWindow* RenderWindow, const QString& File)
{
  vtkWindowToImageFilter* const capture = vtkWindowToImageFilter::New();
  capture->SetInput(RenderWindow);
  capture->Update();

  bool success = false;

  const QFileInfo file(File);
  if (file.completeSuffix() == "bmp")
    success = saveImage<vtkBMPWriter>(capture, file);
  else if (file.completeSuffix() == "tif")
    success = saveImage<vtkTIFFWriter>(capture, file);
  else if (file.completeSuffix() == "ppm")
    success = saveImage<vtkPNMWriter>(capture, file);
  else if (file.completeSuffix() == "png")
    success = saveImage<vtkPNGWriter>(capture, file);
  else if (file.completeSuffix() == "jpg")
    success = saveImage<vtkJPEGWriter>(capture, file);

  capture->Delete();

  return success;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareImage(vtkRenderWindow* renderWindow, const QString& referenceImage,
  double threshold, ostream& vtkNotUsed(output), const QString& tempDirectory, const QSize& size)
{
  // Store the original size
  int originalSize[2];
  int* tmpSize = renderWindow->GetSize();
  originalSize[0] = tmpSize[0];
  originalSize[1] = tmpSize[1];
  if (size.isValid() && !size.isEmpty())
  {
    renderWindow->SetSize(size.width(), size.height());
  }

  vtkSmartPointer<vtkTesting> testing = vtkSmartPointer<vtkTesting>::New();
  testing->AddArgument("-T");
  testing->AddArgument(tempDirectory.toUtf8().data());
  testing->AddArgument("-V");
  testing->AddArgument(referenceImage.toUtf8().data());
  testing->SetRenderWindow(renderWindow);
  bool ret = testing->RegressionTest(threshold) == vtkTesting::PASSED;
  renderWindow->SetSize(originalSize);
  return ret;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareImage(vtkImageData* testImage, const QString& ReferenceImage,
  double Threshold, ostream& vtkNotUsed(Output), const QString& TempDirectory)
{
  vtkSmartPointer<vtkTesting> testing = vtkSmartPointer<vtkTesting>::New();
  testing->AddArgument("-T");
  testing->AddArgument(TempDirectory.toUtf8().data());
  testing->AddArgument("-V");
  testing->AddArgument(ReferenceImage.toUtf8().data());
  vtkSmartPointer<vtkTrivialProducer> tp = vtkSmartPointer<vtkTrivialProducer>::New();
  tp->SetOutput(testImage);
  if (testing->RegressionTest(tp, Threshold) == vtkTesting::PASSED)
  {
    return true;
  }
  return false;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareImage(QWidget* widget, const QString& referenceImage,
  double threshold, ostream& vtkNotUsed(output), const QString& tempDirectory,
  const QSize& size /*=QSize(300, 300)*/)
{
  assert(widget != nullptr);

  // try to locate a pqView, if any associated with the QWidget.
  QList<pqView*> views =
    pqApplicationCore::instance()->getServerManagerModel()->findItems<pqView*>();
  Q_FOREACH (pqView* view, views)
  {
    if (view && (view->widget() == widget))
    {
      cout << "Using View API for capture" << endl;
      return pqCoreTestUtility::CompareView(view, referenceImage, threshold, tempDirectory, size);
    }
  }

  // try to recover the render window directly
  QVTKOpenGLStereoWidget* glWidget = qobject_cast<QVTKOpenGLStereoWidget*>(widget);
  if (glWidget)
  {
    vtkRenderWindow* rw = glWidget->renderWindow();
    if (rw)
    {
      cout << "Using QVTKOpenGLStereoWidget RenderWindow API for capture" << endl;
      return pqCoreTestUtility::CompareImage(
        rw, referenceImage, threshold, std::cerr, tempDirectory, size);
    }
  }
  QVTKOpenGLNativeWidget* nativeWidget = qobject_cast<QVTKOpenGLNativeWidget*>(widget);
  if (nativeWidget)
  {
    vtkRenderWindow* rw = nativeWidget->renderWindow();
    if (rw)
    {
      cout << "Using QVTKOpenGLNativeWidget RenderWindow API for capture" << endl;
      return pqCoreTestUtility::CompareImage(
        rw, referenceImage, threshold, std::cerr, tempDirectory, size);
    }
  }

  if (pqQVTKWidget* const qvtkWidget = qobject_cast<pqQVTKWidget*>(widget))
  {
    vtkRenderWindow* rw = qvtkWidget->renderWindow();
    if (rw)
    {
      cout << "Using QVTKOpenGLNativeWidget RenderWindow API for capture" << endl;
      return pqCoreTestUtility::CompareImage(
        rw, referenceImage, threshold, std::cerr, tempDirectory, size);
    }
  }

  qFatal("CompareImage not supported!");
  return false;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareView(pqView* curView, const QString& referenceImage,
  double threshold, const QString& tempDirectory, const QSize& size /*=QSize()*/)
{
  assert(curView != nullptr);

  SCOPED_UNDO_EXCLUDE();

  auto viewProxy = curView->getViewProxy();

  // update size and 2d text dpi for tests.
  int original_size[2];
  vtkSMPropertyHelper sizeHelper(viewProxy, "ViewSize");
  sizeHelper.Get(original_size, 2);

  vtkSMPropertyHelper dpiHelper(viewProxy, "PPI");
  const int original_dpi = dpiHelper.GetAsInt();

  if (size.isValid() && !size.isEmpty())
  {
    const int new_size[2] = { size.width(), size.height() };
    sizeHelper.Set(new_size, 2);
    dpiHelper.Set(72); // fixed DPI for testing.
  }
  viewProxy->UpdateVTKObjects();

  auto test_image = vtkSmartPointer<vtkImageData>::Take(viewProxy->CaptureWindow(1));

  // restore size and dpi.
  sizeHelper.Set(original_size, 2);
  dpiHelper.Set(original_dpi);
  viewProxy->UpdateVTKObjects();

  curView->widget()->update();

  if (!test_image)
  {
    qCritical() << "ERROR: Failed to capture snapshot.";
    return false;
  }

  // The returned image will have extents translated to match the view position,
  // we shift them back.
  int view_position[2];
  vtkSMPropertyHelper(viewProxy, "ViewPosition").Get(view_position, 2);
  // Update image extents based on ViewPosition
  int extents[6];
  test_image->GetExtent(extents);
  for (int cc = 0; cc < 4; cc++)
  {
    extents[cc] -= view_position[cc / 2];
  }
  test_image->SetExtent(extents);
  bool ret =
    pqCoreTestUtility::CompareImage(test_image, referenceImage, threshold, cout, tempDirectory);
  return ret;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareImage(const QString& testPNGImage, const QString& referenceImage,
  double threshold, ostream& output, const QString& tempDirectory)
{
  // We need to make sure that all the screenshot saved are entirely written to disk.
  // This is done by looking at the collected futures in vtkRemoteWriterHelper.
  vtkRemoteWriterHelper::Wait(testPNGImage.toStdString());

  vtkNew<vtkPNGReader> reader;
  if (!reader->CanReadFile(testPNGImage.toUtf8().data()))
  {
    output << qPrintable(tr("Cannot read file : %1").arg(testPNGImage)) << endl;
    return false;
  }
  reader->SetFileName(testPNGImage.toUtf8().data());
  reader->Update();
  return pqCoreTestUtility::CompareImage(
    reader->GetOutput(), referenceImage, threshold, output, tempDirectory);
}

//-----------------------------------------------------------------------------
QString pqCoreTestUtility::fixPath(const QString& path)
{
  QString newpath = path;
  newpath.replace("$PARAVIEW_TEST_ROOT", pqCoreTestUtility::TestDirectory());
  newpath.replace("$PARAVIEW_TEST_BASELINE_DIR", pqCoreTestUtility::BaselineDirectory());
  newpath.replace("$PARAVIEW_DATA_ROOT", pqCoreTestUtility::DataRoot());
  newpath.replace("$PARAVIEW_PID", QString::number(QCoreApplication::applicationPid()));
  return newpath;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareTile(QWidget* widget, int rank, int tdx, int tdy,
  const QString& baseline, double threshold, ostream& output, const QString& tempDirectory)
{
  // try to locate a pqView, if any associated with the QWidget.
  auto views = pqApplicationCore::instance()->getServerManagerModel()->findItems<pqView*>();
  for (pqView* view : views)
  {
    if (view && (view->widget() == widget))
    {
      return pqCoreTestUtility::CompareTile(
        view, rank, tdx, tdy, baseline, threshold, output, tempDirectory);
    }
  }

  qFatal("CompareTile not supported on the provided widget");
  return false;
}

//-----------------------------------------------------------------------------
bool pqCoreTestUtility::CompareTile(pqView* view, int rank, int tdx, int tdy,
  const QString& baseline, double threshold, ostream& output, const QString& tempDirectory)
{
  auto layout = view ? vtkSMViewLayoutProxy::FindLayout(view->getViewProxy()) : nullptr;
  if (!layout)
  {
    return false;
  }

  if (tdx >= 1 && tdy >= 1)
  {
    auto serverInfo = view->getServer()->getServerInformation();
    if (!serverInfo || serverInfo->GetTileDimensions()[0] != tdx ||
      serverInfo->GetTileDimensions()[1] != tdy)
    {
      // skip compare.
      return true;
    }
  }

  const QString imagepath =
    QString("%1/tile-%2.png").arg(tempDirectory).arg(QFileInfo(baseline).baseName());
  layout->SaveAsPNG(rank, imagepath.toUtf8().data());
  return pqCoreTestUtility::CompareImage(imagepath, baseline, threshold, output, tempDirectory);
}

// ----------------------------------------------------------------------------
void pqCoreTestUtility::setDashboardMode(bool value)
{
  if (value)
  {
    qputenv(DASHBOARD_MODE_ENV_VAR, "1");
  }
  else
  {
    qunsetenv(DASHBOARD_MODE_ENV_VAR);
  }
  this->updatePlayers();
  this->updateTranslators();
}
