/*
    SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "gbfsbackend.h"
#include "gbfsservice.h"
#include "gbfsstore.h"
#include "gbfsjob.h"
#include "gbfsreader.h"
#include "gbfsvehicletypes.h"
#include "datatypes/rentalvehicleutil_p.h"

#include <KPublicTransport/Attribution>
#include <KPublicTransport/Location>
#include <KPublicTransport/LocationReply>
#include <KPublicTransport/LocationRequest>
#include <KPublicTransport/RentalVehicle>

#include <QColor>
#include <QCoreApplication>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTimer>

#include <cmath>
#include <functional>

using namespace KPublicTransport;
using namespace Qt::Literals;

GBFSBackend::GBFSBackend()
{
    QTimer::singleShot(std::chrono::seconds(10), Qt::CoarseTimer, QCoreApplication::instance(), []() { GBFSStore::expire(); });
}

GBFSBackend::~GBFSBackend() = default;

AbstractBackend::Capabilities GBFSBackend::capabilities() const
{
    return Secure;
}

Location::Types GBFSBackend::supportedLocationTypes() const
{
    return Location::RentedVehicleStation | Location::RentedVehicle;
}

static bool isServiceApplicable(const GBFSService &s, const LocationRequest &req)
{
    if (req.hasCoordinate()) {
        return s.boundingBox.contains(QPointF(req.longitude(), req.latitude()));
    } else {
        // TODO
    }
    return false;
}

struct QueryContext {
    bool stillStarting = true;
    bool hasError = false;
    int pendingJobs = 0;
    std::vector<Location> result;
    std::vector<Attribution> attributions;
    QString errorMessage;
};

static QString stationIdToString(const QJsonValue &id)
{
    if (id.isDouble()) {
        return QString::number(id.toInt());
    }
    return id.toString();
}

// we get some address values just being " , "...
static QString cleanAddress(const QString &input)
{
    if (std::any_of(input.begin(), input.end(), std::mem_fn(qOverload<>(&QChar::isLetter)))) {
        return input;
    }
    return {};
}

static void appendResults(const GBFSService &service, const LocationRequest &req, QueryContext *context)
{
    GBFSStore store(service.systemId);
    GBFSVehicleTypes vehicleTypes(store.loadData(GBFS::VehicleTypes));

    const auto sysInfoDoc = store.loadData(GBFS::SystemInformation);
    const auto sysInfo = GBFSReader::dataObject(sysInfoDoc);
    const auto network = GBFSReader::readSystemInformation(sysInfo);

    const auto stationsDoc = store.loadData(GBFS::StationInformation);
    const auto stations = GBFSReader::dataValue(stationsDoc, "stations"_L1).toArray();

    std::vector<QString> selectedStationIds;
    for (const auto &stationV : stations) {
        const auto station = stationV.toObject();
        const auto lat = GBFSReader::readLatitude(station);
        const auto lon = GBFSReader::readLongitude(station);
        if (std::isnan(lat) || std::isnan(lon) || Location::distance(lat, lon, req.latitude(), req.longitude()) > req.maximumDistance()) {
            continue;
        }
        Location loc;
        loc.setType(Location::RentedVehicleStation);
        loc.setCoordinate(lat, lon);
        loc.setName(GBFSReader::readLocalizedString(station, "name"_L1));
        const auto stationId = stationIdToString(station.value("station_id"_L1));
        loc.setIdentifier(service.systemId, stationId);
        loc.setStreetAddress(cleanAddress(station.value("address"_L1).toString()));
        loc.setPostalCode(station.value("post_code"_L1).toString());
        loc.setLocality(station.value("city"_L1).toString()); // non-standard extension
        // TODO cover more properties

        auto s = GBFSReader::readStationInformation(station, vehicleTypes);
        s.setNetwork(network);

        loc.setData(s);
        selectedStationIds.push_back(stationId);
        context->result.push_back(loc);
    }

    const auto statusDoc = store.loadData(GBFS::StationStatus);
    const auto status = GBFSReader::dataValue(statusDoc, QLatin1String("stations")).toArray();
    for (const auto &statV : status) {
        const auto stat = statV.toObject();
        const auto id = stationIdToString(stat.value(QLatin1String("station_id")));
        const auto it = std::find(selectedStationIds.begin(), selectedStationIds.end(), id);
        if (it == selectedStationIds.end()) {
            continue;
        }

        auto &loc = context->result[context->result.size() - selectedStationIds.size() + std::distance(selectedStationIds.begin(), it)];
        auto s = loc.rentalVehicleStation();
        GBFSReader::readStationStatus(stat, s, vehicleTypes);
        loc.setData(s);
    }

    const auto floatingDoc = store.loadData(GBFS::VehicleStatus);
    const auto floating = GBFSReader::dataValue(floatingDoc, {"vehicles"_L1, "bikes"_L1}).toArray();
    for (const auto &bikeV : floating) {
        const auto bike = bikeV.toObject();
        if (bike.value(QLatin1String("is_reserved")).toBool() || bike.value(QLatin1String("is_disabled")).toBool()) {
            continue;
        }
        const auto lat = GBFSReader::readLatitude(bike);
        const auto lon = GBFSReader::readLongitude(bike);
        const bool selectedByCoord = !std::isnan(lat) && !std::isnan(lon) && Location::distance(lat, lon, req.latitude(), req.longitude()) <= req.maximumDistance();

        // GBFS v2.1: docked vehicle status - TODO do we want to drop the corresponding station in that case?
        const auto id = stationIdToString(bike.value(QLatin1String("station_id")));
        const auto it = std::find(selectedStationIds.begin(), selectedStationIds.end(), id);
        if (it == selectedStationIds.end() && !selectedByCoord) {
            continue;
        }

        Location loc;
        loc.setName(network.name());
        loc.setType(Location::RentedVehicle);
        if (selectedByCoord) {
            loc.setCoordinate(lat, lon);
        } else {
            const auto &station = context->result[context->result.size() - selectedStationIds.size() + std::distance(selectedStationIds.begin(), it)];
            loc.setCoordinate(station.latitude(), station.longitude());
        }
        for (const auto key : {"vehicle_id"_L1, "bike_id"_L1}) {
            if (auto id = bike.value(key).toString(); !id.isEmpty()) {
                loc.setIdentifier(service.systemId, id);
                break;
            }
        }

        auto vehicle = GBFSReader::readVehicleStatus(bike, vehicleTypes);
        vehicle.setNetwork(network);
        if (!vehicle.vehicleType().name().isEmpty()) {
            loc.setName(vehicle.vehicleType().name());
        }
        loc.setData(vehicle);
        context->result.push_back(loc);
    }

    Attribution attr;
    attr.setLicense(sysInfo.value("license_id"_L1).toString());
    attr.setLicenseUrl(QUrl(sysInfo.value("license_url"_L1).toString()));
    attr.setName(GBFSReader::readLocalizedString(sysInfo, "attribution_organization_name"_L1));
    if (attr.name().isEmpty()) {
        attr.setName(network.name());
    }
    attr.setUrl(QUrl(sysInfo.value("attribution_url"_L1).toString()));
    if (attr.url().isEmpty()) {
        attr.setUrl(QUrl(sysInfo.value("url"_L1).toString()));
    }

    if (attr.hasLicense()) {
        context->attributions.push_back(std::move(attr));
    }
}

bool GBFSBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
{
    // (1) find all applicable services
    // (2) fetch updates where needed
    // (3) look for matching locations in their data

    QueryContext *context = nullptr;
    const auto &services = GBFSServiceRepository::services();
    for (const auto &s : services) {
        if (!isServiceApplicable(s, req) || s.discoveryUrl.scheme() != QLatin1String("https")) {
            continue;
        }
        qDebug() << "  " << s.systemId << "applicable for request";

        if (!context) {
            context = new QueryContext;
        }

        context->pendingJobs++;
        auto updateJob = new GBFSJob(nam, reply);
        updateJob->setRequestedData({GBFS::StationInformation, GBFS::StationStatus, GBFS::VehicleStatus, GBFS::VehicleTypes});
        QObject::connect(updateJob, &GBFSJob::finished, reply, [this, context, reply, updateJob, req]() {
            context->pendingJobs--;
            updateJob->deleteLater();
            if (updateJob->error() != GBFSJob::NoError) {
                context->errorMessage = updateJob->errorMessage();
                context->hasError = true;
            } else {
                appendResults(updateJob->service(), req, context);
            }

            if (context->pendingJobs == 0 &&  !context->stillStarting) {
                if (context->hasError && context->result.empty()) {
                    addError(reply, Reply::NetworkError, context->errorMessage);
                } else {
                    addResult(reply, std::move(context->result));
                }
                delete context;
            }
        });
        updateJob->discoverAndUpdate(s);
    }

    if (context && context->pendingJobs == 0) {
        if (!context->result.empty()) {
            addAttributions(reply, std::move(context->attributions));
            addResult(reply, std::move(context->result));
        }
        delete context;
        return false;
    } else if (context) {
        context->stillStarting = false;
        return true;
    }
    return false;
}
