// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-FileCopyrightText: Copyright (c) 2006 Atamai, Inc.
// SPDX-License-Identifier: BSD-3-Clause

#include "vtkMNIObjectReader.h"

#include "vtkArrayDispatch.h"
#include "vtkCellArray.h"
#include "vtkCellData.h"
#include "vtkFloatArray.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkIntArray.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkPointData.h"
#include "vtkPoints.h"
#include "vtkPolyData.h"
#include "vtkProperty.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkStringScanner.h"
#include "vtkUnsignedCharArray.h"

#include <vtksys/FStream.hxx>
#include <vtksys/SystemTools.hxx>

#include <cctype>
#include <string>
#include <vector>

#ifndef VTK_BINARY
#define VTK_ASCII 1
#define VTK_BINARY 2
#endif

//------------------------------------------------------------------------------
VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkMNIObjectReader);

#define VTK_MNIOBJ_LINE_LENGTH 256

//------------------------------------------------------------------------------
vtkMNIObjectReader::vtkMNIObjectReader()
{
  this->SetNumberOfInputPorts(0);

  this->FileName = nullptr;
  this->Property = vtkProperty::New();

  // Whether file is binary or ASCII
  this->FileType = VTK_ASCII;

  // File line number for error reporting (ASCII only)
  this->LineNumber = 0;

  // State information for reading files
  this->InputStream = nullptr;
  this->LineText = new char[VTK_MNIOBJ_LINE_LENGTH];
  this->CharPointer = this->LineText;
}

//------------------------------------------------------------------------------
vtkMNIObjectReader::~vtkMNIObjectReader()
{
  if (this->Property)
  {
    this->Property->Delete();
  }
  delete[] this->FileName;
  delete[] this->LineText;
}

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

  os << indent << "FileName: " << (this->FileName ? this->FileName : "none") << "\n";
  os << indent << "Property: " << this->Property << "\n";
  if (this->Property)
  {
    this->Property->PrintSelf(os, indent.GetNextIndent());
  }
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::CanReadFile(const char* fname)
{
  // First make sure the file exists.  This prevents an empty file
  // from being created on older compilers.
  vtksys::SystemTools::Stat_t fs;
  if (vtksys::SystemTools::Stat(fname, &fs) != 0)
  {
    return 0;
  }

  // Try to read the first line of the file.
  int status = 0;

  vtksys::ifstream infile(fname);

  if (infile.good())
  {
    int objType = infile.get();

    if (infile.good())
    {
      objType = toupper(objType);

      if (objType == 'P' || objType == 'L' || objType == 'M' || objType == 'F' || objType == 'X' ||
        objType == 'Q' || objType == 'T')
      {
        status = 1;
      }
    }

    infile.close();
  }

  return status;
}

//------------------------------------------------------------------------------
// Internal function to read in a line up to 256 characters and then
// skip to the next line in the file.
int vtkMNIObjectReader::ReadLine(char* line, unsigned int maxlen)
{
  this->LineNumber++;
  istream& infile = *this->InputStream;

  infile.getline(line, maxlen);
  this->CharPointer = line;

  if (infile.fail())
  {
    if (infile.eof())
    {
      return 0;
    }
    if (infile.gcount() == 255)
    {
      // Read 256 chars; ignoring the rest of the line.
      infile.clear();
      infile.ignore(VTK_INT_MAX, '\n');
      vtkWarningMacro(
        "Overlength line (limit is 255) in " << this->FileName << ":" << this->LineNumber);
    }
  }

  return 1;
}

//------------------------------------------------------------------------------
// Skip all whitespace, reading additional lines if necessary
int vtkMNIObjectReader::SkipWhitespace()
{
  if (this->FileType == VTK_BINARY)
  {
    return 1;
  }

  // Only skip whitespace in ASCII files
  do
  {
    char* cp = this->CharPointer;

    // Skip leading whitespace
    while (isspace(*cp))
    {
      cp++;
    }

    if (*cp != '\0')
    {
      this->CharPointer = cp;
      return 1;
    }
  } while (this->ReadLine(this->LineText, VTK_MNIOBJ_LINE_LENGTH));

  return 0;
}

//------------------------------------------------------------------------------
struct vtkMNIObjectReaderFunctor
{
  int Result;
  template <class TArray>
  void operator()(TArray* array, vtkMNIObjectReader* self, vtkIdType n)
  {
    using ValueType = typename TArray::ValueType;
    array->SetNumberOfTuples(n / array->GetNumberOfComponents());

    if (self->FileType == VTK_BINARY)
    {
      auto* data = array->GetPointer(0);
      // The .obj files use native machine endianness
      self->InputStream->read(reinterpret_cast<char*>(data), n * array->GetDataTypeSize());

      // Switch ABGR to RGBA colors
      if (std::is_same_v<ValueType, unsigned char> && array->GetNumberOfComponents() == 4)
      {
        for (vtkIdType i = 0; i < n; i += 4)
        {
          unsigned char abgr[4];
          abgr[0] = data[0];
          abgr[1] = data[1];
          abgr[2] = data[2];
          abgr[3] = data[3];
          data[0] = abgr[3];
          data[1] = abgr[2];
          data[2] = abgr[1];
          data[3] = abgr[0];

          data += 4;
        }
      }

      this->Result = !self->InputStream->fail();
    }
    else
    {
      // The rest of the code is for ASCII files
      for (vtkIdType i = 0; i < n; i++)
      {
        if (!self->SkipWhitespace())
        {
          vtkErrorWithObjectMacro(
            self, "Unexpected end of file " << self->FileName << ":" << self->LineNumber);
          this->Result = 0;
          return;
        }

        char* cp = self->CharPointer;

        if (!std::is_same_v<ValueType, unsigned char>)
        {
          auto result = vtk::scan_value<ValueType>(std::string_view(cp));
          auto val = result ? result->value() : 0;
          cp = const_cast<char*>(result->range().data());
          array->SetValue(i, val);
        }
        else
        {
          auto result = vtk::scan_value<double>(std::string_view(cp));
          auto val = result ? result->value() : 0;
          cp = const_cast<char*>(result->range().data());
          if (val < 0.0 || val > 1.0)
          {
            vtkErrorWithObjectMacro(
              self, "Color value must be [0..1] " << self->FileName << ":" << self->LineNumber);
            this->Result = 0;
            return;
          }
          unsigned char color = static_cast<unsigned char>(val * 255.0);
          array->SetValue(i, color);
        }

        // If nothing was read, there was a syntax error
        if (cp == self->CharPointer)
        {
          vtkErrorWithObjectMacro(
            self, "Syntax error " << self->FileName << ":" << self->LineNumber);
          this->Result = 0;
          return;
        }

        self->CharPointer = cp;
      }
      this->Result = 1;
    }
  }
};

//------------------------------------------------------------------------------
// Read floating-point values into a vtkFloatArray.
int vtkMNIObjectReader::ParseValues(vtkDataArray* array, vtkIdType n)
{
  auto aos = array->ToAOSDataArray();
  vtkMNIObjectReaderFunctor functor;
  if (!vtkArrayDispatch::DispatchByArray<vtkArrayDispatch::AOSArrays>::Execute(
        aos, functor, this, n))
  {
    vtkErrorMacro(<< "WriteValues: Array " << array->GetClassName() << " not supported.");
    return 0;
  }
  return functor.Result;
}

//------------------------------------------------------------------------------
// Read an integer value
int vtkMNIObjectReader::ParseIdValue(vtkIdType* value)
{
  if (this->FileType == VTK_BINARY)
  {
    int val;
    this->InputStream->read((char*)(&val), sizeof(int));
    *value = val;

    return !this->InputStream->fail();
  }

  // The rest of the code is for ASCII files
  if (!this->SkipWhitespace())
  {
    vtkErrorMacro("Unexpected end of file " << this->FileName << ":" << this->LineNumber);
    return 0;
  }

  char* cp = this->CharPointer;

  auto result = vtk::scan_int<long long>(std::string_view(cp));
  auto lval = result ? result->value() : 0;
  cp = const_cast<char*>(result->range().data());
  if (lval > static_cast<long long>(VTK_INT_MAX) || lval < static_cast<long long>(VTK_INT_MIN))
  {
    vtkErrorMacro(
      "Value " << lval << " is too large for int " << this->FileName << ":" << this->LineNumber);
    return 0;
  }

  *value = static_cast<int>(lval);

  // If no bytes were read, that means there was a syntax error
  if (cp == this->CharPointer)
  {
    vtkErrorMacro("Syntax error " << this->FileName << ":" << this->LineNumber);
    return 0;
  }

  this->CharPointer = cp;

  return 1;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadProperty(vtkProperty* property)
{
  vtkFloatArray* tmpArray = vtkFloatArray::New();

  int status = this->ParseValues(tmpArray, 5);

  if (status != 0)
  {
    property->SetAmbient(tmpArray->GetValue(0));
    property->SetDiffuse(tmpArray->GetValue(1));
    property->SetSpecular(tmpArray->GetValue(2));
    property->SetSpecularPower(tmpArray->GetValue(3));
    property->SetOpacity(tmpArray->GetValue(4));
  }

  tmpArray->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadLineThickness(vtkProperty* property)
{
  vtkFloatArray* tmpArray = vtkFloatArray::New();

  int status = this->ParseValues(tmpArray, 1);

  if (status != 0)
  {
    property->SetLineWidth(tmpArray->GetValue(0));
  }

  tmpArray->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadNumberOfPoints(vtkIdType* numPoints)
{
  int status = this->ParseIdValue(numPoints);

  if (status != 0)
  {
    if (*numPoints < 0)
    {
      // Don't support "compressed" data yet
      vtkErrorMacro("Bad number of points -> " << *numPoints << " " << this->FileName << ":"
                                               << this->LineNumber);
      status = 0;
    }
    else if (*numPoints > VTK_ID_MAX / 4)
    {
      vtkErrorMacro(
        "Too many points -> " << *numPoints << " " << this->FileName << ":" << this->LineNumber);
      status = 0;
    }
  }

  return status;
}
//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadNumberOfCells(vtkIdType* numCells)
{
  int status = this->ParseIdValue(numCells);

  if (status != 0)
  {
    if (*numCells < 0)
    {
      vtkErrorMacro(
        "Bad number of cells -> " << *numCells << " " << this->FileName << ":" << this->LineNumber);
      status = 0;
    }
    else if (*numCells > VTK_ID_MAX / 4)
    {
      vtkErrorMacro(
        "Too many cells -> " << *numCells << " " << this->FileName << ":" << this->LineNumber);
      status = 0;
    }
  }

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadPoints(vtkPolyData* data, vtkIdType numPoints)
{
  vtkPoints* points = vtkPoints::New();
  int status = this->ParseValues(points->GetData(), 3 * numPoints);

  if (status != 0)
  {
    data->SetPoints(points);
  }

  points->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadNormals(vtkPolyData* data, vtkIdType numPoints)
{
  vtkFloatArray* normals = vtkFloatArray::New();
  normals->SetNumberOfComponents(3);
  int status = this->ParseValues(normals, 3 * numPoints);

  if (status != 0)
  {
    data->GetPointData()->SetNormals(normals);
  }

  normals->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadColors(
  vtkProperty* property, vtkPolyData* data, vtkIdType numPoints, vtkIdType numCells)
{
  // Find out what kind of coloring is used
  vtkIdType colorType = 0;
  if (this->ParseIdValue(&colorType) == 0)
  {
    return 0;
  }

  // Set the number of colors
  vtkIdType numColors = 1;
  if (colorType == 1)
  {
    numColors = numCells;
  }
  else if (colorType == 2)
  {
    numColors = numPoints;
  }
  else if (colorType != 0)
  {
    vtkErrorMacro("Color number must be 0, 1 or 2 " << this->FileName << ":" << this->LineNumber);
    return 0;
  }

  // Read the colors
  vtkUnsignedCharArray* colors = vtkUnsignedCharArray::New();
  colors->SetName("Colors");
  colors->SetNumberOfComponents(4);
  int status = this->ParseValues(colors, 4 * numColors);

  if (status != 0)
  {
    if (colorType == 0)
    {
      data->GetCellData()->SetScalars(nullptr);
      data->GetPointData()->SetScalars(nullptr);
      property->SetColor(
        colors->GetValue(0) / 255.0, colors->GetValue(1) / 255.0, colors->GetValue(2) / 255.0);
    }
    else if (colorType == 1)
    {
      data->GetPointData()->SetScalars(nullptr);
      data->GetCellData()->SetScalars(colors);
      property->SetColor(1.0, 1.0, 1.0);
    }
    else if (colorType == 2)
    {
      data->GetCellData()->SetScalars(nullptr);
      data->GetPointData()->SetScalars(colors);
      property->SetColor(1.0, 1.0, 1.0);
    }
  }

  colors->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadCells(vtkPolyData* data, vtkIdType numCells, int cellType)
{
  vtkIntArray* endIndices = vtkIntArray::New();
  vtkIntArray* cellIndices = vtkIntArray::New();
  vtkCellArray* cellArray = vtkCellArray::New();

  // Read the cell end indices
  int status = this->ParseValues(endIndices, numCells);

  // Read the cell point indices
  vtkIdType numIndices = 0;
  if (status != 0)
  {
    if (numCells > 0)
    {
      numIndices = endIndices->GetValue(numCells - 1);
    }
    status = this->ParseValues(cellIndices, numIndices);
  }

  // Create the cell array
  if (status != 0)
  {
    cellArray->AllocateExact(numCells, endIndices->GetValue(numCells - 1));

    vtkPoints* points = data->GetPoints();
    vtkIdType numPoints = points->GetNumberOfPoints();
    vtkIdType lastEndIndex = 0;
    for (vtkIdType i = 0; i < numCells; i++)
    {
      vtkIdType endIndex = endIndices->GetValue(i);
      numIndices = endIndex - lastEndIndex;

      cellArray->InsertNextCell(numIndices);

      // Check that the index values are okay and create the cell
      for (vtkIdType j = 0; j < numIndices; j++)
      {
        vtkIdType idx = cellIndices->GetValue(lastEndIndex + j);
        if (idx > numPoints)
        {
          vtkErrorMacro("Index " << idx << " is greater than the"
                                 << " total number of points " << numPoints << " "
                                 << this->FileName);
          return 0;
        }
        cellArray->InsertCellPoint(idx);
      }

      lastEndIndex = endIndex;
    }

    if (cellType == VTK_POLYGON)
    {
      data->SetPolys(cellArray);
    }
    else if (cellType == VTK_POLY_LINE)
    {
      data->SetLines(cellArray);
    }
  }

  endIndices->Delete();
  cellIndices->Delete();
  cellArray->Delete();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadPolygonObject(vtkPolyData* output)
{
  // Read the surface property
  if (this->ReadProperty(this->Property) == 0)
  {
    return 0;
  }

  // Read the number of points
  vtkIdType numPoints = 0;
  if (this->ReadNumberOfPoints(&numPoints) == 0)
  {
    return 0;
  }

  // Read the points
  if (this->ReadPoints(output, numPoints) == 0)
  {
    return 0;
  }

  // Read the normals
  if (this->ReadNormals(output, numPoints) == 0)
  {
    return 0;
  }

  // Read the number of items
  vtkIdType numCells = 0;
  if (this->ReadNumberOfCells(&numCells) == 0)
  {
    return 0;
  }

  // Read the colors
  if (this->ReadColors(this->Property, output, numPoints, numCells) == 0)
  {
    return 0;
  }

  // Read the cells
  if (this->ReadCells(output, numCells, VTK_POLYGON) == 0)
  {
    return 0;
  }

  return 1;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadLineObject(vtkPolyData* output)
{
  // Read the line thickness
  if (this->ReadLineThickness(this->Property) == 0)
  {
    return 0;
  }

  // Read the number of points
  vtkIdType numPoints = 0;
  if (this->ReadNumberOfPoints(&numPoints) == 0)
  {
    return 0;
  }

  // Read the points
  if (this->ReadPoints(output, numPoints) == 0)
  {
    return 0;
  }

  // Read the number of items
  vtkIdType numCells = 0;
  if (this->ReadNumberOfCells(&numCells) == 0)
  {
    return 0;
  }

  // Read the colors
  if (this->ReadColors(this->Property, output, numPoints, numCells) == 0)
  {
    return 0;
  }

  // Read the cells
  if (this->ReadCells(output, numCells, VTK_POLY_LINE) == 0)
  {
    return 0;
  }

  return 1;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::ReadFile(vtkPolyData* output)
{
  // Initialize the property to default values
  vtkProperty* property = vtkProperty::New();
  this->Property->DeepCopy(property);
  property->Delete();

  // Check that the file name has been set.
  if (!this->FileName)
  {
    vtkErrorMacro("ReadFile: No file name has been set");
    return 0;
  }

  // Make sure that the file exists.
  vtksys::SystemTools::Stat_t fs;
  if (vtksys::SystemTools::Stat(this->FileName, &fs) != 0)
  {
    vtkErrorMacro("ReadFile: Can't open file " << this->FileName);
    return 0;
  }

  // Make sure that the file is readable.
  vtksys::ifstream infile(this->FileName, ios::in);

  if (infile.fail())
  {
    vtkErrorMacro("ReadFile: Can't read the file " << this->FileName);
    return 0;
  }

  // Check object type
  int objType = infile.get();
  int fileType = VTK_ASCII;

  if (infile.fail())
  {
    vtkErrorMacro("ReadFile: I/O error for file " << this->FileName);
    infile.close();
    return 0;
  }

  if (islower(objType))
  {
    objType = toupper(objType);
    fileType = VTK_BINARY;
  }

  if (objType != 'P' && objType != 'L' && objType != 'M' && objType != 'F' && objType != 'X' &&
    objType != 'Q' && objType != 'T' && objType != 'V')
  {
    vtkErrorMacro("ReadFile: File is not a MNI obj file: " << this->FileName);
    infile.close();
    return 0;
  }

#ifdef _WIN32
  // Re-open file as binary (only necessary on Windows)
  if (fileType == VTK_BINARY)
  {
    infile.close();
    infile.open(this->FileName, ios::in | ios::binary);
    infile.get();
  }
#endif

  this->InputStream = &infile;
  this->LineNumber = 0;
  this->FileType = fileType;

  int status = 1;

  if (this->FileType == VTK_ASCII)
  {
    // Read the line, include the type char in line text for
    // use in error reporting
    this->LineText[0] = objType;
    status = this->ReadLine(&this->LineText[1], VTK_MNIOBJ_LINE_LENGTH - 1);
  }

  if (status != 0)
  {
    switch (objType)
    {
      case 'P':
        status = this->ReadPolygonObject(output);
        break;
      case 'L':
        status = this->ReadLineObject(output);
        break;
      case 'M':
      case 'F':
      case 'X':
      case 'Q':
      case 'T':
      case 'V':
      {
        vtkErrorMacro("ReadFile: Reading of obj type \""
          << (char)objType << "\" is not supported: " << this->FileName);
        status = 0;
      }
      break;
    }
  }

  if (this->FileType == VTK_BINARY)
  {
    if (infile.fail())
    {
      if (infile.eof())
      {
        vtkErrorMacro("Premature end of binary file " << this->FileName);
      }
      else
      {
        vtkErrorMacro("Error encountered while reading " << this->FileName);
      }
    }
  }

  this->InputStream = nullptr;
  infile.close();

  return status;
}

//------------------------------------------------------------------------------
int vtkMNIObjectReader::RequestData(vtkInformation* vtkNotUsed(request),
  vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
{
  // get the info object
  vtkInformation* outInfo = outputVector->GetInformationObject(0);

  // get the output
  vtkPolyData* output = vtkPolyData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT()));

  // all of the data in the first piece.
  if (outInfo->Get(vtkStreamingDemandDrivenPipeline::UPDATE_PIECE_NUMBER()) > 0)
  {
    return 0;
  }

  // read the file
  return this->ReadFile(output);
}
VTK_ABI_NAMESPACE_END
