/*=========================================================================

  Program:   Visualization Toolkit
  Module:    vtkGeoJSONFeature.cxx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/

#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 "vtkPolygon.h"
#include "vtkPolyLine.h"
#include "vtkStringArray.h"

#include <sstream>
#include <string>

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 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 x = coordinates[0];
    Json::Value 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 x = coordinates[0];
    Json::Value y = coordinates[1];
    Json::Value 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 ? true : false;

  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 typeNode = root["type"];
  if (typeNode.isNull() || "Feature" != typeNode.asString())
  {
    vtkErrorMacro (<< "Unknown type. \"Feature\" expected");
    return;
  }

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

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

  // Check for feature id
  std::string featureString;
  Json::Value 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 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 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.size() < 1 )
  {
    vtkErrorMacro (<< "Expected at least 1 value at " << root);
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value 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.size() < 1 )
  {
    vtkErrorMacro (<< "Expected at least 1 value at " << root);
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value 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;
  }

  if ( ! ( root.size() > 0 && root.size() < 4 ) )
  {
    vtkErrorMacro (<< "Expected 3 or less dimension values at " << root << " for point");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value 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.size() < 1 )
  {
      vtkErrorMacro (<< "Expected at least 1 value at " << root << " for multipoint");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value 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.size() < 1 )
  {
    vtkErrorMacro (<< "Expected at least 1 value at " << root << "for polygon");
    return false;
  }

  for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
  {
    Json::Value 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.size() < 1 )
  {
      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 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);
}
