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

#include "pqApplicationCore.h"
#include "pqMultiViewWidget.h"
#include "pqServerManagerModel.h"
#include "pqTabbedMultiViewWidget.h"
#include "vtkEventQtSlotConnect.h"
#include "vtkSMOutputPort.h"
#include "vtkSMProxyManager.h"
#include "vtkSMProxySelectionModel.h"
#include "vtkSMSession.h"
#include "vtkSMViewLayoutProxy.h"

#include <QSignalBlocker>
#include <algorithm>

namespace
{
pqOutputPort* getPortFromPipelineProxy(pqProxy* proxy)
{
  if (auto port = qobject_cast<pqOutputPort*>(proxy))
  {
    return port;
  }
  if (auto source = qobject_cast<pqPipelineSource*>(proxy))
  {
    return source->getNumberOfOutputPorts() > 0 ? source->getOutputPort(0) : nullptr;
  }
  return nullptr;
}
}

//-----------------------------------------------------------------------------
pqActiveObjects& pqActiveObjects::instance()
{
  static pqActiveObjects activeObject;
  return activeObject;
}

//-----------------------------------------------------------------------------
pqActiveObjects::pqActiveObjects()
  : CachedServer(nullptr)
  , CachedPipelineProxy(nullptr)
  , CachedSource(nullptr)
  , CachedPort(nullptr)
  , CachedView(nullptr)
  , CachedRepresentation(nullptr)
{
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  QObject::connect(smmodel, SIGNAL(serverAdded(pqServer*)), this, SLOT(serverAdded(pqServer*)));
  QObject::connect(smmodel, SIGNAL(serverRemoved(pqServer*)), this, SLOT(serverRemoved(pqServer*)));
  QObject::connect(smmodel, SIGNAL(preItemRemoved(pqServerManagerModelItem*)), this,
    SLOT(proxyRemoved(pqServerManagerModelItem*)));

  QObject::connect(this, SIGNAL(viewChanged(pqView*)), this, SLOT(updateRepresentation()));
  QObject::connect(this, SIGNAL(portChanged(pqOutputPort*)), this, SLOT(updateRepresentation()));

  this->VTKConnector->Connect(vtkSMProxyManager::GetProxyManager(),
    vtkSMProxyManager::ActiveSessionChanged, this, SLOT(onActiveServerChanged()));

  QList<pqServer*> servers = smmodel->findItems<pqServer*>();
  if (servers.size() == 1)
  {
    this->setActiveServer(servers[0]);
  }
}

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

//-----------------------------------------------------------------------------
void pqActiveObjects::resetActives()
{
  if (auto port = ::getPortFromPipelineProxy(this->ActivePipelineProxy))
  {
    port->disconnect(this);
    auto source = port->getSource();
    source->disconnect(this);
  }

  this->ActivePipelineProxy = nullptr;

  if (this->ActiveView)
  {
    this->ActiveView->disconnect(this);
  }
  this->ActiveView = nullptr;

  if (this->ActiveRepresentation)
  {
    this->ActiveRepresentation->disconnect(this);
  }
  this->ActiveRepresentation = nullptr;
  this->Selection.clear();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::triggerSignals()
{
  if (this->signalsBlocked())
  {
    // don't update cached variables when signals are blocked.
    return;
  }

  if (this->ActiveServer.data() != this->CachedServer)
  {
    this->CachedServer = this->ActiveServer.data();
    Q_EMIT this->serverChanged(this->ActiveServer);
  }

  if (this->ActivePipelineProxy.data() != this->CachedPipelineProxy)
  {
    this->CachedPipelineProxy = this->ActivePipelineProxy.data();

    auto port = getPortFromPipelineProxy(this->ActivePipelineProxy);
    auto source = port ? port->getSource() : nullptr;

    if (source != this->CachedSource)
    {
      this->CachedSource = source;
      Q_EMIT this->sourceChanged(source);
    }

    if (port != this->CachedPort)
    {
      this->CachedPort = port;
      Q_EMIT this->portChanged(port);
    }

    Q_EMIT this->pipelineProxyChanged(this->ActivePipelineProxy);
  }

  if (this->ActiveRepresentation.data() != this->CachedRepresentation)
  {
    this->CachedRepresentation = this->ActiveRepresentation.data();
    Q_EMIT this->representationChanged(this->ActiveRepresentation);
    Q_EMIT this->representationChanged(
      static_cast<pqRepresentation*>(this->ActiveRepresentation.data()));
  }

  if (this->ActiveView.data() != this->CachedView)
  {
    this->CachedView = this->ActiveView.data();
    Q_EMIT this->viewChanged(this->ActiveView);
  }

  if (this->CachedSelection != this->Selection)
  {
    this->CachedSelection = this->Selection;
    Q_EMIT this->selectionChanged(this->Selection);
  }
}

//-----------------------------------------------------------------------------
void pqActiveObjects::serverAdded(pqServer* server)
{
  if (this->activeServer() == nullptr && server)
  {
    this->setActiveServer(server);
  }
}

//-----------------------------------------------------------------------------
void pqActiveObjects::serverRemoved(pqServer* server)
{
  if (this->activeServer() == server)
  {
    this->setActiveServer(nullptr);
  }
}

//-----------------------------------------------------------------------------
void pqActiveObjects::proxyRemoved(pqServerManagerModelItem* proxy)
{
  bool prev = this->blockSignals(true);

  if (this->ActivePipelineProxy == proxy)
  {
    this->setActivePipelineProxy(nullptr);
  }
  else if (this->ActiveView == proxy)
  {
    this->setActiveView(nullptr);
  }

  this->blockSignals(prev);
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::viewSelectionChanged()
{
  pqServer* server = this->activeServer();
  if (!this->ActiveServer)
  {
    this->resetActives();
    this->triggerSignals();
    return;
  }

  if (server->activeViewSelectionModel() == nullptr)
  {
    // This mean that the servermanager is currently updating itself and no
    // selection manager is set yet...
    return;
  }

  vtkSMProxy* selectedProxy = nullptr;
  vtkSMProxySelectionModel* viewSelection = server->activeViewSelectionModel();
  if (viewSelection->GetNumberOfSelectedProxies() == 1)
  {
    selectedProxy = viewSelection->GetSelectedProxy(0);
  }
  else if (viewSelection->GetNumberOfSelectedProxies() > 1)
  {
    selectedProxy = viewSelection->GetCurrentProxy();
    if (selectedProxy && !viewSelection->IsSelected(selectedProxy))
    {
      selectedProxy = nullptr;
    }
  }

  pqView* view =
    pqApplicationCore::instance()->getServerManagerModel()->findItem<pqView*>(selectedProxy);

  if (this->ActiveView)
  {
    this->ActiveView->disconnect(this);
  }

  this->ActiveView = view;

  if (this->ActiveView)
  {
    this->connect(this->ActiveView, SIGNAL(representationAdded(pqRepresentation*)),
      SLOT(updateRepresentation()), Qt::UniqueConnection);
    this->connect(this->ActiveView, SIGNAL(representationRemoved(pqRepresentation*)),
      SLOT(updateRepresentation()), Qt::UniqueConnection);
    this->connect(
      this->ActiveView, SIGNAL(updateDataEvent()), SIGNAL(viewUpdated()), Qt::UniqueConnection);
  }

  // if view changed, then the active representation may have changed as well.
  this->updateRepresentation();
  // updateRepresentation calls triggerSignals().
}

//-----------------------------------------------------------------------------
void pqActiveObjects::sourceSelectionChanged()
{
  pqServer* server = this->activeServer();
  if (!this->ActiveServer)
  {
    this->resetActives();
    this->triggerSignals();
    return;
  }

  if (server->activeSourcesSelectionModel() == nullptr)
  {
    // This mean that the servermanager is currently updating itself and no
    // selection manager is set yet...
    return;
  }

  if (auto port = ::getPortFromPipelineProxy(this->ActivePipelineProxy))
  {
    port->disconnect(this);
    auto source = port->getSource();
    source->disconnect(this);
  }

  // Get the pqProxy for the current selected item.

  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  vtkSMProxySelectionModel* selModel = server->activeSourcesSelectionModel();

  vtkSMProxy* currentProxy = selModel->GetCurrentProxy();
  auto proxy = smmodel->findItem<pqProxy*>(currentProxy);
  this->ActivePipelineProxy = proxy;

  if (auto port = ::getPortFromPipelineProxy(proxy))
  {
    this->connect(port, SIGNAL(representationAdded(pqOutputPort*, pqDataRepresentation*)),
      SLOT(updateRepresentation()));
    auto source = port->getSource();
    this->connect(
      source, SIGNAL(dataUpdated(pqPipelineSource*)), SIGNAL(dataUpdated()), Qt::UniqueConnection);
  }

  // Update the Selection.
  pqProxySelectionUtilities::copy(selModel, this->Selection);

  this->updateRepresentation();
  // updateRepresentation calls triggerSignals().
}

//-----------------------------------------------------------------------------
void pqActiveObjects::onActiveServerChanged()
{
  vtkSMSession* activeSession = vtkSMProxyManager::GetProxyManager()->GetActiveSession();
  if (activeSession == nullptr)
  {
    return;
  }

  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  pqServer* newActiveServer = smmodel->findServer(activeSession);

  if (newActiveServer)
  {
    this->setActiveServer(newActiveServer);
  }
}
//-----------------------------------------------------------------------------

void pqActiveObjects::setActiveServer(pqServer* server)
{
  // Make sure the active server has the proper listener setup
  // in case of collaboration the object initialisation can cause a server to
  // become active before any selection model get setup
  if (this->ActiveServer == server && this->VTKConnector->GetNumberOfConnections() > 1)
  {
    return;
  }

  QSignalBlocker blocker(this);

  this->VTKConnector->Disconnect();
  this->ActiveServer = server;
  vtkSMProxyManager::GetProxyManager()->SetActiveSession(server ? server->session() : nullptr);

  // Need to connect it back to know if python is changing the active one
  this->VTKConnector->Connect(vtkSMProxyManager::GetProxyManager(),
    vtkSMProxyManager::ActiveSessionChanged, this, SLOT(onActiveServerChanged()));

  if (server && server->activeSourcesSelectionModel() && server->activeViewSelectionModel())
  {
    this->VTKConnector->Connect(server->activeSourcesSelectionModel(),
      vtkCommand::CurrentChangedEvent, this, SLOT(sourceSelectionChanged()));
    this->VTKConnector->Connect(server->activeSourcesSelectionModel(),
      vtkCommand::SelectionChangedEvent, this, SLOT(sourceSelectionChanged()));

    this->VTKConnector->Connect(server->activeViewSelectionModel(), vtkCommand::CurrentChangedEvent,
      this, SLOT(viewSelectionChanged()));
    this->VTKConnector->Connect(server->activeViewSelectionModel(),
      vtkCommand::SelectionChangedEvent, this, SLOT(viewSelectionChanged()));
  }

  this->sourceSelectionChanged();
  this->viewSelectionChanged();

  blocker.unblock();
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::setActiveView(pqView* view)
{
  bool prev = this->blockSignals(true);

  // ensure that the appropriate server is active.
  if (view)
  {
    this->setActiveServer(view->getServer());
  }

  pqServer* server = this->activeServer();
  if (server && server->activeViewSelectionModel())
  {
    server->activeViewSelectionModel()->SetCurrentProxy(
      view ? view->getProxy() : nullptr, vtkSMProxySelectionModel::CLEAR_AND_SELECT);

    // this triggers a call to viewSelectionChanged(); but in collaboration mode
    // the ProxySelectionModel has been changed underneath therefore this
    // current proxy call do not trigger anything as it is already set with
    // the given value so we force our current class to update itself.
    this->viewSelectionChanged();
  }
  else
  {
    // if there's no-active server, it implies that setActiveView() must be
    // called with nullptr. In that case, we cannot really clear the selection
    // since we have no clue what's the active server. So nothing to do here.
  }

  this->blockSignals(prev);
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::setActivePipelineProxy(pqProxy* proxy)
{
  bool prev = this->blockSignals(true);

  // ensure that the appropriate server is active.
  if (proxy)
  {
    this->setActiveServer(proxy->getServer());
  }

  pqServer* server = this->activeServer();
  if (server && server->activeSourcesSelectionModel())
  {
    server->activeSourcesSelectionModel()->SetCurrentProxy(
      proxy ? proxy->getProxy() : nullptr, vtkSMProxySelectionModel::CLEAR_AND_SELECT);
    // this triggers a call to sourceSelectionChanged();
  }
  else
  {
    // if there's no-active server, it implies that setActivePipelineProxy() must be
    // called with nullptr when no server was already active.
    // In that case, we cannot really clear the selection
    // since we have no clue what's the active server. So nothing to do here.
  }

  this->blockSignals(prev);
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::setSelection(
  const pqProxySelection& selection_, pqServerManagerModelItem* current)
{
  pqProxy* current_proxy = qobject_cast<pqProxy*>(current);
  pqOutputPort* current_port = qobject_cast<pqOutputPort*>(current);
  pqServer* server = current_proxy ? current_proxy->getServer()
                                   : (current_port ? current_port->getServer() : nullptr);

  // ascertain that all items in the selection have the same server.
  Q_FOREACH (pqServerManagerModelItem* item, selection_)
  {
    pqProxy* proxy = qobject_cast<pqProxy*>(item);
    pqOutputPort* port = qobject_cast<pqOutputPort*>(item);
    pqServer* cur_server =
      proxy ? proxy->getServer() : (port ? port->getServer() : qobject_cast<pqServer*>(item));
    if (cur_server != nullptr && cur_server != server)
    {
      if (server == nullptr)
      {
        server = cur_server;
      }
      else
      {
        qCritical("Selections with heterogeneous servers are not supported.");
        return;
      }
    }
  }

  bool prev = this->blockSignals(true);

  // ensure that the appropriate server is active.
  if (server)
  {
    this->setActiveServer(server);

    pqProxySelectionUtilities::copy(selection_, server->activeSourcesSelectionModel());
    // this triggers a call to selectionChanged() if selection actually changed.

    vtkSMProxy* proxy = current_proxy
      ? current_proxy->getProxy()
      : (current_port ? current_port->getOutputPortProxy() : nullptr);
    server->activeSourcesSelectionModel()->SetCurrentProxy(proxy, vtkSMProxySelectionModel::SELECT);
  }
  else
  {
    // if there's no-active server, it implies that setSelection() must be
    // called with nullptr. In that case, we cannot really clear the selection
    // since we have no clue what's the active server. So nothing to do here.
  }
  this->blockSignals(prev);
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
void pqActiveObjects::updateRepresentation()
{
  if (this->ActiveRepresentation)
  {
    QObject::disconnect(
      this->ActiveRepresentation, SIGNAL(updated()), this, SIGNAL(representationUpdated()));
  }

  pqOutputPort* port = this->activePort();
  if (port)
  {
    this->ActiveRepresentation = port->getRepresentation(this->activeView());
    if (this->ActiveRepresentation)
    {
      this->connect(this->ActiveRepresentation, SIGNAL(updated()), SIGNAL(representationUpdated()),
        Qt::UniqueConnection);
    }
  }
  else
  {
    this->ActiveRepresentation = nullptr;
  }
  this->triggerSignals();
}

//-----------------------------------------------------------------------------
vtkSMSessionProxyManager* pqActiveObjects::proxyManager() const
{
  return this->activeServer() ? this->activeServer()->proxyManager() : nullptr;
}

//-----------------------------------------------------------------------------
vtkSMViewLayoutProxy* pqActiveObjects::activeLayout() const
{
  if (pqView* view = this->activeView())
  {
    if (auto l = vtkSMViewLayoutProxy::FindLayout(view->getViewProxy()))
    {
      return l;
    }
  }

  auto server = this->activeServer();
  if (!server)
  {
    // if not active server, then don't even attempt to deduce the active layout
    // using the pqTabbedMultiViewWidget since that doesn't matter, there's no
    // active server.
    return nullptr;
  }

  // if no active view is present, let's attempt to look for
  // pqTabbedMultiViewWidget and use its current tab if it is on the active
  // server.
  auto core = pqApplicationCore::instance();
  auto tmvwidget = qobject_cast<pqTabbedMultiViewWidget*>(core->manager("MULTIVIEW_WIDGET"));
  auto layoutProxy = tmvwidget ? tmvwidget->layoutProxy() : nullptr;
  return (layoutProxy && server->session() == layoutProxy->GetSession()) ? layoutProxy : nullptr;
}

//-----------------------------------------------------------------------------
int pqActiveObjects::activeLayoutLocation() const
{
  if (auto layout = this->activeLayout())
  {
    auto core = pqApplicationCore::instance();
    if (auto tmvwidget = qobject_cast<pqTabbedMultiViewWidget*>(core->manager("MULTIVIEW_WIDGET")))
    {
      if (auto mvwidget = tmvwidget->findTab(layout))
      {
        return std::max(mvwidget->activeFrameLocation(), 0);
      }
    }
  }
  return 0;
}

//-----------------------------------------------------------------------------
pqPipelineSource* pqActiveObjects::activeSource() const
{
  auto port = ::getPortFromPipelineProxy(this->ActivePipelineProxy);
  return port ? port->getSource() : nullptr;
}

//-----------------------------------------------------------------------------
pqOutputPort* pqActiveObjects::activePort() const
{
  return ::getPortFromPipelineProxy(this->ActivePipelineProxy);
}
