// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkActor.h"
#include "vtkAxes.h"
#include "vtkContourFilter.h"
#include "vtkDataSet.h"
#include "vtkFloatArray.h"
#include "vtkGaussianSplatter.h"
#include "vtkImageData.h"
#include "vtkPointData.h"
#include "vtkPoints.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkSmartPointer.h"
#include "vtkStringScanner.h"
#include "vtkTubeFilter.h"
#include "vtkUnstructuredGrid.h"
#include <vtkRegressionTestImage.h>
#include <vtksys/SystemTools.hxx>

#include <cstring>

#include <iostream>

#if defined(_MSC_VER)           /* Visual C++ (and Intel C++) */
#pragma warning(disable : 4996) // 'function': was declared deprecated
#endif

static vtkSmartPointer<vtkDataSet> ReadFinancialData(
  const char* fname, const char* x, const char* y, const char* z, const char* s);
static int ParseFile(FILE* file, const char* tag, float* data);

int main(int argc, char* argv[])
{
  double bounds[6];

  if (argc < 2)
  {
    std::cout << "Usage: " << argv[0] << " financial_file" << endl;
    return EXIT_FAILURE;
  }
  char* fname = argv[1];

  // read data
  vtkSmartPointer<vtkDataSet> dataSet =
    ReadFinancialData(fname, "MONTHLY_PAYMENT", "INTEREST_RATE", "LOAN_AMOUNT", "TIME_LATE");
  // construct pipeline for original population
  vtkSmartPointer<vtkGaussianSplatter> popSplatter = vtkSmartPointer<vtkGaussianSplatter>::New();
  popSplatter->SetInputData(dataSet);
  popSplatter->SetSampleDimensions(50, 50, 50);
  popSplatter->SetRadius(0.05);
  popSplatter->ScalarWarpingOff();

  vtkSmartPointer<vtkContourFilter> popSurface = vtkSmartPointer<vtkContourFilter>::New();
  popSurface->SetInputConnection(popSplatter->GetOutputPort());
  popSurface->SetValue(0, 0.01);

  vtkSmartPointer<vtkPolyDataMapper> popMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
  popMapper->SetInputConnection(popSurface->GetOutputPort());
  popMapper->ScalarVisibilityOff();

  vtkSmartPointer<vtkActor> popActor = vtkSmartPointer<vtkActor>::New();
  popActor->SetMapper(popMapper);
  popActor->GetProperty()->SetOpacity(0.3);
  popActor->GetProperty()->SetColor(.9, .9, .9);

  // construct pipeline for delinquent population
  vtkSmartPointer<vtkGaussianSplatter> lateSplatter = vtkSmartPointer<vtkGaussianSplatter>::New();
  lateSplatter->SetInputData(dataSet);
  lateSplatter->SetSampleDimensions(50, 50, 50);
  lateSplatter->SetRadius(0.05);
  lateSplatter->SetScaleFactor(0.005);

  vtkSmartPointer<vtkContourFilter> lateSurface = vtkSmartPointer<vtkContourFilter>::New();
  lateSurface->SetInputConnection(lateSplatter->GetOutputPort());
  lateSurface->SetValue(0, 0.01);

  vtkSmartPointer<vtkPolyDataMapper> lateMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
  lateMapper->SetInputConnection(lateSurface->GetOutputPort());
  lateMapper->ScalarVisibilityOff();

  vtkSmartPointer<vtkActor> lateActor = vtkSmartPointer<vtkActor>::New();
  lateActor->SetMapper(lateMapper);
  lateActor->GetProperty()->SetColor(1.0, 0.0, 0.0);

  // create axes
  popSplatter->Update();
  popSplatter->GetOutput()->GetBounds(bounds);

  vtkSmartPointer<vtkAxes> axes = vtkSmartPointer<vtkAxes>::New();
  axes->SetOrigin(bounds[0], bounds[2], bounds[4]);
  axes->SetScaleFactor(popSplatter->GetOutput()->GetLength() / 5);

  vtkSmartPointer<vtkTubeFilter> axesTubes = vtkSmartPointer<vtkTubeFilter>::New();
  axesTubes->SetInputConnection(axes->GetOutputPort());
  axesTubes->SetRadius(axes->GetScaleFactor() / 25.0);
  axesTubes->SetNumberOfSides(6);

  vtkSmartPointer<vtkPolyDataMapper> axesMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
  axesMapper->SetInputConnection(axesTubes->GetOutputPort());

  vtkSmartPointer<vtkActor> axesActor = vtkSmartPointer<vtkActor>::New();
  axesActor->SetMapper(axesMapper);

  // graphics stuff
  vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();

  vtkSmartPointer<vtkRenderWindow> renWin = vtkSmartPointer<vtkRenderWindow>::New();
  renWin->AddRenderer(renderer);

  vtkSmartPointer<vtkRenderWindowInteractor> iren =
    vtkSmartPointer<vtkRenderWindowInteractor>::New();
  iren->SetRenderWindow(renWin);

  // read data  //set up renderer
  renderer->AddActor(lateActor);
  renderer->AddActor(axesActor);
  renderer->AddActor(popActor);
  renderer->SetBackground(1, 1, 1);
  renWin->SetSize(300, 300);

  // For testing, check if "-V" is used to provide a regression test image
  if (argc >= 4 && strcmp(argv[2], "-V") == 0)
  {
    renWin->Render();
    int retVal = vtkRegressionTestImage(renWin);

    if (retVal == vtkTesting::FAILED)
    {
      return EXIT_FAILURE;
    }
    else if (retVal != vtkTesting::DO_INTERACTOR)
    {
      return EXIT_SUCCESS;
    }
  }

  // interact with data
  iren->Initialize();
  iren->Start();

  return EXIT_SUCCESS;
}

static vtkSmartPointer<vtkDataSet> ReadFinancialData(
  const char* filename, const char* x, const char* y, const char* z, const char* s)
{
  float xyz[3];
  FILE* file;
  int i;

  if ((file = vtksys::SystemTools::Fopen(filename, "r")) == nullptr)
  {
    std::cerr << "ERROR: Can't open file: " << filename << std::endl;
    return nullptr;
  }

  auto resultNumPts = vtk::scan<std::string, int>(file, "{:s} {:d}"); // read number of points
  if (!resultNumPts)
  {
    std::cerr << "ERROR: Can't read file: " << filename << std::endl;
    fclose(file);
    return nullptr;
  }
  auto& [tag, npts] = resultNumPts->values();
  // Check for a reasonable npts
  if (npts <= 0)
  {
    std::cerr << "ERROR: Number of points must be greater that 0" << std::endl;
    fclose(file);
    return nullptr;
  }
  // We arbitrarily pick a large upper limit on npts
  if (npts > VTK_INT_MAX / 10)
  {
    std::cerr << "ERROR: npts (" << npts << ") is unreasonably large" << std::endl;
    fclose(file);
    return nullptr;
  }
  vtkSmartPointer<vtkUnstructuredGrid> dataSet = vtkSmartPointer<vtkUnstructuredGrid>::New();
  float* xV = new float[npts];
  float* yV = new float[npts];
  float* zV = new float[npts];
  float* sV = new float[npts];

  if (!ParseFile(file, x, xV) || !ParseFile(file, y, yV) || !ParseFile(file, z, zV) ||
    !ParseFile(file, s, sV))
  {
    std::cerr << "ERROR: Couldn't read data!" << std::endl;
    delete[] xV;
    delete[] yV;
    delete[] zV;
    delete[] sV;
    fclose(file);
    return nullptr;
  }

  vtkSmartPointer<vtkPoints> newPts = vtkSmartPointer<vtkPoints>::New();
  vtkSmartPointer<vtkFloatArray> newScalars = vtkSmartPointer<vtkFloatArray>::New();

  for (i = 0; i < npts; i++)
  {
    xyz[0] = xV[i];
    xyz[1] = yV[i];
    xyz[2] = zV[i];
    newPts->InsertPoint(i, xyz);
    newScalars->InsertValue(i, sV[i]);
  }

  dataSet->SetPoints(newPts);
  dataSet->GetPointData()->SetScalars(newScalars);

  // cleanup
  delete[] xV;
  delete[] yV;
  delete[] zV;
  delete[] sV;
  fclose(file);

  return dataSet;
}

static int ParseFile(FILE* file, const char* label, float* data)
{
  int i, readData = 0;
  float min = VTK_FLOAT_MAX;
  float max = (-VTK_FLOAT_MAX);

  if (file == nullptr || label == nullptr)
    return 0;

  clearerr(file);           // clear error and EOF flags
  fseek(file, 0, SEEK_SET); // move to beginning

  auto resultNumPts = vtk::scan<std::string, int>(file, "{:s} {:d}"); // read number of points
  if (!resultNumPts)
  {
    std::cerr << "ERROR: IO Error " << __FILE__ << ":" << __LINE__ << std::endl;
    return 0;
  }
  auto& [tag, npts] = resultNumPts->values();

  vtk::scan_result_type<std::FILE*&, std::string> resultTag;
  while (!readData && ((resultTag = vtk::scan<std::string>(file, "{:s}"))))
  {
    tag = resultTag->value();
    if (tag == label)
    {
      readData = 1;
      for (i = 0; i < npts; i++)
      {
        auto resultData = vtk::scan_value<float>(file);
        if (!resultData)
        {
          std::cerr << "ERROR: IO Error " << __FILE__ << ":" << __LINE__ << std::endl;
          return 0;
        }
        data[i] = resultData->value();
        if (data[i] < min)
          min = data[i];
        if (data[i] > min)
          max = data[i];
      }
      // normalize data
      for (i = 0; i < npts; i++)
        data[i] = min + data[i] / (max - min);
    }
    else
    {
      for (i = 0; i < npts; i++)
      {
        auto resultData = vtk::scan_value<float>(file);
        if (!resultData)
        {
          std::cerr << "ERROR: IO Error " << __FILE__ << ":" << __LINE__ << std::endl;
          return 0;
        }
      }
    }
  }

  if (!readData)
    return 0;
  else
    return 1;
}
