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

#include "vtkDataSetAttributes.h"
#include "vtkDelimitedTextReader.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkSmartPointer.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkStringArray.h"
#include "vtkTable.h"

#include <algorithm>
#include <cassert>
#include <cctype>
#include <functional>
#include <vector>
#include <vtksys/SystemTools.hxx>

//-----------------------------------------------------------------------------
class vtkEnsembleDataReader::vtkInternal
{
public:
  typedef std::vector<vtkSmartPointer<vtkAlgorithm>> VectorOfReaders;
  VectorOfReaders Readers;

  std::vector<std::string> FilePaths;
  vtkSmartPointer<vtkTable> MetaData;

  vtkTimeStamp ReadMetaDataMTime;
  std::string PreviousFileName;
};

vtkStandardNewMacro(vtkEnsembleDataReader);
//-----------------------------------------------------------------------------
vtkEnsembleDataReader::vtkEnsembleDataReader()
  : FileName(nullptr)
  , CurrentMember(0)
  , Internal(new vtkEnsembleDataReader::vtkInternal())
{
  this->CurrentMemberRange[0] = this->CurrentMemberRange[1] = 0;
  this->SetNumberOfInputPorts(0);
  this->SetNumberOfOutputPorts(1);
}

//-----------------------------------------------------------------------------
vtkEnsembleDataReader::~vtkEnsembleDataReader()
{
  delete this->Internal;
  this->Internal = nullptr;
}

//-----------------------------------------------------------------------------
unsigned int vtkEnsembleDataReader::GetNumberOfMembers() const
{
  return this->Internal->MetaData != nullptr
    ? static_cast<unsigned int>(this->Internal->FilePaths.size())
    : 0;
}

//-----------------------------------------------------------------------------
std::string vtkEnsembleDataReader::GetFilePath(unsigned int member) const
{
  if (member < static_cast<unsigned int>(this->Internal->FilePaths.size()))
  {
    return this->Internal->FilePaths[member];
  }
  return std::string();
}

//-----------------------------------------------------------------------------
void vtkEnsembleDataReader::SetReader(unsigned int rowIndex, vtkAlgorithm* reader)
{
  bool modified = false;
  if (rowIndex >= static_cast<unsigned int>(this->Internal->Readers.size()))
  {
    this->Internal->Readers.resize(rowIndex + 1);
    modified = true;
  }
  if (modified || (this->Internal->Readers[rowIndex] != reader))
  {
    this->Internal->Readers[rowIndex] = reader;
    this->Modified();
  }
}

//-----------------------------------------------------------------------------
void vtkEnsembleDataReader::ResetReaders()
{
  if (!this->Internal->Readers.empty())
  {
    this->Internal->Readers.clear();
    this->Modified();
  }
}

//-----------------------------------------------------------------------------
int vtkEnsembleDataReader::ProcessRequest(
  vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
{
  if (!this->FileName)
  {
    vtkErrorMacro("No filename is set!");
    return false;
  }

  if (this->Internal->Readers.empty())
  {
    vtkErrorMacro("No reader have been specified! Things may not work as expected.");
  }

  // Read meta data if it hasn't already been read or if FileName has changed
  if (!this->UpdateMetaData())
  {
    return 0;
  }

  if (!this->Superclass::ProcessRequest(request, inputVector, outputVector))
  {
    return 0;
  }

  vtkAlgorithm* currentReader = this->GetCurrentReader();
  if (!currentReader)
  {
    vtkErrorMacro("Cannot determine reader to use for member: " << this->CurrentMember);
    return 0;
  }
  if (!currentReader->ProcessRequest(request, inputVector, outputVector))
  {
    return 0;
  }

  if (request->Has(vtkDemandDrivenPipeline::REQUEST_DATA()))
  {
    // Add ensemble information to the data object.
    vtkDataObject* output = vtkDataObject::GetData(outputVector, 0);
    vtkFieldData* outputFD = output->GetFieldData();

    // Use a temporary to attempt to preserve field data generated by the internal reader.
    vtkNew<vtkFieldData> tmp;
    tmp->CopyStructure(this->Internal->MetaData->GetRowData());
    tmp->InsertNextTuple(
      static_cast<vtkIdType>(this->CurrentMember), this->Internal->MetaData->GetRowData());
    for (int cc = 0; cc < tmp->GetNumberOfArrays(); ++cc)
    {
      outputFD->AddArray(tmp->GetAbstractArray(cc));
    }
  }

  return 1;
}

//-----------------------------------------------------------------------------
// string trimmin'
static void ltrim(std::string& s)
{
  s.erase(s.begin(),
    std::find_if(s.begin(), s.end(), [](std::string::value_type c) { return !isspace(c); }));
}
static void rtrim(std::string& s)
{
  s.erase(std::find_if(s.rbegin(), s.rend(), [](std::string::value_type c) { return !isspace(c); })
            .base(),
    s.end());
}
static void trim(std::string& s)
{
  ltrim(s);
  rtrim(s);
}

//-----------------------------------------------------------------------------
vtkAlgorithm* vtkEnsembleDataReader::GetCurrentReader()
{
  if (this->CurrentMember >= static_cast<unsigned int>(this->Internal->Readers.size()))
  {
    return nullptr;
  }
  return this->Internal->Readers[this->CurrentMember];
}

//-----------------------------------------------------------------------------
bool vtkEnsembleDataReader::UpdateMetaData()
{
  if (!this->FileName)
  {
    return false;
  }

  if (this->Internal->MetaData != nullptr && this->Internal->PreviousFileName == this->FileName)
  {
    return true;
  }

  if (this->GetMTime() < this->Internal->ReadMetaDataMTime)
  {
    // Nothing has changed since the last time we did this; no reason why the reading of meta-data
    // is going to succeed now.
    return false;
  }

  this->Internal->FilePaths.clear();
  this->Internal->MetaData = nullptr;
  this->Internal->PreviousFileName = this->FileName;
  this->Internal->ReadMetaDataMTime.Modified();
  this->CurrentMemberRange[0] = this->CurrentMemberRange[1] = 0;

  // Read CSV -- Just pick sensible defaults for now
  vtkNew<vtkDelimitedTextReader> csvReader;
  assert(csvReader.GetPointer());
  csvReader->SetFieldDelimiterCharacters(",");
  csvReader->SetHaveHeaders(true);
  csvReader->SetDetectNumericColumns(true);
  csvReader->SetFileName(FileName);
  csvReader->Update();

  // Initialize a reader for each row in the CSV
  vtkTable* table = csvReader->GetOutput();
  assert(table);
  vtkIdType rowCount = table->GetNumberOfRows();
  vtkIdType columnCount = table->GetNumberOfColumns();
  if (rowCount <= 0 || columnCount <= 0)
  {
    vtkErrorMacro("Empty table loaded.");
    return false;
  }

  this->Internal->FilePaths.resize(rowCount);

  // Read the file names

  // For now, the last column contains a file path.
  vtkStringArray* fileColumn = vtkStringArray::SafeDownCast(table->GetColumn(columnCount - 1));
  if (!fileColumn)
  {
    vtkErrorMacro("The last column must contain file names.");
    return false;
  }

  std::string fdir = vtksys::SystemTools::GetFilenamePath(this->FileName);
  for (vtkIdType row = 0; row < rowCount; ++row)
  {
    std::string filePath = fileColumn->GetValue(row);
    trim(filePath); // fileName may have leading & trailing whitespace

    std::vector<std::string> components;
    components.push_back(fdir);
    components.push_back(filePath);

    // Internal file paths should be relative to the .pve file
    const std::string curFileName = std::string(fdir).append("/").append(filePath);
    this->Internal->FilePaths[row] = curFileName;
  }

  this->Internal->MetaData = table;

  this->CurrentMemberRange[1] =
    this->GetNumberOfMembers() > 0 ? (this->GetNumberOfMembers() - 1) : 0;
  return true;
}

//-----------------------------------------------------------------------------
void vtkEnsembleDataReader::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);

  // File name
  os << indent << "FileName: " << (this->FileName ? this->FileName : "(none)") << endl;

  // Current member
  os << indent << "Current member: " << this->CurrentMember << endl;

  // Meta data
  os << indent << "MetaData: ";
  if (this->Internal->MetaData)
  {
    os << endl;
    this->Internal->MetaData->PrintSelf(os, indent.GetNextIndent());
  }
  else
  {
    os << "(none)" << endl;
  }
}
