// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2010 Siddharth Srivastava <akssps011@gmail.com>
// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
//

#include "RoutingPlugin.h"

#include "ui_RoutingConfigDialog.h"
#include "ui_RoutingPlugin.h"

#include "AudioOutput.h"
#include "GeoDataCoordinates.h"
#include "GeoDataLookAt.h"
#include "GeoPainter.h"
#include "MarbleDirs.h"
#include "MarbleGraphicsGridLayout.h"
#include "MarbleLocale.h"
#include "MarbleModel.h"
#include "MarbleWidget.h"
#include "Planet.h"
#include "PluginManager.h"
#include "PositionProviderPlugin.h"
#include "PositionTracking.h"
#include "ViewportParams.h"
#include "WidgetGraphicsItem.h"
#include "routing/Route.h"
#include "routing/RouteRequest.h"
#include "routing/RoutingManager.h"
#include "routing/RoutingModel.h"
#include "routing/SpeakersModel.h"

#include <QDialog>
#include <QPushButton>

namespace Marble
{

namespace
{
int const thresholdDistance = 1000; // in meter
}

class RoutingPluginPrivate
{
public:
    MarbleWidget *m_marbleWidget = nullptr;
    WidgetGraphicsItem *m_widgetItem = nullptr;
    RoutingModel *m_routingModel = nullptr;
    Ui::RoutingPlugin m_widget;
    bool m_nearNextInstruction;
    bool m_guidanceModeEnabled;
    AudioOutput *m_audio = nullptr;
    QDialog *m_configDialog = nullptr;
    Ui::RoutingConfigDialog m_configUi;
    bool m_routeCompleted;
    SpeakersModel *m_speakersModel = nullptr;

    RoutingPluginPrivate(RoutingPlugin *parent);

    void updateZoomButtons(int zoomValue);

    void updateZoomButtons();

    void updateGuidanceModeButton();

    void forceRepaint();

    void updateButtonVisibility();

    void reverseRoute();

    void toggleGuidanceMode(bool enabled);

    void updateDestinationInformation();

    void updateGpsButton(PositionProviderPlugin *activePlugin);

    void togglePositionTracking(bool enabled);

    static QString richText(const QString &source);

    static QString fuzzyDistance(qreal distanceMeter);

    void readSettings();

    qreal nextInstructionDistance() const;

    qreal remainingDistance() const;

private:
    RoutingPlugin *m_parent = nullptr;
};

RoutingPluginPrivate::RoutingPluginPrivate(RoutingPlugin *parent)
    : m_marbleWidget(nullptr)
    , m_widgetItem(nullptr)
    , m_routingModel(nullptr)
    , m_nearNextInstruction(false)
    , m_guidanceModeEnabled(false)
    , m_audio(new AudioOutput(parent))
    , m_configDialog(nullptr)
    , m_routeCompleted(false)
    , m_speakersModel(nullptr)
    , m_parent(parent)
{
    m_audio->setMuted(false);
    m_audio->setSoundEnabled(true);
}

QString RoutingPluginPrivate::richText(const QString &source)
{
    return QLatin1StringView(R"(<font size="+1" color="black">)") + source + QLatin1StringView("</font>");
}

QString RoutingPluginPrivate::fuzzyDistance(qreal length)
{
    int precision = 0;
    QString distanceUnit = QLatin1StringView("m");

    if (MarbleGlobal::getInstance()->locale()->measurementSystem() != MarbleLocale::MetricSystem) {
        precision = 1;
        distanceUnit = QStringLiteral("mi");
        length *= METER2KM;
        length *= KM2MI;
    } else if (MarbleGlobal::getInstance()->locale()->measurementSystem() == MarbleLocale::MetricSystem) {
        if (length >= 1000) {
            length /= 1000;
            distanceUnit = QStringLiteral("km");
            precision = 1;
        } else if (length >= 200) {
            length = 50 * qRound(length / 50);
        } else if (length >= 100) {
            length = 25 * qRound(length / 25);
        } else {
            length = 10 * qRound(length / 10);
        }
    } else if (MarbleGlobal::getInstance()->locale()->measurementSystem() == MarbleLocale::NauticalSystem) {
        precision = 2;
        distanceUnit = QStringLiteral("nm");
        length *= METER2KM;
        length *= KM2NM;
    }

    return QStringLiteral("%1 %2").arg(length, 0, 'f', precision).arg(distanceUnit);
}

void RoutingPluginPrivate::updateZoomButtons(int zoomValue)
{
    int const minZoom = m_marbleWidget ? m_marbleWidget->minimumZoom() : 900;
    int const maxZoom = m_marbleWidget ? m_marbleWidget->maximumZoom() : 2400;

    bool const zoomInEnabled = zoomValue < maxZoom;
    bool const zoomOutEnabled = zoomValue > minZoom;

    if ((zoomInEnabled != m_widget.zoomInButton->isEnabled()) || (zoomOutEnabled != m_widget.zoomOutButton->isEnabled())) {
        m_widget.zoomInButton->setEnabled(zoomInEnabled);
        m_widget.zoomOutButton->setEnabled(zoomOutEnabled);
        forceRepaint();
    }
}

void RoutingPluginPrivate::updateGuidanceModeButton()
{
    bool const hasRoute = m_routingModel->rowCount() > 0;
    m_widget.routingButton->setEnabled(hasRoute);
    forceRepaint();
}

void RoutingPluginPrivate::forceRepaint()
{
    m_parent->update();
    Q_EMIT m_parent->repaintNeeded();
}

void RoutingPluginPrivate::updateButtonVisibility()
{
    bool const show = m_guidanceModeEnabled;
    bool const near = show && m_nearNextInstruction;
    m_widget.progressBar->setVisible(near);
    m_widget.instructionIconLabel->setVisible(show);
    m_widget.spacer->changeSize(show ? 10 : 0, 20);
    m_widget.instructionLabel->setVisible(show);

    // m_widget.followingInstructionIconLabel->setVisible( show );
    // Disabling the next instruction turn icon for now, it seems to confuse first time users.
    m_widget.followingInstructionIconLabel->setVisible(false);

    m_widget.destinationDistanceLabel->setVisible(show);

    m_widget.gpsButton->setVisible(!show);
    m_widget.zoomOutButton->setVisible(!show);
    m_widget.zoomInButton->setVisible(!show);

    m_widgetItem->widget()->updateGeometry();
    QSize const size = m_widgetItem->widget()->sizeHint();
    m_widgetItem->widget()->resize(size);
    m_widgetItem->setContentSize(size);

    bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
    if (smallScreen) {
        qreal const pluginWidth = size.width();
        int x = -10;
        if (m_guidanceModeEnabled) {
            int const parentWidth = m_marbleWidget->width();
            x = qRound((parentWidth - pluginWidth) / 2.0);
        }
        m_parent->setPosition(QPointF(x, m_parent->position().y()));
    }
}

void RoutingPluginPrivate::updateZoomButtons()
{
    if (m_marbleWidget) {
        updateZoomButtons(m_marbleWidget->zoom());
    }
}

void RoutingPluginPrivate::toggleGuidanceMode(bool enabled)
{
    if (!m_marbleWidget || m_guidanceModeEnabled == enabled) {
        return;
    }

    m_guidanceModeEnabled = enabled;
    updateButtonVisibility();

    if (enabled) {
        QObject::connect(m_routingModel, SIGNAL(positionChanged()), m_parent, SLOT(updateDestinationInformation()));
    } else {
        QObject::disconnect(m_routingModel, SIGNAL(positionChanged()), m_parent, SLOT(updateDestinationInformation()));
    }

    if (enabled) {
        QString const text = QObject::tr("Starting guidance mode, please wait...");
        m_widget.instructionLabel->setText(richText(QStringLiteral("%1").arg(text)));
    }

    if (enabled) {
        RouteRequest *request = m_marbleWidget->model()->routingManager()->routeRequest();
        if (request && request->size() > 0) {
            GeoDataCoordinates source = request->source();
            if (source.isValid()) {
                GeoDataLookAt view;
                view.setCoordinates(source);
                // By happy coincidence this equals OpenStreetMap tile level 15
                view.setRange(851.807);
                m_marbleWidget->flyTo(view);
            }
        }
    }

    m_marbleWidget->model()->routingManager()->setGuidanceModeEnabled(enabled);

    if (enabled) {
        m_routeCompleted = false;
    }

    forceRepaint();
}

void RoutingPluginPrivate::updateDestinationInformation()
{
    if (m_routingModel->route().currentSegment().isValid()) {
        qreal remaining = remainingDistance();
        qreal distanceLeft = nextInstructionDistance();
        m_audio->update(m_routingModel->route(), distanceLeft, remaining, m_routingModel->deviatedFromRoute());

        m_nearNextInstruction = distanceLeft < thresholdDistance;

        QString pixmapHtml = QStringLiteral("<img src=\":/flag.png\" /><br />");
        m_widget.destinationDistanceLabel->setText(pixmapHtml + richText(fuzzyDistance(remaining)));

        m_widget.instructionIconLabel->setEnabled(m_nearNextInstruction);
        m_widget.progressBar->setMaximum(thresholdDistance);
        m_widget.progressBar->setValue(qRound(distanceLeft));

        updateButtonVisibility();

        QString pixmap = MarbleDirs::path(QStringLiteral("bitmaps/routing_step.png"));
        pixmapHtml = QStringLiteral("<img src=\"%1\" />").arg(pixmap);

        qreal planetRadius = m_marbleWidget->model()->planet()->radius();
        GeoDataCoordinates const onRoute = m_routingModel->route().positionOnRoute();
        GeoDataCoordinates const ego = m_routingModel->route().position();
        qreal const distanceToRoute = planetRadius * ego.sphericalDistanceTo(onRoute);

        if (!m_routingModel->route().currentSegment().isValid()) {
            m_widget.instructionLabel->setText(richText(QObject::tr("Calculate a route to get directions.")));
            m_widget.instructionIconLabel->setText(pixmapHtml);
        } else if (distanceToRoute > 300.0) {
            m_widget.instructionLabel->setText(richText(QObject::tr("Route left.")));
            m_widget.instructionIconLabel->setText(pixmapHtml);
        } else if (!m_routingModel->route().currentSegment().nextRouteSegment().isValid()) {
            m_widget.instructionLabel->setText(richText(QObject::tr("Destination ahead.")));
            m_widget.instructionIconLabel->setText(pixmapHtml);
        } else {
            pixmap = m_routingModel->route().currentSegment().nextRouteSegment().maneuver().directionPixmap();
            QString const instructionText = m_routingModel->route().currentSegment().nextRouteSegment().maneuver().instructionText();
            m_widget.instructionLabel->setText(richText(QStringLiteral("%1").arg(instructionText)));
            pixmapHtml = QStringLiteral("<p align=\"center\"><img src=\"%1\" /><br />%2</p>").arg(pixmap);
            m_widget.instructionIconLabel->setText(pixmapHtml.arg(richText(fuzzyDistance(distanceLeft))));

            if (remaining > 50) {
                m_routeCompleted = false;
            } else {
                if (!m_routeCompleted) {
                    QString content = QObject::tr("Arrived at destination. <a href=\"#reverse\">Calculate the way back.</a>");
                    m_widget.instructionLabel->setText(richText(QStringLiteral("%1").arg(content)));
                }
                m_routeCompleted = true;
            }
        }

        forceRepaint();
    }
}

void RoutingPluginPrivate::updateGpsButton(PositionProviderPlugin *activePlugin)
{
    m_widget.gpsButton->setChecked(activePlugin != nullptr);
    forceRepaint();
}

void RoutingPluginPrivate::togglePositionTracking(bool enabled)
{
    PositionProviderPlugin *plugin = nullptr;
    if (enabled) {
        const PluginManager *pluginManager = m_marbleWidget->model()->pluginManager();
        const QList<const PositionProviderPlugin *> plugins = pluginManager->positionProviderPlugins();
        if (!plugins.isEmpty()) {
            plugin = plugins.constFirst()->newInstance();
        }
    }
    m_parent->marbleModel()->positionTracking()->setPositionProviderPlugin(plugin);
}

void RoutingPluginPrivate::reverseRoute()
{
    if (m_marbleWidget) {
        m_marbleWidget->model()->routingManager()->reverseRoute();
    }
}

void RoutingPluginPrivate::readSettings()
{
    if (m_configDialog) {
        if (!m_speakersModel) {
            m_speakersModel = new SpeakersModel(m_parent);
        }
        int const index = m_speakersModel->indexOf(m_audio->speaker());
        m_configUi.speakerComboBox->setModel(m_speakersModel);
        m_configUi.speakerComboBox->setCurrentIndex(index);
        m_configUi.voiceNavigationCheckBox->setChecked(!m_audio->isMuted());
        m_configUi.soundRadioButton->setChecked(m_audio->isSoundEnabled());
        m_configUi.speakerRadioButton->setChecked(!m_audio->isSoundEnabled());
    }
}

qreal RoutingPluginPrivate::nextInstructionDistance() const
{
    GeoDataCoordinates position = m_routingModel->route().position();
    GeoDataCoordinates interpolated = m_routingModel->route().positionOnRoute();
    GeoDataCoordinates onRoute = m_routingModel->route().currentWaypoint();
    qreal planetRadius = m_marbleWidget->model()->planet()->radius();
    const qreal distance = planetRadius * (position.sphericalDistanceTo(interpolated) + interpolated.sphericalDistanceTo(onRoute));
    const RouteSegment &segment = m_routingModel->route().currentSegment();
    for (int i = 0; i < segment.path().size(); ++i) {
        if (segment.path()[i] == onRoute) {
            return distance + segment.path().length(planetRadius, i);
        }
    }

    return distance;
}

qreal RoutingPluginPrivate::remainingDistance() const
{
    GeoDataCoordinates position = m_routingModel->route().currentSegment().maneuver().position();
    bool foundSegment = false;
    qreal distance = nextInstructionDistance();
    for (int i = 0; i < m_routingModel->route().size(); ++i) {
        if (foundSegment) {
            distance += m_routingModel->route().at(i).distance();
        } else {
            foundSegment = m_routingModel->route().at(i).maneuver().position() == position;
        }
    }

    return distance;
}

void RoutingPlugin::writeSettings()
{
    Q_ASSERT(d->m_configDialog);
    int const index = d->m_configUi.speakerComboBox->currentIndex();
    if (index >= 0) {
        QModelIndex const idx = d->m_speakersModel->index(index);
        d->m_audio->setSpeaker(d->m_speakersModel->data(idx, SpeakersModel::Path).toString());
        if (!d->m_speakersModel->data(idx, SpeakersModel::IsLocal).toBool()) {
            d->m_speakersModel->install(index);
        }
    }
    d->m_audio->setMuted(!d->m_configUi.voiceNavigationCheckBox->isChecked());
    d->m_audio->setSoundEnabled(d->m_configUi.soundRadioButton->isChecked());
    d->readSettings();
    Q_EMIT settingsChanged(nameId());
}

RoutingPlugin::RoutingPlugin()
    : AbstractFloatItem(nullptr)
    , d(nullptr)
{
}

RoutingPlugin::RoutingPlugin(const MarbleModel *marbleModel)
    : AbstractFloatItem(marbleModel, QPointF(-10, -10))
    , d(new RoutingPluginPrivate(this))
{
    setEnabled(true);
    // plugin is visible by default on small screen devices
    setVisible(MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen);
    setPadding(0.5);
    setBorderWidth(1);
    setBackground(QBrush(QColor("white")));
}

RoutingPlugin::~RoutingPlugin()
{
    delete d;
}

QStringList RoutingPlugin::backendTypes() const
{
    return QStringList(QStringLiteral("routing"));
}

QString RoutingPlugin::name() const
{
    return tr("Routing");
}

QString RoutingPlugin::guiString() const
{
    return tr("&Routing");
}

QString RoutingPlugin::nameId() const
{
    return QStringLiteral("routing");
}

QString RoutingPlugin::version() const
{
    return QStringLiteral("1.0");
}

QString RoutingPlugin::description() const
{
    return tr("Routing information and navigation controls");
}

QString RoutingPlugin::copyrightYears() const
{
    return QStringLiteral("2010");
}

QList<PluginAuthor> RoutingPlugin::pluginAuthors() const
{
    return QList<PluginAuthor>() << PluginAuthor(QStringLiteral("Siddharth Srivastava"), QStringLiteral("akssps011@gmail.com"))
                                 << PluginAuthor(QStringLiteral("Dennis Nienhüser"), QStringLiteral("nienhueser@kde.org"));
}

QIcon RoutingPlugin::icon() const
{
    return QIcon(QStringLiteral(":/icons/routeplanning.png"));
}

void RoutingPlugin::initialize()
{
    auto widget = new QWidget;
    d->m_widget.setupUi(widget);
    d->m_widgetItem = new WidgetGraphicsItem(this);
    d->m_widgetItem->setWidget(widget);

    PositionProviderPlugin *activePlugin = marbleModel()->positionTracking()->positionProviderPlugin();
    d->updateGpsButton(activePlugin);
    connect(marbleModel()->positionTracking(),
            SIGNAL(positionProviderPluginChanged(PositionProviderPlugin *)),
            this,
            SLOT(updateGpsButton(PositionProviderPlugin *)));

    d->m_widget.routingButton->setEnabled(false);
    connect(d->m_widget.instructionLabel, SIGNAL(linkActivated(QString)), this, SLOT(reverseRoute()));

    auto layout = new MarbleGraphicsGridLayout(1, 1);
    layout->addItem(d->m_widgetItem, 0, 0);
    setLayout(layout);
    d->updateButtonVisibility();
}

bool RoutingPlugin::isInitialized() const
{
    return d->m_widgetItem;
}

bool RoutingPlugin::eventFilter(QObject *object, QEvent *e)
{
    if (d->m_marbleWidget || !enabled() || !visible()) {
        return AbstractFloatItem::eventFilter(object, e);
    }

    auto widget = dynamic_cast<MarbleWidget *>(object);

    if (widget && !d->m_marbleWidget) {
        d->m_marbleWidget = widget;
        d->m_routingModel = d->m_marbleWidget->model()->routingManager()->routingModel();

        connect(d->m_widget.routingButton, SIGNAL(clicked(bool)), this, SLOT(toggleGuidanceMode(bool)));
        connect(d->m_widget.gpsButton, SIGNAL(clicked(bool)), this, SLOT(togglePositionTracking(bool)));
        connect(d->m_widget.zoomInButton, SIGNAL(clicked()), d->m_marbleWidget, SLOT(zoomIn()));
        connect(d->m_widget.zoomOutButton, SIGNAL(clicked()), d->m_marbleWidget, SLOT(zoomOut()));
        connect(d->m_marbleWidget, SIGNAL(themeChanged(QString)), this, SLOT(updateZoomButtons()));
        connect(d->m_marbleWidget, SIGNAL(zoomChanged(int)), this, SLOT(updateZoomButtons(int)));
        connect(d->m_routingModel, SIGNAL(currentRouteChanged()), this, SLOT(updateGuidanceModeButton()));
        d->updateGuidanceModeButton();
    }
    return AbstractFloatItem::eventFilter(object, e);
}

QHash<QString, QVariant> RoutingPlugin::settings() const
{
    QHash<QString, QVariant> result = AbstractFloatItem::settings();

    result.insert(QStringLiteral("muted"), d->m_audio->isMuted());
    result.insert(QStringLiteral("sound"), d->m_audio->isSoundEnabled());
    result.insert(QStringLiteral("speaker"), d->m_audio->speaker());

    return result;
}

void RoutingPlugin::setSettings(const QHash<QString, QVariant> &settings)
{
    AbstractFloatItem::setSettings(settings);

    d->m_audio->setMuted(settings.value(QStringLiteral("muted"), false).toBool());
    d->m_audio->setSoundEnabled(settings.value(QStringLiteral("sound"), true).toBool());
    d->m_audio->setSpeaker(settings.value(QStringLiteral("speaker")).toString());

    d->readSettings();
}

QDialog *RoutingPlugin::configDialog()
{
    if (!d->m_configDialog) {
        d->m_configDialog = new QDialog;
        d->m_configUi.setupUi(d->m_configDialog);
        d->readSettings();

        connect(d->m_configDialog, &QDialog::accepted, this, &RoutingPlugin::writeSettings);
        connect(d->m_configDialog, SIGNAL(rejected()), this, SLOT(readSettings()));
        connect(d->m_configUi.buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &RenderPlugin::restoreDefaultSettings);
    }

    return d->m_configDialog;
}

}

#include "moc_RoutingPlugin.cpp"
