/*
    SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org>
    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "deutschebahnparser.h"

#include "hafasconstants_p.h"
#include "hafasmgateparser.h"
#include "datatypes/featureutil_p.h"

#include <KPublicTransport/StopInformation>

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrlQuery>

using namespace Qt::Literals;
using namespace KPublicTransport;

struct {
    const char *name;
    Line::Mode mode;
} static constexpr const product_to_mode_map[] = {
    { "ICE", Line::LongDistanceTrain },
    { "EC_IC", Line::LongDistanceTrain },
    { "IR", Line::Train },
    { "REGIONAL", Line::LocalTrain },
    { "SBAHN", Line::RapidTransit },
    { "BUS", Line::Bus },
    { "SCHIFF", Line::Boat },
    { "UBAHN", Line::Metro },
    { "TRAM", Line::Tramway },
    { "ANRUFPFLICHTIG", Line::Taxi },
    { "ERSATZVERKEHR", Line::Bus },
};

[[nodiscard]] static Line::Mode parseLineMode(const QJsonValue &product)
{
    for (const auto &m :product_to_mode_map) {
        if (QLatin1StringView(m.name) == product) {
            return m.mode;
        }
    }
    return Line::Unknown;
}

std::vector<Location> DeutscheBahnParser::parseLocations(const QByteArray &data, const HafasMgateParser &hafasParser)
{
    const auto array = QJsonDocument::fromJson(data).array();
    std::vector<Location> locs;
    locs.reserve(array.size());
    for (const auto &locV : array) {
        const auto locObj = locV.toObject();
        Location loc = hafasParser.fromLocationId(locObj.value("id"_L1).toString());
        loc.setName(locObj.value("name"_L1).toString());
        loc.setLatitude(locObj.value("lat"_L1).toDouble());
        loc.setLongitude(locObj.value("lon"_L1).toDouble());
        // TODO type (ST, ADR, POI)
        const auto type = locObj.value("type"_L1);
        if (type == "ST"_L1) {
            StopInformation info;
            loc.setType(Location::Stop);
            const auto prods = locObj.value("products"_L1).toArray();
            for (const auto &prod : prods) {
                Line l;
                l.setMode(parseLineMode(prod));
                if (l.mode() != Line::Unknown) {
                    info.addLine(l);
                }
            }
            loc.setData(info);
        }
        hafasParser.setLocationIdentifier(loc, locObj.value("extId"_L1).toString());
        locs.push_back(std::move(loc));
    }
    return locs;
}

[[nodiscard]] static Route parseRoute(const QJsonObject &lineObj)
{
    Line line;
    const auto product = lineObj.value("produktGattung"_L1);
    line.setMode(parseLineMode(product));

    if (line.mode() == Line::Unknown && !product.isUndefined()) {
        qDebug() << "Unkown product category" << product;
    }

    if (product == "TRAM"_L1 || product == "BUS"_L1) {
        line.setName(lineObj.value("linienNummer"_L1).toString());
    } else {
        line.setName(lineObj.value("mittelText"_L1).toString());
    }

    Route route;
    if (product == "REGIONAL"_L1) {
        route.setName(lineObj.value("name"_L1).toString());
    }

    route.setLine(line);
    return route;
}

template <typename T>
static void applyNotes(T &obj, const QJsonArray &notesArray)
{
    for (const auto &notesV : notesArray) {
        const auto notesObj = notesV.toObject();
        obj.addNote(notesObj.value("text"_L1).toString());
        if (const auto type = notesObj.value("type"_L1).toString(); type == "HALT_AUSFALL"_L1) {
            obj.setDisruptionEffect(Disruption::NoService);
        }
    }
}

std::vector<Stopover> DeutscheBahnParser::parseStopovers(const QJsonArray &stopsArray, bool isDeparture, const HafasMgateParser &hafasParser)
{
    std::vector<Stopover> stops;
    stops.reserve(stopsArray.size());
    for (const auto &stopV : stopsArray) {
        const auto stopObj = stopV.toObject();
        Stopover stop;
        stop.setTripIdentifier(hafasParser.locationIdentifierType(), stopObj.value("journeyId"_L1).toString());

        if (isDeparture) {
            stop.setScheduledDepartureTime(QDateTime::fromString(stopObj.value("zeit"_L1).toString(), Qt::ISODate));
            stop.setExpectedDepartureTime(QDateTime::fromString(stopObj.value("ezZeit"_L1).toString(), Qt::ISODate));
        } else {
            stop.setScheduledArrivalTime(QDateTime::fromString(stopObj.value("zeit"_L1).toString(), Qt::ISODate));
            stop.setExpectedArrivalTime(QDateTime::fromString(stopObj.value("ezZeit"_L1).toString(), Qt::ISODate));
        }
        stop.setScheduledPlatform(stopObj.value("gleis"_L1).toString());
        stop.setExpectedPlatform(stopObj.value("ezGleis"_L1).toString());

        Location loc;
        hafasParser.setLocationIdentifier(loc, stopObj.value("bahnhofsId"_L1).toString());
        const auto vias = stopObj.value("ueber"_L1).toArray();
        if (!vias.isEmpty()) {
            loc.setName(isDeparture ? vias.first().toString() : vias.last().toString());
        }
        stop.setStopPoint(loc);

        const auto lineObj = stopObj.value("verkehrmittel"_L1).toObject();
        auto route = parseRoute(lineObj);
        route.setDirection(stopObj.value("terminus"_L1).toString());
        stop.setRoute(route);

        applyNotes(stop, stopObj.value("meldungen"_L1).toArray());
        stops.push_back(std::move(stop));
    }

    return stops;
}

template <typename T>
static void applyRisNotes(T &obj, const QJsonArray &risNotesArray)
{
    for (const auto &risNoteV : risNotesArray) {
        const auto risNote = risNoteV.toObject();
        obj.addNote(risNote.value("value"_L1).toString());
        const auto key = risNote.value("key"_L1).toString();
        if (key == "text.realtime.stop.cancelled"_L1) {
            obj.setDisruptionEffect(Disruption::NoService);
        }
        if constexpr (!std::is_same_v<T, JourneySection>) {
            if (key == HAFAS_RT_ENTRY_DISABLED || key == HAFAS_RT_ENTRY_EXIT_DISABLED) {
                obj.setPickupType(PickupDropoff::NotAllowed);
            }
            if (key == HAFAS_RT_EXIT_DISABLED || key == HAFAS_RT_ENTRY_EXIT_DISABLED) {
                obj.setDropoffType(PickupDropoff::NotAllowed);
            }
        }
    }
}

static std::vector<LoadInfo> parseOccupancyInformation(const QJsonObject &obj)
{
    const auto occupancyArray = obj.value("auslastungsmeldungen"_L1).toArray();
    std::vector<LoadInfo> occupancies;
    occupancies.reserve(occupancyArray.size());
    for (const auto &occupancyV : occupancyArray) {
        const auto occupancyObj = occupancyV.toObject();
        const auto level = HafasParser::parseLoadLevel(occupancyObj.value("stufe"_L1).toInt(-1));
        if (level == Load::Unknown) {
            continue;
        }
        LoadInfo load;
        load.setLoad(level);
        load.setSeatingClass(occupancyObj.value("klasse"_L1).toString());
        if (load.seatingClass().startsWith("KLASSE_"_L1)) {
            load.setSeatingClass(load.seatingClass().mid(7));
        }
        occupancies.push_back(std::move(load));
    }
    return occupancies;
}

[[nodiscard]] static JourneySection parseJourneySection(const QJsonObject &sectionObj)
{
    JourneySection section;
    if (const auto depObj = sectionObj.value("abfahrt"_L1).toObject(); !depObj.isEmpty()) {
        section.setScheduledDepartureTime(QDateTime::fromString(depObj.value("sollzeit"_L1).toString(), Qt::ISODate));
        section.setExpectedDepartureTime(QDateTime::fromString(depObj.value("echtzeit"_L1).toString(), Qt::ISODate));
    } else {
        section.setScheduledDepartureTime(QDateTime::fromString(sectionObj.value("abfahrtsZeitpunkt"_L1).toString(), Qt::ISODate));
        section.setExpectedDepartureTime(QDateTime::fromString(sectionObj.value("ezAbfahrtsZeitpunkt"_L1).toString(), Qt::ISODate));
    }

    if (const auto arrObj = sectionObj.value("ankunft"_L1).toObject(); !arrObj.isEmpty()) {
        section.setScheduledArrivalTime(QDateTime::fromString(arrObj.value("sollzeit"_L1).toString(), Qt::ISODate));
        section.setExpectedArrivalTime(QDateTime::fromString(arrObj.value("echtzeit"_L1).toString(), Qt::ISODate));
    } else {
        section.setScheduledArrivalTime(QDateTime::fromString(sectionObj.value("ankunftsZeitpunkt"_L1).toString(), Qt::ISODate));
        section.setExpectedArrivalTime(QDateTime::fromString(sectionObj.value("ezAnkunftsZeitpunkt"_L1).toString(), Qt::ISODate));
    }

    applyNotes(section, sectionObj.value("himMeldungen"_L1).toArray());
    applyNotes(section, sectionObj.value("priorisierteMeldungen"_L1).toArray());
    applyRisNotes(section, sectionObj.value("risNotizen"_L1).toArray());

    return section;
}

[[nodiscard]] static Stopover parseIntermediateStop(const QJsonObject &stopObj, const HafasMgateParser &hafasParser)
{
    Stopover stop;
    if (const auto depObj = stopObj.value("abfahrt"_L1).toObject(); !depObj.isEmpty()) {
        stop.setScheduledDepartureTime(QDateTime::fromString(depObj.value("sollzeit"_L1).toString(), Qt::ISODate));
        stop.setExpectedDepartureTime(QDateTime::fromString(depObj.value("echtzeit"_L1).toString(), Qt::ISODate));
    } else {
        stop.setScheduledDepartureTime(QDateTime::fromString(stopObj.value("abfahrtsZeitpunkt"_L1).toString(), Qt::ISODate));
        stop.setExpectedDepartureTime(QDateTime::fromString(stopObj.value("ezAbfahrtsZeitpunkt"_L1).toString(), Qt::ISODate));
    }

    if (const auto arrObj = stopObj.value("ankunft"_L1).toObject(); !arrObj.isEmpty()) {
        stop.setScheduledArrivalTime(QDateTime::fromString(arrObj.value("sollzeit"_L1).toString(), Qt::ISODate));
        stop.setExpectedArrivalTime(QDateTime::fromString(arrObj.value("echtzeit"_L1).toString(), Qt::ISODate));
    } else {
        stop.setScheduledArrivalTime(QDateTime::fromString(stopObj.value("ankunftsZeitpunkt"_L1).toString(), Qt::ISODate));
        stop.setExpectedArrivalTime(QDateTime::fromString(stopObj.value("ezAnkunftsZeitpunkt"_L1).toString(), Qt::ISODate));
    }

    stop.setScheduledPlatform(stopObj.value("gleis"_L1).toString());
    stop.setExpectedPlatform(stopObj.value("ezGleis"_L1).toString());

    Location stopPoint = hafasParser.fromLocationId(stopObj.value("id"_L1).toString());
    stopPoint.setName(stopObj.value("name"_L1).toString());
    hafasParser.setLocationIdentifier(stopPoint, stopObj.value("extId"_L1).toString());
    stop.setStopPoint(stopPoint);

    applyNotes(stop, stopObj.value("himMeldungen"_L1).toArray());
    applyNotes(stop, stopObj.value("priorisierteMeldungen"_L1).toArray());
    applyRisNotes(stop, stopObj.value("risNotizen"_L1).toArray());

    stop.setLoadInformation(parseOccupancyInformation(stopObj));
    return stop;
}

static void parseVehicleFeatures(const QJsonArray &trainAttributes, const HafasAttributeMap &attrMap, JourneySection &section, Line &line)
{
    std::vector<Feature> features;
    for (const auto &trainAttrV : trainAttributes) {
        const auto trainAttrObj = trainAttrV.toObject();
        const auto key = trainAttrObj.value("key"_L1).toString();
        if (key == "BEF"_L1) {
            line.setOperatorName(trainAttrObj.value("value"_L1).toString());
            continue;
        }

        auto value = trainAttrObj.value("value"_L1).toString();
        const auto affectedSegment = trainAttrObj.value("teilstreckenHinweis"_L1).toString();
        if (!affectedSegment.isEmpty() && affectedSegment != '('_L1 + section.from().name() + " - "_L1 + section.to().name() + ')'_L1) {
            value += ' '_L1 + affectedSegment;
        }

        const auto type = trainAttrObj.value("kategorie"_L1).toString();
        if (type == "INFORMATION"_L1 || type == "FAHRRADMITNAHME"_L1 || type == "BORDBISTRO"_L1) {
            const auto remarkData = attrMap.lookup(u"A", key);
            if (remarkData.type() == HafasAttribute::Feature) {
                auto f = remarkData.feature();
                f.setName(value);
                FeatureUtil::add(features, std::move(f));
                continue;
            }
            if (remarkData.type() == HafasAttribute::Ignore) {
                continue;
            }
            if (remarkData.type() == HafasAttribute::PickupDropoff) {
                if (remarkData.pickupType() != PickupDropoff::Normal) {
                    auto dep = section.departure();
                    dep.setPickupType(remarkData.pickupType());
                    section.setDeparture(dep);
                }
                if (remarkData.dropoffType() != PickupDropoff::Normal) {
                    auto arr = section.arrival();
                    arr.setDropoffType(remarkData.dropoffType());
                    section.setArrival(arr);
                }
            }
            if (remarkData.type() == HafasAttribute::Undefined) {
                qDebug() << "unknown vehicle attribute" << type << key << value;
            }
        }

        section.addNote(value);
    }
    section.setFeatures(std::move(features));
}

static void parseCoordinateName(Location &loc, const HafasMgateParser &hafasParser)
{
    if (!loc.identifier(hafasParser.locationIdentifierType()).isEmpty()) {
        return;
    }
    static const QRegularExpression rx(uR"(^(-?\d+\.\d+), (-?\d+\.\d+)$)"_s);
    if (const auto match = rx.match(loc.name()); match.hasMatch()) {
        loc.setCoordinate(match.capturedView(1).toDouble(), match.capturedView(2).toDouble());
    }
}

std::vector<Journey> DeutscheBahnParser::parseJourneys(const QJsonArray &journeysArray, const HafasMgateParser &hafasParser)
{
    HafasAttributeMap attrMap(u"base");

    std::vector<Journey> journeys;
    journeys.reserve(journeysArray.size());
    for (const auto &journeyV : journeysArray) {
        const auto journeyObj = journeyV.toObject();
        const auto sectionArray = journeyObj.value("verbindungsAbschnitte"_L1).toArray();
        if (sectionArray.empty()) {
            continue;
        }

        Journey journey;
        std::vector<JourneySection> sections;
        for (const auto &sectionV : sectionArray) {
            const auto sectionObj = sectionV.toObject();
            auto section = parseJourneySection(sectionObj);

            Location from;
            from.setName(sectionObj.value("abfahrtsOrt"_L1).toString());
            hafasParser.setLocationIdentifier(from, sectionObj.value("abfahrtsOrtExtId"_L1).toString());
            parseCoordinateName(from, hafasParser);
            section.setFrom(from);

            Location to;
            to.setName(sectionObj.value("ankunftsOrt"_L1).toString());
            hafasParser.setLocationIdentifier(to, sectionObj.value("ankunftsOrtExtId"_L1).toString());
            parseCoordinateName(to, hafasParser);
            section.setTo(to);

            const auto routeObj = sectionObj.value("verkehrsmittel"_L1).toObject();
            const auto type = routeObj.value("typ"_L1).toString();
            if (type == "WALK"_L1) {
                section.setMode(JourneySection::Walking);
            } else if (type == "TRANSFER"_L1) {
                section.setMode(JourneySection::Transfer);
            } else {
                section.setMode(JourneySection::PublicTransport);
                section.setLoadInformation(parseOccupancyInformation(sectionObj));
                section.setIdentifier(hafasParser.locationIdentifierType(), sectionObj.value("journeyId"_L1).toString());
            }

            std::vector<Stopover> intermediateStops;
            const auto stopsArray = sectionObj.value("halte"_L1).toArray();
            intermediateStops.reserve(stopsArray.size());
            for (const auto &stopV :stopsArray) {
                const auto stopObj = stopV.toObject();
                auto stop = parseIntermediateStop(stopObj, hafasParser);
                intermediateStops.push_back(std::move(stop));
            }

            if (intermediateStops.size() >= 2) {
                section.setDeparture(intermediateStops.front());
                section.setArrival(intermediateStops.back());
                intermediateStops.pop_back();
                intermediateStops.erase(intermediateStops.begin(), std::next(intermediateStops.begin()));
            }
            section.setIntermediateStops(std::move(intermediateStops));

            auto route = parseRoute(routeObj);
            route.setDirection(routeObj.value("richtung"_L1).toString());
            auto line = route.line();

            parseVehicleFeatures(routeObj.value("zugattribute"_L1).toArray(), attrMap, section, line);

            route.setLine(line);
            section.setRoute(route);
            sections.push_back(std::move(section));
        }

        // propagate missing coordinates
        for (auto it = sections.begin(); it != std::prev(sections.end()); ++it) {
            if ((*it).to().identifier(hafasParser.locationIdentifierType()) != (*std::next(it)).from().identifier(hafasParser.locationIdentifierType())) {
                continue;
            }
            // forward
            if ((*it).to().hasCoordinate() && !(*std::next(it)).from().hasCoordinate()) {
                auto from = (*std::next(it)).from();
                from.setCoordinate((*it).to().latitude(), (*it).to().longitude());
                (*std::next(it)).setFrom(from);
            }
            // backward
            if (!(*it).to().hasCoordinate() && (*std::next(it)).from().hasCoordinate()) {
                auto to = (*it).to();
                to.setCoordinate((*std::next(it)).from().latitude(), (*std::next(it)).from().longitude());
                (*it).setTo(to);
            }
        }

        if (journeyObj.contains("angebotsPreis"_L1)
            || journeyObj.value("isAngebotseinholungNachgelagert"_L1).toBool()) {
            QUrl url(u"https://www.bahn.de/buchung/fahrplan/suche"_s);
            QUrlQuery query;
            query.addQueryItem(u"so"_s, sections.front().departure().stopPoint().name());
            query.addQueryItem(u"zo"_s, sections.back().arrival().stopPoint().name());
            query.addQueryItem(u"soid"_s, sections.front().departure().stopPoint().identifier(hafasParser.locationIdentifierType()));
            query.addQueryItem(u"zoid"_s, sections.back().arrival().stopPoint().identifier(hafasParser.locationIdentifierType()));
            query.addQueryItem(u"cbs"_s, u"true"_s);
            query.addQueryItem(u"hd"_s, sections.front().scheduledDepartureTime().toString(Qt::ISODate));
            query.addQueryItem(u"gh"_s, journeyObj.value("ctxRecon"_L1).toString());
            url.setQuery(query);

            for (auto it = sections.begin(); it != sections.end(); ++it) {
                if ((*it).mode() != JourneySection::PublicTransport) {
                    continue;
                }
                if (std::none_of(std::next(it), sections.end(), [](const auto &sec) { return sec.mode() == JourneySection::PublicTransport; })) {
                    (*it).setBookingUrl(url);
                    url = {};
                }
                break;
            }
            journey.setBookingUrl(url);
        }

        journey.setSections(std::move(sections));
        journeys.push_back(std::move(journey));
    }

    return journeys;
}

JourneySection DeutscheBahnParser::parseTrip(const QJsonObject &sectionObj, const HafasMgateParser &hafasParser)
{
    auto section = parseJourneySection(sectionObj);
    section.setMode(JourneySection::PublicTransport);
    if (sectionObj.value("cancelled"_L1).toBool(false)) {
        section.setDisruptionEffect(Disruption::NoService);
    }

    std::vector<Stopover> intermediateStops;
    const auto stopsArray = sectionObj.value("halte"_L1).toArray();
    intermediateStops.reserve(stopsArray.size());
    for (const auto &stopV : stopsArray) {
        const auto stopObj = stopV.toObject();
        auto stop = parseIntermediateStop(stopObj, hafasParser);
        intermediateStops.push_back(std::move(stop));
    }

    Route route;
    if (intermediateStops.size() >= 2) {
        section.setDeparture(intermediateStops.front());
        section.setArrival(intermediateStops.back());
        route.setDestination(section.to());
        intermediateStops.pop_back();
        intermediateStops.erase(intermediateStops.begin(), std::next(intermediateStops.begin()));
    }
    section.setIntermediateStops(std::move(intermediateStops));

    Line line;
    line.setName(sectionObj.value("zugName"_L1).toString());
    parseVehicleFeatures(sectionObj.value("zugattribute"_L1).toArray(), HafasAttributeMap(u"base"), section, line);
    route.setLine(line);
    section.setRoute(route);

    const auto polyArray = sectionObj.value("polylineGroup"_L1).toObject().value("polylineDescriptions"_L1).toArray();
    std::vector<PathSection> pathSecs;
    pathSecs.reserve(polyArray.size());
    for (const auto &polyV : polyArray) {
        const auto coordinates = polyV.toObject().value("coordinates"_L1).toArray();
        QPolygonF p;
        p.reserve(coordinates.size());
        for (const auto &coordV : coordinates) {
            const auto coordObj = coordV.toObject();
            p.emplace_back(coordObj.value("lng"_L1).toDouble(), coordObj.value("lat"_L1).toDouble());
        }
        PathSection pathSec;
        pathSec.setPath(p);
        pathSecs.push_back(std::move(pathSec));
    }
    Path path;
    path.setSections(std::move(pathSecs));
    section.setPath(path);

    return section;
}
