/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2023-05-15
 * Description : geolocation engine based on Marble.
 *               (c) 2007-2022 Marble Team
 *               https://invent.kde.org/education/marble/-/raw/master/data/credits_authors.html
 *
 * SPDX-FileCopyrightText: 2023-2026 by Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * ============================================================ */

#include "MercatorProjection.h"

// Qt includes

#include <QIcon>

// KDE includes

#include <klocalizedstring.h>

// Local includes

#include "ViewportParams.h"
#include "GeoDataLatLonAltBox.h"
#include "MathHelper.h"
#include "GeoDataPoint.h"
#include "MarbleMath.h"
#include "digikam_debug.h"

namespace Marble
{

MercatorProjection::MercatorProjection()
    : CylindricalProjection(),
      m_lastCenterLat      (200.0),
      m_lastCenterLatInv   (0.0)
{
    // NOTE: virtual calls in constructor: uses dynamic binding

    setMinLat(this->minValidLat());
    setMaxLat(this->maxValidLat());
}

MercatorProjection::~MercatorProjection()
{
}

QString MercatorProjection::name() const
{
    return i18n("Mercator");
}

QString MercatorProjection::description() const
{
    return i18n("<p><b>Mercator Projection</b></p><p>Applications: "
                "popular standard map projection for navigation.</p>");
}

QIcon MercatorProjection::icon() const
{
    return QIcon::fromTheme(QStringLiteral("map-mercator"));
}

qreal MercatorProjection::maxValidLat() const
{
    // This is the max value where gd( lat ) is defined.
    return (+85.05113 * DEG2RAD);
}

qreal MercatorProjection::minValidLat() const
{
    // This is the min value where gd( lat ) is defined.
    return (-85.05113 * DEG2RAD);
}

bool MercatorProjection::screenCoordinates(const GeoDataCoordinates& geopoint,
                                           const ViewportParams* viewport,
                                           qreal& x, qreal& y, bool& globeHidesPoint) const
{
    globeHidesPoint      = false;
    qreal  lon           = 0.0;
    qreal  originalLat   = 0.0;

    geopoint.geoCoordinates(lon, originalLat);
    qreal const lat       = qBound(minLat(), originalLat, maxLat());
    const bool isLatValid = lat == originalLat;

    // Convenience variables

    int  radius           = viewport->radius();
    qreal width           = (qreal)(viewport->width());
    qreal height          = (qreal)(viewport->height());

    qreal rad2Pixel       = 2 * radius / M_PI;

    const qreal centerLon = viewport->centerLongitude();
    const qreal centerLat = viewport->centerLatitude();

    if (centerLat != m_lastCenterLat)
    {
        m_lastCenterLatInv = gdInv(centerLat);
        m_lastCenterLat    = centerLat;
    }

    // Let (x, y) be the position on the screen of the placemark..

    x = (width  / 2 + rad2Pixel * (lon - centerLon));
    y = (height / 2 - rad2Pixel * (gdInv(lat) - m_lastCenterLatInv));

    // Return true if the calculated point is inside the screen area,
    // otherwise return false.
    return (
            isLatValid && (((0 <= y) && (y < height))
                           && (((0 <= x) && (x < width))
                               || ((0 <= (x - 4 * radius)) && ((x - 4 * radius) < width))
                               || ((0 <= (x + 4 * radius)) && ((x + 4 * radius) < width))))
            );
}

bool MercatorProjection::screenCoordinates(const GeoDataCoordinates& coordinates,
                                           const ViewportParams* viewport,
                                           qreal* x, qreal& y, int& pointRepeatNum,
                                           const QSizeF& size,
                                           bool& globeHidesPoint) const
{
    pointRepeatNum = 0;

    // On flat projections the observer's view onto the point won't be
    // obscured by the target planet itself.

    globeHidesPoint = false;

    // Convenience variables

    int  radius   = viewport->radius();
    qreal  width  = (qreal)(viewport->width());
    qreal  height = (qreal)(viewport->height());

    // Let (itX, y) be the first guess for one possible position on screen..

    qreal itX;
    bool visible = screenCoordinates(coordinates, viewport, itX, y);

    // Make sure that the requested point is within the visible y range:

    if ((0 <= (y + size.height() / 2.0)) && (y < (height + size.height() / 2.0)))
    {
        // For the repetition case the same geopoint gets displayed on
        // the map many times.across the longitude.

        int xRepeatDistance = 4 * radius;

        // Finding the leftmost positive x value

        if ((itX + size.width() / 2.0) >= xRepeatDistance)
        {
            const int repeatNum = (int)((itX + size.width() / 2.0) / xRepeatDistance);
            itX                 = itX - repeatNum * xRepeatDistance;
        }

        if (itX + size.width() / 2.0 < 0)
        {
            itX += xRepeatDistance;
        }

        // the requested point is out of the visible x range:

        if (itX > width + size.width() / 2.0)
        {
            return false;
        }

        // Now iterate through all visible x screen coordinates for the point
        // from left to right.

        int itNum = 0;

        while ((itX - size.width() / 2.0) < width)
        {
            *x   = itX;
            ++x;
            ++itNum;
            itX += xRepeatDistance;
        }

        pointRepeatNum = itNum;

        return visible;
    }

    // the requested point is out of the visible y range:

    return false;
}

bool MercatorProjection::geoCoordinates(const int x, const int y,
                                        const ViewportParams* viewport,
                                        qreal& lon, qreal& lat,
                                        GeoDataCoordinates::Unit unit) const
{
    const int radius = viewport->radius();

    Q_ASSERT(radius > 0);

    // Calculate translation of center point

    const qreal centerLon = viewport->centerLongitude();
    const qreal centerLat = viewport->centerLatitude();

    // Calculate how many pixel are being represented per radians.

    const float rad2Pixel = (qreal)(2 * radius) / M_PI;
    const qreal pixel2Rad = M_PI / (2 * radius);

    {
        const int halfImageWidth = viewport->width() / 2;
        const int xPixels        = x - halfImageWidth;
        lon                      = xPixels * pixel2Rad + centerLon;

        while (lon > M_PI)
        {
            lon -= 2 * M_PI;
        }

        while (lon < -M_PI)
        {
            lon += 2 * M_PI;
        }

        if (unit == GeoDataCoordinates::Degree)
        {
            lon *= RAD2DEG;
        }
    }

    {
        const int halfImageHeight = viewport->height() / 2;
        const int yCenterOffset   = (int)(asinh(tan(centerLat)) * rad2Pixel);
        const int yTop            = halfImageHeight - 2 * radius + yCenterOffset;
        const int yBottom         = yTop + 4 * radius;

        if ((y >= yTop) && (y < yBottom))
        {
            lat = gd(((halfImageHeight + yCenterOffset) - y)
                     * pixel2Rad);

            if (unit == GeoDataCoordinates::Degree)
            {
                lat *= RAD2DEG;
            }

            return true; // lat successfully calculated
        }
    }

    return false; // lat unchanged
}

GeoDataLatLonAltBox MercatorProjection::latLonAltBox(const QRect& screenRect,
                                                     const ViewportParams* viewport) const
{
    qreal west;
    qreal north = 85 * DEG2RAD;
    geoCoordinates(screenRect.left(), screenRect.top(), viewport, west, north, GeoDataCoordinates::Radian);

    qreal east;
    qreal south = -85 * DEG2RAD;
    geoCoordinates(screenRect.right(), screenRect.bottom(), viewport, east, south, GeoDataCoordinates::Radian);

    // For the case where the whole viewport gets covered there is a
    // pretty dirty and generic detection algorithm:

    GeoDataLatLonAltBox latLonAltBox;
    latLonAltBox.setNorth(north, GeoDataCoordinates::Radian);
    latLonAltBox.setSouth(south, GeoDataCoordinates::Radian);
    latLonAltBox.setWest(west, GeoDataCoordinates::Radian);
    latLonAltBox.setEast(east, GeoDataCoordinates::Radian);
    latLonAltBox.setMinAltitude(-100000000.0);
    latLonAltBox.setMaxAltitude(100000000000000.0);

    // The remaining algorithm should be pretty generic for all kinds of
    // flat projections:

    // If the whole globe is visible we can easily calculate
    // analytically the lon-/lat- range.
    // qreal pitch = GeoDataPoint::normalizeLat( viewport->planetAxis().pitch() );

    int xRepeatDistance = 4 * viewport->radius();

    if (viewport->width() >= xRepeatDistance)
    {
        latLonAltBox.setWest(-M_PI);
        latLonAltBox.setEast(+M_PI);
    }

    return latLonAltBox;
}

bool MercatorProjection::mapCoversViewport(const ViewportParams* viewport) const
{
    int radius            = viewport->radius();
    int height            = viewport->height();

    // Calculate translation of center point

    const qreal centerLat = viewport->centerLatitude();

    // Calculate how many pixel are being represented per radians.

    const float rad2Pixel = (float)(2 * radius) / M_PI;

    int yCenterOffset     = (int)(asinh(tan(centerLat)) * rad2Pixel);
    int yTop              = height / 2 - 2 * radius + yCenterOffset;
    int yBottom           = yTop + 4 * radius;

    return !((yTop >= 0) || (yBottom < height));
}

} // namespace Marble
