// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Sandia Corporation
// SPDX-License-Identifier: BSD-3-Clause
// Hide PARAVIEW_DEPRECATED_IN_5_12_0() warnings for this class.
#define PARAVIEW_DEPRECATION_LEVEL 0

#include "pqTimeInspectorWidget.h"
#include "ui_pqTimeInspectorWidget.h"

#include "pqActiveObjects.h"
#include "pqAnimationManager.h"
#include "pqAnimationModel.h"
#include "pqAnimationScene.h"
#include "pqAnimationTrack.h"
#include "pqApplicationCore.h"
#include "pqCoreUtilities.h"
#include "pqPVApplicationCore.h"
#include "pqPropertyLinks.h"
#include "pqPropertyLinksConnection.h"
#include "pqProxy.h"
#include "pqServer.h"
#include "pqServerManagerModel.h"
#include "pqTimelineScrollbar.h"
#include "pqUndoStack.h"
#include "vtkCommand.h"
#include "vtkCompositeAnimationPlayer.h"
#include "vtkNew.h"
#include "vtkPVDataInformation.h"
#include "vtkPVGeneralSettings.h"
#include "vtkSMParaViewPipelineController.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMSourceProxy.h"
#include "vtkSMTrace.h"

#include <QHeaderView>
#include <QLineF>
#include <QPainter>
#include <QVariant>
#include <QtDebug>

#include <cassert>

namespace
{
pqProxy* getPQProxy(vtkSMProxy* proxy)
{
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  return smmodel->findItem<pqProxy*>(proxy);
}
}

class pqTimeInspectorWidget::PropertyLinksConnection : public pqPropertyLinksConnection
{
  typedef pqPropertyLinksConnection Superclass;

public:
  PropertyLinksConnection(QObject* qobject, const char* qproperty, const char* qsignal,
    vtkSMProxy* smproxy, vtkSMProperty* smproperty, int smindex, bool use_unchecked_modified_event,
    QObject* parentObject = nullptr)
    : Superclass(qobject, qproperty, qsignal, smproxy, smproperty, smindex,
        use_unchecked_modified_event, parentObject)
  {
  }
  ~PropertyLinksConnection() override = default;

protected:
  /// These are the methods that subclasses can override to customize how
  /// values are updated in either directions.
  void setServerManagerValue(bool use_unchecked, const QVariant& value) override
  {
    assert(use_unchecked == false);
    (void)use_unchecked;

    std::vector<vtkSMProxy*> proxies;
    Q_FOREACH (const QVariant& var, value.toList())
    {
      vtkSMProxy* aproxy = reinterpret_cast<vtkSMProxy*>(var.value<void*>());
      proxies.push_back(aproxy);
    }
    proxies.push_back(nullptr);
    assert(proxies.size() > 0);
    vtkSMPropertyHelper(this->propertySM())
      .Set(&proxies[0], static_cast<unsigned int>(proxies.size() - 1));
  }

  QVariant currentServerManagerValue(bool use_unchecked) const override
  {
    assert(use_unchecked == false);
    (void)use_unchecked;
    QList<QVariant> value;

    vtkSMPropertyHelper helper(this->propertySM());
    for (unsigned int cc = 0, max = helper.GetNumberOfElements(); cc < max; cc++)
    {
      value.push_back(QVariant::fromValue<void*>(helper.GetAsProxy(cc)));
    }
    return value;
  }
};

class pqTimeInspectorWidget::TimeTrack : public pqAnimationTrack
{
  typedef pqAnimationTrack Superclass;
  vtkSmartPointer<vtkSMProxy> Source;
  unsigned long ObserverId1;
  unsigned long ObserverId2;
  std::vector<double> Markers;
  bool HasDataTime = false;
  double DataTime = 0.0;

public:
  TimeTrack(vtkSMProxy* sourceProxy, QObject* parentObj = nullptr)
    : Superclass(parentObj)
    , Source(sourceProxy)
  {
    this->ObserverId1 = sourceProxy->AddObserver(
      vtkCommand::UpdateInformationEvent, this, &TimeTrack::updateTimeSteps);
    this->ObserverId2 =
      sourceProxy->AddObserver(vtkCommand::UpdateDataEvent, this, &TimeTrack::updateTime);
    this->updateTimeSteps();
  }
  ~TimeTrack() override
  {
    this->Source->RemoveObserver(this->ObserverId1);
    this->Source->RemoveObserver(this->ObserverId2);
  }

  vtkSMProxy* source() const { return this->Source.GetPointer(); }

  pqAnimationModel* animationModel() const
  {
    pqAnimationModel* amodel = qobject_cast<pqAnimationModel*>(this->parent());
    assert(amodel);
    return amodel;
  }

protected:
  void paint(QPainter* p, const QStyleOptionGraphicsItem* option, QWidget* widget) override
  {
    Q_UNUSED(option);
    Q_UNUSED(widget);

    // draw border for this track
    p->save();
    p->setBrush(QBrush(QColor(255, 255, 255)));
    QPen pen(QColor(0, 0, 0));
    pen.setWidth(0);
    p->setPen(pen);

    QRectF trackRectF = this->boundingRect();
    p->drawRect(trackRectF);

    pen.setColor(QColor("blue"));
    p->setPen(pen);

    QLineF horzLine(trackRectF.left(), trackRectF.top() + trackRectF.height() / 2,
      trackRectF.left() + trackRectF.width(), trackRectF.top() + trackRectF.height() / 2);
    p->drawLine(horzLine);

    pen.setWidth(2);
    p->setPen(pen);

    pqAnimationModel* model = this->animationModel();
    Q_FOREACH (double mark, this->Markers)
    {
      if (mark >= model->zoomStartTime() && mark <= model->zoomEndTime())
      {
        mark = (mark - model->zoomStartTime()) / (model->zoomEndTime() - model->zoomStartTime());
        mark = trackRectF.left() + mark * trackRectF.width();
        QLineF line(mark, trackRectF.top() + 10, mark, trackRectF.top() + trackRectF.height() - 10);
        p->drawLine(line);
      }
    }
    if (this->HasDataTime)
    {
      if (this->DataTime >= model->zoomStartTime() && this->DataTime <= model->zoomEndTime())
      {
        double time = (this->DataTime - model->zoomStartTime()) /
          (model->zoomEndTime() - model->zoomStartTime());
        time = trackRectF.left() + time * trackRectF.width();
        QLineF line(time, trackRectF.top() + 3, time, trackRectF.top() + trackRectF.height() - 3);
        pen.setColor(QColor("green"));
        p->setPen(pen);
        p->drawLine(line);
      }
    }
    p->restore();
  }

private:
  void updateTimeSteps()
  {
    this->Markers = vtkSMPropertyHelper(this->Source, "TimestepValues",
      /*quiet*/ true)
                      .GetDoubleArray();
    pqAnimationModel* model = this->animationModel();
    model->update();
  }

  void updateTime()
  {
    vtkPVDataInformation* dinfo =
      vtkSMSourceProxy::SafeDownCast(this->Source)->GetDataInformation(0);
    if (dinfo->GetHasTime())
    {
      this->HasDataTime = true;
      this->DataTime = dinfo->GetTime();
    }
    else
    {
      this->HasDataTime = false;
    }
    pqAnimationModel* model = this->animationModel();
    model->update();
  }

  Q_DISABLE_COPY(TimeTrack)
};

class pqTimeInspectorWidget::pqInternals
{
public:
  void* VoidServer = nullptr;
  QPointer<pqAnimationScene> Scene;
  QPointer<pqServer> Server;
  Ui::TimeInspectorWidget Ui;
  pqPropertyLinks Links;
  QList<QVariant> TimeSources;
  QList<QVariant> SuppressedTimeSources;

  pqInternals(pqTimeInspectorWidget* self)
  {
    this->Ui.setupUi(self);
    this->Ui.AnimationWidget->createDeleteHeader()->hide();
    this->Ui.AnimationWidget->animationModel()->setInteractive(true);
    this->Ui.AnimationWidget->animationModel()->setEnabledHeaderToolTip(
      "<p>Uncheck to ignore timesteps from animation.</p>");
  }
};

//-----------------------------------------------------------------------------
pqTimeInspectorWidget::pqTimeInspectorWidget(QWidget* parentObject)
  : Superclass(parentObject)
  , Internals(new pqTimeInspectorWidget::pqInternals(this))
{
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  this->connect(smmodel, SIGNAL(nameChanged(pqServerManagerModelItem*)),
    SLOT(handleProxyNameChanged(pqServerManagerModelItem*)));

  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  this->connect(animationModel, SIGNAL(currentTimeSet(double)), SLOT(setSceneCurrentTime(double)));

  this->connect(this->Internals->Ui.AnimationWidget, SIGNAL(enableTrackClicked(pqAnimationTrack*)),
    SLOT(toggleTrackSuppression(pqAnimationTrack*)));

  this->connect(
    &pqActiveObjects::instance(), SIGNAL(serverChanged(pqServer*)), SLOT(setServer(pqServer*)));
  this->setServer(pqActiveObjects::instance().activeServer());

  pqCoreUtilities::connect(vtkPVGeneralSettings::GetInstance(), vtkCommand::ModifiedEvent, this,
    SLOT(generalSettingsChanged()));

  this->connect(pqPVApplicationCore::instance()->animationManager(),
    SIGNAL(activeSceneChanged(pqAnimationScene*)), SLOT(setAnimationScene(pqAnimationScene*)));

  pqTimelineScrollbar* timelineScrollbar = this->Internals->Ui.TimelineScrollbar;
  timelineScrollbar->linkSpacing(this->Internals->Ui.AnimationWidget);
  timelineScrollbar->setAnimationModel(this->Internals->Ui.AnimationWidget->animationModel());
}

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

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setAnimationScene(pqAnimationScene* scene)
{
  this->Internals->Scene = scene;
  if (scene)
  {
    QObject::connect(scene, SIGNAL(animationTime(double)), this, SLOT(updateSceneTime()));
  }
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setServer(pqServer* aserver)
{
  if (aserver != this->Internals->VoidServer)
  {
    this->Internals->VoidServer = aserver;
    this->Internals->Server = aserver;

    this->updateScene();
  }
}

//-----------------------------------------------------------------------------
pqServer* pqTimeInspectorWidget::server() const
{
  return this->Internals->Server;
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::updateScene()
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);

  pqPropertyLinks& links = this->Internals->Links;
  links.clear();

  pqServer* curServer = this->server();
  if (!curServer)
  {
    animationModel->setStartTime(0.0);
    animationModel->setEndTime(1.0);
    animationModel->setMode(pqAnimationModel::Sequence);
    this->Internals->Ui.AnimationTimeWidget->setAnimationScene(nullptr);
    // FIXME: remove all tracks.
    return;
  }

  vtkSMSession* session = curServer->session();
  vtkNew<vtkSMParaViewPipelineController> controller;

  pqAnimationScene* scene = this->Internals->Scene;

  vtkSMProxy* sceneProxy = scene ? scene->getProxy() : nullptr;

  if (!sceneProxy)
  {
    // The sceneProxy may be null when cleaning up a session. Hence,
    // we handle that case.
    return;
  }

  this->Internals->Ui.AnimationTimeWidget->setAnimationScene(scene);

  links.addTraceablePropertyLink(this, "sceneStartTime", SIGNAL(dummySignal()), sceneProxy,
    sceneProxy->GetProperty("StartTime"));
  links.addTraceablePropertyLink(
    this, "sceneEndTime", SIGNAL(dummySignal()), sceneProxy, sceneProxy->GetProperty("EndTime"));
  links.addTraceablePropertyLink(
    this, "scenePlayMode", SIGNAL(dummySignal()), sceneProxy, sceneProxy->GetProperty("PlayMode"));
  links.addTraceablePropertyLink(this, "sceneNumberOfFrames", SIGNAL(dummySignal()), sceneProxy,
    sceneProxy->GetProperty("NumberOfFrames"));
  // FIXME: update ticks based on play mode.

  vtkSMProxy* timeKeeper = controller->FindTimeKeeper(session);
  assert(timeKeeper);
  links.addPropertyLink(this, "sceneTimeSteps", SIGNAL(dummySignal()), timeKeeper,
    timeKeeper->GetProperty("TimestepValues"));
  links.addPropertyLink<PropertyLinksConnection>(
    this, "timeSources", SIGNAL(dummySignal()), timeKeeper, timeKeeper->GetProperty("TimeSources"));
  links.addPropertyLink<PropertyLinksConnection>(this, "suppressedTimeSources",
    SIGNAL(suppressedTimeSourcesChanged()), timeKeeper,
    timeKeeper->GetProperty("SuppressedTimeSources"));
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSceneStartTime(double val)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  animationModel->setStartTime(val);
}

//-----------------------------------------------------------------------------
double pqTimeInspectorWidget::sceneStartTime() const
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  return animationModel->startTime();
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSceneEndTime(double val)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  animationModel->setEndTime(val);
}

//-----------------------------------------------------------------------------
double pqTimeInspectorWidget::sceneEndTime() const
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  return animationModel->endTime();
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setScenePlayMode(const QString& val)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  if (val == "Sequence")
  {
    animationModel->setMode(pqAnimationModel::Sequence);
  }
  else if (val == "Real Time")
  {
    animationModel->setMode(pqAnimationModel::Real);
  }
  else if (val == "Snap To TimeSteps")
  {
    animationModel->setMode(pqAnimationModel::Custom);
  }
  else
  {
    qCritical() << "pqTimeInspectorWidget detected unknown play mode: " << val;
  }
}

//-----------------------------------------------------------------------------
QString pqTimeInspectorWidget::scenePlayMode() const
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  switch (animationModel->mode())
  {
    case pqAnimationModel::Real:
      return "Real Time";

    case pqAnimationModel::Sequence:
      return "Sequence";

    case pqAnimationModel::Custom:
    default:
      return "Snap To TimeSteps";
  }
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSceneTimeSteps(const QList<QVariant>& val)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  std::vector<double> timeSteps;
  Q_FOREACH (const QVariant& v, val)
  {
    if (v.canConvert<double>())
    {
      timeSteps.push_back(v.toDouble());
    }
  }
  if (!timeSteps.empty())
  {
    animationModel->setTickMarks(static_cast<int>(timeSteps.size()), &timeSteps[0]);
  }
  else
  {
    animationModel->setTickMarks(0, nullptr);
  }

  this->generalSettingsChanged();
}

//-----------------------------------------------------------------------------
QList<QVariant> pqTimeInspectorWidget::sceneTimeSteps() const
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  QList<QVariant> reply;
  Q_FOREACH (double val, animationModel->customTicks())
  {
    reply.push_back(val);
  }
  return reply;
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSceneNumberOfFrames(int val)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  animationModel->setTicks(val);
}

//-----------------------------------------------------------------------------
int pqTimeInspectorWidget::sceneNumberOfFrames() const
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);
  return animationModel->ticks();
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setTimeSources(const QList<QVariant>& value)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);

  QSet<vtkSMProxy*> proxies;
  Q_FOREACH (const QVariant& val, value)
  {
    vtkSMProxy* proxy = reinterpret_cast<vtkSMProxy*>(val.value<void*>());
    proxies.insert(proxy);
  }

  this->Internals->TimeSources = value;
  for (int max = animationModel->count(), cc = max - 1; cc >= 0; cc--)
  {
    TimeTrack* track = dynamic_cast<TimeTrack*>(animationModel->track(cc));
    assert(track);
    if (proxies.remove(track->source()) == false)
    {
      animationModel->removeTrack(track);
    }
  }
  Q_FOREACH (vtkSMProxy* proxy, proxies)
  {
    if (proxy && (proxy->GetProperty("TimestepValues") || proxy->GetProperty("TimeRange")))
    {
      TimeTrack* track = new TimeTrack(proxy, animationModel);
      animationModel->addTrack(track);
      pqProxy* pqproxy = getPQProxy(proxy);
      assert(pqproxy);
      track->setProperty(pqproxy->getSMName());
      track->setEnabled(true);
    }
  }
}

//-----------------------------------------------------------------------------
QList<QVariant> pqTimeInspectorWidget::timeSources() const
{
  return this->Internals->TimeSources;
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSuppressedTimeSources(const QList<QVariant>& value)
{
  pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
  assert(animationModel);

  this->Internals->SuppressedTimeSources = value;
  QSet<vtkSMProxy*> proxies;
  Q_FOREACH (const QVariant& val, value)
  {
    vtkSMProxy* proxy = reinterpret_cast<vtkSMProxy*>(val.value<void*>());
    proxies.insert(proxy);
  }

  for (int max = animationModel->count(), cc = max - 1; cc >= 0; cc--)
  {
    TimeTrack* track = dynamic_cast<TimeTrack*>(animationModel->track(cc));
    assert(track);
    track->setEnabled(!proxies.contains(track->source()));
  }
}

//-----------------------------------------------------------------------------
QList<QVariant> pqTimeInspectorWidget::suppressedTimeSources() const
{
  return this->Internals->SuppressedTimeSources;
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::setSceneCurrentTime(double time)
{
  BEGIN_UNDO_EXCLUDE();

  vtkSMProxy* animationScene = this->Internals->Scene->getProxy();
  {
    // Use another scope to prevent modifications to the TimeKeeper from
    // being traced.
    SM_SCOPED_TRACE(PropertiesModified).arg("proxy", animationScene);
    vtkSMPropertyHelper(animationScene, "AnimationTime").Set(time);
  }
  animationScene->UpdateVTKObjects();

  END_UNDO_EXCLUDE();
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::updateSceneTime()
{
  double time = this->Internals->Scene->getAnimationTime();

  pqAnimationModel* animModel = this->Internals->Ui.AnimationWidget->animationModel();
  animModel->setCurrentTime(time);
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::toggleTrackSuppression(pqAnimationTrack* track)
{
  TimeTrack* ttrack = dynamic_cast<TimeTrack*>(track);
  assert(ttrack);

  bool suppress = ttrack->isEnabled();

  if (suppress)
  {
    this->Internals->SuppressedTimeSources.push_back(QVariant::fromValue<void*>(ttrack->source()));
    ttrack->setEnabled(false);
  }
  else
  {
    ttrack->setEnabled(true);
    QList<QVariant> newValue;
    Q_FOREACH (const QVariant& val, this->Internals->SuppressedTimeSources)
    {
      vtkSMProxy* proxy = reinterpret_cast<vtkSMProxy*>(val.value<void*>());
      if (proxy != ttrack->source())
      {
        newValue.push_back(val);
      }
    }
    this->Internals->SuppressedTimeSources = newValue;
  }
  Q_EMIT this->suppressedTimeSourcesChanged();
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::handleProxyNameChanged(pqServerManagerModelItem* item)
{
  pqProxy* pqproxy = qobject_cast<pqProxy*>(item);
  if (pqproxy)
  {
    pqAnimationModel* animationModel = this->Internals->Ui.AnimationWidget->animationModel();
    assert(animationModel);
    for (int max = animationModel->count(), cc = max - 1; cc >= 0; cc--)
    {
      TimeTrack* track = dynamic_cast<TimeTrack*>(animationModel->track(cc));
      assert(track);
      if (track->source() == pqproxy->getProxy())
      {
        track->setProperty(pqproxy->getSMName());
      }
    }
  }
}

//-----------------------------------------------------------------------------
void pqTimeInspectorWidget::generalSettingsChanged()
{
  const int timePrecision = vtkPVGeneralSettings::GetInstance()->GetAnimationTimePrecision();
  this->Internals->Ui.AnimationTimeWidget->setPrecision(timePrecision);
  this->Internals->Ui.AnimationWidget->animationModel()->setTimePrecision(timePrecision);

  const int timeNotation = vtkPVGeneralSettings::GetInstance()->GetAnimationTimeNotation();
  this->Internals->Ui.AnimationTimeWidget->setNotation(
    static_cast<pqAnimationTimeWidget::RealNumberNotation>(timeNotation));

  // XXX should be reworked to use pqCoreUtilities::formatDouble
  QChar cnotation;
  switch (timeNotation)
  {
    case vtkPVGeneralSettings::FIXED:
      cnotation = 'f';
      break;

    case vtkPVGeneralSettings::SCIENTIFIC:
      cnotation = 'e';
      break;

    case vtkPVGeneralSettings::MIXED:
    default:
      cnotation = 'g';
      break;
  }

  this->Internals->Ui.AnimationWidget->animationModel()->setTimeNotation(cnotation);
}
