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

// Server Manager Includes.
#include "vtkPVDataInformation.h"
#include "vtkProcessModule.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMSelectionHelper.h"
#include "vtkSMSessionProxyManager.h"
#include "vtkSMSourceProxy.h"
#include "vtkSMTrace.h"
#include "vtkSMVectorProperty.h"
#include "vtkSelectionNode.h"

// Qt Includes.
#include <QtDebug>

// ParaView Includes.
#include "pqDataRepresentation.h"
#include "pqOutputPort.h"
#include "pqPipelineSource.h"
#include "pqSMAdaptor.h"
#include "pqServer.h"
#include "pqSpreadSheetViewModel.h"

namespace
{
class pqScopedBool
{
  bool& Variable;

public:
  pqScopedBool(bool& var)
    : Variable(var)
  {
    this->Variable = true;
  }
  ~pqScopedBool() { this->Variable = false; }
};
}

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
using pqHashType = uint;
#else
using pqHashType = size_t;
#endif

static size_t qHash(pqSpreadSheetViewModel::vtkIndex index)
{
  return qHash(index[2]);
}

//-----------------------------------------------------------------------------
pqSpreadSheetViewSelectionModel::pqSpreadSheetViewSelectionModel(
  pqSpreadSheetViewModel* amodel, QObject* _parent)
  : Superclass(amodel, _parent)
{
  this->UpdatingSelection = false;
  this->Model = amodel;

  QObject::connect(amodel, SIGNAL(selectionChanged(const QItemSelection&)), this,
    SLOT(serverSelectionChanged(const QItemSelection&)));
}

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

//-----------------------------------------------------------------------------
void pqSpreadSheetViewSelectionModel::serverSelectionChanged(const QItemSelection& sel)
{
  if (this->UpdatingSelection)
  {
    return;
  }

  // turn on this->UpdatingSelection so that we don't attempt to update the
  // SM-selection as the UI is being updated.
  pqScopedBool updatingSelection(this->UpdatingSelection);

  // Don't simple call ClearAndSelect, that causes the UI to flicker. Instead
  // determine what changed and update those.
  QSet<int> currentRows, newRows;
  Q_FOREACH (const QModelIndex& idx, this->selectedIndexes())
  {
    currentRows.insert(idx.row());
  }
  Q_FOREACH (const QModelIndex& idx, sel.indexes())
  {
    newRows.insert(idx.row());
  }

  QSet<int> toDeselectRows = currentRows - newRows;
  QSet<int> toSelectRows = newRows - currentRows;
  Q_FOREACH (int idx, toDeselectRows)
  {
    this->select(
      this->Model->index(idx, 0), QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
  }
  Q_FOREACH (int idx, toSelectRows)
  {
    this->select(
      this->Model->index(idx, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
  }
}

//-----------------------------------------------------------------------------
void pqSpreadSheetViewSelectionModel::select(
  const QItemSelection& sel, QItemSelectionModel::SelectionFlags command)
{
  this->Superclass::select(sel, command);
  if (this->UpdatingSelection || command == QItemSelectionModel::NoUpdate)
  {
    return;
  }

  // Obtain the currently set (or create a new one) append selections with its selection source
  // We then update the ids selected on the selection source
  vtkSmartPointer<vtkSMSourceProxy> appendSelections;
  appendSelections.TakeReference(this->getSelectionSource());
  if (!appendSelections)
  {
    Q_EMIT this->selection(nullptr);
    return;
  }

  auto selectionSource = vtkSMPropertyHelper(appendSelections, "Input").GetAsProxy(0);
  vtkSMVectorProperty* vp = vtkSMVectorProperty::SafeDownCast(selectionSource->GetProperty("IDs"));
  QList<QVariant> ids = pqSMAdaptor::getMultipleElementProperty(vp);
  int numElementsPerCommand = vp->GetNumberOfElementsPerCommand();
  if (command & QItemSelectionModel::Clear)
  {
    ids.clear();
  }
  if (command & QItemSelectionModel::Select || command & QItemSelectionModel::Deselect ||
    command & QItemSelectionModel::Toggle)
  {
    // Get the (process id, index) pairs for the indices indicated in the
    // selection.
    QSet<pqSpreadSheetViewModel::vtkIndex> vtkIndices = this->Model->getVTKIndices(sel.indexes());

    QSet<pqSpreadSheetViewModel::vtkIndex> currentIndices;
    for (int cc = 0; (cc + numElementsPerCommand) <= ids.size();)
    {
      pqSpreadSheetViewModel::vtkIndex index;
      if (numElementsPerCommand == 3)
      {
        index[0] = ids[cc++].value<vtkIdType>();
        index[1] = ids[cc++].value<vtkIdType>();
        index[2] = ids[cc++].value<vtkIdType>();
      }
      else // numElementsPerCommand == 2
      {
        index[0] = 0;
        index[1] = ids[cc++].value<vtkIdType>();
        index[2] = ids[cc++].value<vtkIdType>();
      }
      currentIndices.insert(index);
    }

    if (command & QItemSelectionModel::Select)
    {
      currentIndices += vtkIndices;
    }
    if (command & QItemSelectionModel::Deselect)
    {
      currentIndices -= vtkIndices;
    }
    if (command & QItemSelectionModel::Toggle)
    {
      QSet<pqSpreadSheetViewModel::vtkIndex> toSelect = vtkIndices - currentIndices;
      QSet<pqSpreadSheetViewModel::vtkIndex> toDeselect = vtkIndices - toSelect;
      currentIndices -= toDeselect;
      currentIndices += toSelect;
    }

    ids.clear();
    for (const auto& index : currentIndices)
    {
      if (numElementsPerCommand == 3)
      {
        ids.push_back(index[0]);
        ids.push_back(index[1]);
        ids.push_back(index[2]);
      }
      else // numElementsPerCommand == 2
      {
        ids.push_back(index[1]);
        ids.push_back(index[2]);
      }
    }
  }

  if (ids.empty())
  {
    appendSelections = nullptr;
  }
  else
  {
    pqSMAdaptor::setMultipleElementProperty(vp, ids);
    selectionSource->UpdateVTKObjects();

    // Map from selection source proxy name to trace function
    std::string functionName(selectionSource->GetXMLName());
    functionName.erase(functionName.size() - sizeof("SelectionSource") + 1);
    functionName.append("s");
    functionName.insert(0, "Select");

    // Trace the selection
    SM_SCOPED_TRACE(CallFunction)
      .arg(functionName.c_str())
      .arg("IDs", vtkSMPropertyHelper(selectionSource, "IDs").GetIntArray())
      .arg("FieldType", vtkSMPropertyHelper(selectionSource, "FieldType").GetAsInt())
      .arg("ContainingCells", vtkSMPropertyHelper(selectionSource, "ContainingCells").GetAsInt());
  }

  Q_EMIT this->selection(appendSelections);
}

//-----------------------------------------------------------------------------
// Locate the selection source currently set on the representation being shown.
// If no selection exists, or selection present is not "updatable" by this
// model, we create a new selection.
vtkSMSourceProxy* pqSpreadSheetViewSelectionModel::getSelectionSource()
{
  pqDataRepresentation* repr = this->Model->activeRepresentation();
  if (!repr)
  {
    return nullptr;
  }

  // Convert fieldType to selection field type if convert-able.
  int fieldType = this->Model->getFieldType();
  int selectionFieldType = -1;
  switch (fieldType)
  {
    case vtkDataObject::FIELD_ASSOCIATION_POINTS:
      selectionFieldType = vtkSelectionNode::POINT;
      break;

    case vtkDataObject::FIELD_ASSOCIATION_CELLS:
      selectionFieldType = vtkSelectionNode::CELL;
      break;

    case vtkDataObject::FIELD_ASSOCIATION_VERTICES:
      selectionFieldType = vtkSelectionNode::VERTEX;
      break;

    case vtkDataObject::FIELD_ASSOCIATION_EDGES:
      selectionFieldType = vtkSelectionNode::EDGE;
      break;

    case vtkDataObject::FIELD_ASSOCIATION_ROWS:
      selectionFieldType = vtkSelectionNode::ROW;
      break;

    default:
      return nullptr;
  }

  pqOutputPort* opport = repr->getOutputPortFromInput();
  vtkSMSourceProxy* appendSelections = opport->getSelectionInput();

  // Determine what selection proxy name we want.
  const char* proxyname = "IDSelectionSource";
  vtkPVDataInformation* dinfo = opport->getDataInformation();
  if (dinfo->IsCompositeDataSet())
  {
    proxyname = "CompositeDataIDSelectionSource";
  }

  // We may be able to simply update the currently existing selection.
  bool updatable = false;
  if (appendSelections != nullptr &&
    vtkSMPropertyHelper(appendSelections, "Input").GetNumberOfElements() == 1)
  {
    auto selectionSource = vtkSMPropertyHelper(appendSelections, "Input").GetAsProxy(0);
    updatable = strcmp(selectionSource->GetXMLName(), proxyname) == 0 &&
      pqSMAdaptor::getElementProperty(selectionSource->GetProperty("FieldType")).toInt() ==
        selectionFieldType;
  }

  if (updatable)
  {
    appendSelections->Register(nullptr);
  }
  else
  {
    // create a new selection source
    vtkSMSessionProxyManager* pxm = repr->proxyManager();
    vtkSmartPointer<vtkSMSourceProxy> selectionSource;
    selectionSource.TakeReference(
      vtkSMSourceProxy::SafeDownCast(pxm->NewProxy("sources", proxyname)));
    pqSMAdaptor::setElementProperty(selectionSource->GetProperty("FieldType"), selectionFieldType);
    selectionSource->UpdateVTKObjects();
    // create a new append Selections filter and append the selection source
    appendSelections = vtkSMSourceProxy::SafeDownCast(
      vtkSMSelectionHelper::NewAppendSelectionsFromSelectionSource(selectionSource));
  }

  return appendSelections;
}
