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

#include "pqApplicationCore.h"
#include "pqContextView.h"
#include "pqCoreUtilities.h"
#include "pqPipelineSource.h"
#include "pqQVTKWidget.h"
#include "pqServer.h"
#include "pqServerManagerModel.h"
#include "pqView.h"
#include "vtkChart.h"
#include "vtkEventQtSlotConnect.h"
#include "vtkNew.h"
#include "vtkPVMultiClientsInformation.h"
#include "vtkSMCollaborationManager.h"
#include "vtkSMContextViewProxy.h"
#include "vtkSMMessage.h"
#include "vtkSMProxy.h"
#include "vtkSMProxyManager.h"
#include "vtkSMSession.h"
#include "vtkSMSessionClient.h"
#include "vtkWeakPointer.h"

#include <map>
#include <set>

// Qt includes.
#include <QAction>
#include <QEvent>
#include <QMap>
#include <QMouseEvent>
#include <QPointer>
#include <QTimer>
#include <QVariant>
#include <QWidget>
#include <QtDebug>

//***************************************************************************
//                           Internal class
//***************************************************************************
struct ChartBounds
{
  void SetRange(double* otherRange)
  {
    for (int i = 0; i < 8; i++)
    {
      this->Range[i] = otherRange[i];
    }
  }

  double Range[8];
};

class pqCollaborationManager::pqInternals
{
public:
  pqInternals(pqCollaborationManager* owner)
  {
    this->Owner = owner;
    this->BroadcastMouseLocation = false;
    this->CollaborativeTimer.setInterval(100);
    QObject::connect(&this->CollaborativeTimer, SIGNAL(timeout()), this->Owner,
      SLOT(sendMousePointerLocationToOtherClients()));
    QObject::connect(&this->CollaborativeTimer, SIGNAL(timeout()), this->Owner,
      SLOT(sendChartViewBoundsToOtherClients()));
    // we delay starting the timer till we have an active collaboration session.

    this->ProxyManager = vtkSMProxyManager::GetProxyManager();
    this->ProxyManagerObserverTag =
      this->ProxyManager->AddObserver(vtkSMProxyManager::ActiveSessionChanged, this,
        &pqCollaborationManager::pqInternals::activeServerChanged);
  }

  //-------------------------------------------------
  ~pqInternals()
  {
    if (this->ProxyManager)
    {
      this->ProxyManager->RemoveObserver(this->ProxyManagerObserverTag);
      this->ProxyManagerObserverTag = 0;
    }
  }
  //-------------------------------------------------
  void activeServerChanged(vtkObject*, unsigned long, void*)
  {
    this->ActiveCollaborationManager = nullptr;
    this->AciveSession = nullptr;
    this->CollaborativeTimer.stop();
    if (!this->ProxyManager)
    {
      return; // No more proxy manager
    }
    this->AciveSession = this->ProxyManager->GetActiveSession();
    if (this->AciveSession && this->AciveSession->IsMultiClients())
    {
      this->ActiveCollaborationManager = this->AciveSession->GetCollaborationManager();
      this->ActiveCollaborationManager->UpdateUserInformations();
      this->LastMousePointerPosition.set_client_id(this->ActiveCollaborationManager->GetUserId());
      this->CollaborativeTimer.start();
    }
    this->Owner->updateEnabledState();
  }

  //-------------------------------------------------
  void registerServer(pqServer* server)
  {
    if (server == nullptr || !server->session()->GetCollaborationManager())
    {
      return;
    }
    QObject::connect(server, SIGNAL(sentFromOtherClient(pqServer*, vtkSMMessage*)), this->Owner,
      SLOT(onClientMessage(pqServer*, vtkSMMessage*)));
    QObject::connect(
      server, SIGNAL(triggeredMasterUser(int)), this->Owner, SIGNAL(triggeredMasterUser(int)));
    QObject::connect(
      server, SIGNAL(triggeredUserListChanged()), this->Owner, SIGNAL(triggeredUserListChanged()));
    QObject::connect(server, SIGNAL(triggeredUserName(int, QString&)), this->Owner,
      SIGNAL(triggeredUserName(int, QString&)));
    QObject::connect(
      server, SIGNAL(triggerFollowCamera(int)), this->Owner, SIGNAL(triggerFollowCamera(int)));

    this->VTKConnect->Connect(server->session()->GetCollaborationManager(),
      vtkSMCollaborationManager::CameraChanged, pqApplicationCore::instance(), SLOT(render()));
  }
  //-------------------------------------------------
  void unRegisterServer(pqServer* server)
  {
    if (server == nullptr || !server->session()->GetCollaborationManager())
    {
      return;
    }
    QObject::disconnect(server, SIGNAL(sentFromOtherClient(pqServer*, vtkSMMessage*)), this->Owner,
      SLOT(onClientMessage(pqServer*, vtkSMMessage*)));
    QObject::disconnect(
      server, SIGNAL(triggeredMasterUser(int)), this->Owner, SIGNAL(triggeredMasterUser(int)));
    QObject::disconnect(
      server, SIGNAL(triggeredUserListChanged()), this->Owner, SIGNAL(triggeredUserListChanged()));
    QObject::disconnect(server, SIGNAL(triggeredUserName(int, QString&)), this->Owner,
      SIGNAL(triggeredUserName(int, QString&)));
    QObject::disconnect(
      server, SIGNAL(triggerFollowCamera(int)), this->Owner, SIGNAL(triggerFollowCamera(int)));

    this->VTKConnect->Disconnect(server->session()->GetCollaborationManager(),
      vtkSMCollaborationManager::CameraChanged, pqApplicationCore::instance(), SLOT(render()));
  }
  //-------------------------------------------------
  int GetClientId(int idx)
  {
    if (this->ActiveCollaborationManager)
    {
      return this->ActiveCollaborationManager->GetUserId(idx);
    }
    return -1;
  }

  QMap<int, QString> UserNameMap;
  vtkWeakPointer<vtkSMSession> AciveSession;
  vtkWeakPointer<vtkSMCollaborationManager> ActiveCollaborationManager;
  vtkWeakPointer<vtkSMProxyManager> ProxyManager;
  vtkSMMessage LastMousePointerPosition;
  bool MousePointerLocationUpdated;
  bool BroadcastMouseLocation;
  std::map<vtkTypeUInt32, ChartBounds> ContextViewBoundsToShare;
  vtkNew<vtkEventQtSlotConnect> VTKConnect;

protected:
  QPointer<pqCollaborationManager> Owner;
  QTimer CollaborativeTimer;
  unsigned long ProxyManagerObserverTag;
};
//***************************************************************************/
pqCollaborationManager::pqCollaborationManager(QObject* parentObject)
  : Superclass(parentObject)
{
  this->Internals = new pqInternals(this);

  // Chat management + User list panel
  QObject::connect(this, SIGNAL(triggerChatMessage(pqServer*, int, QString&)), this,
    SLOT(onChatMessage(pqServer*, int, QString&)));

  QObject::connect(this, SIGNAL(triggeredMasterUser(int)), this, SLOT(updateEnabledState()));
}

//-----------------------------------------------------------------------------
pqCollaborationManager::~pqCollaborationManager()
{
  // Chat management + User list panel
  QObject::disconnect(this, SIGNAL(triggerChatMessage(pqServer*, int, QString&)), this,
    SLOT(onChatMessage(pqServer*, int, QString&)));

  delete this->Internals;
}
//-----------------------------------------------------------------------------
void pqCollaborationManager::onClientMessage(pqServer* server, vtkSMMessage* msg)
{
  if (msg->HasExtension(QtEvent::type))
  {
    int userId = 0;
    QString userName;
    QString chatMsg;
    switch (msg->GetExtension(QtEvent::type))
    {
      case QtEvent::CHAT:
        userId = msg->GetExtension(ChatMessage::author);
        userName = server->session()->GetCollaborationManager()->GetUserLabel(userId);
        chatMsg = msg->GetExtension(ChatMessage::txt).c_str();
        Q_EMIT triggerChatMessage(server, userId, chatMsg);
        break;
      case QtEvent::OTHER:
        // Custom handling
        break;
    }
  }
  else if (msg->HasExtension(MousePointer::view) &&
    (this->Internals->AciveSession ==
      server->session()) && // Make sure we share the same active server
    (msg->GetExtension(MousePointer::forceShow) ||
      (static_cast<int>(msg->client_id()) ==
        this->activeCollaborationManager()->GetFollowedUser())))
  {
    this->showMousePointer(msg->GetExtension(MousePointer::view),
      msg->GetExtension(MousePointer::x), msg->GetExtension(MousePointer::y),
      msg->GetExtension(MousePointer::ratioType));
  }
  else
  {
    Q_EMIT triggerStateClientOnlyMessage(server, msg);
  }
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::onChatMessage(pqServer* server, int userId, QString& msgContent)
{
  // Broadcast to others only if its our message
  if (this->activeCollaborationManager() &&
    userId == this->activeCollaborationManager()->GetUserId())
  {
    vtkSMMessage chatMsg;
    chatMsg.SetExtension(QtEvent::type, QtEvent::CHAT);
    chatMsg.SetExtension(ChatMessage::author, this->activeCollaborationManager()->GetUserId());
    chatMsg.SetExtension(ChatMessage::txt, msgContent.toStdString());

    server->sendToOtherClients(&chatMsg);
  }
  else if (!this->activeCollaborationManager())
  {
    qDebug() << "The active server is not a Collaborative one.";
  }
}
//-----------------------------------------------------------------------------
vtkSMCollaborationManager* pqCollaborationManager::activeCollaborationManager()
{
  return this->Internals->ActiveCollaborationManager;
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::onServerAdded(pqServer* s)
{
  this->Internals->registerServer(s);
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::onServerRemoved(pqServer* s)
{
  this->Internals->unRegisterServer(s);
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::updateMousePointerLocation(QMouseEvent* e)
{
  pqQVTKWidget* widget = qobject_cast<pqQVTKWidget*>(QObject::sender());
  if (widget && this->activeCollaborationManager())
  {
    bool isChartView = (vtkSMContextViewProxy::SafeDownCast(
                          this->activeCollaborationManager()->GetSession()->GetRemoteObject(
                            widget->getProxyId())) != nullptr);

    double w2 = widget->width() / 2.0;
    double h2 = widget->height() / 2.0;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    auto pos = e->localPos();
#else
    auto pos = e->position();
#endif
    double px = (pos.x() - w2) / (isChartView ? w2 : h2);
    double py = (pos.y() - h2) / h2;

    this->Internals->LastMousePointerPosition.SetExtension(
      MousePointer::view, widget->getProxyId());
    this->Internals->LastMousePointerPosition.SetExtension(MousePointer::x, px);
    this->Internals->LastMousePointerPosition.SetExtension(MousePointer::y, py);
    this->Internals->LastMousePointerPosition.SetExtension(
      MousePointer::ratioType, isChartView ? MousePointer::BOTH : MousePointer::HEIGHT);
    this->Internals->MousePointerLocationUpdated = true;
  }
  else if (this->activeCollaborationManager())
  {
    qCritical("Invalid cast");
  }
}
//-----------------------------------------------------------------------------
void pqCollaborationManager::sendMousePointerLocationToOtherClients()
{
  if (this->Internals->BroadcastMouseLocation && this->Internals->MousePointerLocationUpdated &&
    this->activeCollaborationManager())
  {
    this->activeCollaborationManager()->SendToOtherClients(
      &this->Internals->LastMousePointerPosition);
    this->Internals->MousePointerLocationUpdated = false;
  }
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::showMousePointer(
  vtkTypeUInt32 viewId, double x, double y, int useBothHeightWidth)
{
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  pqView* view = smmodel->findItem<pqView*>(viewId);
  pqQVTKWidget* widget = nullptr;
  if (view && (widget = qobject_cast<pqQVTKWidget*>(view->widget())))
  {
    double xRatioBase = 1;
    double yRatioBase = 1;
    double w2 = widget->width() / 2.0;
    double h2 = widget->height() / 2.0;
    switch (useBothHeightWidth)
    {
      case 0: // both
        xRatioBase = w2;
        yRatioBase = h2;
        break;
      case 1: // height
        xRatioBase = h2;
        yRatioBase = h2;
        break;
      case 2: // width
        xRatioBase = w2;
        yRatioBase = w2;
        break;
    }

    int px = static_cast<int>(xRatioBase * x + w2);
    int py = static_cast<int>(yRatioBase * y + h2);
    widget->paintMousePointer(px, py);
  }
}
//-----------------------------------------------------------------------------
void pqCollaborationManager::updateEnabledState()
{
  bool enabled =
    (this->activeCollaborationManager() ? this->activeCollaborationManager()->IsMaster() : true);
  QWidget* mainWidget = pqCoreUtilities::mainWidget();
  Q_FOREACH (QWidget* wdg, mainWidget->findChildren<QWidget*>())
  {
    QVariant val = wdg->property("PV_MUST_BE_MASTER");
    if (val.isValid() && val.toBool())
    {
      wdg->setEnabled(enabled);
    }
    val = wdg->property("PV_MUST_BE_MASTER_TO_SHOW");
    if (val.isValid() && val.toBool())
    {
      wdg->setVisible(enabled);
    }
  }
  Q_FOREACH (QAction* actn, mainWidget->findChildren<QAction*>())
  {
    // some actions are hidden, if the process is not a master.
    QVariant val = actn->property("PV_MUST_BE_MASTER_TO_SHOW");
    if (val.isValid() && val.toBool())
    {
      actn->setVisible(enabled);
    }
    // some other actions are merely 'blocked", if the process is not a master.
    // We don't use the enable/disable mechanism for actions, since most actions
    // are have their enabled state updated by logic that will need to be made
    // "collaboration aware" if we go that route. Instead, we install an event
    // filter that eats away clicks, instead.

    // Currently I cannot figure out how to do this. Event filters don't work on
    // Actions. There's no means of disabling action-activations besides marking
    // them disabled or hidden, it seems. block-signals seems to be the
    // crappiest way out of this. The problem is used has no indication with
    // block-signals that the action is not allowed in collaboration-mode. So
    // we'll stay away from it.
    val = actn->property("PV_MUST_BE_MASTER");
    if (val.isValid() && val.toBool())
    {
      actn->blockSignals(!enabled);
    }
  }

  Q_EMIT triggeredMasterChanged(enabled);
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::attachMouseListenerTo3DViews()
{
  // I am deliberately commenting this out. Collaboration is slated for
  // deprecation. Since we removed the unnecessary `mouseEvent`, it no longer
  // possible to support this. Rather than add more code to support a deprecated
  // use-case, we just drop it for now.

  // QWidget* mainWidget = pqCoreUtilities::mainWidget();
  // Q_FOREACH (pqQVTKWidget* widget, mainWidget->findChildren<pqQVTKWidget*>())
  //{
  //  QObject::connect(widget, SIGNAL(mouseEvent(QMouseEvent*)), this,
  //    SLOT(updateMousePointerLocation(QMouseEvent*)), Qt::UniqueConnection);
  //}
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::enableMousePointerSharing(bool enable)
{
  this->Internals->BroadcastMouseLocation = enable;
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::disableFurtherConnections(bool disable)
{
  if (this->Internals->ActiveCollaborationManager)
  {
    this->Internals->ActiveCollaborationManager->DisableFurtherConnections(disable);
  }
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::setConnectID(int connectID)
{
  if (this->Internals->ActiveCollaborationManager)
  {
    this->Internals->ActiveCollaborationManager->SetConnectID(connectID);
  }
}

//-----------------------------------------------------------------------------
void pqCollaborationManager::onChartViewChange(vtkTypeUInt32 gid, double* bounds)
{
  pqContextView* chartView = qobject_cast<pqContextView*>(QObject::sender());
  if (chartView && activeCollaborationManager() &&
    activeCollaborationManager()->GetSession() == chartView->getServer()->session())
  {
    this->Internals->ContextViewBoundsToShare[gid].SetRange(bounds);
  }
}
//-----------------------------------------------------------------------------
void pqCollaborationManager::sendChartViewBoundsToOtherClients()
{
  // FIXME:UDA
  // if(this->Internals->ContextViewBoundsToShare.size() > 0)
  //   {
  //   std::map<vtkTypeUInt32, ChartBounds>::iterator iter;
  //   iter = this->Internals->ContextViewBoundsToShare.begin();
  //   while(iter != this->Internals->ContextViewBoundsToShare.end())
  //     {
  //     vtkSMMessage msg;
  //     msg.SetExtension(QtEvent::type, QtEvent::CHART_BOUNDS);
  //     msg.SetExtension(ChartViewBounds::view, iter->first);
  //     for(int i=0;i<8;i++)
  //       {
  //       msg.AddExtension(ChartViewBounds::range, iter->second.Range[i]);
  //       }

  //     this->activeCollaborationManager()->SendToOtherClients(&msg);

  //     // Move forward
  //     iter++;
  //     }
  //   // Clean up stack
  //   this->Internals->ContextViewBoundsToShare.clear();
  //   }
}
