// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Sandia Corporation
// SPDX-License-Identifier: BSD-3-Clause
#include "pqApplyBehavior.h"

#include "pqActiveObjects.h"
#include "pqApplicationCore.h"
#include "pqDataRepresentation.h"
#include "pqLiveInsituManager.h"
#include "pqObjectBuilder.h"
#include "pqPVApplicationCore.h"
#include "pqPipelineFilter.h"
#include "pqPropertiesPanel.h"
#include "pqProxyModifiedStateUndoElement.h"
#include "pqRenderView.h"
#include "pqServerManagerModel.h"
#include "pqUndoStack.h"
#include "vtkDataObject.h"
#include "vtkNew.h"
#include "vtkPVArrayInformation.h"
#include "vtkPVCatalystChannelInformation.h"
#include "vtkPVDataInformation.h"
#include "vtkPVGeneralSettings.h"
#include "vtkSMAnimationSceneProxy.h"
#include "vtkSMColorMapEditorHelper.h"
#include "vtkSMLiveInsituLinkProxy.h"
#include "vtkSMParaViewPipelineControllerWithRendering.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMRepresentationProxy.h"
#include "vtkSMSession.h"
#include "vtkSMSessionProxyManager.h"
#include "vtkSMSourceProxy.h"
#include "vtkSMTrace.h"
#include "vtkSMTransferFunctionManager.h"
#include "vtkSMViewProxy.h"
#include "vtkWeakPointer.h"

#include <QList>
#include <QSet>
#include <QtDebug>

#include <cassert>

class pqApplyBehavior::pqInternals
{
public:
  typedef QPair<vtkWeakPointer<vtkSMRepresentationProxy>, vtkWeakPointer<vtkSMViewProxy>> PairType;
  QList<PairType> NewlyCreatedRepresentations;
  pqTimer AutoApplyTimer;
  int PanelCount = 0;
  QMetaObject::Connection ShortcutApplyConnection;
};

//-----------------------------------------------------------------------------
pqApplyBehavior::pqApplyBehavior(QObject* parentObject)
  : Superclass(parentObject)
  , Internals(new pqApplyBehavior::pqInternals())
{
  // Connect auto apply timer to triggerApply signal
  this->Internals->AutoApplyTimer.setSingleShot(true);
  QObject::connect(
    &this->Internals->AutoApplyTimer, &pqTimer::timeout, this, &pqApplyBehavior::triggerApply);

  // Connect pqPVApplicationCore::triggerApply to triggerApply signal
  QObject::connect(pqPVApplicationCore::instance(), &pqPVApplicationCore::triggerApply, this,
    &pqApplyBehavior::triggerApply);

  // Connect the shortcut triggerApply signal to onApplied, used only if no panel are registered
  // later.
  this->Internals->ShortcutApplyConnection = QObject::connect(
    this, &pqApplyBehavior::triggerApply, this, QOverload<>::of(&pqApplyBehavior::onApplied));

  // Connect builder creation signals to onModified
  // to allow using auto apply without registered properties panels
  const auto* builder = pqApplicationCore::instance()->getObjectBuilder();
  QObject::connect(builder, &pqObjectBuilder::sourceCreated, this, &pqApplyBehavior::onModified);
  QObject::connect(builder, &pqObjectBuilder::filterCreated, this, &pqApplyBehavior::onModified);
}

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

//-----------------------------------------------------------------------------
void pqApplyBehavior::registerPanel(pqPropertiesPanel* panel)
{
  assert(panel);

  // Disconnect to rely on panel behavior
  QObject::disconnect(this->Internals->ShortcutApplyConnection);

  // Connect both overload on applied to onApplied
  QObject::connect(panel, QOverload<pqProxy*>::of(&pqPropertiesPanel::applied), this,
    QOverload<pqProxy*>::of(&pqApplyBehavior::onApplied));
  QObject::connect(panel, QOverload<>::of(&pqPropertiesPanel::applied), this,
    QOverload<>::of(&pqApplyBehavior::onApplied));

  // Connect modified and resetDone
  QObject::connect(panel, &pqPropertiesPanel::modified, this, &pqApplyBehavior::onModified);
  QObject::connect(panel, &pqPropertiesPanel::resetDone, this, &pqApplyBehavior::onResetDone);

  // Connect triggerApply to the panel apply
  QObject::connect(this, &pqApplyBehavior::triggerApply, panel, &pqPropertiesPanel::apply);

  this->Internals->PanelCount++;
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::unregisterPanel(pqPropertiesPanel* panel)
{
  assert(panel);
  this->Internals->PanelCount--;
  this->disconnect(panel);
  if (this->Internals->PanelCount == 0)
  {
    // If there is no panel, restore the behavior without panel
    this->Internals->ShortcutApplyConnection = QObject::connect(
      this, &pqApplyBehavior::triggerApply, this, QOverload<>::of(&pqApplyBehavior::onApplied));
  }
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::onApplied(pqProxy* proxy)
{
  pqPropertiesPanel* panel = qobject_cast<pqPropertiesPanel*>(this->sender());
  if (panel)
  {
    this->applied(panel, proxy);
  }
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::onApplied()
{
  this->applied();
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::onModified()
{
  vtkPVGeneralSettings* generalSettings = vtkPVGeneralSettings::GetInstance();
  if (generalSettings->GetAutoApply())
  {
    this->Internals->AutoApplyTimer.start(generalSettings->GetAutoApplyDelay());
  }
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::onResetDone()
{
  this->Internals->AutoApplyTimer.stop();
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::applied(pqPropertiesPanel*, pqProxy* pqproxy)
{
  this->Internals->AutoApplyTimer.stop();
  if (pqproxy->modifiedState() == pqProxy::UNINITIALIZED)
  {
    if (auto pqsource = qobject_cast<pqPipelineSource*>(pqproxy))
    {
      // if this is first apply after creation, show the data in the view.
      this->showData(pqsource, pqActiveObjects::instance().activeView());
    }

    // add undo-element to ensure this state change happens when
    // undoing/redoing.
    pqProxyModifiedStateUndoElement* undoElement = pqProxyModifiedStateUndoElement::New();
    undoElement->SetSession(pqproxy->getServer()->session());
    undoElement->MadeUnmodified(pqproxy);
    ADD_UNDO_ELEM(undoElement);
    undoElement->Delete();
  }
  pqproxy->setModifiedState(pqProxy::UNMODIFIED);

  // Make sure filters menu enable state is updated
  Q_EMIT pqApplicationCore::instance()->forceFilterMenuRefresh();

  auto pqfilter = qobject_cast<pqPipelineFilter*>(pqproxy);
  auto pqsource = qobject_cast<pqPipelineSource*>(pqproxy);
  if (pqsource != nullptr && pqfilter == nullptr)
  {
    // if we have a dataset from a source that has a Catalyst channel name we now rename
    // the proxy to be the channel name if the user didn't modify the name already
    if (pqproxy->userModifiedSMName() == false)
    {
      vtkSMSourceProxy* proxy = pqsource->getSourceProxy();

      vtkNew<vtkPVCatalystChannelInformation> information;
      information->Initialize();

      // Ask the server to fill out the rest of the information:
      proxy->GatherInformation(information);

      std::string name = information->GetChannelName();
      if (!name.empty())
      {
        pqproxy->rename(name.c_str());
      }
    }
  }
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::applied(pqPropertiesPanel* panel)
{
  this->Internals->AutoApplyTimer.stop();

  // Update all proxies if needed
  if (this->Internals->PanelCount == 0)
  {
    pqServerManagerModel* model = pqApplicationCore::instance()->getServerManagerModel();
    QList<pqPipelineSource*> sources =
      model->findItems<pqPipelineSource*>(pqActiveObjects::instance().activeServer());
    for (pqPipelineSource* source : sources)
    {
      this->applied(panel, source);
    }
  }

  //---------------------------------------------------------------------------
  // Update animation timesteps.
  vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;
  vtkSMAnimationSceneProxy::UpdateAnimationUsingDataTimeSteps(
    controller->GetAnimationScene(pqActiveObjects::instance().activeServer()->session()));

  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();

  //---------------------------------------------------------------------------
  // If there is a catalyst session, push its updates to the server
  Q_FOREACH (pqServer* server, smmodel->findItems<pqServer*>())
  {
    if (pqLiveInsituManager::isInsituServer(server))
    {
      pqLiveInsituManager::linkProxy(server)->PushUpdatedStates();
    }
  }

  QList<pqView*> dirty_views;

  //---------------------------------------------------------------------------
  // find views that need updating and update them.
  Q_FOREACH (pqView* view, smmodel->findItems<pqView*>())
  {
    if (view && view->getViewProxy()->GetNeedsUpdate())
    {
      dirty_views.push_back(view);
    }
  }

  //---------------------------------------------------------------------------
  // Update all the views separately. This ensures that all pipelines are
  // up-to-date before we render as we may need to change some rendering
  // properties like color transfer functions before the actual render.
  Q_FOREACH (pqView* view, dirty_views)
  {
    SM_SCOPED_TRACE(CallMethod)
      .arg(view->getViewProxy())
      .arg("Update")
      .arg("comment", qPrintable(tr("update the view to ensure updated data information")));
    view->getViewProxy()->Update();
  }

  vtkPVGeneralSettings* gsettings = vtkPVGeneralSettings::GetInstance();
  if (gsettings->GetColorByBlockColorsOnApply())
  {
    Q_FOREACH (const pqInternals::PairType& pair, this->Internals->NewlyCreatedRepresentations)
    {
      vtkSMRepresentationProxy* reprProxy = pair.first;
      vtkSMViewProxy* viewProxy = pair.second;

      // If not scalar coloring, we make an attempt to color using
      // 'vtkBlockColors' array, if present.
      if (!vtkSMColorMapEditorHelper::GetUsingScalarColoring(reprProxy))
      {
        auto dataInfo = reprProxy->GetRepresentedDataInformation();
        auto arrayInfo = dataInfo->GetArrayInformation("vtkBlockColors", vtkDataObject::FIELD);
        if (dataInfo->IsCompositeDataSet() && arrayInfo != nullptr &&
          arrayInfo->GetComponentRange(0)[1] > 0)
        {
          vtkSMColorMapEditorHelper::SetScalarColoring(
            reprProxy, "vtkBlockColors", vtkDataObject::FIELD);
          if (gsettings->GetScalarBarMode() ==
            vtkPVGeneralSettings::AUTOMATICALLY_SHOW_AND_HIDE_SCALAR_BARS)
          {
            vtkSMColorMapEditorHelper::SetScalarBarVisibility(reprProxy, viewProxy, true);
          }
        }
      }
    }
  }

  //---------------------------------------------------------------------------
  // If user chose it, update all transfer function data range.
  // FIXME: This should happen for all servers available.
  vtkNew<vtkSMTransferFunctionManager> tmgr;
  tmgr->ResetAllTransferFunctionRangesUsingCurrentData(
    pqActiveObjects::instance().activeServer()->proxyManager(), false /*animating*/);

  //---------------------------------------------------------------------------
  // Perform the render on visible views.
  Q_FOREACH (pqView* view, dirty_views)
  {
    if (view->widget()->isVisible())
    {
      view->forceRender();
    }
  }

  this->Internals->NewlyCreatedRepresentations.clear();
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::showData(pqPipelineSource* source, pqView* view)
{
  // HACK: Skip catalyst proxies.
  if (source->getServer()->getResource().scheme() == "catalyst")
  {
    return;
  }

  vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();

  vtkSMViewProxy* currentViewProxy = view ? view->getViewProxy() : nullptr;

  const auto& activeObjects = pqActiveObjects::instance();
  auto activeLayout = activeObjects.activeLayout();
  const auto location = activeObjects.activeLayoutLocation();

  QSet<vtkSMProxy*> updated_views;

  // create representations for all output ports.
  for (int outputPort = 0; outputPort < source->getNumberOfOutputPorts(); outputPort++)
  {
    vtkSMViewProxy* preferredView = controller->ShowInPreferredView(
      vtkSMSourceProxy::SafeDownCast(source->getProxy()), outputPort, currentViewProxy);
    if (!preferredView)
    {
      continue;
    }
    // if new view was created, let's make sure it is assigned to a layout.
    controller->AssignViewToLayout(preferredView, activeLayout, location);
    updated_views.insert(preferredView);

    // if active layout changed, let's use that from this point on.
    activeLayout = activeObjects.activeLayout();

    // reset camera if this is the only visible dataset.
    pqView* pqPreferredView = smmodel->findItem<pqView*>(preferredView);
    assert(pqPreferredView);
    if (preferredView != currentViewProxy)
    {
      // implying a new view was created, always reset that.
      pqPreferredView->resetDisplay();
    }
    else if (view && view->getNumberOfVisibleDataRepresentations() == 1)
    {
      // old view is being used, reset only if this is the only representation.
      view->resetDisplay();
    }

    // reset interaction mode for render views. Not a huge fan, but we'll fix
    // this some other time.
    // @sa `pqPipelineBrowserWidget` for BUG #20521 fix.
    if (pqRenderView* rview = qobject_cast<pqRenderView*>(pqPreferredView))
    {
      if (rview->getNumberOfVisibleDataRepresentations() == 1)
      {
        rview->updateInteractionMode(source->getOutputPort(outputPort));
      }
    }

    if (preferredView == currentViewProxy)
    {
      // Hide input, since the data wasn't shown in a new view, but an existing
      // view.
      if (pqPipelineFilter* filter = qobject_cast<pqPipelineFilter*>(source))
      {
        this->hideInputIfRequired(filter, view);
      }
    }

    vtkSMRepresentationProxy* reprProxy =
      preferredView->FindRepresentation(source->getSourceProxy(), outputPort);
    // show scalar bar, if applicable.
    vtkPVGeneralSettings* gsettings = vtkPVGeneralSettings::GetInstance();
    if (gsettings->GetScalarBarMode() ==
      vtkPVGeneralSettings::AUTOMATICALLY_SHOW_AND_HIDE_SCALAR_BARS)
    {
      if (vtkSMColorMapEditorHelper::GetUsingScalarColoring(reprProxy))
      {
        vtkSMColorMapEditorHelper::SetScalarBarVisibility(reprProxy, preferredView, true);
      }
      if (vtkSMColorMapEditorHelper::GetAnyBlockUsingScalarColoring(reprProxy))
      {
        vtkSMColorMapEditorHelper::SetBlocksScalarBarVisibility(reprProxy, preferredView,
          vtkSMColorMapEditorHelper::GetColorArraysBlockSelectors(reprProxy), true);
      }
    }

    // Save the newly created representation for further fine-tuning in
    // pqApplyBehavior::applied().
    this->Internals->NewlyCreatedRepresentations.push_back(
      pqInternals::PairType(reprProxy, preferredView));

    // If the currentViewProxy is undefined, then a new view has been created for
    // the first output port. Attempt to use it for the remaining output ports.
    if (currentViewProxy == nullptr)
    {
      currentViewProxy = preferredView;
    }
  }
}

//-----------------------------------------------------------------------------
void pqApplyBehavior::hideInputIfRequired(pqPipelineFilter* filter, pqView* view)
{
  int replace_input = filter->replaceInput();
  if (replace_input > 0)
  {
    vtkNew<vtkSMParaViewPipelineControllerWithRendering> controller;

    // hide input source.
    QList<pqOutputPort*> inputs = filter->getAllInputs();
    Q_FOREACH (pqOutputPort* input, inputs)
    {
      pqDataRepresentation* inputRepr = input->getRepresentation(view);
      if (inputRepr)
      {
        if (replace_input == 2)
        {
          // Conditionally turn off the input. The input should be turned
          // off if the representation is surface and the opacity is 1.
          QString reprType =
            vtkSMPropertyHelper(inputRepr->getProxy(), "Representation", /*quiet=*/true)
              .GetAsString();
          double opacity =
            vtkSMPropertyHelper(inputRepr->getProxy(), "Opacity", /*quiet=*/true).GetAsDouble();
          if ((reprType != "Surface" && reprType != "Surface With Edges") ||
            (opacity != 0.0 && opacity < 1.0))
          {
            continue;
          }
        }
        // we use the controller API so that the scalar bars are updated as
        // needed.
        controller->SetVisibility(
          input->getSourceProxy(), input->getPortNumber(), view->getViewProxy(), false);
      }
    }
  }
}
