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

#include "pqEventTranslator.h"

#include "pq3DViewEventTranslator.h"
#include "pqAbstractButtonEventTranslator.h"
#include "pqAbstractItemViewEventTranslator.h"
#include "pqAbstractSliderEventTranslator.h"
#include "pqBasicWidgetEventTranslator.h"
#include "pqCheckEventOverlay.h"
#include "pqComboBoxEventTranslator.h"
#include "pqDoubleSpinBoxEventTranslator.h"
#include "pqEventComment.h"
#include "pqEventTypes.h"
#include "pqLineEditEventTranslator.h"
#include "pqListViewEventTranslator.h"
#include "pqMenuEventTranslator.h"
#include "pqNativeFileDialogEventTranslator.h"
#include "pqObjectNaming.h"
#include "pqSpinBoxEventTranslator.h"
#include "pqTabBarEventTranslator.h"
#include "pqTableViewEventTranslator.h"
#include "pqTreeViewEventTranslator.h"

#include <QApplication>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QSet>
#include <QToolBar>
#include <QVector>
#include <QtDebug>

////////////////////////////////////////////////////////////////////////////////
// pqEventTranslator::pqImplementation
struct pqEventTranslator::pqImplementation
{
  pqImplementation()
  {
    this->EventComment = 0;
    this->Checking = false;
    this->Recording = false;

    // Create overlay and hide it
    this->CheckOverlay = new pqCheckEventOverlay(NULL);
    this->hideOverlay();

    this->RecordInteractionTimings = false;
  }

  ~pqImplementation()
  {
    if (this->EventComment)
    {
      delete this->EventComment;
    }
    delete this->CheckOverlay;
    this->CheckOverlayWidgetOn = NULL;
  }

  void hideOverlay()
  {
    // Hide the overlay
    this->CheckOverlay->hide();

    // Nullified it's parent
    this->CheckOverlay->setParent(NULL);

    // Set the overlaid widget to null
    this->CheckOverlayWidgetOn = NULL;
  }

  pqEventComment* EventComment;
  /// Stores the working set of widget translators
  QList<pqWidgetEventTranslator*> Translators;
  /// Stores the set of objects that should be ignored when translating events
  QMap<QObject*, QRegularExpression> IgnoredObjects;

  // list of widgets for which mouse propagation will happen
  // we'll only translate the first and ignore the rest
  QList<QWidget*> MouseParents;

  // Checking Flag
  bool Checking;

  // Recording flag
  bool Recording;

  // Pointer to the overlay
  QPointer<pqCheckEventOverlay> CheckOverlay;

  // Pointer to the overlaid widget
  QPointer<QWidget> CheckOverlayWidgetOn;

  // Record interaction timings flag
  bool RecordInteractionTimings;

  // Timer to track time between user interactions
  QElapsedTimer InteractionsTimer;
};

////////////////////////////////////////////////////////////////////////////////
// pqEventTranslator

// ----------------------------------------------------------------------------
pqEventTranslator::pqEventTranslator(QObject* p)
  : QObject(p)
  , Implementation(new pqImplementation())
{
  // Ignore the overlay so it is transparent to events.
  this->ignoreObject(this->Implementation->CheckOverlay);
}

// ----------------------------------------------------------------------------
pqEventTranslator::~pqEventTranslator()
{
  this->stop();
  delete Implementation;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::start()
{
  QCoreApplication::instance()->installEventFilter(this);
  Q_EMIT this->started();
}

// ----------------------------------------------------------------------------
void pqEventTranslator::stop()
{
  QCoreApplication::instance()->removeEventFilter(this);
  this->check(false);
  Q_EMIT this->stopped();
}

// ----------------------------------------------------------------------------
void pqEventTranslator::addDefaultWidgetEventTranslators(pqTestUtility* util)
{
  // Add general translator first, then specific, in order for this to work
  addWidgetEventTranslator(new pqBasicWidgetEventTranslator());
  addWidgetEventTranslator(new pqAbstractButtonEventTranslator());
  addWidgetEventTranslator(new pqAbstractItemViewEventTranslator());
  addWidgetEventTranslator(new pqAbstractSliderEventTranslator());
  addWidgetEventTranslator(new pqComboBoxEventTranslator());
  addWidgetEventTranslator(new pqDoubleSpinBoxEventTranslator());
  addWidgetEventTranslator(new pqLineEditEventTranslator());
  addWidgetEventTranslator(new pqMenuEventTranslator());
  addWidgetEventTranslator(new pqSpinBoxEventTranslator());
  addWidgetEventTranslator(new pqTabBarEventTranslator());
  addWidgetEventTranslator(new pqTreeViewEventTranslator());
  addWidgetEventTranslator(new pqTableViewEventTranslator());
  addWidgetEventTranslator(new pqListViewEventTranslator());
  addWidgetEventTranslator(new pq3DViewEventTranslator("QGLWidget"));
  addWidgetEventTranslator(new pqNativeFileDialogEventTranslator(util));
}

// ----------------------------------------------------------------------------
void pqEventTranslator::addWidgetEventTranslator(pqWidgetEventTranslator* Translator)
{
  if (Translator)
  {
    // We Check if the translator has already been added previously.
    int index = this->getWidgetEventTranslatorIndex(Translator->metaObject()->className());
    if (index != -1)
    {
      return;
    }

    this->Implementation->Translators.push_front(Translator);
    Translator->setParent(this);

    // Legacy Connection, for translator not using event types
    QObject::connect(Translator, SIGNAL(recordEvent(QObject*, const QString&, const QString&)),
      this, SLOT(onRecordEvent(QObject*, const QString&, const QString&)));

    // Connect record event
    QObject::connect(Translator, SIGNAL(recordEvent(QObject*, const QString&, const QString&, int)),
      this, SLOT(onRecordEvent(QObject*, const QString&, const QString&, int)));

    // Connect resize specific overlay
    QObject::connect(Translator, SIGNAL(specificOverlay(const QRect&)), this,
      SLOT(setOverlayGeometry(const QRect&)));
  }
}

// ----------------------------------------------------------------------------
bool pqEventTranslator::removeWidgetEventTranslator(const QString& className)
{
  int index = this->getWidgetEventTranslatorIndex(className);
  if (index == -1)
  {
    return false;
  }

  this->Implementation->Translators.removeAt(index);
  return true;
}

// ----------------------------------------------------------------------------
pqWidgetEventTranslator* pqEventTranslator::getWidgetEventTranslator(const QString& className)
{
  int index = this->getWidgetEventTranslatorIndex(className);
  if (index == -1)
  {
    return 0;
  }

  return this->Implementation->Translators.at(index);
}

// ----------------------------------------------------------------------------
int pqEventTranslator::getWidgetEventTranslatorIndex(const QString& className)
{
  for (int i = 0; i < this->Implementation->Translators.count(); ++i)
  {
    if (this->Implementation->Translators.at(i)->metaObject()->className() == className)
    {
      return i;
    }
  }
  return -1;
}

// ----------------------------------------------------------------------------
QList<pqWidgetEventTranslator*> pqEventTranslator::translators() const
{
  return this->Implementation->Translators;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::addDefaultEventManagers(pqTestUtility* util)
{
  this->Implementation->EventComment = new pqEventComment(util);
  QObject::connect(this->Implementation->EventComment,
    SIGNAL(recordComment(QObject*, QString, QString)), this,
    SLOT(onRecordEvent(QObject*, QString, QString)));
}

// ----------------------------------------------------------------------------
pqEventComment* pqEventTranslator::eventComment() const
{
  return this->Implementation->EventComment;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::ignoreObject(QObject* object, QRegularExpression commandFilter)
{
  this->Implementation->IgnoredObjects.insert(object, commandFilter);
}

// ----------------------------------------------------------------------------
bool pqEventTranslator::eventFilter(QObject* object, QEvent* event)
{
  if (this->Implementation->Recording)
  {
    if (object->isWindowType())
    {
      return false;
    }

    // Only widgets
    QWidget* widget = qobject_cast<QWidget*>(object);
    if (widget != NULL)
    {

      // mouse events are propagated to parents
      // our event translators/players don't quite like that,
      // so lets consume those extra ones
      if (event->type() == QEvent::MouseButtonPress ||
        event->type() == QEvent::MouseButtonDblClick || event->type() == QEvent::MouseMove ||
        event->type() == QEvent::Enter || event->type() == QEvent::Leave ||
        event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::ContextMenu)
      {
        if (!this->Implementation->MouseParents.empty() &&
          this->Implementation->MouseParents.first() == object)
        {
          // right on track
          this->Implementation->MouseParents.removeFirst();
          return false;
        }

        // find the chain of parent that will get this mouse event
        this->Implementation->MouseParents.clear();
        for (QWidget* w = widget->parentWidget(); w; w = w->parentWidget())
        {
          this->Implementation->MouseParents.append(w);
          if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))
          {
            break;
          }
        }
      }

      // Checking mode
      if (this->Implementation->Checking)
      {
        // In Gl Case, parentless widget is not transparent to mouse event
        // The event is  applied to the overlaid widget or an another top widget
        // (before ignoredObjects)
        // TODO : use mask instead

        // Look behind the overlay widget, this is relevant only if :
        // There is already an overlay drawn
        // This is the gl case
        // This is a mouse event we manage
        // The overlay receive the event
        if (this->Implementation->CheckOverlayWidgetOn != NULL &&
          this->Implementation->CheckOverlay->GlWidget &&
          widget == this->Implementation->CheckOverlay &&
          (event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseMove))
        {

          // We are about to look for another top widget window behind the mouse cursor
          bool foundTop = false;
          QWidget* topWidget;
          // recover all top widgets
          QWidgetList topWidgets = QApplication::topLevelWidgets();
          Q_FOREACH (topWidget, topWidgets)
          {
            // only the visible ones
            if (!topWidget->isHidden())
            {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
              auto pos = static_cast<QMouseEvent*>(event)->globalPos();
#else
              auto pos = static_cast<QMouseEvent*>(event)->globalPosition().toPoint();
#endif
              // Check it is not the overlay, and it contains the mouse cursor
              if (topWidget != this->Implementation->CheckOverlay &&
                topWidget->geometry().contains(pos, true))
              {
                // Recover the child widget under the cursor, if any
                QWidget* childWidget = topWidget->childAt(topWidget->mapFromGlobal(pos));

                // If child exist, check it is not the overlaid widget and indeed a new widget
                if (childWidget == NULL ||
                  (childWidget != NULL &&
                    childWidget != this->Implementation->CheckOverlayWidgetOn))
                {
                  // if a child exist, use it
                  if (childWidget != NULL)
                  {
                    topWidget = childWidget;
                  }

                  // Position top widget flag
                  foundTop = true;
                  break;
                }
              }
            }
          }
          if (foundTop)
          {
            // If we found a top widget behind the cursor, use it
            widget = topWidget;
          }
          else
          {
            // If not, use the widget behind the overlay
            widget = this->Implementation->CheckOverlayWidgetOn;
          }
        }

        // Leaving the checkOverlay when a widget without grand parent
        // then hiding overlay (before ignoredObjects)
        if (this->Implementation->CheckOverlay == widget &&
          !this->Implementation->CheckOverlay->Specific)
        {
          if (event->type() == QEvent::Leave)
          {
            this->Implementation->hideOverlay();
          }
        }

        // Ignore object if specified
        if (this->Implementation->IgnoredObjects.contains(widget))
        {
          return false;
        }

        // Do not check QToolBar as they can generate buggy overlay
        if (qobject_cast<QToolBar*>(widget) != NULL)
        {
          return false;
        }

        // Mouse Move on a non-previously overlaid widget
        if (event->type() == QEvent::MouseMove &&
          this->Implementation->CheckOverlayWidgetOn != widget)
        {
          // Check for any valid translator
          bool validTranslator = false;
          bool error;
          for (int i = 0; i != this->Implementation->Translators.size(); ++i)
          {
            // This will not record a check event, only check if a translator can record a check
            // event
            if (this->Implementation->Translators[i]->translateEvent(
                  widget, event, pqEventTypes::CHECK_EVENT, error))
            {
              validTranslator = true;
              break;
            }
          }

          // Draw overlay
          this->Implementation->hideOverlay();

          // Only on non-window widget
          // TODO handle the overlay on window widget
          if (!widget->isWindow() && widget->parent() != NULL)
          {
            // Check if widget is glWidget
            this->Implementation->CheckOverlay->GlWidget = widget->inherits("QVTKWidget");

            // Check if widget is parent to gl widget
            QList<QWidget*> children = widget->findChildren<QWidget*>();
            Q_FOREACH (QWidget* child, children)
            {
              if (child->inherits("QVTKWidget"))
              {
                // Ignore widget containing a Gl widget as a child
                this->ignoreObject(widget);
                return false;
              }
            }

            // Check widget size is valid, if not do not draw overlay
            // Widget can still be clicked and checked
            int sizeThreshold =
              pqCheckEventOverlay::OVERLAY_MARGIN + 2 * pqCheckEventOverlay::OVERLAY_PEN_WIDTH;
            if (widget->width() < sizeThreshold || widget->height() < sizeThreshold)
            {
              return false;
            }

            // Set the validity of the overlay
            this->Implementation->CheckOverlay->Valid = validTranslator;

            // Set parent of the overlay to be parent of the overlaid widget
            this->Implementation->CheckOverlay->setParent(qobject_cast<QWidget*>(widget->parent()));

            if (this->Implementation->CheckOverlay->GlWidget)
            {
              // Cannot draw QPainter directive in OpenGL context, bust use another context, aka
              // another window
              // this->Implementation->CheckOverlay->setWindowFlags(Qt::ToolTip |
              // Qt::FramelessWindowHint); // ToolTip is always on top
              this->Implementation->CheckOverlay->setWindowFlags(
                Qt::Tool | Qt::FramelessWindowHint); // Tool generate avatar
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_NoSystemBackground, true);
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_TranslucentBackground, true);
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_PaintOnScreen, true);

              // Resize and move widget
              QRect geometry = widget->geometry();
              geometry.moveTo(widget->mapToGlobal(QPoint(0, 0)));
              this->setOverlayGeometry(geometry, false);
            }
            else
            {
              // Restore window flags
              this->Implementation->CheckOverlay->setWindowFlags(Qt::Widget);
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_NoSystemBackground, false);
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_TranslucentBackground, false);
              this->Implementation->CheckOverlay->setAttribute(Qt::WA_PaintOnScreen, false);

              // Set overlay geometry to be the same as overlaid widget
              this->setOverlayGeometry(widget->geometry(), false);
            }

            // Show and Register overlay
            this->Implementation->CheckOverlay->show();
            this->Implementation->CheckOverlayWidgetOn = widget;
          }
        }
        // Resize event
        if (event->type() == QEvent::Resize)
        {
          // Set overlay geometry
          QRect geometry = widget->geometry();
          if (this->Implementation->CheckOverlay->GlWidget)
          {
            geometry.moveTo(widget->mapToGlobal(QPoint(0, 0)));
          }
          this->setOverlayGeometry(geometry, false);
        }

        // Mouse button release -> Check Event
        if (event->type() == QEvent::MouseButtonRelease)
        {
          // Check Translators
          for (int i = 0; i != this->Implementation->Translators.size(); ++i)
          {
            bool error = false;
            if (this->Implementation->Translators[i]->translateEvent(
                  widget, event, pqEventTypes::CHECK_EVENT, error))
            {
              if (error)
              {
                qWarning() << "Error translating a check event for object " << widget;
              }
              return true;
            }
          }

          // Cannot check widget , Inform user trying to check uncheckable widget
          qWarning() << "Error checking an event for object, widget type not supported.";
          qWarning() << "Name of the widget:" << widget->objectName()
                     << ". Type of the widget:" << widget->metaObject()->className();
          if (widget->parent() != NULL)
          {
            qWarning() << "Type of parent widget:" << widget->parent()->metaObject()->className();
          }
        }
        // Block all input events, so the UI is static but still drawn.
        // Except for MouseMove
        if (dynamic_cast<QInputEvent*>(event) != NULL && event->type() != QEvent::MouseMove)
        {
          return true;
        }
      }
      else
      {
        // Event Recording
        for (int i = 0; i != this->Implementation->Translators.size(); ++i)
        {
          bool error = false;
          if (this->Implementation->Translators[i]->translateEvent(
                object, event, pqEventTypes::ACTION_EVENT, error))
          {
            if (error)
            {
              qWarning() << "Error translating an event for object " << object;
            }
            return false;
          }
        }
      }
    }
  }
  return false;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::onRecordEvent(
  QObject* Object, const QString& Command, const QString& Arguments)
{
  this->onRecordEvent(Object, Command, Arguments, pqEventTypes::ACTION_EVENT);
}

// ----------------------------------------------------------------------------
void pqEventTranslator::onRecordEvent(
  QObject* Object, const QString& Command, const QString& Arguments, int eventType)
{
  if (this->Implementation->IgnoredObjects.contains(Object))
  {
    QRegularExpression commandFilter = this->Implementation->IgnoredObjects.value(Object);
    if (Command.contains(commandFilter))
    {
      return;
    }
  }

  QString name;
  if (Object)
  {
    QVariant blockRecordCommands = Object->property("BlockRecordCommands");
    if (blockRecordCommands.isValid() &&
      Command.contains(blockRecordCommands.toRegularExpression()))
    {
      return;
    }

    if (eventType == pqEventTypes::ACTION_EVENT)
    {
      // When sender is pqEventObject, the Object name can be NULL.
      if (!qobject_cast<pqEventComment*>(this->sender()) || Object)
      {
        name = pqObjectNaming::GetName(*Object);
        if (name.isEmpty())
          return;
      }
    }
    else
    {
      // Check the QObject does have a name
      name = pqObjectNaming::GetName(*Object);
      if (name.isEmpty())
      {
        return;
      }
    }
  }

  // Record user interaction time
  if (this->Implementation->RecordInteractionTimings)
  {
    if (this->Implementation->InteractionsTimer.isValid())
    {
      Q_EMIT recordEvent(name, "pause",
        QString::number(this->Implementation->InteractionsTimer.restart()),
        pqEventTypes::ACTION_EVENT);
    }
    else
    {
      this->Implementation->InteractionsTimer.start();
    }
  }

  // Record the event
  Q_EMIT recordEvent(name, Command, Arguments, eventType);
}

// ----------------------------------------------------------------------------
void pqEventTranslator::check(bool value)
{
  this->Implementation->Checking = value;

  // Hide overlay when not checking
  if (!value)
  {
    this->Implementation->hideOverlay();
  }
}

// ----------------------------------------------------------------------------
void pqEventTranslator::record(bool value)
{
  this->Implementation->Recording = value;

  // Hide overlay when not recording
  if (!value)
  {
    this->Implementation->hideOverlay();
  }
}

// ----------------------------------------------------------------------------
bool pqEventTranslator::isRecording()
{
  return this->Implementation->Recording;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::recordInteractionTimings(bool value)
{
  if (value != this->Implementation->RecordInteractionTimings)
  {
    this->Implementation->RecordInteractionTimings = value;
    this->Implementation->InteractionsTimer.invalidate();
  }
}

// ----------------------------------------------------------------------------
void pqEventTranslator::setOverlayGeometry(const QRect& geometry, bool specific)
{
  if (this->Implementation->CheckOverlay != NULL)
  {
    this->Implementation->CheckOverlay->setGeometry(geometry);
  }
  this->Implementation->CheckOverlay->Specific = specific;
}

// ----------------------------------------------------------------------------
void pqEventTranslator::recordDashboardModeToggle(QObject* object, bool toggle)
{
  QString name = pqObjectNaming::GetName(*object);
  if (name.isEmpty())
  {
    qWarning() << "Error recording a dashboard mode event";
    return;
  }

  Q_EMIT recordEvent(
    name, "dashboard_mode", QVariant(toggle).toString(), pqEventTypes::ACTION_EVENT);
}
