// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
// SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <illya.kovalevskyy@gmail.com>
//

#include "RoutingInputWidget.h"

#include "MarblePlacemarkModel.h"
#include "RouteRequest.h"

#ifdef MARBLE_NO_WEBKITWIDGETS
#include "NullTinyWebBrowser.h"
#else
#include "TinyWebBrowser.h"
#endif

#include "BookmarkManager.h"
#include "GeoDataData.h"
#include "GeoDataExtendedData.h"
#include "GeoDataFolder.h"
#include "GeoDataPlacemark.h"
#include "GoToDialog.h"
#include "MarbleColors.h"
#include "MarbleLineEdit.h"
#include "MarbleModel.h"
#include "MarbleWidget.h"
#include "PositionTracking.h"
#include "ReverseGeocodingRunnerManager.h"
#include "SearchRunnerManager.h"
#include "routing/RoutingManager.h"

#include <QHBoxLayout>
#include <QIcon>
#include <QKeyEvent>
#include <QMenu>
#include <QPainter>
#include <QPushButton>
#include <QTimer>

namespace Marble
{

/**
 * A MarbleLineEdit that swallows enter/return pressed
 * key events
 */
class RoutingInputLineEdit : public MarbleLineEdit
{
public:
    explicit RoutingInputLineEdit(QWidget *parent = nullptr);

protected:
    void keyPressEvent(QKeyEvent *) override;
};

class RoutingInputWidgetPrivate
{
public:
    MarbleModel *m_marbleModel = nullptr;

    RoutingInputLineEdit *m_lineEdit = nullptr;

    QPushButton *m_removeButton = nullptr;

    SearchRunnerManager m_placemarkRunnerManager;
    ReverseGeocodingRunnerManager m_reverseGeocodingRunnerManager;

    MarblePlacemarkModel *m_placemarkModel = nullptr;

    RouteRequest *m_route = nullptr;

    int m_index;

    QTimer m_nominatimTimer;

    QAction *m_bookmarkAction;

    QAction *m_mapInput;

    QAction *m_currentLocationAction;

    QAction *m_centerAction;

    QMenu *m_menu;

    /** Constructor */
    RoutingInputWidgetPrivate(MarbleModel *model, int index, QWidget *parent);

    /** Initiate reverse geocoding request to download address */
    void adjustText();

    void createMenu(RoutingInputWidget *parent);

    QMenu *createBookmarkMenu(RoutingInputWidget *parent);

    static void createBookmarkActions(QMenu *menu, GeoDataFolder *bookmarksFolder, QObject *parent);

    static QPixmap addDropDownIndicator(const QPixmap &pixmap);

    void updateDescription();
};

void RoutingInputWidgetPrivate::updateDescription()
{
    GeoDataPlacemark const placemark = (*m_route)[m_index];
    GeoDataExtendedData const address = placemark.extendedData();
    if (address.contains(QStringLiteral("road")) && address.contains(QStringLiteral("city"))) {
        QString const road = address.value(QStringLiteral("road")).value().toString();
        QString const city = address.value(QStringLiteral("city")).value().toString();

        if (address.contains(QStringLiteral("house_number"))) {
            QString const houseNumber = address.value(QStringLiteral("house_number")).value().toString();
            QString const name = QObject::tr("%1 %2, %3", "An address with parameters %1=house number, %2=road, %3=city");
            m_lineEdit->setText(name.arg(houseNumber, road, city));
        } else {
            QString const name = QObject::tr("%2, %3", "An address with parameters %1=road, %2=city");
            m_lineEdit->setText(name.arg(road, city));
        }
    } else if (m_route->name(m_index).isEmpty()) {
        if (!placemark.address().isEmpty()) {
            m_lineEdit->setText(placemark.address());
        } else {
            m_lineEdit->setText(placemark.coordinate().toString().trimmed());
        }
    } else {
        m_lineEdit->setText(placemark.name());
    }
    m_lineEdit->setCursorPosition(0);
}

RoutingInputLineEdit::RoutingInputLineEdit(QWidget *parent)
    : MarbleLineEdit(parent)
{
    setPlaceholderText(QObject::tr("Address or search term..."));
}

void RoutingInputLineEdit::keyPressEvent(QKeyEvent *event)
{
    MarbleLineEdit::keyPressEvent(event);
    bool const returnPressed = event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter;
    if (returnPressed) {
        event->accept();
    }
}

RoutingInputWidgetPrivate::RoutingInputWidgetPrivate(MarbleModel *model, int index, QWidget *parent)
    : m_marbleModel(model)
    , m_lineEdit(nullptr)
    , m_placemarkRunnerManager(m_marbleModel)
    , m_reverseGeocodingRunnerManager(m_marbleModel)
    , m_placemarkModel(nullptr)
    , m_route(m_marbleModel->routingManager()->routeRequest())
    , m_index(index)
    , m_bookmarkAction(nullptr)
    , m_mapInput(nullptr)
    , m_currentLocationAction(nullptr)
    , m_centerAction(nullptr)
    , m_menu(nullptr)
{
    m_lineEdit = new RoutingInputLineEdit(parent);
    m_lineEdit->setDecorator(addDropDownIndicator(m_route->pixmap(m_index)));

    m_removeButton = new QPushButton(parent);
    m_removeButton->setIcon(QIcon(QStringLiteral(":/marble/routing/icon-remove.png")));
    m_removeButton->setToolTip(QObject::tr("Remove via point"));
    m_removeButton->setFlat(true);
    m_removeButton->setMaximumWidth(18);

    m_nominatimTimer.setInterval(1000);
    m_nominatimTimer.setSingleShot(true);
}

void RoutingInputWidgetPrivate::adjustText()
{
    m_nominatimTimer.start();
}

void RoutingInputWidgetPrivate::createMenu(RoutingInputWidget *parent)
{
    auto result = new QMenu(parent);

    m_centerAction = result->addAction(QIcon(m_route->pixmap(m_index)), QObject::tr("&Center Map here"), parent, SLOT(requestActivity()));
    result->addSeparator();

    m_currentLocationAction = result->addAction(QIcon(QStringLiteral(":/icons/gps.png")), QObject::tr("Current &Location"), parent, SLOT(setCurrentLocation()));
    m_currentLocationAction->setEnabled(false);

    m_mapInput = result->addAction(QIcon(QStringLiteral(":/icons/crosshairs.png")), QObject::tr("From &Map..."));
    m_mapInput->setCheckable(true);
    QObject::connect(m_mapInput, SIGNAL(triggered(bool)), parent, SLOT(setMapInputModeEnabled(bool)));

    m_bookmarkAction = result->addAction(QIcon(QStringLiteral(":/icons/bookmarks.png")), QObject::tr("From &Bookmark"));
    m_bookmarkAction->setMenu(createBookmarkMenu(parent));

    m_menu = result;
}

QMenu *RoutingInputWidgetPrivate::createBookmarkMenu(RoutingInputWidget *parent)
{
    auto result = new QMenu(parent);
    result->addAction(QIcon(QStringLiteral(":/icons/go-home.png")), QObject::tr("&Home"), parent, SLOT(setHomePosition()));

    QList<GeoDataFolder *> folders = m_marbleModel->bookmarkManager()->folders();

    if (folders.size() == 1) {
        createBookmarkActions(result, folders.first(), parent);
    } else {
        QList<GeoDataFolder *>::const_iterator i = folders.constBegin();
        QList<GeoDataFolder *>::const_iterator end = folders.constEnd();

        for (; i != end; ++i) {
            QMenu *menu = result->addMenu(QIcon(QStringLiteral(":/icons/folder-bookmark.png")), (*i)->name());
            createBookmarkActions(menu, *i, parent);
        }
    }

    return result;
}

void RoutingInputWidgetPrivate::createBookmarkActions(QMenu *menu, GeoDataFolder *bookmarksFolder, QObject *parent)
{
    QList<GeoDataPlacemark *> bookmarks = bookmarksFolder->placemarkList();
    QList<GeoDataPlacemark *>::const_iterator i = bookmarks.constBegin();
    QList<GeoDataPlacemark *>::const_iterator end = bookmarks.constEnd();

    for (; i != end; ++i) {
        auto bookmarkAction = new QAction((*i)->name(), parent);
        bookmarkAction->setData(QVariant::fromValue((*i)->coordinate()));
        menu->addAction(bookmarkAction);
        QObject::connect(menu, SIGNAL(triggered(QAction *)), parent, SLOT(setBookmarkPosition(QAction *)));
    }
}

QPixmap RoutingInputWidgetPrivate::addDropDownIndicator(const QPixmap &pixmap)
{
    QPixmap result(pixmap.size() + QSize(8, pixmap.height()));
    result.fill(QColor(Qt::transparent));
    QPainter painter(&result);
    painter.drawPixmap(0, 0, pixmap);
    QPoint const one(pixmap.width() + 1, pixmap.height() - 8);
    QPoint const two(one.x() + 6, one.y());
    QPoint const three(one.x() + 3, one.y() + 4);
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(Oxygen::aluminumGray4));
    painter.drawConvexPolygon(QPolygon() << one << two << three);
    return result;
}

RoutingInputWidget::RoutingInputWidget(MarbleModel *model, int index, QWidget *parent)
    : QWidget(parent)
    , d(new RoutingInputWidgetPrivate(model, index, this))
{
    auto layout = new QHBoxLayout(this);
    layout->setSizeConstraint(QLayout::SetMinimumSize);
    layout->setSpacing(0);
    layout->setContentsMargins({});
    layout->addWidget(d->m_lineEdit);
    layout->addWidget(d->m_removeButton);

    bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
    if (smallScreen) {
        connect(d->m_lineEdit, &MarbleLineEdit::decoratorButtonClicked, this, &RoutingInputWidget::openTargetSelectionDialog);
    } else {
        d->createMenu(this);
        connect(d->m_lineEdit, &MarbleLineEdit::decoratorButtonClicked, this, &RoutingInputWidget::showMenu);
    }

    connect(d->m_removeButton, &QAbstractButton::clicked, this, &RoutingInputWidget::requestRemoval);
    connect(d->m_marbleModel->bookmarkManager(), SIGNAL(bookmarksChanged()), this, SLOT(reloadBookmarks()));
    connect(d->m_marbleModel->positionTracking(),
            SIGNAL(statusChanged(PositionProviderStatus)),
            this,
            SLOT(updateCurrentLocationButton(PositionProviderStatus)));
    connect(&d->m_placemarkRunnerManager, SIGNAL(searchResultChanged(QAbstractItemModel *)), this, SLOT(setPlacemarkModel(QAbstractItemModel *)));
    connect(&d->m_reverseGeocodingRunnerManager,
            SIGNAL(reverseGeocodingFinished(GeoDataCoordinates, GeoDataPlacemark)),
            this,
            SLOT(retrieveReverseGeocodingResult(GeoDataCoordinates, GeoDataPlacemark)));
    connect(d->m_lineEdit, SIGNAL(returnPressed()), this, SLOT(findPlacemarks()));
    connect(d->m_lineEdit, &QLineEdit::textEdited, this, &RoutingInputWidget::setInvalid);
    connect(&d->m_placemarkRunnerManager, SIGNAL(searchFinished(QString)), this, SLOT(finishSearch()));
    connect(d->m_marbleModel->routingManager()->routeRequest(),
            SIGNAL(positionChanged(int, GeoDataCoordinates)),
            this,
            SLOT(updatePosition(int, GeoDataCoordinates)));
    connect(&d->m_nominatimTimer, &QTimer::timeout, this, &RoutingInputWidget::reverseGeocoding);
    connect(this, SIGNAL(targetValidityChanged(bool)), this, SLOT(updateCenterButton(bool)));
    updateCenterButton(hasTargetPosition());

    d->adjustText();
}

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

void RoutingInputWidget::reverseGeocoding()
{
    if (!hasTargetPosition()) {
        return;
    }

    QString const name = d->m_route->name(d->m_index);
    if (name.isEmpty() || name == tr("Current Location")) {
        d->m_reverseGeocodingRunnerManager.reverseGeocoding(targetPosition());
    } else {
        d->updateDescription();
    }
}

void RoutingInputWidget::setPlacemarkModel(QAbstractItemModel *model)
{
    d->m_placemarkModel = dynamic_cast<MarblePlacemarkModel *>(model);
}

void RoutingInputWidget::setTargetPosition(const GeoDataCoordinates &position, const QString &name)
{
    if (d->m_mapInput) {
        d->m_mapInput->setChecked(false);
    }
    d->m_route->setPosition(d->m_index, position, name);
    if (!name.isEmpty()) {
        d->updateDescription();
    }
    Q_EMIT targetValidityChanged(true);
}

bool RoutingInputWidget::hasTargetPosition() const
{
    return targetPosition().isValid();
}

GeoDataCoordinates RoutingInputWidget::targetPosition() const
{
    if (d->m_index < d->m_route->size()) {
        return d->m_route->at(d->m_index);
    } else {
        return {};
    }
}

void RoutingInputWidget::findPlacemarks()
{
    QString text = d->m_lineEdit->text();
    if (text.isEmpty()) {
        setInvalid();
    } else {
        d->m_lineEdit->setBusy(true);
        d->m_placemarkRunnerManager.findPlacemarks(text);
    }
}

MarblePlacemarkModel *RoutingInputWidget::searchResultModel()
{
    return d->m_placemarkModel;
}

void RoutingInputWidget::requestActivity()
{
    if (hasTargetPosition()) {
        Q_EMIT activityRequest(this);
    }
}

void RoutingInputWidget::requestRemoval()
{
    Q_EMIT removalRequest(this);
}

bool RoutingInputWidget::hasInput() const
{
    return !d->m_lineEdit->text().isEmpty();
}

void RoutingInputWidget::setMapInputModeEnabled(bool enabled)
{
    Q_EMIT mapInputModeEnabled(this, enabled);
}

void RoutingInputWidget::finishSearch()
{
    d->m_lineEdit->setBusy(false);
    Q_EMIT searchFinished(this);
}

void RoutingInputWidget::setInvalid()
{
    d->m_route->setPosition(d->m_index, GeoDataCoordinates());
    Q_EMIT targetValidityChanged(false);
}

void RoutingInputWidget::abortMapInputRequest()
{
    if (d->m_mapInput) {
        d->m_mapInput->setChecked(false);
    }
}

void RoutingInputWidget::setIndex(int index)
{
    d->m_index = index;
    d->m_lineEdit->setBusy(false);
    d->m_lineEdit->setDecorator(d->addDropDownIndicator(d->m_route->pixmap(index)));
}

void RoutingInputWidget::updatePosition(int index, const GeoDataCoordinates &)
{
    if (index == d->m_index) {
        d->m_lineEdit->setBusy(false);
        Q_EMIT targetValidityChanged(hasTargetPosition());
        d->adjustText();
    }
}

void RoutingInputWidget::clear()
{
    d->m_nominatimTimer.stop();
    d->m_lineEdit->setBusy(false);
    d->m_route->setPosition(d->m_index, GeoDataCoordinates());
    d->m_lineEdit->clear();
    Q_EMIT targetValidityChanged(false);
}

void RoutingInputWidget::retrieveReverseGeocodingResult(const GeoDataCoordinates &, const GeoDataPlacemark &placemark)
{
    (*d->m_route)[d->m_index] = placemark;
    d->updateDescription();
}

void RoutingInputWidget::reloadBookmarks()
{
    if (d->m_bookmarkAction) {
        d->m_bookmarkAction->setMenu(d->createBookmarkMenu(this));
    }
}

void RoutingInputWidget::setHomePosition()
{
    qreal lon(0.0), lat(0.0);
    int zoom(0);
    d->m_marbleModel->home(lon, lat, zoom);
    GeoDataCoordinates home(lon, lat, 0.0, GeoDataCoordinates::Degree);
    setTargetPosition(home);
    requestActivity();
}

void RoutingInputWidget::updateCurrentLocationButton(PositionProviderStatus status)
{
    if (d->m_currentLocationAction) {
        d->m_currentLocationAction->setEnabled(status == PositionProviderStatusAvailable);
    }
}

void RoutingInputWidget::setCurrentLocation()
{
    setTargetPosition(d->m_marbleModel->positionTracking()->currentLocation());
    requestActivity();
}

void RoutingInputWidget::updateCenterButton(bool hasPosition)
{
    if (d->m_centerAction) {
        d->m_centerAction->setEnabled(hasPosition);
    }
}

void RoutingInputWidget::setBookmarkPosition(QAction *bookmark)
{
    if (!bookmark->data().isNull()) {
        setTargetPosition(bookmark->data().value<GeoDataCoordinates>());
        requestActivity();
    }
}

void RoutingInputWidget::openTargetSelectionDialog()
{
    QPointer<GoToDialog> dialog = new GoToDialog(d->m_marbleModel, this);
    dialog->setWindowTitle(tr("Choose Placemark"));
    dialog->setShowRoutingItems(false);
    dialog->setSearchEnabled(false);
    if (dialog->exec() == QDialog::Accepted) {
        const GeoDataCoordinates coordinates = dialog->coordinates();
        setTargetPosition(coordinates);
    }
    delete dialog;
}

void RoutingInputWidget::showMenu()
{
    d->m_menu->exec(mapToGlobal(QPoint(0, size().height())));
}

} // namespace Marble

#include "moc_RoutingInputWidget.cpp"
