/**********************************************************************************************
    Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
    Copyright (C) 2018 Norbert Truchsess <norbert.truchsess@t-online.de>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#include "gis/search/CGeoSearch.h"

#include <QMutexLocker>
#include <QtNetwork>
#include <QtWidgets>
#include <QtXml>

#include "canvas/CCanvas.h"
#include "gis/CGisListWks.h"
#include "gis/search/CGeoSearchConfigDialog.h"
#include "gis/wpt/CGisItemWpt.h"
#include "helpers/CWptIconManager.h"
#include "misc.h"
#include "version.h"

// the "accumulative results" icon is composed, so it cannot scale like the plain SVG one does.
// Build it large enough that every cell it lands in only ever downscales.
#define COMPOSED_SIZE 128

CGeoSearch::CGeoSearch(CGisListWks* parent)
    : IGisProject(eTypeGeoSearch, "", parent), searchConfig(&CGeoSearchConfig::self()) {
  setFlags(flags() | Qt::ItemIsEditable);
  networkAccessManager = new QNetworkAccessManager(this);

  parent->takeTopLevelItem(parent->indexOfTopLevelItem(this));
  parent->insertTopLevelItem(0, this);

  connect(networkAccessManager, &QNetworkAccessManager::finished, this, &CGeoSearch::slotRequestFinished);

  setIcon();
}

CGeoSearch::~CGeoSearch() {}

QPixmap CGeoSearch::getWptIcon() const {
  QPointF focus;
  return CWptIconManager::self().getWptIconByName(searchConfig->symbolName, focus);
}

QString CGeoSearch::getServiceName() const {
  switch (searchConfig->currentService) {
    case CGeoSearchConfig::service_e::eServiceGeonamesSearch:
      return tr("Geonames Places");
    case CGeoSearchConfig::service_e::eServiceGeonamesAddress:
      return tr("Geonames Address");
    case CGeoSearchConfig::service_e::eServiceNominatim:
      return tr("OSM Nominatim");
    case CGeoSearchConfig::service_e::eServiceGoogle:
      return tr("Google");
  }
  return "";
}

void CGeoSearch::setIcon() {
  if (searchConfig->accumulativeResults) {
    // A composition is a raster whatever its sources are, so it is built once at COMPOSED_SIZE --
    // well above any cell it lands in -- and downscaled from there. The proportions are the
    // original 48px ones scaled up.
    const int badge = COMPOSED_SIZE * 26 / 48;
    const int offset = COMPOSED_SIZE * 22 / 48;

    QPixmap displayIcon(COMPOSED_SIZE, COMPOSED_SIZE);
    displayIcon.fill(Qt::transparent);

    QPainter painter(&displayIcon);
    searchConfig->getCurrentIcon().paint(&painter, QRect(0, 0, COMPOSED_SIZE, COMPOSED_SIZE));
    QIcon("://icons/AddGreen.svgt").paint(&painter, QRect(offset, offset, badge, badge));
    icon = QIcon(displayIcon);
  } else {
    icon = searchConfig->getCurrentIcon();
  }
}

void CGeoSearch::changeSymbol() {
  QString iconName = CWptIconManager::self().selectWptIcon(treeWidget());
  if (!iconName.isEmpty()) {
    searchConfig->symbolName = iconName;
    searchConfig->emitChanged();
  }
}

void CGeoSearch::selectService(const QRect& rect) {
  QTreeWidget* tree = treeWidget();
  if (tree == nullptr) {
    return;
  }

  QMenu* menu = new QMenu(tree);

  menu->addSection(tr("Select Service"));

  QActionGroup* actionGroup = new QActionGroup(menu);

  actionGroup->addAction(addService(CGeoSearchConfig::eServiceNominatim, tr("OSM Nominatim"), menu));
  actionGroup->addAction(addService(CGeoSearchConfig::eServiceGeonamesSearch, tr("Geonames Places"), menu));
  actionGroup->addAction(addService(CGeoSearchConfig::eServiceGeonamesAddress, tr("Geonames Address"), menu));
  actionGroup->addAction(addService(CGeoSearchConfig::eServiceGoogle, tr("Google"), menu));

  menu->addSeparator();
  QAction* actAccu = menu->addAction(QIcon("://icons/AddGreen.svgt"), tr("Accumulative Results"));
  actAccu->setCheckable(true);
  actAccu->setChecked(searchConfig->accumulativeResults);
  connect(actAccu, &QAction::triggered, this, &CGeoSearch::slotAccuResults);

  QAction* actReset = menu->addAction(QIcon("://icons/Reset.svgt"), tr("Reset Results"));
  actReset->setEnabled(childCount() != 0);
  connect(actReset, &QAction::triggered, this, &CGeoSearch::slotResetResults);

  menu->addSeparator();

  QAction* actSetup = menu->addAction(QIcon("://icons/Apply.svgt"), tr("Configure Services"));
  actSetup->setToolTip(toRichText(tr("configure providers of geocoding search services")));
  connect(actSetup, &QAction::triggered, this, &CGeoSearch::slotSetupGeoSearch);

  menu->move(tree->mapToGlobal(rect.topRight()));
  menu->exec();
}

QAction* CGeoSearch::addService(CGeoSearchConfig::service_e service, const QString& name, QMenu* menu) {
  QAction* action = menu->addAction(name);
  action->setCheckable(true);

  connect(action, &QAction::triggered, this, [this, service](bool checked) { slotServiceSelected(service, checked); });
  action->setChecked(searchConfig->currentService == service);

  return action;
}

void CGeoSearch::slotServiceSelected(CGeoSearchConfig::service_e service, bool checked) {
  if (!checked) {
    return;
  }
  searchConfig->currentService = service;
  searchConfig->emitChanged();
}

void CGeoSearch::slotSetupGeoSearch() {
  CGeoSearchConfigDialog dlg(this->treeWidget(), searchConfig);
  dlg.exec();
}

void CGeoSearch::startSearch(const QString& address) {
  if (lastAddress == address && lastService == searchConfig->currentService) {
    return;
  }

  QMS_DELETE(itemStatus);

  if (!searchConfig->accumulativeResults) {
    qDeleteAll(takeChildren());
  }

  lastService = searchConfig->currentService;
  lastAddress = address;
  QString addr = address;

  switch (searchConfig->currentService) {
    case CGeoSearchConfig::eServiceNone: {
      this->createErrorItem(tr("no service configured - please click setup-icon in search-field"));
      setExpanded(true);
      break;
    }

    case CGeoSearchConfig::eServiceGoogle: {
      requestGoogle(addr);
      inputEnabled = false;
      break;
    }

    case CGeoSearchConfig::eServiceGeonamesSearch: {
      requestGeonamesSearch(addr);
      inputEnabled = false;
      break;
    }

    case CGeoSearchConfig::eServiceGeonamesAddress: {
      requestGeonamesAddress(addr);
      inputEnabled = false;
      break;
    }

    case CGeoSearchConfig::eServiceNominatim: {
      requestNominatim(addr);
      inputEnabled = false;
      break;
    }

    default:
      searchConfig->currentService = CGeoSearchConfig::eServiceNone;
  }
}

void CGeoSearch::slotRequestFinished(QNetworkReply* reply) {
  QMutexLocker lock2(&IGisItem::mutexItems);

  inputEnabled = true;

  if (reply->error() != QNetworkReply::NoError) {
    createErrorItem(reply->errorString());
    reply->deleteLater();
    return;
  }

  QByteArray data = reply->readAll();
  CGeoSearchConfig::service_e service = CGeoSearchConfig::service_e(reply->property("service").toInt());

  reply->deleteLater();

  if (data.isEmpty()) {
    createErrorItem("Response is empty");
    return;
  }

  switch (service) {
    case CGeoSearchConfig::eServiceGoogle: {
      parseGoogle(data);
      break;
    }

    case CGeoSearchConfig::eServiceGeonamesSearch: {
      parseGeonamesSearch(data);
      break;
    }

    case CGeoSearchConfig::eServiceGeonamesAddress: {
      parseGeonamesAddress(data);
      break;
    }

    case CGeoSearchConfig::eServiceNominatim: {
      parseNominatim(data);
      break;
    }

    default: {
      createErrorItem(tr("unexpected service-identifier %1").arg(service));
    }
  }

  setExpanded(true);

  CCanvas::triggerCompleteUpdate(CCanvas::eRedrawGis);
}

void CGeoSearch::requestGoogle(QString& addr) const {
  QUrl url("https://maps.googleapis.com");
  url.setPath("/maps/api/geocode/xml");

  QUrlQuery urlQuery;
  urlQuery.addQueryItem("address", addr.replace(" ", "+"));
  if (!searchConfig->googleApiKey.isEmpty()) {
    urlQuery.addQueryItem("key", searchConfig->googleApiKey);
  }
  urlQuery.addQueryItem("sensor", "false");
  url.setQuery(urlQuery);

  QNetworkRequest request;
  request.setUrl(url);
  QNetworkReply* reply = networkAccessManager->get(request);
  reply->setProperty("service", QVariant(CGeoSearchConfig::eServiceGoogle));
}

void CGeoSearch::requestGeonamesSearch(QString& addr) const {
  QUrl url("https://secure.geonames.org");
  url.setPath("/search");

  QUrlQuery urlQuery;
  urlQuery.addQueryItem("q", addr.replace(" ", "+"));
  if (!searchConfig->geonamesUsername.isEmpty()) {
    urlQuery.addQueryItem("username", searchConfig->geonamesUsername);
  }
  url.setQuery(urlQuery);

  QNetworkRequest request;
  request.setUrl(url);
  QNetworkReply* reply = networkAccessManager->get(request);
  reply->setProperty("service", QVariant(CGeoSearchConfig::eServiceGeonamesSearch));
}

void CGeoSearch::requestGeonamesAddress(QString& addr) const {
  QUrl url("http://api.geonames.org");
  url.setPath("/geoCodeAddress");

  QUrlQuery urlQuery;
  urlQuery.addQueryItem("q", addr.replace(" ", "+"));
  if (!searchConfig->geonamesUsername.isEmpty()) {
    urlQuery.addQueryItem("username", searchConfig->geonamesUsername);
  }
  url.setQuery(urlQuery);

  QNetworkRequest request;
  request.setUrl(url);
  QNetworkReply* reply = networkAccessManager->get(request);
  reply->setProperty("service", QVariant(CGeoSearchConfig::eServiceGeonamesAddress));
}

void CGeoSearch::requestNominatim(QString& addr) const {
  QString userAgent(WHAT_STR);
  userAgent.append(" (Nokia; Qt)");

  QUrl url("https://nominatim.openstreetmap.org");
  url.setPath("/search");

  QString limit;
  limit.setNum(searchConfig->nominatimLimit);

  QUrlQuery urlQuery;
  urlQuery.addQueryItem("q", addr.replace(" ", "+"));
  urlQuery.addQueryItem("format", "xml");
  urlQuery.addQueryItem("addressdetails", "1");
  urlQuery.addQueryItem("limit", limit);
  if (!searchConfig->nominatimEmail.isEmpty()) {
    urlQuery.addQueryItem("email", searchConfig->nominatimEmail);
  }
  url.setQuery(urlQuery);

  QNetworkRequest request;
  request.setUrl(url);
  request.setHeader(QNetworkRequest::UserAgentHeader, userAgent);
  QNetworkReply* reply = networkAccessManager->get(request);
  reply->setProperty("service", QVariant(CGeoSearchConfig::eServiceNominatim));
}

void CGeoSearch::parseGoogle(const QByteArray& data) {
  QString status;
  QDomDocument xml;

  xml.setContent(data);

  QDomElement root = xml.documentElement();

  if (root.tagName() != "GeocodeResponse") {
    status = tr("Unknown response");
    createErrorItem(status);
    return;
  }

  status = root.namedItem("status").toElement().text();
  if (status != "OK") {
    status = tr("Error: ");
    status += root.namedItem("error_message").toElement().text();
    createErrorItem(status);
    return;
  }

  {
    QDomNodeList xmlEntries = root.elementsByTagName("result");
    const qint32 N = xmlEntries.size();
    if (N) {
      for (int i = 0; i < N; i++) {
        QString address;
        QDomElement xmlEntry = xmlEntries.item(i).toElement();
        QDomElement xmlAddress = xmlEntry.namedItem("formatted_address").toElement();
        if (xmlAddress.isElement()) {
          address = xmlAddress.text();
        }

        QDomNode xmlGeometry = xmlEntry.namedItem("geometry");
        QDomNode xmlLocation = xmlGeometry.namedItem("location");
        qreal lon = xmlLocation.namedItem("lng").toElement().text().toDouble();
        qreal lat = xmlLocation.namedItem("lat").toElement().text().toDouble();

        new CGisItemWpt(QPointF(lon, lat), address, searchConfig->symbolName, this);
      }
    }
  }
}

void CGeoSearch::parseGeonamesSearch(const QByteArray& data) {
  QString status;
  QDomDocument xml;

  xml.setContent(data);

  QDomElement root = xml.documentElement();

  if (root.tagName() != "geonames") {
    status = tr("Unknown response");
    createErrorItem(status);
    return;
  }

  QDomElement xmlStatus = root.namedItem("status").toElement();
  if (xmlStatus.isElement()) {
    status = tr("Error: ");
    status.append(xmlStatus.attribute("message"));
    createErrorItem(status);
    return;
  }

  QDomNodeList xmlEntries = root.elementsByTagName("geoname");
  const qint32 N = xmlEntries.size();
  if (N) {
    for (int i = 0; i < N; i++) {
      QString address;
      bool isNotFirst = false;
      QDomElement xmlEntry = xmlEntries.item(i).toElement();

      if (xmlEntry.isElement()) {
        QDomElement xmlLng = xmlEntry.namedItem("lng").toElement();
        QDomElement xmlLat = xmlEntry.namedItem("lat").toElement();

        if (xmlLng.isElement() && xmlLat.isElement()) {
          QDomElement xmlName = xmlEntry.namedItem("name").toElement();
          QDomElement xmlCountryCode = xmlEntry.namedItem("countryCode").toElement();
          QDomElement xmlCountryName = xmlEntry.namedItem("countryName").toElement();

          if (xmlName.isElement() && !xmlName.text().isEmpty()) {
            address.append(xmlName.text());
            isNotFirst = true;
          }
          if (xmlCountryCode.isElement() && !xmlCountryCode.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlCountryCode.text());
            isNotFirst = true;
          }
          if (xmlCountryName.isElement() && !xmlCountryName.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlCountryName.text());
            //                        isNotFirst = true;
          }

          qreal lon = xmlLng.text().toDouble();
          qreal lat = xmlLat.text().toDouble();

          new CGisItemWpt(QPointF(lon, lat), address, searchConfig->symbolName, this);
        }
      }
    }
  }
}

void CGeoSearch::parseGeonamesAddress(const QByteArray& data) {
  QString status;
  QDomDocument xml;

  xml.setContent(data);

  QDomElement root = xml.documentElement();

  if (root.tagName() != "geonames") {
    status = tr("Unknown response");
    createErrorItem(status);
    return;
  }

  QDomElement xmlStatus = root.namedItem("status").toElement();
  if (xmlStatus.isElement()) {
    status = tr("Error: ");
    status.append(xmlStatus.attribute("message"));
    createErrorItem(status);
    return;
  }

  QDomNodeList xmlEntries = root.elementsByTagName("address");
  const qint32 N = xmlEntries.size();
  if (N) {
    for (int i = 0; i < N; i++) {
      QString address;
      bool isNotFirst = false;
      QDomElement xmlEntry = xmlEntries.item(i).toElement();

      if (xmlEntry.isElement()) {
        QDomElement xmlLng = xmlEntry.namedItem("lng").toElement();
        QDomElement xmlLat = xmlEntry.namedItem("lat").toElement();

        if (xmlLng.isElement() && xmlLat.isElement()) {
          QDomElement xmlStreet = xmlEntry.namedItem("street").toElement();
          QDomElement xmlHouseNumber = xmlEntry.namedItem("houseNumber").toElement();
          QDomElement xmlLocality = xmlEntry.namedItem("locality").toElement();
          QDomElement xmlPostalcode = xmlEntry.namedItem("postalcode").toElement();
          QDomElement xmlAdminCode1 = xmlEntry.namedItem("adminCode1").toElement();
          QDomElement xmlAdminName1 = xmlEntry.namedItem("adminName1").toElement();
          QDomElement xmlAdminCode2 = xmlEntry.namedItem("adminCode2").toElement();
          QDomElement xmlAdminName2 = xmlEntry.namedItem("adminName2").toElement();
          QDomElement xmlAdminCode3 = xmlEntry.namedItem("adminCode3").toElement();
          QDomElement xmlCountryCode = xmlEntry.namedItem("countryCode").toElement();

          if (xmlStreet.isElement() && !xmlStreet.text().isEmpty()) {
            address.append(xmlStreet.text());
            isNotFirst = true;
          }
          if (xmlHouseNumber.isElement() && !xmlHouseNumber.text().isEmpty()) {
            if (isNotFirst) {
              address.append(" ");
            }
            address.append(xmlHouseNumber.text());
            isNotFirst = true;
          }
          if (xmlLocality.isElement() && !xmlLocality.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlLocality.text());
            isNotFirst = true;
          }
          if (xmlPostalcode.isElement() && !xmlPostalcode.text().isEmpty()) {
            if (isNotFirst) {
              address.append(" ");
            }
            address.append(xmlPostalcode.text());
            isNotFirst = true;
          }
          if (xmlAdminCode1.isElement() && !xmlAdminCode1.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlAdminCode1.text());
            isNotFirst = true;
          }
          if (xmlAdminName1.isElement() && !xmlAdminName1.text().isEmpty()) {
            if (isNotFirst) {
              address.append(" ");
            }
            address.append(xmlAdminName1.text());
            isNotFirst = true;
          }
          if (xmlAdminCode2.isElement() && !xmlAdminCode2.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlAdminCode2.text());
            isNotFirst = true;
          }
          if (xmlAdminName2.isElement() && !xmlAdminName2.text().isEmpty()) {
            if (isNotFirst) {
              address.append(" ");
            }
            address.append(xmlAdminName2.text());
            isNotFirst = true;
          }
          if (xmlAdminCode3.isElement() && !xmlAdminCode3.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlAdminCode3.text());
            isNotFirst = true;
          }
          if (xmlCountryCode.isElement() && !xmlCountryCode.text().isEmpty()) {
            if (isNotFirst) {
              address.append(", ");
            }
            address.append(xmlCountryCode.text());
            //                        isNotFirst = true;
          }

          qreal lon = xmlLng.text().toDouble();
          qreal lat = xmlLat.text().toDouble();

          new CGisItemWpt(QPointF(lon, lat), address, searchConfig->symbolName, this);
        }
      }
    }
  }
}

void CGeoSearch::parseNominatim(const QByteArray& data) {
  QString status;
  QDomDocument xml;

  xml.setContent(data);

  QDomElement root = xml.documentElement();

  if (root.tagName() != "searchresults") {
    status = tr("Unknown response");
    createErrorItem(status);
    return;
  }

  QDomNodeList xmlEntries = root.elementsByTagName("place");
  const qint32 N = xmlEntries.size();
  if (N) {
    for (int i = 0; i < N; i++) {
      QString address;
      bool isNotFirst = false;
      bool hasPostcode = false;
      QDomElement xmlEntry = xmlEntries.item(i).toElement();

      if (xmlEntry.isElement()) {
        qreal lon = xmlEntry.attribute("lon").toDouble();
        qreal lat = xmlEntry.attribute("lat").toDouble();

        QDomElement xmlHouseNumber = xmlEntry.namedItem("house_number").toElement();
        QDomElement xmlRoad = xmlEntry.namedItem("road").toElement();
        QDomElement xmlVillage = xmlEntry.namedItem("village").toElement();
        QDomElement xmlTown = xmlEntry.namedItem("town").toElement();
        QDomElement xmlCity = xmlEntry.namedItem("city").toElement();
        QDomElement xmlCounty = xmlEntry.namedItem("county").toElement();
        QDomElement xmlPostcode = xmlEntry.namedItem("postcode").toElement();
        QDomElement xmlCountry = xmlEntry.namedItem("country").toElement();
        QDomElement xmlCountryCode = xmlEntry.namedItem("country_code").toElement();

        if (xmlHouseNumber.isElement() && !xmlHouseNumber.text().isEmpty()) {
          address.append(xmlHouseNumber.text());
          isNotFirst = true;
        }
        if (xmlRoad.isElement() && !xmlRoad.text().isEmpty()) {
          if (isNotFirst) {
            address.append(" ");
          }
          address.append(xmlRoad.text());
          isNotFirst = true;
        }
        if (xmlPostcode.isElement() && !xmlPostcode.text().isEmpty()) {
          if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlPostcode.text());
          isNotFirst = false;
          hasPostcode = true;
        }
        if (xmlVillage.isElement() && !xmlVillage.text().isEmpty()) {
          if (hasPostcode) {
            address.append(" ");
            hasPostcode = false;
          } else if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlVillage.text());
          isNotFirst = true;
        }
        if (xmlTown.isElement() && !xmlTown.text().isEmpty()) {
          if (hasPostcode) {
            address.append(" ");
            hasPostcode = false;
          } else if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlTown.text());
          isNotFirst = true;
        }
        if (xmlCity.isElement() && !xmlCity.text().isEmpty()) {
          if (hasPostcode) {
            address.append(" ");
            //                        hasPostcode = false;
          } else if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlCity.text());
          isNotFirst = true;
        }
        if (xmlCounty.isElement() && !xmlCounty.text().isEmpty()) {
          if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlCounty.text());
          isNotFirst = true;
        }
        if (xmlCountry.isElement() && !xmlCountry.text().isEmpty()) {
          if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlCountry.text());
          isNotFirst = true;
        }
        if (xmlCountryCode.isElement() && !xmlCountryCode.text().isEmpty()) {
          if (isNotFirst) {
            address.append(", ");
          }
          address.append(xmlCountryCode.text());
          //                    isNotFirst = true;
        }

        new CGisItemWpt(QPointF(lon, lat), address, searchConfig->symbolName, this);
      }
    }
  }
}

class CGeoSearchError : public IWksItem {
 public:
  CGeoSearchError(const QString& status, CGeoSearch* parent) : IWksItem(parent, eTypeGeoSearchError) {
    name = status;
    icon = QIcon("://icons/Error.svgt");
  }
  virtual ~CGeoSearchError() = default;
  bool hasUserFocus() const override { return false; }
  void gainUserFocus(bool yes) override {}
  QString getInfo(quint32) const override { return name; }
};

void CGeoSearch::createErrorItem(const QString& status) { itemStatus = new CGeoSearchError(status, this); }

void CGeoSearch::slotConfigChanged() { setIcon(); }

void CGeoSearch::slotAccuResults(bool yes) {
  searchConfig->accumulativeResults = yes;
  setIcon();
}

void CGeoSearch::slotResetResults() {
  qDeleteAll(takeChildren());
  updateDecoration();
}
