/**********************************************************************************************
    Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
    Copyright (C) 2017 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/rte/router/CRouterBRouter.h"

#include <QMutex>
#include <QtNetwork>
#include <QtWidgets>

#include "CMainWindow.h"
#include "canvas/CCanvas.h"
#include "gis/CGisWorkspace.h"
#include "gis/rte/CGisItemRte.h"
#include "gis/rte/router/brouter/CRouterBRouterInfo.h"
#include "gis/rte/router/brouter/CRouterBRouterLocal.h"
#include "gis/rte/router/brouter/CRouterBRouterSetup.h"
#include "gis/rte/router/brouter/CRouterBRouterSetupWizard.h"
#include "gis/wpt/CGisItemWpt.h"
#include "helpers/CProgressDialog.h"
#include "helpers/CSettings.h"

CRouterBRouter* CRouterBRouter::pSelf;

CRouterBRouter::CRouterBRouter(QWidget* parent) : IRouter(false, parent) {
  pSelf = this;
  setupUi(this);

  labelBRouterWarning->hide();

  setup = new CRouterBRouterSetup(this);
  setup->load();

  connect(toolSetup, &QToolButton::clicked, this, &CRouterBRouter::slotToolSetupClicked);
  connect(toolProfileInfo, &QToolButton::clicked, this, &CRouterBRouter::slotToolProfileInfoClicked);
  connect(setup, &CRouterBRouterSetup::sigDisplayOnlineProfileFinished, this, &CRouterBRouter::slotDisplayProfileInfo);
  connect(setup, &CRouterBRouterSetup::sigError, this, &CRouterBRouter::slotDisplayError);

  comboAlternative->addItem(tr("original"), "0");
  comboAlternative->addItem(tr("first alternative"), "1");
  comboAlternative->addItem(tr("second alternative"), "2");
  comboAlternative->addItem(tr("third alternative"), "3");

  networkAccessManager = new QNetworkAccessManager(this);
  connect(setup, &CRouterBRouterSetup::sigVersionChanged, this, &CRouterBRouter::slotVersionChanged);

  timerCloseStatusMsg = new QTimer(this);
  timerCloseStatusMsg->setSingleShot(true);
  timerCloseStatusMsg->setInterval(5000);
  connect(timerCloseStatusMsg, &QTimer::timeout, this, &CRouterBRouter::slotCloseStatusMsg);

  routerSetup = dynamic_cast<CRouterSetup*>(parent);
  Q_ASSERT(routerSetup != nullptr);

  connect(toolConsole, &QToolButton::clicked, this, &CRouterBRouter::slotToggleConsole);
  connect(toolToggleBRouter, &QToolButton::clicked, this, &CRouterBRouter::slotToggleBRouter);
  connect(pushBRouterError, &QPushButton::clicked, this, &CRouterBRouter::slotClearError);

  textBRouterOutput->setVisible(false);
  textBRouterError->setVisible(false);
  pushBRouterError->setVisible(false);

  localBRouter = new CRouterBRouterLocal(*this);

  updateDialog();

  SETTINGS;

  cfg.beginGroup("Route/brouter");
  comboProfile->setCurrentIndex(cfg.value("profile", 0).toInt());
  comboAlternative->setCurrentIndex(cfg.value("alternative", 0).toInt());
  cfg.endGroup();
}

CRouterBRouter::~CRouterBRouter() {
  isShutdown = true;
  if (!localBRouter->isBRouterNotRunning()) {
    localBRouter->stopBRouter();
  }
  SETTINGS;
  cfg.beginGroup("Route/brouter");
  cfg.setValue("profile", comboProfile->currentIndex());
  cfg.setValue("alternative", comboAlternative->currentIndex());
  cfg.endGroup();
}

void CRouterBRouter::slotToolSetupClicked() {
  localBRouter->stopBRouter();
  CRouterBRouterSetupWizard setupWizard;
  setupWizard.exec();
  slotClearError();
  setup->load();
  getBRouterVersion();
  updateDialog();
}

void CRouterBRouter::slotToolProfileInfoClicked() const {
  const int index = comboProfile->currentIndex();
  if (index > -1) {
    setup->displayProfileAsync(setup->getProfiles().at(index));
  }
}

void CRouterBRouter::slotDisplayError(const QString& error, const QString& details) const {
  textBRouterError->setText(error);
  if (!details.isEmpty()) {
    textBRouterError->append(details);
  }
  QTextCursor cursor = textBRouterError->textCursor();
  cursor.movePosition(QTextCursor::Start);
  textBRouterError->setTextCursor(cursor);
  textBRouterError->setVisible(true);
  textBRouterOutput->setVisible(false);
  pushBRouterError->setVisible(true);
}

void CRouterBRouter::slotClearError() {
  textBRouterError->clear();
  textBRouterError->setVisible(false);
  pushBRouterError->setVisible(false);
  localBRouter->clearBRouterError();
}

void CRouterBRouter::slotDisplayProfileInfo(const QString& profile, const QString& content) {
  slotClearError();
  CRouterBRouterInfo info;
  info.setLabel(profile);
  info.setInfo(content);
  info.exec();
}

void CRouterBRouter::setupLocalDir(QString localDir) {
  if (setup->isLocalBRouterDefaultDir()) {
    setup->localDir = localDir;
    setup->save();
  }
}

void CRouterBRouter::updateDialog() const {
  if (setup->installMode == CRouterBRouterSetup::eModeLocal) {
    routerSetup->setRouterTitle(CRouterSetup::RouterBRouter, tr("BRouter (offline)"));
    labelCopyrightBRouter->setVisible(true);
    labelCopyrightBRouterWeb->setVisible(false);
  } else {
    Q_ASSERT(setup->installMode == CRouterBRouterSetup::eModeOnline);
    routerSetup->setRouterTitle(CRouterSetup::RouterBRouter, tr("BRouter (online)"));
    labelCopyrightBRouter->setVisible(false);
    labelCopyrightBRouterWeb->setVisible(true);
  }
  comboProfile->clear();
  const QStringList& profiles = setup->getProfiles();
  const bool hasItems = !profiles.isEmpty();
  for (const QString& profile : profiles) {
    comboProfile->addItem(profile, profile);
  }
  comboProfile->setEnabled(hasItems);
  toolProfileInfo->setEnabled(hasItems);
  comboAlternative->setEnabled(hasItems);
  updateBRouterStatus();
}

void CRouterBRouter::slotCloseStatusMsg() const {
  timerCloseStatusMsg->stop();
  CCanvas* canvas = CMainWindow::self().getVisibleCanvas();
  if (canvas) {
    canvas->slotTriggerCompleteUpdate(CCanvas::eRedrawGis);
    canvas->reportStatus("BRouter", "");
  }
}

QString CRouterBRouter::getOptions() {
  return QString(
      tr("profile: %1, alternative: %2").arg(comboProfile->currentText()).arg(comboAlternative->currentData().toInt()));
}

void CRouterBRouter::routerSelected() { getBRouterVersion(); }

bool CRouterBRouter::hasFastRouting() {
  return setup->installMode == CRouterBRouterSetup::eModeLocal && setup->isLocalBRouterValid;
}

QNetworkRequest CRouterBRouter::getRequest(const QVector<QPointF>& routePoints, const QList<IGisItem*>& nogos) const {
  QString lonLats;

  for (const QPointF& pt : routePoints) {
    if (!lonLats.isEmpty()) {
      lonLats.append("|");
    }
    lonLats.append(QString("%1,%2").arg(pt.x(), 0, 'f', 6).arg(pt.y(), 0, 'f', 6));
  }

  QString nogoStr;
  QString nogoPolygons;
  QString nogoPolylines;

  for (IGisItem* const& item : nogos) {
    switch (item->type()) {
      case IGisItem::eTypeWpt: {
        CGisItemWpt* wpt = static_cast<CGisItemWpt*>(item);
        const qreal& rad = wpt->getProximity();
        if (rad != NOFLOAT && rad > 0.) {
          const QPointF& pos = wpt->getPosition();
          if (!nogoStr.isEmpty()) {
            nogoStr.append("|");
          }
          nogoStr.append(QString("%1,%2,%3").arg(pos.x(), 0, 'f', 6).arg(pos.y(), 0, 'f', 6).arg(rad, 0, 'f', 0));
        }
        break;
      }

      case IGisItem::eTypeOvl:
      case IGisItem::eTypeRte:
      case IGisItem::eTypeTrk: {
        IGisLine* line = dynamic_cast<IGisLine*>(item);
        Q_ASSERT(line != nullptr);
        QPolygonF polygon;
        line->getPolylineDegFromData(polygon);
        QString nogoPoints;
        for (const QPointF point : std::as_const(polygon)) {
          if (!nogoPoints.isEmpty()) {
            nogoPoints.append(",");
          }
          nogoPoints.append(QString("%1,%2").arg(point.x(), 0, 'f', 6).arg(point.y(), 0, 'f', 6));
        }
        if (item->type() == IGisItem::eTypeOvl) {
          if (!nogoPolygons.isEmpty()) {
            nogoPolygons.append("|");
          }
          nogoPolygons.append(nogoPoints);
        } else {
          if (!nogoPolylines.isEmpty()) {
            nogoPolylines.append("|");
          }
          nogoPolylines.append(nogoPoints);
        }
        break;
      }

      default: {
        Q_ASSERT(false);
      }
    }
  }

  QUrlQuery urlQuery;
  urlQuery.addQueryItem("lonlats", lonLats);
  if (!nogoStr.isEmpty()) {
    urlQuery.addQueryItem("nogos", nogoStr);
  }
  if (!nogoPolygons.isEmpty()) {
    urlQuery.addQueryItem("polygons", nogoPolygons);
  }
  if (!nogoPolylines.isEmpty()) {
    urlQuery.addQueryItem("polylines", nogoPolylines);
  }
  urlQuery.addQueryItem("profile", comboProfile->currentData().toString());
  urlQuery.addQueryItem("alternativeidx", comboAlternative->currentData().toString());
  urlQuery.addQueryItem("format", "gpx");
  urlQuery.addQueryItem("timode", "8");

  QUrl url = setup->getServiceUrl();
  url.setQuery(urlQuery);

  return QNetworkRequest(url);
}

int CRouterBRouter::calcRoute(const QPointF& p1, const QPointF& p2, QPolygonF& coords, qreal* costs) {
  if (!hasFastRouting()) {
    return -1;
  }

  const QVector<QPointF> points = {p1 * RAD_TO_DEG, p2 * RAD_TO_DEG};

  QList<IGisItem*> nogos;
  CGisWorkspace::self().getNogoAreas(nogos);

  try {
    return synchronousRequest(points, nogos, coords, costs);
  } catch (const QString& msg) {
    slotDisplayError(tr("Calculate route failed."), msg);
    return -1;
  }
}

int CRouterBRouter::synchronousRequest(const QVector<QPointF>& points, const QList<IGisItem*>& nogos, QPolygonF& coords,
                                       qreal* costs) {
  if (!mutex.tryLock()) {
    // skip further on-the-fly-requests as long a previous request is still running
    return -1;
  }

  if (setup->installMode == CRouterBRouterSetup::eModeLocal && localBRouter->isBRouterNotRunning()) {
    localBRouter->startBRouter();
  }

  try {
    coords.clear();
    QNetworkReply* reply = networkAccessManager->get(getRequest(points, nogos));

    auto guard = qScopeGuard([this, reply]() {
      reply->deleteLater();
      mutex.unlock();
    });

    reply->setProperty("options", getOptions());
    reply->setProperty("time", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch());

    CProgressDialog* progress = new CProgressDialog(tr("Calculate route with %1").arg(getOptions()), 0, NOINT, this);
    QEventLoop eventLoop;
    connect(progress, &CProgressDialog::rejected, reply, &QNetworkReply::abort);
    connect(progress, &CProgressDialog::rejected, &eventLoop, &QEventLoop::quit);
    connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
    eventLoop.exec();
    progress->deleteLater();

    const QNetworkReply::NetworkError& netErr = reply->error();
    if (netErr == QNetworkReply::OperationCanceledError) {
      return -1;  // user cancelled via progress dialog — not an error
    } else if (netErr == QNetworkReply::RemoteHostClosedError && nogos.size() > 1 && !isMinimumVersion(1, 4, 10)) {
      throw tr("this version of BRouter does not support more than 1 nogo-area");
    } else if (netErr != QNetworkReply::NoError) {
      throw reply->errorString();
    }
    slotClearError();

    const QByteArray& res = reply->readAll();

    if (res.isEmpty()) {
      throw tr("response is empty");
    }

    QDomDocument xml;
    const QDomDocument::ParseResult& xmlRes = xml.setContent(res);
    if (!xmlRes) {
      throw tr("Failed to parse BRouter response (line %1): %2").arg(xmlRes.errorLine).arg(xmlRes.errorMessage);
    }

    const QDomElement& xmlGpx = xml.documentElement();
    if (xmlGpx.isNull() || xmlGpx.tagName() != "gpx") {
      throw QString(res.data());
    }
    setup->parseBRouterVersion(xmlGpx.attribute("creator"));

    // read the shape
    const QDomNodeList& xmlLatLng =
        xmlGpx.firstChildElement("trk").firstChildElement("trkseg").elementsByTagName("trkpt");
    for (int n = 0; n < xmlLatLng.size(); n++) {
      const QDomElement& elem = xmlLatLng.item(n).toElement();
      coords << QPointF();
      QPointF& point = coords.last();
      point.setX(elem.attribute("lon").toDouble() * DEG_TO_RAD);
      point.setY(elem.attribute("lat").toDouble() * DEG_TO_RAD);
    }

    // find costs of route (copied and adapted from CGisItemRte::setResultFromBrouter)
    if (costs != nullptr) {
      *costs = 0;
      const QDomNodeList& nodes = xml.childNodes();
      for (int i = 0; i < nodes.count(); i++) {
        const QDomNode& node = nodes.at(i);
        if (node.isComment()) {
          const QString& comment = node.toComment().data();
          // ' track-length = 3181 filtered ascend = 70 plain-ascend = 5 cost=8491 energy=.0kwh time=16m 30s '
          static const QRegularExpression re("cost\\s*=\\s*(-?\\d+)");
          const QRegularExpressionMatch& match = re.match(comment);
          if (match.hasMatch()) {
            *costs = match.captured(1).toDouble();
          }
          break;
        }
      }
    }
  } catch (const QString& msg) {
    coords.clear();
    if (!msg.isEmpty()) {
      throw tr("Bad response from server: %1").arg(msg);
    }
  }

  slotCloseStatusMsg();
  return coords.size();
}

void CRouterBRouter::calcRoute(const IGisItem::key_t& key) {
  QMutexLocker lock(&mutex);

  if (setup->installMode == CRouterBRouterSetup::eModeLocal && localBRouter->isBRouterNotRunning()) {
    localBRouter->startBRouter();
  }

  CGisItemRte* rte = dynamic_cast<CGisItemRte*>(CGisWorkspace::self().getItemByKey(key));
  if (nullptr == rte) {
    return;
  }

  QList<IGisItem*> nogos;
  CGisWorkspace::self().getNogoAreas(nogos);

  rte->reset();

  slotCloseStatusMsg();

  QVector<QPointF> points;
  for (const CGisItemRte::rtept_t& pt : rte->getRoute().pts) {
    points << QPointF(pt.lon, pt.lat);
  }

  QNetworkReply* reply = networkAccessManager->get(getRequest(points, nogos));
  auto guard = qScopeGuard([reply]() { reply->deleteLater(); });

  reply->setProperty("options", getOptions());
  reply->setProperty("time", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch());

  CCanvas* canvas = CMainWindow::self().getVisibleCanvas();
  if (canvas) {
    canvas->slotTriggerCompleteUpdate(CCanvas::eRedrawGis);
    canvas->reportStatus("BRouter", tr("<b>BRouter</b><br/>Routing request sent to server. Please wait..."));
  }

  QEventLoop eventLoop;
  CProgressDialog* progress = new CProgressDialog(tr("Calculate route with %1").arg(getOptions()), 0, NOINT, this);
  connect(progress, &CProgressDialog::rejected, reply, &QNetworkReply::abort);
  connect(progress, &CProgressDialog::rejected, &eventLoop, &QEventLoop::quit);
  connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
  eventLoop.exec();
  progress->deleteLater();

  try {
    const QNetworkReply::NetworkError& netErr = reply->error();
    if (netErr == QNetworkReply::OperationCanceledError) {
      return;  // user cancelled via progress dialog — not an error
    } else if (netErr == QNetworkReply::RemoteHostClosedError && nogos.size() > 1 &&
               !isMinimumVersion(1, 4, 10)) {
      throw tr("this version of BRouter does not support more than 1 nogo-area");
    } else if (netErr != QNetworkReply::NoError) {
      throw reply->errorString();
    }

    const QByteArray& res = reply->readAll();
    if (res.isEmpty()) {
      throw tr("response is empty");
    }

    slotClearError();

    QDomDocument xml;

    const QDomDocument::ParseResult& xmlRes = xml.setContent(res);
    if (!xmlRes) {
      throw tr("Failed to parse BRouter response (line %1): %2").arg(xmlRes.errorLine).arg(xmlRes.errorMessage);
    }

    const QDomElement& xmlGpx = xml.documentElement();
    if (xmlGpx.isNull() || xmlGpx.tagName() != "gpx") {
      throw QString(res.data());
    }

    qint64 time = reply->property("time").toLongLong();
    time = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - time;

    CGisItemRte* rte = dynamic_cast<CGisItemRte*>(CGisWorkspace::self().getItemByKey(key));
    if (rte != nullptr) {
      rte->setResultFromBRouter(
          xml, reply->property("options").toString() + tr("<br/>Calculation time: %1s").arg(time / 1000.0, 0, 'f', 2));
    }
  } catch (const QString& msg) {
    if (!msg.isEmpty()) {
      CCanvas* canvas = CMainWindow::self().getVisibleCanvas();
      if (canvas) {
        canvas->reportStatus("BRouter", tr("<b>BRouter</b><br/>Bad response from server:<br/>%1").arg(msg));
      }
      timerCloseStatusMsg->start();
      return;
    }
  }

  slotCloseStatusMsg();
}

void CRouterBRouter::slotToggleConsole() const {
  textBRouterOutput->setVisible(!textBRouterOutput->isVisible());
  bool showError = localBRouter->isBRouterError() && !textBRouterOutput->isVisible();
  textBRouterError->setVisible(showError);
  pushBRouterError->setVisible(showError);
}

void CRouterBRouter::slotToggleBRouter() const {
  if (localBRouter->isBRouterNotRunning()) {
    localBRouter->startBRouter();
  } else {
    localBRouter->stopBRouter();
  }
}

void CRouterBRouter::getBRouterVersion() {
  if (setup->installMode == CRouterBRouterSetup::eModeLocal) {
    localBRouter->getBRouterVersion();
  } else {
    setup->loadOnlineVersion();
  }
}

void CRouterBRouter::slotVersionChanged() {
  if (setup->versionMajor != NOINT && setup->versionMinor != NOINT && setup->versionPatch != NOINT) {
    labelBRouter->setToolTip(
        tr("BRouter (Version %1.%2.%3)").arg(setup->versionMajor).arg(setup->versionMinor).arg(setup->versionPatch));
  } else {
    labelBRouter->setToolTip("BRouter: (failed to read version)");
  }
}

bool CRouterBRouter::isMinimumVersion(int major, int minor, int patch) const {
  if (setup->versionMajor == NOINT || setup->versionMinor == NOINT || setup->versionPatch == NOINT) {
    return false;
  }
  if (setup->versionMajor > major) {
    return true;
  }
  if (setup->versionMajor < major) {
    return false;
  }
  if (setup->versionMinor > minor) {
    return true;
  }
  if (setup->versionMinor < minor) {
    return false;
  }
  if (setup->versionPatch > patch) {
    return true;
  }
  if (setup->versionPatch < patch) {
    return false;
  }
  return true;
}

void CRouterBRouter::updateBRouterStatus() const {
  if (isShutdown) {
    return;
  }

  labelBRouterWarning->hide();
  if (setup->installMode == CRouterBRouterSetup::eModeLocal) {
    localBRouter->updateLocalBRouterStatus();
  } else {
    Q_ASSERT(setup->installMode == CRouterBRouterSetup::eModeOnline);
    labelStatus->setText(tr("online"));
    toolConsole->setVisible(false);
    toolToggleBRouter->setVisible(false);
    textBRouterOutput->clear();
    textBRouterOutput->setVisible(false);
  }
}
