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

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

#include "timelineelement.h"

#include "publictransport.h"
#include "reservationmanager.h"
#include "timelinemodel.h"
#include "transfer.h"

#include <KItinerary/Event>
#include <KItinerary/LocationUtil>
#include <KItinerary/Place>
#include <KItinerary/Reservation>
#include <KItinerary/SortUtil>
#include <KItinerary/Visit>

#include <KPublicTransport/Journey>
#include <KPublicTransport/Stopover>

using namespace KItinerary;

static TimelineElement::ElementType elementType(const QVariant &res)
{
    if (JsonLd::isA<FlightReservation>(res)) {
        return TimelineElement::Flight;
    }
    if (JsonLd::isA<LodgingReservation>(res)) {
        return TimelineElement::Hotel;
    }
    if (JsonLd::isA<TrainReservation>(res)) {
        return TimelineElement::TrainTrip;
    }
    if (JsonLd::isA<BusReservation>(res)) {
        return TimelineElement::BusTrip;
    }
    if (JsonLd::isA<BoatReservation>(res)) {
        return TimelineElement::BoatTrip;
    }
    if (JsonLd::isA<FoodEstablishmentReservation>(res)) {
        return TimelineElement::Restaurant;
    }
    if (JsonLd::isA<TouristAttractionVisit>(res)) {
        return TimelineElement::TouristAttraction;
    }
    if (JsonLd::isA<EventReservation>(res)) {
        return TimelineElement::Event;
    }
    if (JsonLd::isA<RentalCarReservation>(res)) {
        return TimelineElement::CarRental;
    }
    return {};
}

TimelineElement::TimelineElement() = default;

TimelineElement::TimelineElement(TimelineModel *model, TimelineElement::ElementType type, const QDateTime &dateTime, const QVariant &data)
    : dt(dateTime)
    , elementType(type)
    , m_content(data)
    , m_model(model)
{
}

TimelineElement::TimelineElement(TimelineModel *model, const QString &resId, const QVariant &res, TimelineElement::RangeType rt)
    : dt(relevantDateTime(res, rt))
    , elementType(::elementType(res))
    , rangeType(rt)
    , m_content(resId)
    , m_model(model)
{
}

TimelineElement::TimelineElement(TimelineModel *model, const ::Transfer &transfer)
    : dt(transfer.alignment() == Transfer::Before ? transfer.anchorTime().addSecs(-transfer.anchorTimeDelta()) : transfer.anchorTime())
    , elementType(Transfer)
    , rangeType(SelfContained)
    , m_content(QVariant::fromValue(transfer))
    , m_model(model)
{
}

static bool operator<(TimelineElement::RangeType lhs, TimelineElement::RangeType rhs)
{
    static const int order_map[] = {1, 0, 2};
    return order_map[lhs] < order_map[rhs];
}

bool TimelineElement::operator<(const TimelineElement &other) const
{
    if (dt == other.dt) {
        if (rangeType == other.rangeType && elementType == other.elementType) {
            return endDateTime().isValid() && endDateTime() > other.endDateTime();
        }
        if (rangeType == RangeEnd || other.rangeType == RangeEnd) {
            if (rangeType == RangeBegin || other.rangeType == RangeBegin) {
                return rangeType < other.rangeType;
            }
            return elementType > other.elementType;
        }
        return elementType < other.elementType;
    }
    return dt < other.dt;
}

bool TimelineElement::operator<(const QDateTime &otherDt) const
{
    return dt < otherDt;
}

bool TimelineElement::operator==(const TimelineElement &other) const
{
    if (elementType != other.elementType || rangeType != other.rangeType || batchId() != other.batchId()) {
        return false;
    }

    switch (elementType) {
    case Transfer: {
        const auto lhsT = m_content.value<::Transfer>();
        const auto rhsT = other.m_content.value<::Transfer>();
        return lhsT.alignment() == rhsT.alignment();
    }
    default:
        return dt == other.dt;
    }
}

bool TimelineElement::isReservation() const
{
    switch (elementType) {
    case Flight:
    case TrainTrip:
    case CarRental:
    case BusTrip:
    case BoatTrip:
    case Restaurant:
    case TouristAttraction:
    case Event:
    case Hotel:
        return true;
    case Undefined:
    case TodayMarker:
    case WeatherForecast:
    case LocationInfo:
    case Transfer:
        return false;
    }

    Q_UNREACHABLE();
    return false;
}

QString TimelineElement::batchId() const
{
    if (isReservation()) {
        return m_content.toString();
    }
    if (elementType == Transfer) {
        return m_content.value<::Transfer>().reservationId();
    }
    return {};
}

QVariant TimelineElement::content() const
{
    return m_content;
}

void TimelineElement::setContent(const QVariant &content)
{
    m_content = content;
}

QDateTime TimelineElement::relevantDateTime(const QVariant &res, TimelineElement::RangeType range)
{
    if (range == TimelineElement::RangeBegin || range == TimelineElement::SelfContained) {
        return SortUtil::startDateTime(res);
    }
    if (range == TimelineElement::RangeEnd) {
        return SortUtil::endDateTime(res);
    }

    return {};
}

bool TimelineElement::isLocationChange() const
{
    if (isReservation()) {
        // ### can be done without reservation lookup for some of the reservation element types
        const auto res = m_model->m_resMgr->reservation(batchId());
        return LocationUtil::isLocationChange(res);
    }

    if (elementType == Transfer) {
        return m_content.value<::Transfer>().state() == Transfer::Selected;
    }

    return false;
}

bool TimelineElement::isTimeBoxed() const
{
    switch (elementType) {
    case Undefined:
    case TodayMarker:
    case WeatherForecast:
    case LocationInfo:
        return false;
    case Transfer:
        return m_content.value<::Transfer>().state() == Transfer::Selected;
    case Flight:
    case TrainTrip:
    case BusTrip:
    case BoatTrip:
        return true;
    case Hotel:
    case CarRental:
        return false;
    case Restaurant:
    case TouristAttraction:
    case Event:
        return SortUtil::endDateTime(m_model->m_resMgr->reservation(batchId())).isValid();
    }
    return false;
}

bool TimelineElement::isInformational() const
{
    switch (elementType) {
    case Undefined:
    case TodayMarker:
    case WeatherForecast:
    case LocationInfo:
        return true;
    case Transfer:
    case Flight:
    case TrainTrip:
    case BusTrip:
    case BoatTrip:
    case Hotel:
    case CarRental:
    case Restaurant:
    case TouristAttraction:
    case Event:
        return false;
    }
    Q_UNREACHABLE();
}

bool TimelineElement::isCanceled() const
{
    if (isReservation()) {
        const auto res = m_model->m_resMgr->reservation(batchId());
        return JsonLd::canConvert<Reservation>(res) && JsonLd::convert<Reservation>(res).reservationStatus() == Reservation::ReservationCancelled;
    }

    // TODO transfers with Disruption::NoService
    return false;
}

static KPublicTransport::Location destinationOfJourney(const KPublicTransport::Journey &jny)
{
    if (jny.sections().empty()) {
        return {};
    }
    const auto &sections = jny.sections();
    auto loc = sections.back().arrival().stopPoint();

    if (sections.size() == 1 || sections.back().mode() != KPublicTransport::JourneySection::Walking || sections.back().distance() > 1000) {
        return loc;
    }

    // the last section is a short walk, its arrival location might not have all the data we have on the previous stop
    const auto prevLoc = sections[sections.size() - 2].arrival().stopPoint();
    const auto locName = loc.name();

    // "anonymous" location with the coordinates as name
    if (std::none_of(locName.begin(),
                     locName.end(),
                     [](QChar c) {
                         return c.isLetter();
                     })
        && !prevLoc.name().isEmpty()) {
        return prevLoc;
    }

    // propagate address information from the last stop to the final destination
    if (loc.locality().isEmpty()) {
        loc.setLocality(prevLoc.locality());
    }
    if (loc.region().isEmpty()) {
        loc.setRegion(prevLoc.region());
    }
    if (loc.country().isEmpty()) {
        loc.setLocality(prevLoc.country());
    }

    return loc;
}

QVariant TimelineElement::destination() const
{
    if (isReservation()) {
        const auto res = m_model->m_resMgr->reservation(batchId());
        if (LocationUtil::isLocationChange(res)) {
            return LocationUtil::arrivalLocation(res);
        }
        return LocationUtil::location(res);
    }

    if (elementType == Transfer) {
        const auto transfer = m_content.value<::Transfer>();
        if (transfer.state() == Transfer::Selected) {
            const auto loc = destinationOfJourney(transfer.journey());
            return loc.isEmpty() ? QVariant() : PublicTransport::placeFromLocation<KItinerary::Place>(loc);
        }
    }

    return {};
}

QDateTime TimelineElement::endDateTime() const
{
    if (isReservation()) {
        const auto res = m_model->m_resMgr->reservation(batchId());
        return SortUtil::endDateTime(res);
    }

    if (elementType == Transfer) {
        const auto transfer = m_content.value<::Transfer>();
        if (transfer.state() == Transfer::Selected) {
            return transfer.journey().scheduledArrivalTime();
        }
    }

    return {};
}

#include "moc_timelineelement.cpp"
