/**
  * @RDE_HEADER@
  * ViewshedGeoProcessing
  * $Id$
  * $HeadURL$
  * $Author$
  */

// Own
#include "viewshed_geo_processing.h"

#include "map_impl.h"
#include <GraphicalObjects/graphical_object_path.h>
#include <Map/situation_map_utils.h>

#include <MapImpl/layer_factory_impl.h>


// ESRI
#include <arcgis/Map.h>
#include <arcgis/GraphicsOverlay.h>
#include <arcgis/Graphic.h>
#include <arcgis/GeoprocessingTask.h>
#include <arcgis/GeoprocessingJob.h>
#include <arcgis/GeoprocessingFeatures.h>
#include <arcgis/GeoprocessingRaster.h>
#include <arcgis/GeoprocessingParameter.h>
#include <arcgis/GeoprocessingParameters.h>
#include <arcgis/GeoprocessingResult.h>
#include <arcgis/FeatureCollectionTable.h>
#include <arcgis/Feature.h>
#include <arcgis/SimpleMarkerSymbol.h>
#include <arcgis/SimpleFillSymbol.h>

#include <arcgis/GeoprocessingLinearUnit.h>
#include <arcgis/ImageServiceRaster.h>
#include <arcgis/RasterLayer.h>



// Qt
#include <QDebug>


namespace SituationMap
{

//-----------------------------------------------------------------------------
// class ViewshedBase
//-----------------------------------------------------------------------------
ViewshedGeoProcessing::ViewshedGeoProcessing(const QString &url,
                                             Esri::ArcGISRuntime::GraphicsOverlay *graphicsLayer,
                                             const Esri::ArcGISRuntime::SpatialReference &spatialReference,
                                             QObject *parent)
  : ViewshedBase(parent)
  , m_url(url)
  , m_resultsOverlay(graphicsLayer)
  , m_srMap(spatialReference)
  , m_logger(getLogger("ViewshedGeoProcessing"))
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START";
  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}

ViewshedGeoProcessing::~ViewshedGeoProcessing()
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START";
  clearGraphics();
  delete m_viewshedTask;
  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}

bool ViewshedGeoProcessing::init(SituationMap::MapImpl *mapImpl)
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START";

  if (m_url.isEmpty())
  {
    ERROR_LOG(m_logger) << LOGFUNCTION << " url is empty";
    TRACE_LOG(m_logger) << LOGFUNCTION << " END";
    return false;
  }


  m_mapImpl = mapImpl;

  delete m_viewshedTask;
  m_viewshedTask = new Esri::ArcGISRuntime::GeoprocessingTask(QUrl(m_url), this);
  m_viewshedTask->setObjectName(QLatin1Literal("GeoprocessingTask"));

  // Handle errors of GeoprocessingTask:
  connect(m_viewshedTask, &Esri::ArcGISRuntime::GeoprocessingTask::errorOccurred,
          this, &ViewshedGeoProcessing::onErrorOccuredGeoprocessingTask);

  m_initialized = true;

  TRACE_LOG(m_logger) << LOGFUNCTION << " END" << " m_initialized=" << m_initialized;
  return m_initialized;
}


bool ViewshedGeoProcessing::execute(const QPointF &viewshedPoint_map, const double distance, double zOffset)
{
  NOTICE_LOG(m_logger) << LOGFUNCTION << " START"
                      << " viewshedPoint_map=" << viewshedPoint_map
                      << " m_srMap.wkid=" << m_srMap.wkid()
                      << " distance=" << distance
                      << " zOffset=" << zOffset
                      << " m_viewshedInProgress=" << m_viewshedInProgress
                      << " url='" << m_url << "'";

  // check point is the viewshed point
  Esri::ArcGISRuntime::Point esriViewshedPoint(viewshedPoint_map.x(), viewshedPoint_map.y(), m_srMap);   // Esri::ArcGISRuntime::SpatialReference::webMercator());

  DEBUG_LOG(m_logger) << LOGFUNCTION << " esriViewshedPoint.toJson: " << esriViewshedPoint.toJson();

  m_viewshedInProgress = true;

  createInputGraphic(esriViewshedPoint);
  clearResultGraphics();

  // Create a new feature collection table based upon point geometries using the current map view spatial reference
  Esri::ArcGISRuntime::FeatureCollectionTable *inputFeatures =
    new Esri::ArcGISRuntime::FeatureCollectionTable(QList<Esri::ArcGISRuntime::Field>()
                                                    , Esri::ArcGISRuntime::GeometryType::Point
                                                    , m_srMap
                                                    , this);

  // Create a new feature from the feature collection table. It will not have a coordinate location (x,y) yet
  Esri::ArcGISRuntime::Feature *inputFeature = inputFeatures->createFeature(this);

  // Assign a physical location to the new point feature based upon where the user clicked on the map view
//  inputFeature->setGeometry(m_inputOverlay->graphics()->at(0)->geometry());
  inputFeature->setGeometry(esriViewshedPoint);

  // connect to addFeature status changed signal
  connect(inputFeatures, &Esri::ArcGISRuntime::FeatureCollectionTable::addFeatureCompleted, this, [this, inputFeatures, distance, zOffset](QUuid, bool success)
  {
    INFO_LOG(m_logger) << LOGFUNCTION << " addFeatureCompleted...";

    if (!success)
    {
      ERROR_LOG(m_logger) << LOGFUNCTION << " no success";
      return;
    }

    // Create the parameters that are passed to the used geoprocessing task
    Esri::ArcGISRuntime::GeoprocessingParameters viewshedParameters =
      Esri::ArcGISRuntime::GeoprocessingParameters(Esri::ArcGISRuntime::GeoprocessingExecutionType::SynchronousExecute); // AsynchronousSubmit | SynchronousExecute
    // GeoprocessingExecutionType should match Esri::ArcGISRuntime::GeoprocessingServiceType of LocalGeoprocessingService


    // Request the output features to use the same SpatialReference as the map view
    viewshedParameters.setOutputSpatialReference(m_srMap);

    // Add an input location to the geoprocessing parameters
    QMap<QString, Esri::ArcGISRuntime::GeoprocessingParameter *> inputs;
    inputs[QLatin1Literal("Input_point_or_polyline_observer_features")] = new Esri::ArcGISRuntime::GeoprocessingFeatures(inputFeatures, this); //QStringLiteral("ViewshedGeoProcessing")

    // Radius
    inputs[QLatin1Literal("Inner_radius")]        = new Esri::ArcGISRuntime::GeoprocessingLinearUnit(100.0, Esri::ArcGISRuntime::GeoprocessingLinearUnits::Meter, this);
    inputs[QLatin1Literal("Outer_radius")]        = new Esri::ArcGISRuntime::GeoprocessingLinearUnit(distance, Esri::ArcGISRuntime::GeoprocessingLinearUnits::Meter, this);

    // Observer
// //!!!    inputs[QLatin1Literal("Observer_elevation")]  = new Esri::ArcGISRuntime::GeoprocessingLinearUnit(0.0, Esri::ArcGISRuntime::GeoprocessingLinearUnits::Meter, this);
    inputs[QLatin1Literal("Observer_offset")]     = new Esri::ArcGISRuntime::GeoprocessingLinearUnit(zOffset, Esri::ArcGISRuntime::GeoprocessingLinearUnits::Meter, this);

    // Target
    inputs[QLatin1Literal("Surface_offset")]      = new Esri::ArcGISRuntime::GeoprocessingLinearUnit(0.0, Esri::ArcGISRuntime::GeoprocessingLinearUnits::Meter, this);

    // setInputs
    viewshedParameters.setInputs(inputs);

    // Print geoprocessingParameters for debugging purpose:
    QMapIterator<QString, Esri::ArcGISRuntime::GeoprocessingParameter *> inputsIt(inputs);
    while (inputsIt.hasNext())
    {
      inputsIt.next();
      QString value(QLatin1Literal("-"));
      Esri::ArcGISRuntime::GeoprocessingParameter *gpPara = inputsIt.value();
      Esri::ArcGISRuntime::GeoprocessingLinearUnit *gpParaLU = dynamic_cast<Esri::ArcGISRuntime::GeoprocessingLinearUnit *>(gpPara);
      if (gpParaLU)
        value = QString(QLatin1Literal("%1 %2")).arg(gpParaLU->distance()).arg(gpParaLU->linearUnit().name());
      NOTICE_LOG(m_logger) << LOGFUNCTION << " key= '" << inputsIt.key() << "'" << " value= '" << value << "'";
    } // while



    // Create the job that handles the communication between the application and the geoprocessing task
    m_viewshedJob = m_viewshedTask->createJob(viewshedParameters);
    m_viewshedJob->setObjectName(QLatin1Literal("GeoprocessingJob"));

    // Handle errors of GeoprocessingJob:
    connect(m_viewshedJob, &Esri::ArcGISRuntime::GeoprocessingJob::errorOccurred, this, &ViewshedGeoProcessing::onErrorOccuredGeoprocessingJob);

    // Create signal handler for the job
    connect(m_viewshedJob, &Esri::ArcGISRuntime::GeoprocessingJob::jobStatusChanged, this, &ViewshedGeoProcessing::onJobStatusChanged);

    // start the job
    m_viewshedJob->start();
  });
  // Add the new feature with (x,y) location to the feature collection table
  inputFeatures->addFeature(inputFeature);

  TRACE_LOG(m_logger) << LOGFUNCTION << " END" << " m_viewshedInProgress=" << m_viewshedInProgress;
  return true;
}

void ViewshedGeoProcessing::onJobStatusChanged()
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START";

  //      qCritical() << "jobStatusChanged... viewshedJob->jobStatus()=" << (int)viewshedJob->jobStatus();

  switch (m_viewshedJob->jobStatus())
  {
  case Esri::ArcGISRuntime::JobStatus::NotStarted:
    qCritical() << "Geoprocessing Task failed";
    m_viewshedInProgress = false;
    m_jobStatus = QLatin1Literal("Job NotStarted");
    break;
  case Esri::ArcGISRuntime::JobStatus::Failed:
  {
    qCritical() << "Geoprocessing Task failed";

    const Esri::ArcGISRuntime::Error &err = m_viewshedJob->error();
    qCritical() << "Geoprocessing Task failed message: "
                << " code = " << err.code()
                << " domain = " << (int)err.domain()
                << " message = " << err.message()
                << " additionalMessage = " << err.additionalMessage()
                << " extendedErrorType = " << (int)err.extendedErrorType();

    m_viewshedInProgress = false;
    m_jobStatus = QLatin1Literal("Job failed");
  }
    break;
  case Esri::ArcGISRuntime::JobStatus::Started:
    m_viewshedInProgress = true;
    m_jobStatus = QLatin1Literal("Job in progress...");
    break;
  case Esri::ArcGISRuntime::JobStatus::Paused:
    m_viewshedInProgress = false;
    m_jobStatus = QLatin1Literal("Job paused...");
    break;
  case Esri::ArcGISRuntime::JobStatus::Succeeded:
    m_viewshedInProgress = false;
    m_jobStatus = QLatin1Literal("Job succeeded");
    // handle the results
    processResults(m_viewshedJob->result());
    break;
  default:
    break;
  }

  NOTICE_LOG(m_logger) << LOGFUNCTION << " jobStatusChanged is now (m_jobStatus): " << m_jobStatus;


  // emit signals
  //      emit viewshedInProgressChanged();
  //      emit jobStatusChanged();
  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}


bool ViewshedGeoProcessing::processResults(Esri::ArcGISRuntime::GeoprocessingResult *results)
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START";

  if (!results)
  {
    ERROR_LOG(m_logger) << LOGFUNCTION << " no results";
    return false;
  }

  const QMap<QString, Esri::ArcGISRuntime::GeoprocessingParameter *> &outputsMap = results->outputs();

  if (outputsMap.isEmpty())
  {
    ERROR_LOG(m_logger) << LOGFUNCTION << " no keys, outputsMap is empty ";
    return false;
  }

  // clear graphics:
  clearResultGraphics();


  QString key;

  key = QStringLiteral("Output_polygon_features");
  if (outputsMap.contains(key))
  {
    // Get the results from the outputs as GeoprocessingFeatures
    Esri::ArcGISRuntime::GeoprocessingFeatures *viewshedResultFeatures =
      static_cast<Esri::ArcGISRuntime::GeoprocessingFeatures *>(outputsMap[key]);

    return processFeatureResults(viewshedResultFeatures);
  }

  key = QStringLiteral("Output_raster");
  if (outputsMap.contains(key))
  {
    Esri::ArcGISRuntime::GeoprocessingRaster *viewshedResultRaster =
      static_cast<Esri::ArcGISRuntime::GeoprocessingRaster *>(outputsMap[key]);
    return processRasterResults(viewshedResultRaster);
  }

  key = QStringLiteral("Output_polygon_features");
  if (outputsMap.contains(key))
  {
    // Get the results from the outputs as GeoprocessingFeatures
    Esri::ArcGISRuntime::GeoprocessingFeatures *viewshedResultFeatures =
      static_cast<Esri::ArcGISRuntime::GeoprocessingFeatures *>(outputsMap[key]);

    return processFeatureResults(viewshedResultFeatures);
  }

  ERROR_LOG(m_logger) << LOGFUNCTION << " no adeqaute keyword found in outputsMap";
  return false;
}

bool ViewshedGeoProcessing::processFeatureResults(Esri::ArcGISRuntime::GeoprocessingFeatures *viewshedResultFeatures)
{
  NOTICE_LOG(m_logger) << LOGFUNCTION << " processFeatureResults";

  if (!viewshedResultFeatures)
  {
    ERROR_LOG(m_logger) << LOGFUNCTION << " no viewshedResultFeatures";
    return false;
  }

  Esri::ArcGISRuntime::SimpleFillSymbol sfsVisi  (Esri::ArcGISRuntime::SimpleFillSymbolStyle::Solid, QColor(0, 226, 0, 255), this);
  Esri::ArcGISRuntime::SimpleFillSymbol sfsInVisi(Esri::ArcGISRuntime::SimpleFillSymbolStyle::Solid, QColor(226, 0, 0, 255), this);

  // Add all the features from the result feature set as a graphics to the map
  Esri::ArcGISRuntime::FeatureIterator features = viewshedResultFeatures->features()->iterator();
  while (features.hasNext())
  {
    Esri::ArcGISRuntime::Feature *feat = features.next(this);

    const QString &attKey(QLatin1String("gridcode"));
    if (feat->attributes()->containsAttribute(attKey))
    {
      DEBUG_LOG(m_logger) << LOGFUNCTION << " attributes()->attributeValue=== " << feat->attributes()->attributeValue(attKey).toString();

      if (feat->attributes()->attributeValue(attKey).toInt())
        m_resultGraphics.append(new Esri::ArcGISRuntime::Graphic(feat->geometry(), &sfsVisi));
      else
        m_resultGraphics.append(new Esri::ArcGISRuntime::Graphic(feat->geometry(), &sfsInVisi));

      // ERROR_LOG(m_logger) << LOGFUNCTION << " attributeValue " << feat->attributes()->attributeValue(attKey).toInt();
    } else
      ERROR_LOG(m_logger) << LOGFUNCTION << " no key attributeValue ";

  }

  // transfer from own container to resultsOverlay:
  m_resultsOverlay->graphics()->append(m_resultGraphics);

  return true;
}

bool ViewshedGeoProcessing::processRasterResults(Esri::ArcGISRuntime::GeoprocessingRaster *viewshedResultRaster)
{
  NOTICE_LOG(m_logger) << LOGFUNCTION << " viewshedResultRaster -> url = " << viewshedResultRaster->url().toString();

  connect(viewshedResultRaster, &Esri::ArcGISRuntime::GeoprocessingRaster::fetchFileCompleted, this, [this] (QUuid taskId, const QString& filePath) {
    NOTICE_LOG(m_logger) << LOGFUNCTION << " filePath=" << filePath;

    Esri::ArcGISRuntime::Raster *raster = new Esri::ArcGISRuntime::Raster(filePath, this);
    if (!raster)
    {
      ERROR_LOG(m_logger) << LOGFUNCTION << " raster failed";
      return;
    }
    connect(raster, &Esri::ArcGISRuntime::Raster::errorOccurred, this, [this, raster] (const Esri::ArcGISRuntime::Error &error) {
      ERROR_LOG(m_logger) << LOGFUNCTION << " raster EsriError occured: "
                          << ", code="              << error.code()
                          << ", domain="            << (int)error.domain()
                          << ", extendedErrorType=" << (int)error.extendedErrorType()
                          << ", message='"         << error.message() << "'"
                          << ", additionalMessage='"         << error.additionalMessage() << "'";
    });
    connect(raster, &Esri::ArcGISRuntime::Raster::loadStatusChanged,  this, [this, raster] (Esri::ArcGISRuntime::LoadStatus loadStatus) {
      ERROR_LOG(m_logger) << LOGFUNCTION << " raster loadStatus="              << loadStatus;

      if (loadStatus == Esri::ArcGISRuntime::LoadStatus::Loaded)
      {


        Esri::ArcGISRuntime::RasterLayer *rl = new Esri::ArcGISRuntime::RasterLayer(raster, this);
        if (!rl)
        {
          ERROR_LOG(m_logger) << LOGFUNCTION << " rl failed";
          return;
        }
        connect(rl, &Esri::ArcGISRuntime::RasterLayer::errorOccurred, this, [this] (const Esri::ArcGISRuntime::Error &error) {
          ERROR_LOG(m_logger) << LOGFUNCTION << " RasterLayer EsriError occured: "
                              << ", code="              << error.code()
                              << ", domain="            << (int)error.domain()
                              << ", extendedErrorType=" << (int)error.extendedErrorType()
                              << ", message='"         << error.message() << "'"
                              << ", additionalMessage='"         << error.additionalMessage() << "'";
        });
        connect(rl, &Esri::ArcGISRuntime::RasterLayer::loadStatusChanged,  this, [this] (Esri::ArcGISRuntime::LoadStatus loadStatus) {
          ERROR_LOG(m_logger) << LOGFUNCTION << " RasterLayer loadStatus="              << loadStatus;
        });


        NOTICE_LOG(m_logger) << LOGFUNCTION << " getEsriMap()->operationalLayers()->size" << m_mapImpl->getEsriMap()->operationalLayers()->size();
        m_mapImpl->getEsriMap()->operationalLayers()->append(rl);
        NOTICE_LOG(m_logger) << LOGFUNCTION << " getEsriMap()->operationalLayers()->size" << m_mapImpl->getEsriMap()->operationalLayers()->size();


        NOTICE_LOG(m_logger) << LOGFUNCTION << " rasterLayer loading ...";
        rl->load();

      }


    });

    NOTICE_LOG(m_logger) << LOGFUNCTION << " raster loading ...";
    raster->load();

  });

  Esri::ArcGISRuntime::TaskWatcher tw = viewshedResultRaster->fetchFile(QString(QLatin1Literal("/tmp/viewshedFetched.tif")));

  if (!tw.isValid())
  {
    ERROR_LOG(m_logger) << LOGFUNCTION << " m_mapImpl failed";
    return false;
  }

  return true;
}


void ViewshedGeoProcessing::clearGraphics()
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START" << this;
  clearInputGraphics();
  clearResultGraphics();
  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}

void ViewshedGeoProcessing::createInputGraphic(const Esri::ArcGISRuntime::Point &origin)
{
  if (!m_originGraphic)
  {
    m_originGraphic = new Esri::ArcGISRuntime::Graphic(this);
    m_resultsOverlay->graphics()->append(m_originGraphic);
  }

  m_originGraphic->setGeometry(origin);
  Esri::ArcGISRuntime::SimpleMarkerSymbol sms(Esri::ArcGISRuntime::SimpleMarkerSymbolStyle::Circle, QColor("red"), 12.0);
  m_originGraphic->setSymbol(&sms);
  m_originGraphic->setZIndex(32000);
  m_resultsOverlay->graphics()->append(m_originGraphic);
}


void ViewshedGeoProcessing::clearInputGraphics()
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START" << this;
  if (m_originGraphic)
  {
    m_resultsOverlay->graphics()->removeOne(m_originGraphic);
    delete m_originGraphic;
    m_originGraphic = nullptr;
  }

  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}


void ViewshedGeoProcessing::clearResultGraphics()
{
  TRACE_LOG(m_logger) << LOGFUNCTION << " START" << this;

  for (auto graphic : qAsConst(m_resultGraphics))
  {
    m_resultsOverlay->graphics()->removeOne(graphic);
    delete graphic;
  }
  m_resultGraphics.clear();

  TRACE_LOG(m_logger) << LOGFUNCTION << " END";
}


void ViewshedGeoProcessing::onErrorOccuredGeoprocessingTask(const Esri::ArcGISRuntime::Error &error)
{
  QString senderName;
  Esri::ArcGISRuntime::GeoprocessingTask *senderObj = qobject_cast<Esri::ArcGISRuntime::GeoprocessingTask *>(sender());
  if (senderObj)
    senderName = senderObj->objectName();

  ERROR_LOG(m_logger) << LOGFUNCTION << " GeoprocessingTask -> EsriError occured: "
                      << " sender= '" << senderName << "'"
                      << ", code="              << error.code()
                      << ", domain="            << (int)error.domain()
                      << ", extendedErrorType=" << (int)error.extendedErrorType()
                      << ", message='"         << error.message() << "'"
                      << ", additionalMessage='"         << error.additionalMessage() << "'";
}

void ViewshedGeoProcessing::onErrorOccuredGeoprocessingJob(const Esri::ArcGISRuntime::Error &error)
{
  QString senderName;
  Esri::ArcGISRuntime::GeoprocessingJob *senderObj = qobject_cast<Esri::ArcGISRuntime::GeoprocessingJob *>(sender());
  if (senderObj)
    senderName = senderObj->objectName();

  ERROR_LOG(m_logger) << LOGFUNCTION << " GeoprocessingJob -> EsriError occured: "
                      << " sender= '" << senderName << "'"
                      << ", code="              << error.code()
                      << ", domain="            << (int)error.domain()
                      << ", extendedErrorType=" << (int)error.extendedErrorType()
                      << ", message='"         << error.message() << "'"
                      << ", additionalMessage='"         << error.additionalMessage() << "'";
}


} // namespace SituationMap
