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

#include "vtkGeoJSONFeature.h"

// VTK Includes
#include "vtkAbstractArray.h"
#include "vtkBitArray.h"
#include "vtkCellArray.h"
#include "vtkCellData.h"
#include "vtkDoubleArray.h"
#include "vtkIntArray.h"
#include "vtkLine.h"
#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkPoints.h"
#include "vtkPolyData.h"
#include "vtkPolyLine.h"
#include "vtkPolygon.h"
#include "vtkStringArray.h"

#include <sstream>
#include <string>

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkGeoJSONFeature);

namespace
{
vtkOStreamWrapper& operator<<(vtkOStreamWrapper& os, const Json::Value& root)
{
  Json::StreamWriterBuilder builder;
  builder["commentStyle"] = "All";
  builder["indentation"] = "  ";
  std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

  writer->write(root, &os.GetOStream());
  return os;
}
}

//------------------------------------------------------------------------------
vtkGeoJSONFeature::vtkGeoJSONFeature()
{
  this->FeatureId = nullptr;
  this->OutlinePolygons = false;
}

//------------------------------------------------------------------------------
vtkGeoJSONFeature::~vtkGeoJSONFeature()
{
  free(this->FeatureId);
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::CreatePoint(const Json::Value& coordinates, double point[3])
{
  // Check if Coordinates corresponds to Point
  if (!IsPoint(coordinates))
  {
    vtkErrorMacro(<< "Wrong data format for a point!");
    return false;
  }

  // Do isDouble before asDouble to prevent inconsistency
  // Probably check for float/int too

  if (coordinates.size() == 1)
  {
    // Update the 3D Coordinates using the 1 Value in the array and rest of the 2 as 0
    Json::Value const& x = coordinates[0];
    point[0] = x.asDouble();
    point[1] = 0;
    point[2] = 0;
  }
  else if (coordinates.size() == 2)
  {
    // Update the 3D Coordinates using the 2 Values in the array and 3rd as 0
    Json::Value const& x = coordinates[0];
    Json::Value const& y = coordinates[1];
    point[0] = x.asDouble();
    point[1] = y.asDouble();
    point[2] = 0;
  }
  else if (coordinates.size() == 3)
  {
    // Update the 3D Coordinates using the 3 Values in the array
    Json::Value const& x = coordinates[0];
    Json::Value const& y = coordinates[1];
    Json::Value const& z = coordinates[2];
    point[0] = x.asDouble();
    point[1] = y.asDouble();
    point[2] = z.asDouble();
  }

  // Return that we properly created the point
  return true;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractPoint(
  const Json::Value& coordinates, vtkPolyData* outputData)
{
  // Check if Coordinates corresponds to Single Point
  if (!IsPoint(coordinates))
  {
    vtkErrorMacro(<< "Wrong data format for a point!");
    return nullptr;
  }

  // Obtain point data from Json structure and add to outputData
  double point[3];
  CreatePoint(coordinates, point);

  const int PID_SIZE = 1;
  vtkIdType pid;

  vtkPoints* points = outputData->GetPoints();
  pid = points->InsertNextPoint(point);

  vtkCellArray* verts = outputData->GetVerts();
  verts->InsertNextCell(PID_SIZE, &pid);

  vtkAbstractArray* array = outputData->GetCellData()->GetAbstractArray("feature-id");
  vtkStringArray* ids = vtkArrayDownCast<vtkStringArray>(array);
  ids->InsertNextValue(this->FeatureId);

  return outputData;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractMultiPoint(
  const Json::Value& coordinates, vtkPolyData* outputData)
{
  // Check if Coordinates corresponds to Multi Points
  if (!IsMultiPoint(coordinates))
  {
    vtkErrorMacro(<< "Wrong data format for a Multi Point!");
    return nullptr;
  }

  if (coordinates.isArray())
  {
    vtkPoints* points = outputData->GetPoints(); // Contain the locations of the points
    vtkCellArray* verts =
      outputData->GetVerts(); // Contain the indices corresponding to the position of the vertices

    vtkAbstractArray* array = outputData->GetCellData()->GetAbstractArray("feature-id");
    vtkStringArray* ids = vtkArrayDownCast<vtkStringArray>(array);

    const int PID_SIZE = coordinates.size();
    vtkIdType* pids = new vtkIdType[PID_SIZE];
    double point[3];
    for (int i = 0; i < PID_SIZE; i++)
    {
      // Parse point from Json object to double array and add it to the points array
      CreatePoint(coordinates, point);
      pids[i] = points->InsertNextPoint(point);
    }

    // Update polyData vertices to store multiple points
    verts->InsertNextCell(PID_SIZE, pids);
    ids->InsertNextValue(this->FeatureId);

    delete[] pids;
  }

  return outputData;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractLineString(
  const Json::Value& coordinates, vtkPolyData* outputData)
{
  // Check if Coordinates corresponds to Line String
  if (!IsLineString(coordinates))
  {
    vtkErrorMacro(<< "Wrong data format for a Line String!");
    return nullptr;
  }

  vtkPoints* points = outputData->GetPoints();
  vtkNew<vtkPolyLine> polyLine;
  double xyz[3];
  vtkIdList* pointIdList = polyLine->GetPointIds();
  vtkIdType pointId;
  for (Json::Value::ArrayIndex i = 0; i < coordinates.size(); i++)
  {
    this->CreatePoint(coordinates[i], xyz);
    pointId = points->InsertNextPoint(xyz);
    pointIdList->InsertNextId(pointId);
  }
  outputData->GetLines()->InsertNextCell(polyLine);
  vtkAbstractArray* array = outputData->GetCellData()->GetAbstractArray("feature-id");
  vtkStringArray* ids = vtkArrayDownCast<vtkStringArray>(array);
  ids->InsertNextValue(this->FeatureId);

  return outputData;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractMultiLineString(
  const Json::Value& coordinateArray, vtkPolyData* outputData)
{
  // Check if Coordinate Array corresponds to Multi Line String
  if (!IsMultiLineString(coordinateArray))
  {
    vtkErrorMacro(<< "Wrong data format for a Multi Line String!");
    return nullptr;
  }

  for (Json::Value::ArrayIndex i = 0; i < coordinateArray.size(); i++)
  {
    this->ExtractLineString(coordinateArray[i], outputData);
  }

  return outputData;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractPolygon(
  const Json::Value& coordinate, vtkPolyData* outputData)
{
  // Check if Coordinate Array corresponds to Polygon
  if (!IsPolygon(coordinate))
  {
    vtkErrorMacro(<< "Wrong data format for a Polygon!");
    return nullptr;
  }

  bool POLYGON_WITH_HOLES = coordinate.size() > 1;

  vtkPoints* points = outputData->GetPoints();
  vtkAbstractArray* array = outputData->GetCellData()->GetAbstractArray("feature-id");
  vtkStringArray* ids = vtkArrayDownCast<vtkStringArray>(array);

  // Output is either vtkPolygon or vtkPolyLine,
  // depending on OutputPolygons option.
  vtkCellArray* polys = nullptr;
  vtkCell* exteriorPoly = nullptr;
  if (this->OutlinePolygons)
  {
    polys = outputData->GetLines();
    exteriorPoly = vtkPolyLine::New();
  }
  else
  {
    polys = outputData->GetPolys();
    exteriorPoly = vtkPolygon::New();
  }

  // For exterior Polygon
  Json::Value exteriorPolygon = coordinate[0];

  int EXTERIOR_POLYGON_VERTEX_COUNT = exteriorPolygon.size() - 1;
  exteriorPoly->GetPointIds()->SetNumberOfIds(EXTERIOR_POLYGON_VERTEX_COUNT);

  double point[3];
  // Remember first point, if needed for polyline
  this->CreatePoint(exteriorPolygon[0], point);
  vtkIdType idPoint0 = points->InsertNextPoint(point);
  exteriorPoly->GetPointIds()->SetId(0, idPoint0);

  // Add points for rest of polygon
  for (int i = 1; i < EXTERIOR_POLYGON_VERTEX_COUNT; i++)
  {
    this->CreatePoint(exteriorPolygon[i], point);
    vtkIdType id = points->InsertNextPoint(point);
    exteriorPoly->GetPointIds()->SetId(i, id);
  }

  // For outline mode, add first point to the end
  if (this->OutlinePolygons)
  {
    exteriorPoly->GetPointIds()->InsertNextId(idPoint0);
  }

  polys->InsertNextCell(exteriorPoly);
  ids->InsertNextValue(this->FeatureId);
  exteriorPoly->Delete();

  if (!POLYGON_WITH_HOLES)
    return outputData;

  // Todo Modify polydata to support polygon with holes

  return outputData;
}

//------------------------------------------------------------------------------
vtkPolyData* vtkGeoJSONFeature::ExtractMultiPolygon(
  const Json::Value& coordinateArray, vtkPolyData* outputData)
{
  // Check if Coordinate Array corresponds to Multi Polygon
  if (!IsMultiPolygon(coordinateArray))
  {
    vtkErrorMacro(<< "Wrong data format for a Multi Polygon!");
    return nullptr;
  }

  for (Json::Value::ArrayIndex i = 0; i < coordinateArray.size(); i++)
  {
    // Extract polygon into different polyData and append into a common polyData using the
    // appendPolyData Filter
    this->ExtractPolygon(coordinateArray[i], outputData);
  }

  return outputData;
}

//------------------------------------------------------------------------------
void vtkGeoJSONFeature::ExtractGeoJSONFeature(const Json::Value& root, vtkPolyData* outputData)
{
  this->featureRoot = root;

  // Check that type is Feature
  Json::Value const& typeNode = root["type"];
  if (typeNode.isNull() || "Feature" != typeNode.asString())
  {
    vtkErrorMacro(<< "Unknown type. \"Feature\" expected");
    return;
  }

  // Check for geometry node
  Json::Value const& geometryNode = root["geometry"];
  if (geometryNode.isNull())
  {
    vtkErrorMacro(<< "Missing geometry node");
    return;
  }

  // Check for properties node
  Json::Value const& propertiesNode = root["properties"];
  if (propertiesNode.isNull())
  {
    vtkErrorMacro(<< "Missing properties node");
    return;
  }

  // Check for feature id
  std::string featureString;
  Json::Value const& idNode = root["id"];
  // No Json::Value::toString() method, so homebrew one here
  std::stringstream oss;
  switch (idNode.type())
  {
    case Json::nullValue:
      break;

    case Json::intValue:
    case Json::uintValue:
      oss << idNode.asInt();
      featureString = oss.str();
      break;

    case Json::realValue:
      oss << idNode.asDouble();
      featureString = oss.str();
      break;

    case Json::stringValue:
      featureString = idNode.asString();
      break;

    default:
      vtkWarningMacro(<< "Unsupported \"id\" type: " << idNode.type());
      break;
  }

  this->FeatureId = strdup(featureString.c_str());
  this->ExtractGeoJSONFeatureGeometry(geometryNode, outputData);
}

//------------------------------------------------------------------------------
void vtkGeoJSONFeature::ExtractGeoJSONFeatureGeometry(
  const Json::Value& geometryRoot, vtkPolyData* outputData)
{
  // Check for geometry-type node
  Json::Value const& geometryTypeNode = geometryRoot["type"];
  if (geometryTypeNode.isNull())
  {
    vtkErrorMacro(<< "Missing geometry-type node");
    return;
  }
  if (!geometryTypeNode.isString())
  {
    vtkErrorMacro(<< "Invalid geometry-type node");
    return;
  }

  std::string typeString = geometryTypeNode.asString();
  if (typeString == GeoJSON_GEOMETRY_COLLECTION)
  {
    // For GeometryCollection
    Json::Value geometries = geometryRoot["geometries"];
    for (Json::Value::ArrayIndex i = 0; i < geometries.size(); i++)
    {
      Json::Value child = geometries[i];
      this->ExtractGeoJSONFeatureGeometry(child, outputData);
    }
    return;
  }

  // (else)
  Json::Value const& coordinates = geometryRoot["coordinates"];
  if (typeString == GeoJSON_POINT)
  {
    this->ExtractPoint(coordinates, outputData);
  }
  else if (typeString == GeoJSON_MULTI_POINT)
  {
    this->ExtractMultiPoint(coordinates, outputData);
  }
  else if (typeString == GeoJSON_LINE_STRING)
  {
    this->ExtractLineString(coordinates, outputData);
  }
  else if (typeString == GeoJSON_MULTI_LINE_STRING)
  {
    this->ExtractMultiLineString(coordinates, outputData);
  }
  else if (typeString == GeoJSON_POLYGON)
  {
    this->ExtractPolygon(coordinates, outputData);
  }
  else if (typeString == GeoJSON_MULTI_POLYGON)
  {
    this->ExtractMultiPolygon(coordinates, outputData);
  }
  else
  {
    vtkErrorMacro(<< "Unknown or unsupported geometry type " << geometryTypeNode);
  }
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsLineString(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro(<< "Expected Arrays as input for point at " << root);
    return false;
  }

  if (root.empty())
  {
    vtkErrorMacro(<< "Expected at least 1 value at " << root);
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!IsPoint(child))
    {
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsMultiLineString(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro(<< "Expected Array as input for point at " << root);
    return false;
  }

  if (root.empty())
  {
    vtkErrorMacro(<< "Expected at least 1 value at " << root);
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!IsLineString(child))
    {
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsPoint(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro("Expected Array as input for point at " << root);
    return false;
  }

  const size_t root_size = root.size();
  if (root_size == 0 || root_size > 3)
  {
    vtkErrorMacro(<< root_size << " is not between 1 and 3 dimensions at " << root
                  << " for point.");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!child.isNumeric())
    {
      vtkErrorMacro(<< "Value not Numeric as expected at " << child);
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsMultiPoint(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro(<< "Expected Array as input for multi point at " << root);
    return false;
  }

  if (root.empty())
  {
    vtkErrorMacro(<< "Expected at least 1 value at " << root << " for multipoint");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!IsPoint(child))
    {
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsPolygon(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro(<< "Expected Array as input for polygon at " << root);
    return false;
  }

  if (root.empty())
  {
    vtkErrorMacro(<< "Expected at least 1 value at " << root << "for polygon");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!IsLineString(child))
    {
      return false;
    }
    // First and last element of child must be same to complete the polygon loop.
    // Check not added.
  }

  return true;
}

//------------------------------------------------------------------------------
bool vtkGeoJSONFeature::IsMultiPolygon(const Json::Value& root)
{
  if (!root.isArray())
  {
    vtkErrorMacro(<< "Expected Array as input for multi polygon at " << root);
    return false;
  }

  if (root.empty())
  {
    vtkErrorMacro(<< "Expected at least 1 value at " << root << " for multi polygon");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value const& child = root[i];
    if (!IsPolygon(child))
    {
      return false;
    }
  }

  return true;
}

//------------------------------------------------------------------------------
void vtkGeoJSONFeature::PrintSelf(ostream& os, vtkIndent indent)
{
  Superclass::PrintSelf(os, indent);
  os << indent << "vtkGeoJSONFeature" << std::endl;
  os << indent << "Root: ";

  Json::StreamWriterBuilder builder;
  builder["commentStyle"] = "All";
  builder["indentation"] = "  ";
  std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

  writer->write(this->featureRoot, &os);
}
VTK_ABI_NAMESPACE_END
