#include "CircularGenerator.h"

#include "JsonHelper.h"
#include "QGCLoggingCategory.h"
#include "SettingsFact.h"

#define CLIPPER_SCALE 1000000
#include "RoutingThread.h"
#include "geometry/GenericCircle.h"
#include "geometry/MeasurementArea.h"
#include "geometry/SafeArea.h"
#include "geometry/clipper/clipper.hpp"
#include "nemo_interface/MeasurementTile.h"

QGC_LOGGING_CATEGORY(CircularGeneratorLog, "CircularGeneratorLog")

using namespace ClipperLib;
template <> inline auto get<0>(const IntPoint &p) { return p.X; }
template <> inline auto get<1>(const IntPoint &p) { return p.Y; }

namespace routing {

namespace {
GeneratorBase *creator(QObject *parent) {
  return new CircularGenerator(parent);
}

const char *distanceKey = "TransectDistance";
const char *deltaAlphaKey = "DeltaAlpha";
const char *minLengthKey = "MinLength";
const char *referenceKey = "ReferencePoint";
} // namespace

bool circularTransects(const geometry::FPoint &reference,
                       const geometry::FPolygon &polygon,
                       const std::vector<geometry::FPolygon> &tiles,
                       geometry::Length deltaR, geometry::Angle deltaAlpha,
                       geometry::Length minLength,
                       geometry::LineStringArray &transects);

const char *CircularGenerator::settingsGroup = "CircularGenerator";
const char *CircularGenerator::typeString = "CircularGenerator";

REGISTER_GENERATOR(CircularGenerator::typeString, creator)

CircularGenerator::CircularGenerator(QObject *parent)
    : GeneratorBase(parent),
      _metaDataMap(FactMetaData::createMapFromJsonFile(
          QStringLiteral(":/json/CircularGenerator.SettingsGroup.json"), this)),
      _distance(settingsGroup, _metaDataMap[distanceKey]),
      _deltaAlpha(settingsGroup, _metaDataMap[deltaAlphaKey]),
      _minLength(settingsGroup, _metaDataMap[minLengthKey]),
      _measurementArea(nullptr) {
  init();
}

CircularGenerator::CircularGenerator(GeneratorBase::Data d, QObject *parent)
    : GeneratorBase(d, parent),
      _metaDataMap(FactMetaData::createMapFromJsonFile(
          QStringLiteral(":/json/CircularGenerator.SettingsGroup.json"), this)),
      _distance(settingsGroup, _metaDataMap[distanceKey]),
      _deltaAlpha(settingsGroup, _metaDataMap[deltaAlphaKey]),
      _minLength(settingsGroup, _metaDataMap[minLengthKey]),
      _measurementArea(nullptr) {
  init();
}

QString CircularGenerator::editorQml() const {
  return QStringLiteral("CircularGeneratorEditor.qml");
}

QString CircularGenerator::mapVisualQml() const {
  return QStringLiteral("CircularGeneratorMapVisual.qml");
}

QString CircularGenerator::abbreviation() const { return tr("C. Gen."); }

QString CircularGenerator::type() const { return typeString; }

bool CircularGenerator::get(Work &work) {
  if (this->_d) {
    if (this->_d->isCorrect()) {
      // Prepare data.
      auto origin = this->_d->origin();
      origin.setAltitude(0);
      if (!origin.isValid()) {
        qCDebug(CircularGeneratorLog) << "get(): origin invalid." << origin;
        return false;
      }

      auto ref = this->_reference;
      ref.setAltitude(0);
      if (!ref.isValid()) {
        qCDebug(CircularGeneratorLog) << "get(): reference invalid." << ref;
        return false;
      }
      geometry::FPoint reference;
      geometry::toENU(origin, ref, reference);

      auto measurementArea =
          getGeoArea<const MeasurementArea *>(*this->_d->areaList());
      if (measurementArea == nullptr) {
        qCDebug(CircularGeneratorLog) << "get(): measurement area == nullptr";
        return false;
      }
      auto geoPolygon = measurementArea->coordinateList();
      for (auto &v : geoPolygon) {
        if (v.isValid()) {
          v.setAltitude(0);
        } else {
          qCDebug(CircularGeneratorLog) << "get(): measurement area invalid.";
          for (const auto &w : geoPolygon) {
            qCDebug(CircularGeneratorLog) << w;
          }
          return false;
        }
      }
      auto pPolygon = std::make_shared<geometry::FPolygon>();
      geometry::areaToEnu(origin, geoPolygon, *pPolygon);

      // Collect tiles with progress == 100 %.
      const auto *tiles = measurementArea->tiles();
      auto pTiles = std::make_shared<std::vector<geometry::FPolygon>>();
      for (int i = 0; i < tiles->count(); ++i) {
        const auto tile =
            qobject_cast<const MeasurementTile *>(tiles->operator[](i));
        if (tile != nullptr) {
          if (qFuzzyCompare(tile->progress(), 100)) {
            geometry::FPolygon tileENU;
            geometry::areaToEnu(origin, tile->coordinateList(), tileENU);
            pTiles->push_back(std::move(tileENU));
          }
        } else {
          qCDebug(CircularGeneratorLog) << "get(): tile == nullptr.";
          return false;
        }
      }

      auto serviceArea = getGeoArea<const SafeArea *>(*this->_d->areaList());
      if (measurementArea == nullptr) {
        qCDebug(CircularGeneratorLog) << "get(): measurement area == nullptr";
        return false;
      }
      auto geoDepot = serviceArea->depot();
      if (!geoDepot.isValid()) {
        qCDebug(CircularGeneratorLog) << "get(): depot invalid." << geoDepot;
        return false;
      }
      geometry::FPoint depot;
      geometry::toENU(origin, geoDepot, depot);

      // Fetch transect parameter.
      auto distance = geometry::Length(this->_distance.rawValue().toDouble() *
                                       bu::si::meter);
      auto minLength = geometry::Length(this->_minLength.rawValue().toDouble() *
                                        bu::si::meter);
      auto alpha = geometry::Angle(this->_deltaAlpha.rawValue().toDouble() *
                                   bu::degree::degree);

      work = [reference, depot, pPolygon, pTiles, distance, alpha,
              minLength](geometry::LineStringArray &transects) -> bool {
        bool value = circularTransects(reference, *pPolygon, *pTiles, distance,
                                       alpha, minLength, transects);
        transects.insert(transects.begin(), geometry::FLineString{depot});
        return value;
      };
      return true;
    } else {
      qCDebug(CircularGeneratorLog) << "get(): data invalid.";
      return false;
    }
  } else {
    qCDebug(CircularGeneratorLog) << "get(): data member not set.";
    return false;
  }
}

QGeoCoordinate CircularGenerator::reference() const { return _reference; }

void CircularGenerator::setReference(const QGeoCoordinate &reference) {
  if (_reference != reference) {
    _reference = reference;
    emit referenceChanged();
  }
}

bool CircularGenerator::save(QJsonObject &obj) const {
  QJsonObject temp;

  GeneratorBase::save(temp);

  bool ok = false;
  auto variant = _distance.rawValue();
  auto val = variant.toDouble(&ok);
  if (!ok) {
    qCDebug(CircularGeneratorLog)
        << "save(): not able to save distance. Not a double: "
        << variant.typeName();
    return false;
  } else {
    temp[distanceKey] = val;
  }

  variant = _deltaAlpha.rawValue();
  val = variant.toDouble(&ok);
  if (!ok) {
    qCDebug(CircularGeneratorLog)
        << "save(): not able to save deltaAlpha. Not a double: "
        << variant.typeName();
    return false;
  } else {
    temp[deltaAlphaKey] = val;
  }

  variant = _minLength.rawValue();
  val = variant.toDouble(&ok);
  if (!ok) {
    qCDebug(CircularGeneratorLog)
        << "save(): not able to save minLength. Not a double: "
        << variant.typeName();
    return false;
  } else {
    temp[minLengthKey] = val;
  }

  QJsonValue jsonReference;
  JsonHelper::saveGeoCoordinate(_reference, false, jsonReference);
  temp[referenceKey] = jsonReference;

  obj = std::move(temp);
  return true;
}

bool CircularGenerator::load(const QJsonObject &obj, QString &errorString) {
  bool returnValue = true;

  {
    QString e;
    if (!GeneratorBase::load(obj, e)) {
      returnValue = false;
      errorString.append(e);
    }
  }

  // load distance
  {
    QString e;
    QList<JsonHelper::KeyValidateInfo> keyInfo = {
        {distanceKey, QJsonValue::Double, true},
    };
    if (JsonHelper::validateKeys(obj, keyInfo, e)) {
      _distance.setRawValue(obj[distanceKey]);
    } else {
      returnValue = false;
      errorString.append(e);
      errorString.append("\n");
    }
  }

  // load deltaAlpha
  {
    QString e;
    QList<JsonHelper::KeyValidateInfo> keyInfo = {
        {deltaAlphaKey, QJsonValue::Double, true},
    };
    if (JsonHelper::validateKeys(obj, keyInfo, e)) {
      _deltaAlpha.setRawValue(obj[deltaAlphaKey]);
    } else {
      returnValue = false;
      errorString.append(e);
      errorString.append("\n");
    }
  }

  // load distance
  {
    QString e;
    QList<JsonHelper::KeyValidateInfo> keyInfo = {
        {minLengthKey, QJsonValue::Double, true},
    };
    if (JsonHelper::validateKeys(obj, keyInfo, e)) {
      _minLength.setRawValue(obj[minLengthKey]);
    } else {
      returnValue = false;
      errorString.append(e);
      errorString.append("\n");
    }
  }

  // load reference
  {
    QString e;
    QList<JsonHelper::KeyValidateInfo> keyInfo = {
        {referenceKey, QJsonValue::Array, true},
    };
    if (JsonHelper::validateKeys(obj, keyInfo, e)) {
      QGeoCoordinate ref;
      if (JsonHelper::loadGeoCoordinate(obj[referenceKey], false, ref, e)) {
        setReference(ref);
      } else {
        returnValue = false;
        errorString.append(e);
        errorString.append("\n");
      }
    } else {
      returnValue = false;
      errorString.append(e);
      errorString.append("\n");
    }
  }

  return returnValue;
}
void CircularGenerator::resetReferenceIfInvalid() {
  if (!this->_reference.isValid()) {
    resetReference();
  }
}

void CircularGenerator::resetReference() {
  if (this->_d != nullptr) {
    auto measurementArea =
        getGeoArea<const MeasurementArea *>(*this->_d->areaList());

    if (measurementArea != nullptr) {
      if (measurementArea->center().isValid()) {
        setReference(measurementArea->center());
      } else {
        qCWarning(CircularGeneratorLog)
            << "measurement area center" << measurementArea->center();
      }
    } else {
      qCDebug(CircularGeneratorLog)
          << "resetReference(): measurement area == nullptr";
    }
  }
}

Fact *CircularGenerator::distance() { return &_distance; }

Fact *CircularGenerator::deltaAlpha() { return &_deltaAlpha; }

Fact *CircularGenerator::minLength() { return &_minLength; }

void CircularGenerator::onAreaListChanged() {
  if (this->_d != nullptr) {
    auto *measurementArea =
        getGeoArea<MeasurementArea *>(*this->_d->areaList());
    setMeasurementArea(measurementArea);
  }
}

void CircularGenerator::onDataChanged() {
  if (this->_d != nullptr) {
    connect(this->_d, &AreaData::areaListChanged, this,
            &CircularGenerator::onAreaListChanged);
    onAreaListChanged();
  }
}

void CircularGenerator::init() {
  connect(this->distance(), &Fact::rawValueChanged, this,
          &GeneratorBase::generatorChanged);
  connect(this->deltaAlpha(), &Fact::rawValueChanged, this,
          &GeneratorBase::generatorChanged);
  connect(this->minLength(), &Fact::rawValueChanged, this,
          &GeneratorBase::generatorChanged);
  connect(this, &CircularGenerator::referenceChanged, this,
          &GeneratorBase::generatorChanged);
  connect(this, &CircularGenerator::dataChanged, this,
          &CircularGenerator::onDataChanged);
  onDataChanged();
  setName(tr("Circular Generator"));
}

void CircularGenerator::setMeasurementArea(MeasurementArea *area) {
  if (_measurementArea != area) {

    if (_measurementArea != nullptr) {
      disconnect(_measurementArea, &MeasurementArea::progressChanged, this,
                 &GeneratorBase::generatorChanged);
      disconnect(_measurementArea, &MeasurementArea::tilesChanged, this,
                 &GeneratorBase::generatorChanged);
      disconnect(_measurementArea, &MeasurementArea::centerChanged, this,
                 &CircularGenerator::resetReferenceIfInvalid);
      disconnect(_measurementArea, &MeasurementArea::pathChanged, this,
                 &GeneratorBase::generatorChanged);
    }

    _measurementArea = area;

    if (_measurementArea != nullptr) {
      connect(_measurementArea, &MeasurementArea::progressChanged, this,
              &GeneratorBase::generatorChanged);
      connect(_measurementArea, &MeasurementArea::tilesChanged, this,
              &GeneratorBase::generatorChanged);
      connect(_measurementArea, &MeasurementArea::centerChanged, this,
              &CircularGenerator::resetReferenceIfInvalid);
      connect(_measurementArea, &MeasurementArea::pathChanged, this,
              &GeneratorBase::generatorChanged);
      resetReferenceIfInvalid();
    }

    emit generatorChanged();
  }
}

bool circularTransects(const geometry::FPoint &reference,
                       const geometry::FPolygon &polygon,
                       const std::vector<geometry::FPolygon> &tiles,
                       geometry::Length deltaR, geometry::Angle deltaAlpha,
                       geometry::Length minLength,
                       geometry::LineStringArray &transects) {
  auto s1 = std::chrono::high_resolution_clock::now();

  // Check preconitions
  if (polygon.outer().size() >= 3) {
    using namespace boost::units;
    // Convert geo polygon to ENU polygon.
    std::string error;
    // Check validity.
    if (!bg::is_valid(polygon, error)) {
      qCDebug(CircularGeneratorLog) << "circularTransects(): "
                                       "invalid polygon.";
      qCDebug(CircularGeneratorLog) << error.c_str();
      std::stringstream ss;
      ss << bg::wkt(polygon);
      qCDebug(CircularGeneratorLog) << ss.str().c_str();
    } else {
      // Calculate polygon distances and angles.
      std::vector<geometry::Length> distances;
      distances.reserve(polygon.outer().size());
      std::vector<geometry::Angle> angles;
      angles.reserve(polygon.outer().size());
      //      qCDebug(CircularGeneratorLog) << "circularTransects():";
      for (const auto &p : polygon.outer()) {
        geometry::Length distance = bg::distance(reference, p) * si::meter;
        distances.push_back(distance);
        geometry::Angle alpha =
            (std::atan2(p.get<1>(), p.get<0>())) * si::radian;
        alpha = alpha < 0 * si::radian ? alpha + 2 * M_PI * si::radian : alpha;
        angles.push_back(alpha);
        //        qCDebug(CircularGeneratorLog) << "distances, angles,
        //        coordinates:"; qCDebug(CircularGeneratorLog) <<
        //        to_string(distance).c_str(); qCDebug(CircularGeneratorLog)
        //        << to_string(snake::Degree(alpha)).c_str();
        //        qCDebug(CircularGeneratorLog) << "x = " << p.get<0>() << "y
        //        = "
        //        << p.get<1>();
      }

      auto rMin = deltaR; // minimal circle radius
      geometry::Angle alpha1(0 * degree::degree);
      geometry::Angle alpha2(360 * degree::degree);
      // Determine r_min by successive approximation
      if (!bg::within(reference, polygon.outer())) {
        rMin = bg::distance(reference, polygon) * si::meter;
      }

      auto rMax = (*std::max_element(distances.begin(),
                                     distances.end())); // maximal circle radius

      // Scale parameters and coordinates.
      const auto rMinScaled =
          ClipperLib::cInt(std::round(rMin.value() * CLIPPER_SCALE));
      const auto deltaRScaled =
          ClipperLib::cInt(std::round(deltaR.value() * CLIPPER_SCALE));
      auto referenceScaled = ClipperLib::IntPoint{
          ClipperLib::cInt(std::round(reference.get<0>() * CLIPPER_SCALE)),
          ClipperLib::cInt(std::round(reference.get<1>() * CLIPPER_SCALE))};

      // Generate circle sectors.
      auto rScaled = rMinScaled;
      const auto nTran = long(std::ceil(((rMax - rMin) / deltaR).value()));
      std::vector<ClipperLib::Path> sectors(nTran, ClipperLib::Path());
      const auto nSectors =
          long(std::round(((alpha2 - alpha1) / deltaAlpha).value()));
      //      qCDebug(CircularGeneratorLog) << "circularTransects(): sector
      //      parameres:"; qCDebug(CircularGeneratorLog) << "alpha1: " <<
      //      to_string(snake::Degree(alpha1)).c_str();
      //      qCDebug(CircularGeneratorLog) << "alpha2:
      //      "
      //      << to_string(snake::Degree(alpha2)).c_str();
      //      qCDebug(CircularGeneratorLog) << "n: "
      //      << to_string((alpha2 - alpha1) / deltaAlpha).c_str();
      //      qCDebug(CircularGeneratorLog)
      //      << "nSectors: " << nSectors; qCDebug(CircularGeneratorLog) <<
      //      "rMin: " << to_string(rMin).c_str();
      //      qCDebug(CircularGeneratorLog)
      //      << "rMax: " << to_string(rMax).c_str();
      //      qCDebug(CircularGeneratorLog) << "nTran: " << nTran;
      using ClipperCircle =
          GenericCircle<ClipperLib::cInt, ClipperLib::IntPoint>;
      for (auto &sector : sectors) {
        ClipperCircle circle(rScaled, referenceScaled);
        approximate(circle, nSectors, sector);
        rScaled += deltaRScaled;
      }
      // Clip sectors to polygonENU.
      ClipperLib::Path polygonClipper;
      geometry::FPolygon shrinked;
      geometry::offsetPolygon(polygon, shrinked, -0.3);
      auto &outer = shrinked.outer();
      polygonClipper.reserve(outer.size());
      for (auto it = outer.begin(); it < outer.end() - 1; ++it) {
        auto x = ClipperLib::cInt(std::round(it->get<0>() * CLIPPER_SCALE));
        auto y = ClipperLib::cInt(std::round(it->get<1>() * CLIPPER_SCALE));
        polygonClipper.push_back(ClipperLib::IntPoint{x, y});
      }
      ClipperLib::Clipper clipper;
      clipper.AddPath(polygonClipper, ClipperLib::ptClip, true);
      clipper.AddPaths(sectors, ClipperLib::ptSubject, false);
      ClipperLib::PolyTree transectsClipper;
      clipper.Execute(ClipperLib::ctIntersection, transectsClipper,
                      ClipperLib::pftNonZero, ClipperLib::pftNonZero);

      // Subtract holes.
      if (tiles.size() > 0) {
        std::vector<ClipperLib::Path> processedTiles;
        for (const auto &tile : tiles) {
          ClipperLib::Path path;
          for (const auto &v : tile.outer()) {
            path.push_back(ClipperLib::IntPoint{
                static_cast<ClipperLib::cInt>(v.get<0>() * CLIPPER_SCALE),
                static_cast<ClipperLib::cInt>(v.get<1>() * CLIPPER_SCALE)});
          }
          processedTiles.push_back(std::move(path));
        }

        clipper.Clear();
        for (const auto &child : transectsClipper.Childs) {
          clipper.AddPath(child->Contour, ClipperLib::ptSubject, false);
        }
        clipper.AddPaths(processedTiles, ClipperLib::ptClip, true);
        transectsClipper.Clear();
        clipper.Execute(ClipperLib::ctDifference, transectsClipper,
                        ClipperLib::pftNonZero, ClipperLib::pftNonZero);
      }

      // Extract transects from  PolyTree and convert them to
      // BoostLineString
      for (const auto &child : transectsClipper.Childs) {
        geometry::FLineString transect;
        transect.reserve(child->Contour.size());
        for (const auto &vertex : child->Contour) {
          auto x = static_cast<double>(vertex.X) / CLIPPER_SCALE;
          auto y = static_cast<double>(vertex.Y) / CLIPPER_SCALE;
          transect.push_back(geometry::FPoint(x, y));
        }
        transects.push_back(transect);
      }

      // Join sectors which where slit due to clipping.
      const double th = 0.01;
      for (auto ito = transects.begin(); ito < transects.end(); ++ito) {
        for (auto iti = ito + 1; iti < transects.end(); ++iti) {
          auto dist1 = bg::distance(ito->front(), iti->front());
          if (dist1 < th) {
            geometry::FLineString temp;
            for (auto it = iti->end() - 1; it >= iti->begin(); --it) {
              temp.push_back(*it);
            }
            temp.insert(temp.end(), ito->begin(), ito->end());
            *ito = temp;
            transects.erase(iti);
            break;
          }
          auto dist2 = bg::distance(ito->front(), iti->back());
          if (dist2 < th) {
            geometry::FLineString temp;
            temp.insert(temp.end(), iti->begin(), iti->end());
            temp.insert(temp.end(), ito->begin(), ito->end());
            *ito = temp;
            transects.erase(iti);
            break;
          }
          auto dist3 = bg::distance(ito->back(), iti->front());
          if (dist3 < th) {
            geometry::FLineString temp;
            temp.insert(temp.end(), ito->begin(), ito->end());
            temp.insert(temp.end(), iti->begin(), iti->end());
            *ito = temp;
            transects.erase(iti);
            break;
          }
          auto dist4 = bg::distance(ito->back(), iti->back());
          if (dist4 < th) {
            geometry::FLineString temp;
            temp.insert(temp.end(), ito->begin(), ito->end());
            for (auto it = iti->end() - 1; it >= iti->begin(); --it) {
              temp.push_back(*it);
            }
            *ito = temp;
            transects.erase(iti);
            break;
          }
        }
      }

      // Remove short transects
      for (auto it = transects.begin(); it < transects.end();) {
        if (bg::length(*it) < minLength.value()) {
          it = transects.erase(it);
        } else {
          ++it;
        }
      }

      qCDebug(CircularGeneratorLog)
          << "circularTransects(): transect gen. time: "
          << std::chrono::duration_cast<std::chrono::milliseconds>(
                 std::chrono::high_resolution_clock::now() - s1)
                 .count()
          << " ms";
      return true;
    }
  }
  return false;
}

} // namespace routing
