// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
// SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
//

#include "GoToDialog.h"
#include "ui_GoToDialog.h"

#include "BookmarkManager.h"
#include "GeoDataDocument.h"
#include "GeoDataFolder.h"
#include "GeoDataLookAt.h"
#include "GeoDataPlacemark.h"
#include "GeoDataTreeModel.h"
#include "MarbleModel.h"
#include "MarblePlacemarkModel.h"
#include "MarbleWidget.h"
#include "PositionTracking.h"
#include "SearchRunnerManager.h"
#include "routing/RouteRequest.h"
#include "routing/RoutingManager.h"

#include <QAbstractListModel>
#include <QPainter>
#include <QTimer>

namespace Marble
{

class TargetModel : public QAbstractListModel
{
    Q_OBJECT
public:
    TargetModel(MarbleModel *marbleModel, QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

    void setShowRoutingItems(bool show);

private:
    QVariant currentLocationData(int role) const;

    QVariant routeData(const QList<GeoDataPlacemark> &via, int index, int role) const;

    QVariant homeData(int role) const;

    QVariant bookmarkData(int index, int role) const;

    QList<GeoDataPlacemark> viaPoints() const;

    MarbleModel *const m_marbleModel;

    QList<GeoDataPlacemark *> m_bookmarks;

    bool m_hasCurrentLocation;

    bool m_showRoutingItems;
};

class GoToDialogPrivate : public Ui::GoTo
{
public:
    GoToDialog *m_parent;

    GeoDataCoordinates m_coordinates;

    MarbleModel *const m_marbleModel;

    TargetModel m_targetModel;

    SearchRunnerManager m_runnerManager;

    GeoDataDocument *m_searchResult;

    GeoDataTreeModel m_searchResultModel;

    QTimer m_progressTimer;

    int m_currentFrame;

    QList<QIcon> m_progressAnimation;

    GoToDialogPrivate(GoToDialog *parent, MarbleModel *marbleModel);

    void saveSelection(const QModelIndex &index);

    void createProgressAnimation();

    void startSearch();

    void updateSearchResult(const QList<GeoDataPlacemark *> &placemarks);

    void updateSearchMode();

    void updateProgress();

    void stopProgressAnimation();

    void updateResultMessage(int results);
};

TargetModel::TargetModel(MarbleModel *marbleModel, QObject *parent)
    : QAbstractListModel(parent)
    , m_marbleModel(marbleModel)
    , m_hasCurrentLocation(false)
    , m_showRoutingItems(true)
{
    BookmarkManager *manager = m_marbleModel->bookmarkManager();
    for (GeoDataFolder *folder : manager->folders()) {
        QList<GeoDataPlacemark *> bookmarks = folder->placemarkList();
        QList<GeoDataPlacemark *>::const_iterator iter = bookmarks.constBegin();
        QList<GeoDataPlacemark *>::const_iterator end = bookmarks.constEnd();

        for (; iter != end; ++iter) {
            m_bookmarks.push_back(*iter);
        }
    }

    PositionTracking *tracking = m_marbleModel->positionTracking();
    m_hasCurrentLocation = tracking && tracking->status() == PositionProviderStatusAvailable;
}

QList<GeoDataPlacemark> TargetModel::viaPoints() const
{
    if (!m_showRoutingItems) {
        return {};
    }

    RouteRequest *request = m_marbleModel->routingManager()->routeRequest();
    QList<GeoDataPlacemark> result;
    for (int i = 0; i < request->size(); ++i) {
        if (request->at(i).isValid()) {
            GeoDataPlacemark placemark;
            placemark.setCoordinate(request->at(i));
            placemark.setName(request->name(i));
            result.push_back(placemark);
        }
    }
    return result;
}

int TargetModel::rowCount(const QModelIndex &parent) const
{
    int result = 0;
    if (!parent.isValid()) {
        result += m_hasCurrentLocation ? 1 : 0;
        result += viaPoints().size(); // route
        result += 1; // home location
        result += m_bookmarks.size(); // bookmarks
        return result;
    }

    return result;
}

QVariant TargetModel::currentLocationData(int role) const
{
    const PositionTracking *tracking = m_marbleModel->positionTracking();
    if (tracking->status() == PositionProviderStatusAvailable) {
        GeoDataCoordinates currentLocation = tracking->currentLocation();
        switch (role) {
        case Qt::DisplayRole:
            return tr("Current Location: %1").arg(currentLocation.toString());
        case Qt::DecorationRole:
            return QIcon(QStringLiteral(":/icons/gps.png"));
        case MarblePlacemarkModel::CoordinateRole: {
            return QVariant::fromValue(currentLocation);
        }
        }
    }

    return {};
}

QVariant TargetModel::routeData(const QList<GeoDataPlacemark> &via, int index, int role) const
{
    RouteRequest *request = m_marbleModel->routingManager()->routeRequest();
    switch (role) {
    case Qt::DisplayRole:
        return via.at(index).name();
    case Qt::DecorationRole:
        return QIcon(request->pixmap(index));
    case MarblePlacemarkModel::CoordinateRole: {
        const GeoDataCoordinates coordinates = via.at(index).coordinate();
        return QVariant::fromValue(coordinates);
    }
    }

    return {};
}

QVariant TargetModel::homeData(int role) const
{
    switch (role) {
    case Qt::DisplayRole:
        return tr("Home");
    case Qt::DecorationRole:
        return QIcon(QStringLiteral(":/icons/go-home.png"));
    case MarblePlacemarkModel::CoordinateRole: {
        qreal lon(0.0), lat(0.0);
        int zoom(0);
        m_marbleModel->home(lon, lat, zoom);
        const GeoDataCoordinates coordinates = GeoDataCoordinates(lon, lat, 0, GeoDataCoordinates::Degree);
        return QVariant::fromValue(coordinates);
    }
    }

    return {};
}

QVariant TargetModel::bookmarkData(int index, int role) const
{
    switch (role) {
    case Qt::DisplayRole: {
        const GeoDataFolder *folder = geodata_cast<GeoDataFolder>(m_bookmarks[index]->parent());
        Q_ASSERT(folder && "Internal bookmark representation has changed. Please report this as a bug at https://bugs.kde.org.");
        if (folder) {
            return QString(folder->name() + QLatin1StringView(" / ") + m_bookmarks[index]->name());
        }
        return {};
    }
    case Qt::DecorationRole:
        return QIcon(QStringLiteral(":/icons/bookmarks.png"));
    case MarblePlacemarkModel::CoordinateRole:
        return QVariant::fromValue(m_bookmarks[index]->lookAt()->coordinates());
    }

    return {};
}

QVariant TargetModel::data(const QModelIndex &index, int role) const
{
    if (index.isValid() && index.row() >= 0 && index.row() < rowCount()) {
        int row = index.row();
        bool const isCurrentLocation = row == 0 && m_hasCurrentLocation;
        int homeOffset = m_hasCurrentLocation ? 1 : 0;
        QList<GeoDataPlacemark> via = viaPoints();
        bool const isRoute = row >= homeOffset && row < homeOffset + via.size();

        if (isCurrentLocation) {
            return currentLocationData(role);
        } else if (isRoute) {
            int routeIndex = row - homeOffset;
            Q_ASSERT(routeIndex >= 0 && routeIndex < via.size());
            return routeData(via, routeIndex, role);
        } else {
            int bookmarkIndex = row - homeOffset - via.size();
            if (bookmarkIndex == 0) {
                return homeData(role);
            } else {
                --bookmarkIndex;
                Q_ASSERT(bookmarkIndex >= 0 && bookmarkIndex < m_bookmarks.size());
                return bookmarkData(bookmarkIndex, role);
            }
        }
    }

    return {};
}

void TargetModel::setShowRoutingItems(bool show)
{
    m_showRoutingItems = show;
    beginResetModel();
    endResetModel();
}

void GoToDialogPrivate::createProgressAnimation()
{
    bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
    int const iconSize = smallScreen ? 32 : 16;

    // Size parameters
    qreal const h = iconSize / 2.0; // Half of the icon size
    qreal const q = h / 2.0; // Quarter of the icon size
    qreal const d = 7.5; // Circle diameter
    qreal const r = d / 2.0; // Circle radius

    // Canvas parameters
    QImage canvas(iconSize, iconSize, QImage::Format_ARGB32);
    QPainter painter(&canvas);
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.setPen(QColor(Qt::gray));
    painter.setBrush(QColor(Qt::white));

    // Create all frames
    for (double t = 0.0; t < 2 * M_PI; t += M_PI / 8.0) {
        canvas.fill(Qt::transparent);
        QRectF firstCircle(h - r + q * cos(t), h - r + q * sin(t), d, d);
        QRectF secondCircle(h - r + q * cos(t + M_PI), h - r + q * sin(t + M_PI), d, d);
        painter.drawEllipse(firstCircle);
        painter.drawEllipse(secondCircle);
        m_progressAnimation.push_back(QIcon(QPixmap::fromImage(canvas)));
    }
}

GoToDialogPrivate::GoToDialogPrivate(GoToDialog *parent, MarbleModel *marbleModel)
    : m_parent(parent)
    , m_marbleModel(marbleModel)
    , m_targetModel(marbleModel)
    , m_runnerManager(marbleModel)
    , m_searchResult(new GeoDataDocument)
    , m_currentFrame(0)
{
    setupUi(parent);

    m_progressTimer.setInterval(100);
}

void GoToDialogPrivate::saveSelection(const QModelIndex &index)
{
    if (searchButton->isChecked() && m_searchResult->size()) {
        QVariant coordinates = m_searchResultModel.data(index, MarblePlacemarkModel::CoordinateRole);
        m_coordinates = coordinates.value<GeoDataCoordinates>();
    } else {
        QVariant coordinates = index.data(MarblePlacemarkModel::CoordinateRole);
        m_coordinates = coordinates.value<GeoDataCoordinates>();
    }
    m_parent->accept();
}

void GoToDialogPrivate::startSearch()
{
    QString const searchTerm = searchLineEdit->text().trimmed();
    if (searchTerm.isEmpty()) {
        return;
    }

    m_runnerManager.findPlacemarks(searchTerm);
    if (m_progressAnimation.isEmpty()) {
        createProgressAnimation();
    }
    m_progressTimer.start();
    progressButton->setVisible(true);
    searchLineEdit->setEnabled(false);
    updateResultMessage(0);
}

void GoToDialogPrivate::updateSearchResult(const QList<GeoDataPlacemark *> &placemarks)
{
    m_searchResultModel.setRootDocument(nullptr);
    m_searchResult->clear();
    for (GeoDataPlacemark *placemark : placemarks) {
        m_searchResult->append(new GeoDataPlacemark(*placemark));
    }
    m_searchResultModel.setRootDocument(m_searchResult);
    bookmarkListView->setModel(&m_searchResultModel);
    updateResultMessage(m_searchResultModel.rowCount());
}

GoToDialog::GoToDialog(MarbleModel *marbleModel, QWidget *parent, Qt::WindowFlags flags)
    : QDialog(parent, flags)
    , d(new GoToDialogPrivate(this, marbleModel))
{
    d->searchLineEdit->setPlaceholderText(tr("Address or search term"));

    d->m_searchResultModel.setRootDocument(d->m_searchResult);
    d->bookmarkListView->setModel(&d->m_targetModel);
    connect(d->bookmarkListView, SIGNAL(activated(QModelIndex)), this, SLOT(saveSelection(QModelIndex)));
    connect(d->searchLineEdit, SIGNAL(returnPressed()), this, SLOT(startSearch()));
    d->buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(false);
    connect(d->searchButton, SIGNAL(clicked(bool)), this, SLOT(updateSearchMode()));
    connect(d->browseButton, SIGNAL(clicked(bool)), this, SLOT(updateSearchMode()));
    connect(&d->m_progressTimer, SIGNAL(timeout()), this, SLOT(updateProgress()));
    connect(d->progressButton, SIGNAL(clicked(bool)), this, SLOT(stopProgressAnimation()));
    d->updateSearchMode();
    d->progressButton->setVisible(false);

    connect(&d->m_runnerManager, SIGNAL(searchResultChanged(QList<GeoDataPlacemark *>)), this, SLOT(updateSearchResult(QList<GeoDataPlacemark *>)));
    connect(&d->m_runnerManager, SIGNAL(searchFinished(QString)), this, SLOT(stopProgressAnimation()));
}

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

GeoDataCoordinates GoToDialog::coordinates() const
{
    return d->m_coordinates;
}

void GoToDialog::setShowRoutingItems(bool show)
{
    d->m_targetModel.setShowRoutingItems(show);
}

void GoToDialog::setSearchEnabled(bool enabled)
{
    d->browseButton->setVisible(enabled);
    d->searchButton->setVisible(enabled);
    if (!enabled) {
        d->searchButton->setChecked(false);
        d->updateSearchMode();
    }
}

void GoToDialogPrivate::updateSearchMode()
{
    bool const searchEnabled = searchButton->isChecked();
    searchLineEdit->setVisible(searchEnabled);
    descriptionLabel->setVisible(searchEnabled);
    progressButton->setVisible(searchEnabled && m_progressTimer.isActive());
    if (searchEnabled) {
        bookmarkListView->setModel(&m_searchResultModel);
        searchLineEdit->setFocus();
    } else {
        bookmarkListView->setModel(&m_targetModel);
    }
}

void GoToDialogPrivate::updateProgress()
{
    if (!m_progressAnimation.isEmpty()) {
        m_currentFrame = (m_currentFrame + 1) % m_progressAnimation.size();
        QIcon frame = m_progressAnimation[m_currentFrame];
        progressButton->setIcon(frame);
    }
}

void GoToDialogPrivate::stopProgressAnimation()
{
    searchLineEdit->setEnabled(true);
    m_progressTimer.stop();
    updateResultMessage(bookmarkListView->model()->rowCount());
    progressButton->setVisible(false);
}

void GoToDialogPrivate::updateResultMessage(int results)
{
    //~ singular %n result found.
    //~ plural %n results found.
    descriptionLabel->setText(QObject::tr("%n result(s) found.", "Number of search results", results));
}

}

#include "GoToDialog.moc" // needed for Q_OBJECT here in source
#include "moc_GoToDialog.cpp" // needed for private slots in header
