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

// ParaView Server Manager includes.
#include "vtkPVDataInformation.h"
#include "vtkSMSourceProxy.h"

// Qt includes.
#include <QIcon>
#include <QList>
#include <QPointer>
#include <QtAlgorithms>
#include <QtDebug>

// ParaView includes.
#include "pqDataRepresentation.h"
#include "pqOutputPort.h"
#include "pqPipelineSource.h"
#include "pqView.h"

struct pqSourceInfo
{
  QPointer<pqOutputPort> OutputPort;
  int DataType;
  quint64 NumberOfCells;
  quint64 NumberOfPoints;
  double MemorySize;
  bool DataInformationValid;
  bool GeometryInformationValid;
  double GeometrySize;
  double Bounds[6];
  double TimeRange[2];

  QString DataTypeName;

  vtkMTimeType MTime;
  pqSourceInfo() { this->Init(); }

  pqSourceInfo(pqOutputPort* port)
  {
    this->Init();
    this->OutputPort = port;
  }

  operator pqOutputPort*() const { return this->OutputPort; }

  void Init()
  {
    this->MTime = 0;
    this->DataType = 0;
    this->NumberOfCells = 0;
    this->NumberOfPoints = 0;
    this->MemorySize = 0;
    this->DataInformationValid = false;
    this->GeometrySize = 0;
    this->GeometryInformationValid = false;
  }

  QVariant getName() const
  {
    if (this->OutputPort)
    {
      pqPipelineSource* source = this->OutputPort->getSource();
      if (source->getNumberOfOutputPorts() > 1)
      {
        return QVariant(
          QString("%1 (%2)").arg(source->getSMName()).arg(this->OutputPort->getPortNumber()));
      }
      return QVariant(source->getSMName());
    }
    return QVariant("Unknown");
  }

  QVariant getNumberOfCells() const
  {
    if (this->DataInformationValid)
    {
      return QVariant(this->NumberOfCells);
    }
    return QVariant("Unavailable");
  }

  QVariant getNumberOfPoints() const
  {
    if (this->DataInformationValid)
    {
      return QVariant(this->NumberOfPoints);
    }
    return QVariant("Unavailable");
  }

  QVariant getMemorySize() const
  {
    if (this->DataInformationValid)
    {
      return QVariant(this->MemorySize);
    }
    return QVariant("Unavailable");
  }

  QVariant getGeometrySize() const
  {
    if (this->GeometryInformationValid)
    {
      return QVariant(this->GeometrySize);
    }

    return QVariant("Unavailable");
  }

  QVariant getBounds() const
  {
    if (this->DataInformationValid)
    {
      QString bounds("[ %1, %2 ] , [ %3, %4 ] , [ %5, %6 ]");
      for (int i = 0; i < 6; i++)
      {
        bounds = bounds.arg(this->Bounds[i], 0, 'g', 3);
      }
      return QVariant(bounds);
    }
    return QVariant("Unavailable");
  }
  QVariant getTimes() const
  {
    if (this->DataInformationValid)
    {
      if (this->TimeRange[0] > this->TimeRange[1])
      {
        QString times("[ALL]");
        return QVariant(times);
      }
      else
      {
        QString times("[ %1, %2]");
        times = times.arg(this->TimeRange[0], 0, 'g', 3);
        times = times.arg(this->TimeRange[1], 0, 'g', 3);
        return QVariant(times);
      }
    }
    return QVariant("Unavailable");
  }

  // Given a data type ID, returns the string.
  QString getDataTypeAsString() const
  {
    if (this->DataInformationValid)
    {
      return this->DataTypeName;
    }
    return "Unavailable";
  }

  // Given a datatype, returns the icon for that data type.
  QIcon getDataTypeAsIcon() const
  {
    if (!this->DataInformationValid)
    {
      return QIcon(":/pqWidgets/Icons/pqUnknownData16.png");
    }

    switch (this->DataType)
    {
      case VTK_POLY_DATA:
        return QIcon(":/pqWidgets/Icons/pqPolydata16.png");

      case VTK_HYPER_OCTREE:
        return QIcon(":/pqWidgets/Icons/pqOctreeData.svg");

      case VTK_UNSTRUCTURED_GRID:
        return QIcon(":/pqWidgets/Icons/pqUnstructuredGrid16.png");

      case VTK_STRUCTURED_GRID:
        return QIcon(":/pqWidgets/Icons/pqStructuredGrid16.png");

      case VTK_RECTILINEAR_GRID:
        return QIcon(":/pqWidgets/Icons/pqRectilinearGrid16.png");

      case VTK_IMAGE_DATA:
        /*
        {
        int *ext = dataInfo->GetExtent();
        if (ext[0] == ext[1] || ext[2] == ext[3] || ext[4] == ext[5])
        {
        return "Image (Uniform Rectilinear)";
        }
        return "Volume (Uniform Rectilinear)";
        }
        */
        return QIcon(":/pqWidgets/Icons/pqStructuredGrid16.png");

      case VTK_MULTIGROUP_DATA_SET:
        return QIcon(":/pqWidgets/Icons/pqGroup.svg");

      case VTK_MULTIBLOCK_DATA_SET:
        return QIcon(":/pqWidgets/Icons/pqMultiBlockData16.png");

      case VTK_HIERARCHICAL_DATA_SET:
        return QIcon(":/pqWidgets/Icons/pqHierarchicalData16.png");

      case VTK_HIERARCHICAL_BOX_DATA_SET:
        return QIcon(":/pqWidgets/Icons/pqOctreeData.svg");

      default:
        return QIcon(":/pqWidgets/Icons/pqUnknownData16.png");
    }
  }
};

//-----------------------------------------------------------------------------
class pqDataInformationModelInternal
{
public:
  QPointer<pqView> View;
  QList<pqSourceInfo> Sources;
  vtkTimeStamp UpdateTime;

  bool contains(pqPipelineSource* src)
  {
    Q_FOREACH (pqSourceInfo info, this->Sources)
    {
      if (info.OutputPort->getSource() == src)
      {
        return true;
      }
    }
    return false;
  }

  int indexOf(pqPipelineSource* src)
  {
    int index = 0;
    Q_FOREACH (pqSourceInfo info, this->Sources)
    {
      if (info.OutputPort->getSource() == src)
      {
        return index;
      }
      ++index;
    }

    return -1;
  }

  int lastIndexOf(pqPipelineSource* src)
  {
    for (int cc = this->Sources.size() - 1; cc >= 0; --cc)
    {
      pqSourceInfo& info = this->Sources[cc];
      if (info.OutputPort->getSource() == src)
      {
        return cc;
      }
    }

    return -1;
  }
};

//-----------------------------------------------------------------------------
pqDataInformationModel::pqDataInformationModel(QObject* _parent /*=nullptr*/)
  : QAbstractTableModel(_parent)
{
  this->Internal = new pqDataInformationModelInternal();
}

//-----------------------------------------------------------------------------
pqDataInformationModel::~pqDataInformationModel()
{
  delete this->Internal;
}

//-----------------------------------------------------------------------------
int pqDataInformationModel::rowCount(const QModelIndex& vtkNotUsed(parent) /*=QModelIndex()*/) const
{
  return (this->Internal->Sources.size());
}

//-----------------------------------------------------------------------------
int pqDataInformationModel::columnCount(
  const QModelIndex& vtkNotUsed(parent) /*= QModelIndex()*/) const
{
  return pqDataInformationModel::Max_Columns;
}

//-----------------------------------------------------------------------------
QVariant pqDataInformationModel::data(const QModelIndex& idx, int role /*= Qt::DisplayRole*/) const
{
  if (!idx.isValid() || idx.model() != this)
  {
    return QVariant();
  }

  if (idx.row() >= this->Internal->Sources.size())
  {
    qDebug() << "pqDataInformationModel::data called with invalid index: " << idx.row();
    return QVariant();
  }
  if (role == Qt::ToolTipRole)
  {
    return this->headerData(idx.column(), Qt::Horizontal, Qt::DisplayRole);
  }

  pqSourceInfo& info = this->Internal->Sources[idx.row()];

  switch (idx.column())
  {
    case pqDataInformationModel::Name:
      // Name column.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return QVariant(info.getName());
      }
      break;

    case pqDataInformationModel::DataType:
      // Data column.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return QVariant(info.getDataTypeAsString());

        case Qt::DecorationRole:
          return QVariant(info.getDataTypeAsIcon());
      }
      break;

    case pqDataInformationModel::CellCount:
      // Number of cells.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getNumberOfCells();

        case Qt::DecorationRole:
          return QVariant(QIcon(":/pqWidgets/Icons/pqCellData.svg"));
      }
      break;

    case pqDataInformationModel::PointCount:
      // Number of Points.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getNumberOfPoints();

        case Qt::DecorationRole:
          return QVariant(QIcon(":/pqWidgets/Icons/pqPointData.svg"));
      }
      break;

    case pqDataInformationModel::MemorySize:
      // Memory.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getMemorySize();
      }
      break;

    case pqDataInformationModel::GeometrySize:
      // Geometry size for active view.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getGeometrySize();
      }
      break;

    case pqDataInformationModel::Bounds:
      // Spatial Bounds.
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getBounds();
      }
      break;

    case pqDataInformationModel::TimeRange:
      // Temporal Bounds and steps
      switch (role)
      {
        case Qt::DisplayRole:
        case Qt::EditRole:
          return info.getTimes();
      }
      break;
  }
  return QVariant();
}

//-----------------------------------------------------------------------------
QVariant pqDataInformationModel::headerData(
  int section, Qt::Orientation orientation, int role /*=Qt::DisplayRole*/) const
{
  if (orientation == Qt::Horizontal)
  {
    switch (role)
    {
      case Qt::DisplayRole:
        switch (section)
        {
          case pqDataInformationModel::Name:
            return QVariant("Name");

          case pqDataInformationModel::DataType:
            return QVariant("Data Type");

          case pqDataInformationModel::CellCount:
            return QVariant("No. of Cells");

          case pqDataInformationModel::PointCount:
            return QVariant("No. of Points");

          case pqDataInformationModel::MemorySize:
            return QVariant("Memory (MB)");

          case pqDataInformationModel::GeometrySize:
            return QVariant("Geometry Size (MB)");

          case pqDataInformationModel::Bounds:
            return QVariant("Spatial Bounds");

          case pqDataInformationModel::TimeRange:
            return QVariant("Temporal Bounds");
        }
        break;
    }
  }
  return QVariant();
}

//-----------------------------------------------------------------------------
void pqDataInformationModel::dataUpdated(pqPipelineSource* changedSource)
{
  QList<pqSourceInfo>::iterator iter;
  int row_no = 0;
  for (iter = this->Internal->Sources.begin(); iter != this->Internal->Sources.end();
       ++iter, row_no++)
  {
    pqOutputPort* port = iter->OutputPort;
    pqPipelineSource* source = port->getSource();

    if (source != changedSource)
    {
      continue;
    }

    vtkPVDataInformation* dataInfo = port->getDataInformation();
    if (!iter->DataInformationValid || dataInfo->GetMTime() > iter->MTime)
    {
      iter->MTime = dataInfo->GetMTime();
      iter->DataType = dataInfo->GetDataSetType();
      iter->DataTypeName = dataInfo->GetPrettyDataTypeString();
      if (dataInfo->GetCompositeDataSetType() >= 0)
      {
        iter->DataType = dataInfo->GetCompositeDataSetType();
      }
      iter->NumberOfCells = dataInfo->GetNumberOfCells();
      iter->NumberOfPoints = dataInfo->GetNumberOfPoints();
      iter->MemorySize = dataInfo->GetMemorySize() / 1000.0;
      dataInfo->GetBounds(iter->Bounds);
      dataInfo->GetTimeRange(iter->TimeRange);
      iter->DataInformationValid = true;

      Q_EMIT this->dataChanged(
        this->index(row_no, Name), this->index(row_no, pqDataInformationModel::Max_Columns - 1));
    }
  }
}

//-----------------------------------------------------------------------------
void pqDataInformationModel::addSource(pqPipelineSource* source)
{
  if (this->Internal->contains(source))
  {
    return;
  }

  int numOutputPorts = source->getNumberOfOutputPorts();
  this->beginInsertRows(QModelIndex(), this->Internal->Sources.size(),
    this->Internal->Sources.size() + numOutputPorts - 1);

  for (int cc = 0; cc < numOutputPorts; cc++)
  {
    this->Internal->Sources.push_back(source->getOutputPort(cc));
  }
  this->endInsertRows();

  QObject::connect(
    source, SIGNAL(dataUpdated(pqPipelineSource*)), this, SLOT(dataUpdated(pqPipelineSource*)));
}

//-----------------------------------------------------------------------------
void pqDataInformationModel::removeSource(pqPipelineSource* source)
{
  int idx = this->Internal->indexOf(source);

  if (idx != -1)
  {
    int lastIdx = this->Internal->lastIndexOf(source);

    this->beginRemoveRows(QModelIndex(), idx, lastIdx);
    for (int cc = lastIdx; cc >= idx; --cc)
    {
      this->Internal->Sources.removeAt(cc);
    }
    this->endRemoveRows();
  }

  QObject::disconnect(source, nullptr, this, nullptr);
}

//-----------------------------------------------------------------------------
QModelIndex pqDataInformationModel::getIndexFor(pqOutputPort* item) const
{
  if (!this->Internal->Sources.contains(item))
  {
    return QModelIndex();
  }
  return this->index(this->Internal->Sources.indexOf(item), 0);
}

//-----------------------------------------------------------------------------
pqOutputPort* pqDataInformationModel::getItemFor(const QModelIndex& idx) const
{
  if (!idx.isValid() && idx.model() != this)
  {
    return nullptr;
  }
  if (idx.row() >= this->Internal->Sources.size())
  {
    qDebug() << "Index: " << idx.row() << " beyond range.";
    return nullptr;
  }
  return this->Internal->Sources[idx.row()].OutputPort;
}

//-----------------------------------------------------------------------------
void pqDataInformationModel::setActiveView(pqView* view)
{
  if (this->Internal->View == view)
  {
    return;
  }

  if (this->Internal->View)
  {
    QObject::disconnect(this->Internal->View, nullptr, this, nullptr);
  }

  this->Internal->View = view;

  if (view)
  {
    QObject::connect(view, SIGNAL(endRender()), this, SLOT(refreshGeometrySizes()));
  }

  this->refreshGeometrySizes();
}

//-----------------------------------------------------------------------------
void pqDataInformationModel::refreshGeometrySizes()
{
  // Must be called only after endRender() when we are assured that all
  // representations are up-to-date.
  QList<pqSourceInfo>::iterator iter;
  for (iter = this->Internal->Sources.begin(); iter != this->Internal->Sources.end(); ++iter)
  {
    pqSourceInfo& sourceInfo = (*iter);
    sourceInfo.GeometryInformationValid = false;
    pqOutputPort* port = sourceInfo.OutputPort;
    if (this->Internal->View)
    {
      pqDataRepresentation* repr = port->getRepresentation(this->Internal->View);
      if (!repr || !repr->isVisible())
      {
        continue;
      }
      sourceInfo.GeometryInformationValid = true;
      sourceInfo.GeometrySize = repr->getFullResMemorySize() / 1000.0;
    }
  }

  Q_EMIT this->dataChanged(this->index(0, pqDataInformationModel::GeometrySize),
    this->index(this->rowCount() - 1, pqDataInformationModel::GeometrySize));
}
//-----------------------------------------------------------------------------
Qt::ItemFlags pqDataInformationModel::flags(const QModelIndex& idx) const
{
  return QAbstractTableModel::flags(idx) | Qt::ItemIsEditable;
}
//-----------------------------------------------------------------------------
bool pqDataInformationModel::setData(const QModelIndex&, const QVariant&, int)
{
  // Do nothing, we are not supposed to change our data...
  return true;
}
