/*
 * Copyright (C) 2005-2020 Centre National d'Etudes Spatiales (CNES)
 *
 * This file is part of Orfeo Toolbox
 *
 *     https://www.orfeo-toolbox.org/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


#include "otbOSMDataToVectorDataGenerator.h"
#include "otbSpatialReference.h"
#include "itksys/SystemTools.hxx"
#include "itkDataObject.h"
#include "itkMacro.h"

#include "otb_tinyxml.h"

namespace otb
{

OSMDataToVectorDataGenerator::OSMDataToVectorDataGenerator() : m_North(43.62811), m_South(43.60185), m_East(1.59323), m_West(1.54911), m_UseUrl(true)
{
  this->SetNumberOfRequiredOutputs(1);

  // Initialize the url
  m_Url = "https://www.openstreetmap.org/api/0.6/map?";

  m_Curl = CurlHelper::New();

  m_ClassKey      = "Class";
  m_ClassKeyValue = 1;


  // Add the key to search by default
  m_KeyList.push_back("highway");
  m_KeyList.push_back("building");
  m_KeyList.push_back("landuse");
  m_KeyList.push_back("waterway");
  m_KeyList.push_back("cycleway");
  m_KeyList.push_back("public_transport");
  m_KeyList.push_back("railway");
  m_KeyList.push_back("natural");
}

OSMDataToVectorDataGenerator::~OSMDataToVectorDataGenerator()
{
}


void OSMDataToVectorDataGenerator::GenerateData()
{
  // Priority to the URL, if not set check if the filename is
  // used
  if (m_UseUrl)
  {
    // Get the url
    std::ostringstream urlStream;
    urlStream << m_Url;

    // TODO : Replace by the new method RetrieveFileFromInMemory
    m_FileName = "temposmresult.xml";

    // Add the extent to the url
    urlStream << "bbox=" << m_West << "," << m_South << "," << m_East << "," << m_North;

    // First check if the request does not cross the server
    // limitations :
    if (m_Curl->IsCurlReturnHttpError(urlStream.str()))
    {
      itkExceptionMacro(<< "The OSM Server returned an Error > =400,"
                        << " it means that one of server limits are crossed : node/way/relation or area requested");
    }

    // Use Curl to request the OSM Server
    // TODO use the RetrieveUrlInMemory
    m_Curl->RetrieveFile(urlStream.str(), m_FileName);
  }
  else
  {
    // Check if the filename is not empty
    if (m_FileName.empty())
      itkExceptionMacro(<< "The XML output FileName is empty, please set the filename via the method SetFileName");

    // Check that the right extension is given : expected .xml */
    if (itksys::SystemTools::GetFilenameLastExtension(m_FileName) != ".xml" && itksys::SystemTools::GetFilenameLastExtension(m_FileName) != ".osm")
    {
      itkExceptionMacro(<< itksys::SystemTools::GetFilenameLastExtension(m_FileName) << " is a wrong Extension FileName : Expected .xml or osm");
    }
  }

  // Parse the XML File
  this->ParseXmlFile();

  // Remove the osm temp file only if the url request is used
  if (m_UseUrl)
    if (std::remove(m_FileName.c_str()) != 0)
    {
      itkExceptionMacro(<< "Error while deleting the file" << m_FileName);
    }
}

void OSMDataToVectorDataGenerator::ParseXmlFile()
{
  // Open the xml file
  TiXmlDocument doc(m_FileName.c_str());

  if (!doc.LoadFile())
  {
    itkExceptionMacro(<< "Cannot open the file " << m_FileName);
  }

  // Parse the xml
  TiXmlHandle hDoc(&doc);
  TiXmlHandle root = hDoc.FirstChildElement();

  if (!root.ToElement())
    itkExceptionMacro("Invalid or corrupted XML file : The field way is not found");


  ////////////  ////////////  ////////////  ////////////  ////////////
  /*   Step 1 : Store the nodes in a std::map                       */
  ////////////  ////////////  ////////////  ////////////  ////////////

  // Iterate through the tree to get all the nodes and store them in
  // a std::map
  for (TiXmlElement* node = root.FirstChild("node").ToElement(); node != nullptr && strcmp(node->Value(), "node") == 0; node = node->NextSiblingElement())
  {
    int    id        = 0;
    double longitude = 0.;
    double latitude  = 0.;

    if (node->QueryIntAttribute("id", &id) == TIXML_SUCCESS && node->QueryDoubleAttribute("lat", &latitude) == TIXML_SUCCESS &&
        node->QueryDoubleAttribute("lon", &longitude) == TIXML_SUCCESS)
    {
      // current point
      VertexType geoPoint;
      geoPoint[0] = longitude;
      geoPoint[1] = latitude;

      // std::pair to add in the map
      std::pair<int, VertexType> newEntry;
      newEntry.first  = id;
      newEntry.second = geoPoint;
      m_GeoPointContainer.insert(newEntry);
    }
  }
  ////////////  ////////////  ////////////  ////////////  ////////////
  /*   Step 2 : Find the Key and the node ids of the key */
  ////////////  ////////////  ////////////  ////////////  ////////////

  // Iterate through the tree to get all the fields ways with tag k=key
  for (TiXmlElement* currentLayer = root.FirstChildElement("way").ToElement(); currentLayer != nullptr && strcmp(currentLayer->Value(), "way") == 0;
       currentLayer               = currentLayer->NextSiblingElement())
  {
    bool foundKey = false;

    // Is the element "way" have a child tag
    if (currentLayer->FirstChildElement("tag"))
    {
      ElementPairType elementPair;

      // Iterate through the node tag to find the "key"
      for (TiXmlElement* currentTag = currentLayer->FirstChild("tag")->ToElement(); currentTag != nullptr; currentTag = currentTag->NextSiblingElement())
      {
        std::string result = currentTag->Attribute("k");

        if (this->IsKeyPresent(result))
        {
          // Set the flag to true;
          foundKey = true;

          // Get the value of the key
          std::string resultValue = currentTag->Attribute("v");

          // Fill the element information
          elementPair.first  = result;
          elementPair.second = resultValue;

          // Add the key, value to map
          this->AddKeyTypeToMap(result, resultValue);

          break;
        }
      }

      if (foundKey)
      {
        VectorDataElementType vdelement;
        PointTypeList         pointList;

        // put the current layer pointer to the beginning
        for (TiXmlElement* currentNode = currentLayer->FirstChildElement("nd"); currentNode != nullptr; currentNode = currentNode->NextSiblingElement())
        {
          int value = 0;
          if (currentNode->QueryIntAttribute("ref", &value) == TIXML_SUCCESS)
          {
            // Add the current point to the list
            pointList.push_back((*m_GeoPointContainer.find(value)).second);
          }
        }

        // Form the VectorDataElement
        if (pointList.size() > 0)
        {
          vdelement.first  = elementPair;
          vdelement.second = pointList;
          m_VectorDataElementList.push_back(vdelement);
        }
      }

    } // end if tag k exists
  }
}

void OSMDataToVectorDataGenerator::AddKeyTypeToMap(const std::string& key, const std::string& value)
{
  // Is this key present in the map
  KeyMapType::iterator it = m_KeysMap.find(key);

  if (it != m_KeysMap.end())
  {
    // vector containing the types relative to this key
    StringVectorType& currentTypes = (*it).second;

    bool found = false;

    // Add the type if not present
    for (unsigned int i = 0; i < currentTypes.size(); ++i)
    {
      if (currentTypes[i].compare(value) == 0)
        found = true;
    }
    // if the value was not found add it the currentypes vector
    if (!found)
    {
      currentTypes.push_back(value);
    }
  }
  else
  {
    std::pair<std::string, StringVectorType> keytype;
    keytype.first = key;
    keytype.second.push_back(value);
    m_KeysMap.insert(keytype);
  }
}

void OSMDataToVectorDataGenerator::ProcessVectorData(const std::string& key, const std::string& value)
{
  // the output
  m_OutputVectorData = VectorDataType::New();

  // Intitialize the vectordata : EPSG 4326 is for WGS84
  m_OutputVectorData->SetProjectionRef(otb::SpatialReference::FromWGS84().ToWkt());
  DataNodeType::Pointer root     = m_OutputVectorData->GetDataTree()->GetRoot()->Get();
  DataNodeType::Pointer document = DataNodeType::New();
  DataNodeType::Pointer folder   = DataNodeType::New();

  document->SetNodeType(otb::DOCUMENT);
  folder->SetNodeType(otb::FOLDER);

  document->SetNodeId("DOCUMENT");
  folder->SetNodeId("FOLDER");

  m_OutputVectorData->GetDataTree()->Add(document, root);
  m_OutputVectorData->GetDataTree()->Add(folder, document);

  // Form the vector data with the values parsed
  for (unsigned int idx = 0; idx < m_VectorDataElementList.size(); ++idx)
  {
    // Instantiate a datanode
    DataNodeType::Pointer currentDataNode = DataNodeType::New();

    // Get the current PointTypeList
    PointTypeList currentPointList = m_VectorDataElementList[idx].second;

    // Get the current ElementPairList
    ElementPairType elementPair = m_VectorDataElementList[idx].first;

    // Add it to the vector data only if the requested key matches to
    // elementPair key
    if (elementPair.first.compare(key) == 0)
    {

      // value is empty
      // or if it is not empty it has to match the requestd value
      if (value.empty() || elementPair.second.compare(value) == 0)
      {

        // If the first and the last vertex are the same = polygon else
        // line
        if (currentPointList[0].EuclideanDistanceTo(currentPointList[currentPointList.size() - 1]) < 1e-10) // polygon
        {
          currentDataNode->SetNodeId("FEATURE_POLYGON");
          currentDataNode->SetNodeType(otb::FEATURE_POLYGON);
          PolygonType::Pointer polygon = PolygonType::New();
          for (unsigned int curVertexId = 0; curVertexId < currentPointList.size(); curVertexId++)
          {
            polygon->AddVertex(currentPointList[curVertexId]);
          }
          currentDataNode->SetPolygonExteriorRing(polygon);
        }
        else // line
        {
          LineType::Pointer currentLine = LineType::New();
          for (unsigned int curVertexId = 0; curVertexId < currentPointList.size(); curVertexId++)
          {
            currentLine->AddVertex(currentPointList[curVertexId]);
          }
          currentDataNode->SetNodeId("FEATURE_LINE");
          currentDataNode->SetNodeType(otb::FEATURE_LINE);
          currentDataNode->SetLine(currentLine);
        }

        // Add some fields (TODO : mapnik necessary fields for rendering )
        currentDataNode->SetFieldAsString("Key", elementPair.first);
        currentDataNode->SetFieldAsString("type", elementPair.second);
        currentDataNode->SetFieldAsInt(m_ClassKey, m_ClassKeyValue);

        // Add the to vectordata
        m_OutputVectorData->GetDataTree()->Add(currentDataNode, folder);
      }
    }
  }
}

const OSMDataToVectorDataGenerator::VectorDataType* OSMDataToVectorDataGenerator::GetVectorDataByName(const std::string& key)
{
  if (key.empty())
    itkExceptionMacro(<< "Requested key is empty, please set a valid one");

  // First thing to do is to add the key set as parameters in the
  // KeyList
  if (!this->IsKeyPresent(key))
  {
    // TODO leave this functionality for AddKey ?
    m_KeyList.push_back(key);
    this->ParseXmlFile();
  }

  // Build the output
  this->ProcessVectorData(key, "");

  // Get the output
  return m_OutputVectorData;
}

const OSMDataToVectorDataGenerator::VectorDataType* OSMDataToVectorDataGenerator::GetVectorDataByName(const std::string& key, const std::string& value)
{
  if (key.empty())
    itkExceptionMacro(<< "Requested key is empty, please set a valid one");

  // Add the key set as parameters in the KeyList
  // TODO leave this functionality for AddKey ?
  if (!this->IsKeyPresent(key))
  {
    m_KeyList.push_back(key);
    this->ParseXmlFile();
  }

  // Build the output
  this->ProcessVectorData(key, value);

  // Get the output
  return m_OutputVectorData;
}


bool OSMDataToVectorDataGenerator::IsKeyPresent(const std::string& key)
{
  for (unsigned int i = 0; i < m_KeyList.size(); ++i)
    if (m_KeyList[i].compare(key) == 0)
      return true;

  // Otherwise return false;
  return false;
}
}
