// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2011 Dennis Nienhüser <nienhueser@kde.org>
//

#include "RouteSegment.h"

#include "GeoDataLatLonAltBox.h"

namespace Marble
{

RouteSegment::RouteSegment()
    : m_valid(false)
    , m_distance(0.0)
    , m_travelTime(0)
    , m_nextRouteSegment(nullptr)
{
    // nothing to do
}

qreal RouteSegment::distance() const
{
    return m_distance;
}

const Maneuver &RouteSegment::maneuver() const
{
    return m_maneuver;
}

void RouteSegment::setManeuver(const Maneuver &maneuver)
{
    m_maneuver = maneuver;
    m_valid = true;
}

const GeoDataLineString &RouteSegment::path() const
{
    return m_path;
}

void RouteSegment::setPath(const GeoDataLineString &path)
{
    m_path = path;
    m_distance = m_path.length(EARTH_RADIUS);
    m_bounds = m_path.latLonAltBox();
    m_valid = true;
}

int RouteSegment::travelTime() const
{
    return m_travelTime;
}

void RouteSegment::setTravelTime(int seconds)
{
    m_travelTime = seconds;
    m_valid = true;
}

GeoDataLatLonBox RouteSegment::bounds() const
{
    return m_bounds;
}

const RouteSegment &RouteSegment::nextRouteSegment() const
{
    if (m_nextRouteSegment) {
        return *m_nextRouteSegment;
    }

    static RouteSegment invalid;
    return invalid;
}

void RouteSegment::setNextRouteSegment(const RouteSegment *segment)
{
    m_nextRouteSegment = segment;
    if (segment) {
        m_valid = true;
    }
}

bool RouteSegment::isValid() const
{
    return m_valid;
}

qreal RouteSegment::distancePointToLine(const GeoDataCoordinates &p, const GeoDataCoordinates &a, const GeoDataCoordinates &b)
{
    return EARTH_RADIUS * p.sphericalDistanceTo(projected(p, a, b));
}

GeoDataCoordinates RouteSegment::projected(const GeoDataCoordinates &p, const GeoDataCoordinates &a, const GeoDataCoordinates &b)
{
    qreal const y0 = p.latitude();
    qreal const x0 = p.longitude();
    qreal const y1 = a.latitude();
    qreal const x1 = a.longitude();
    qreal const y2 = b.latitude();
    qreal const x2 = b.longitude();
    qreal const y01 = x0 - x1;
    qreal const x01 = y0 - y1;
    qreal const y21 = x2 - x1;
    qreal const x21 = y2 - y1;
    qreal const len = x21 * x21 + y21 * y21;
    qreal const t = (x01 * x21 + y01 * y21) / len;
    if (t < 0.0) {
        return a;
    } else if (t > 1.0) {
        return b;
    } else {
        // a + t (b - a);
        qreal const lon = x1 + t * (x2 - x1);
        qreal const lat = y1 + t * (y2 - y1);
        return {lon, lat};
    }
}

qreal RouteSegment::distanceTo(const GeoDataCoordinates &point, GeoDataCoordinates &closest, GeoDataCoordinates &interpolated) const
{
    Q_ASSERT(!m_path.isEmpty());

    if (m_path.size() == 1) {
        closest = m_path.first();
        return EARTH_RADIUS * m_path.first().sphericalDistanceTo(point);
    }

    qreal minDistance = -1.0;
    int minIndex = 0;
    for (int i = 1; i < m_path.size(); ++i) {
        qreal const distance = distancePointToLine(point, m_path[i - 1], m_path[i]);
        if (minDistance < 0.0 || distance < minDistance) {
            minDistance = distance;
            minIndex = i;
        }
    }

    closest = m_path[minIndex];
    if (minIndex == 0) {
        interpolated = closest;
    } else {
        interpolated = projected(point, m_path[minIndex - 1], m_path[minIndex]);
    }

    return minDistance;
}

qreal RouteSegment::minimalDistanceTo(const GeoDataCoordinates &point) const
{
    if (bounds().contains(point)) {
        return 0.0;
    }

    qreal north(0.0), east(0.0), south(0.0), west(0.0);
    bounds().boundaries(north, south, east, west);
    GeoDataCoordinates const northWest(west, north);
    GeoDataCoordinates const northEast(east, north);
    GeoDataCoordinates const southhWest(west, south);
    GeoDataCoordinates const southEast(east, south);

    qreal distNorth = distancePointToLine(point, northWest, northEast);
    qreal distEast = distancePointToLine(point, northEast, southEast);
    qreal distSouth = distancePointToLine(point, southhWest, southEast);
    qreal distWest = distancePointToLine(point, northWest, southhWest);
    return qMin(qMin(distNorth, distEast), qMin(distWest, distSouth));
}

qreal RouteSegment::projectedDirection(const GeoDataCoordinates &point) const
{
    if (m_path.size() < 2) {
        return 0;
    }

    qreal minDistance = -1.0;
    int minIndex = 0;
    for (int i = 1; i < m_path.size(); ++i) {
        qreal const distance = distancePointToLine(point, m_path[i - 1], m_path[i]);
        if (minDistance < 0.0 || distance < minDistance) {
            minDistance = distance;
            minIndex = i;
        }
    }

    if (minIndex == 0) {
        return m_path[0].bearing(m_path[1], GeoDataCoordinates::Degree, GeoDataCoordinates::FinalBearing);
    } else {
        return m_path[minIndex - 1].bearing(m_path[minIndex], GeoDataCoordinates::Degree, GeoDataCoordinates::FinalBearing);
    }
}

bool RouteSegment::operator==(const RouteSegment &other) const
{
    return m_valid == other.m_valid && m_distance == other.m_distance && m_maneuver == other.m_maneuver && m_travelTime == other.m_travelTime
        && m_bounds == other.m_bounds && m_nextRouteSegment == other.m_nextRouteSegment;
}

bool RouteSegment::operator!=(const RouteSegment &other) const
{
    return !(other == *this);
}

}
