// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2009 Bastian Holst <bastianholst@gmx.de>
//

// Self
#include "WeatherData.h"

// Marble
#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MarbleGlobal.h"

// Qt
#include <QAtomicInt>
#include <QDate>
#include <QDateTime>
#include <QHash>
#include <QImage>
#include <QLocale>

#include <cmath>

namespace Marble
{

// Factors
// m/s to knots
const qreal MPS2KN = 1.9437;
const qreal KN2MPS = 1 / MPS2KN;
// m/s to kilometer per hour
const qreal MPS2KPH = 3.6;
const qreal KPH2MPS = 1 / MPS2KPH;
// m/s to miles per hour
const qreal MPS2MPH = MPS2KPH * KM2MI;
const qreal MPH2MPS = 1 / MPS2MPH;
// HectoPascal to KiloPascal
const qreal HPA2KPA = 10;
const qreal KPA2HPA = 1 / HPA2KPA;
// Bar to HectoPascal
const qreal BAR2HPA = 1000;
const qreal HPA2BAR = 1 / BAR2HPA;
// mmHg to HectoPascal
const qreal HG2HPA = 1.33;
const qreal HPA2HG = 1 / HG2HPA;
// inchHg to HectoPascal
const qreal IHG2HPA = HG2HPA * 25.4;
const qreal HPA2IHG = HPA2HG / 25.4;

// Summands
// Kelvin to degree Celsius
const qreal KEL2CEL = -273.15;
const qreal CEL2KEL = -KEL2CEL;

class WeatherDataPrivate
{
    Q_DECLARE_TR_FUNCTIONS(WeatherDataPrivate)

public:
    WeatherDataPrivate()
        : m_pubTime()
        , m_dataDate()
        , m_condition(WeatherData::ConditionNotAvailable)
        , m_windDirection(WeatherData::DirectionNotAvailable)
        , m_windSpeed(-1.0)
        , m_temperature(-1.0)
        , m_maxTemperature(-1.0)
        , m_minTemperature(-1.0)
        , m_visibility(WeatherData::VisibilityNotAvailable)
        , m_pressure(-1.0)
        , m_pressureDevelopment(WeatherData::PressureDevelopmentNotAvailable)
        , m_humidity(-1.0)
        , ref(1)
    {
        initializeIcons();
    }

    WeatherDataPrivate(const WeatherDataPrivate &other)
        : m_pubTime(other.m_pubTime)
        , m_dataDate(other.m_dataDate)
        , m_condition(other.m_condition)
        , m_windDirection(other.m_windDirection)
        , m_windSpeed(other.m_windSpeed)
        , m_temperature(other.m_temperature)
        , m_maxTemperature(other.m_maxTemperature)
        , m_minTemperature(other.m_minTemperature)
        , m_visibility(other.m_visibility)
        , m_pressure(other.m_pressure)
        , m_pressureDevelopment(other.m_pressureDevelopment)
        , m_humidity(other.m_humidity)
        , ref(other.ref)
    {
        initializeIcons();
    }

    static void initializeIcons()
    {
        if (s_iconPath.isEmpty()) {
            // Clouds
            s_iconPath.insert(WeatherData::ConditionNotAvailable, MarbleDirs::path(QStringLiteral("weather/weather-none-available.png")));
            s_iconPath.insert(WeatherData::ClearDay, MarbleDirs::path(QStringLiteral("weather/weather-clear.png")));
            s_iconPath.insert(WeatherData::ClearNight, MarbleDirs::path(QStringLiteral("weather/weather-clear-night.png")));
            s_iconPath.insert(WeatherData::FewCloudsDay, MarbleDirs::path(QStringLiteral("weather/weather-few-clouds.png")));
            s_iconPath.insert(WeatherData::FewCloudsNight, MarbleDirs::path(QStringLiteral("weather/weather-few-clouds-night.png")));
            s_iconPath.insert(WeatherData::PartlyCloudyDay, MarbleDirs::path(QStringLiteral("weather/weather-clouds.png")));
            s_iconPath.insert(WeatherData::PartlyCloudyNight, MarbleDirs::path(QStringLiteral("weather/weather-clouds-night.png")));
            s_iconPath.insert(WeatherData::Overcast, MarbleDirs::path(QStringLiteral("weather/weather-many-clouds.png")));

            // Rain
            s_iconPath.insert(WeatherData::LightShowersDay, MarbleDirs::path(QStringLiteral("weather/weather-showers-scattered-day.png")));
            s_iconPath.insert(WeatherData::LightShowersNight, MarbleDirs::path(QStringLiteral("weather/weather-showers-scattered-night.png")));
            s_iconPath.insert(WeatherData::ShowersDay, MarbleDirs::path(QStringLiteral("weather/weather-showers-day.png")));
            s_iconPath.insert(WeatherData::ShowersNight, MarbleDirs::path(QStringLiteral("weather/weather-showers-night.png")));
            s_iconPath.insert(WeatherData::LightRain, MarbleDirs::path(QStringLiteral("weather/weather-showers-scattered.png")));
            s_iconPath.insert(WeatherData::Rain, MarbleDirs::path(QStringLiteral("weather/weather-showers.png")));

            // Special
            s_iconPath.insert(WeatherData::ChanceThunderstormDay, MarbleDirs::path(QStringLiteral("weather/weather-storm-day.png")));
            s_iconPath.insert(WeatherData::ChanceThunderstormNight, MarbleDirs::path(QStringLiteral("weather/weather-storm-night.png")));
            s_iconPath.insert(WeatherData::Thunderstorm, MarbleDirs::path(QStringLiteral("weather/weather-storm.png")));
            s_iconPath.insert(WeatherData::Hail, MarbleDirs::path(QStringLiteral("weather/weather-hail.png")));
            s_iconPath.insert(WeatherData::ChanceSnowDay, MarbleDirs::path(QStringLiteral("weather/weather-snow-scattered-day.png")));
            s_iconPath.insert(WeatherData::ChanceSnowNight, MarbleDirs::path(QStringLiteral("weather/weather-snow-scattered-night.png")));
            s_iconPath.insert(WeatherData::LightSnow, MarbleDirs::path(QStringLiteral("weather/weather-snow-scattered.png")));
            s_iconPath.insert(WeatherData::Snow, MarbleDirs::path(QStringLiteral("weather/weather-snow.png")));
            s_iconPath.insert(WeatherData::RainSnow, MarbleDirs::path(QStringLiteral("weather/weather-snow-rain.png")));
            s_iconPath.insert(WeatherData::Mist, MarbleDirs::path(QStringLiteral("weather/weather-mist.png")));
            s_iconPath.insert(WeatherData::SandStorm, MarbleDirs::path(QStringLiteral("weather/weather-none-available.png")));
        }
    }

    static qreal fromKelvin(qreal temp, WeatherData::TemperatureUnit format)
    {
        if (WeatherData::Kelvin == format) {
            return temp;
        } else if (WeatherData::Celsius == format) {
            return temp + KEL2CEL;
        } else if (WeatherData::Fahrenheit == format) {
            return (temp * 1.8) - 459.67;
        } else {
            mDebug() << "Wrong temperature format";
            return 0;
        }
    }

    static qreal toKelvin(qreal temp, WeatherData::TemperatureUnit format)
    {
        if (WeatherData::Kelvin == format) {
            return temp;
        } else if (WeatherData::Celsius == format) {
            return temp + CEL2KEL;
        } else if (WeatherData::Fahrenheit == format) {
            return (temp + 459.67) / 1.8;
        } else {
            mDebug() << "Wrong temperature format";
            return 0;
        }
    }

    static bool isPositiveValue(qreal value)
    {
        // A small tolerance
        return value > -0.5;
    }

    static QString generateTemperatureString(qreal temp, WeatherData::TemperatureUnit format)
    {
        QLocale locale = QLocale::system();
        // We round to integer.
        QString string = locale.toString(floor(fromKelvin(temp, format) + 0.5));
        switch (format) {
        case WeatherData::Kelvin:
            string += QLatin1StringView(" K");
            break;
        case WeatherData::Celsius:
            string += QString::fromUtf8("°C");
            break;
        case WeatherData::Fahrenheit:
            string += QString::fromUtf8("°F");
            break;
        }
        return string;
    }

    WeatherDataPrivate &operator=(const WeatherDataPrivate &other) = default;

    QDateTime m_pubTime;
    QDate m_dataDate;
    WeatherData::WeatherCondition m_condition;
    WeatherData::WindDirection m_windDirection;

    // Wind speed stored in m/s
    qreal m_windSpeed;

    // Temperatures stored in Kelvin
    qreal m_temperature;
    qreal m_maxTemperature;
    qreal m_minTemperature;

    WeatherData::Visibility m_visibility;

    // Pressure stored in hecto pascal
    qreal m_pressure;

    WeatherData::PressureDevelopment m_pressureDevelopment;

    // Relative humidity
    qreal m_humidity;

    QAtomicInt ref;

    static QHash<WeatherData::WeatherCondition, QImage> s_icons;
    static QHash<WeatherData::WeatherCondition, QString> s_iconPath;
    static WeatherData::TemperatureUnit s_standardTemperatureUnit;
};

QHash<WeatherData::WeatherCondition, QImage> WeatherDataPrivate::s_icons = QHash<WeatherData::WeatherCondition, QImage>();
QHash<WeatherData::WeatherCondition, QString> WeatherDataPrivate::s_iconPath = QHash<WeatherData::WeatherCondition, QString>();
WeatherData::TemperatureUnit WeatherDataPrivate::s_standardTemperatureUnit = WeatherData::Celsius;

WeatherData::WeatherData()
    : d(new WeatherDataPrivate())
{
}

WeatherData::WeatherData(const WeatherData &other)
    : d(other.d)
{
    d->ref.ref();
}

WeatherData::~WeatherData()
{
    if (!d->ref.deref())
        delete d;
}

bool WeatherData::isValid() const
{
    return hasValidPublishingTime() || hasValidDataDate() || hasValidCondition() || hasValidWindDirection() || hasValidWindSpeed() || hasValidTemperature()
        || hasValidMaxTemperature() || hasValidMinTemperature() || hasValidVisibility() || hasValidPressure() || hasValidPressureDevelopment()
        || hasValidHumidity();
}

QDateTime WeatherData::publishingTime() const
{
    return d->m_pubTime;
}

void WeatherData::setPublishingTime(const QDateTime &dateTime)
{
    detach();
    d->m_pubTime = dateTime.toUTC();
}

bool WeatherData::hasValidPublishingTime() const
{
    return d->m_pubTime.isValid();
}

QDate WeatherData::dataDate() const
{
    return d->m_dataDate;
}

void WeatherData::setDataDate(const QDate &date)
{
    detach();
    d->m_dataDate = date;
}

bool WeatherData::hasValidDataDate() const
{
    return d->m_dataDate.isValid();
}

WeatherData::WeatherCondition WeatherData::condition() const
{
    return d->m_condition;
}

void WeatherData::setCondition(WeatherData::WeatherCondition condition)
{
    detach();
    d->m_condition = condition;
}

bool WeatherData::hasValidCondition() const
{
    return d->m_condition != WeatherData::ConditionNotAvailable;
}

QString WeatherData::conditionString() const
{
    switch (condition()) {
    case ClearDay:
        return tr("sunny");
    case ClearNight:
        return tr("clear");
    case FewCloudsDay:
    case FewCloudsNight:
        return tr("few clouds");
    case PartlyCloudyDay:
    case PartlyCloudyNight:
        return tr("partly cloudy");
    case Overcast:
        return tr("overcast");
    case LightShowersDay:
    case LightShowersNight:
        return tr("light showers");
    case ShowersDay:
    case ShowersNight:
        return tr("showers");
    case LightRain:
        return tr("light rain");
    case Rain:
        return tr("rain");
    case ChanceThunderstormDay:
    case ChanceThunderstormNight:
        return tr("occasionally thunderstorm");
    case Thunderstorm:
        return tr("thunderstorm");
    case Hail:
        return tr("hail");
    case ChanceSnowDay:
    case ChanceSnowNight:
        return tr("occasionally snow");
    case LightSnow:
        return tr("light snow");
    case Snow:
        return tr("snow");
    case RainSnow:
        return tr("rain and snow");
    case Mist:
        return tr("mist");
    case SandStorm:
        return tr("sandstorm");
    default:
        return QStringLiteral("Condition not available");
    }
}

QImage WeatherData::icon() const
{
    QImage icon = WeatherDataPrivate::s_icons.value(condition());

    // If the icon is in the hash, simply return it.
    if (!icon.isNull()) {
        return icon;
    }
    // If it isn't in the hash, the icon will be created (from the value stored in s_iconPath).
    else {
        icon = QImage(WeatherDataPrivate::s_iconPath.value(condition()));
        WeatherDataPrivate::s_icons.insert(condition(), icon);
        return icon;
    }
}

QString WeatherData::iconSource() const
{
    QString const invalid = MarbleDirs::path(QStringLiteral("weather/weather-none-available.png"));
    QString const icon = WeatherDataPrivate::s_iconPath.value(condition());
    return icon == invalid ? QString() : icon;
}

WeatherData::WindDirection WeatherData::windDirection() const
{
    return d->m_windDirection;
}

void WeatherData::setWindDirection(WeatherData::WindDirection direction)
{
    detach();
    d->m_windDirection = direction;
}

bool WeatherData::hasValidWindDirection() const
{
    return d->m_windDirection != WeatherData::DirectionNotAvailable;
}

QString WeatherData::windDirectionString() const
{
    switch (windDirection()) {
    case N:
        return tr("N");
    case NNE:
        return tr("NNE");
    case NE:
        return tr("NE");
    case ENE:
        return tr("ENE");
    case E:
        return tr("E");
    case SSE:
        return tr("SSE");
    case SE:
        return tr("SE");
    case ESE:
        return tr("ESE");
    case S:
        return tr("S");
    case NNW:
        return tr("NNW");
    case NW:
        return tr("NW");
    case WNW:
        return tr("WNW");
    case W:
        return tr("W");
    case SSW:
        return tr("SSW");
    case SW:
        return tr("SW");
    case WSW:
        return tr("WSW");
    default:
        return {};
    }
}

qreal WeatherData::windSpeed(WeatherData::SpeedUnit format) const
{
    if (WeatherData::mps == format) {
        return d->m_windSpeed;
    }
    if (WeatherData::kph == format) {
        return d->m_windSpeed * MPS2KPH;
    } else if (WeatherData::mph == format) {
        return d->m_windSpeed * MPS2MPH;
    } else if (WeatherData::knots == format) {
        return d->m_windSpeed * MPS2KN;
    } else if (WeatherData::beaufort == format) {
        if (d->m_windSpeed < 0.3)
            return 0;
        else if (d->m_windSpeed < 1.6)
            return 1;
        else if (d->m_windSpeed < 3.4)
            return 2;
        else if (d->m_windSpeed < 5.5)
            return 3;
        else if (d->m_windSpeed < 8.0)
            return 4;
        else if (d->m_windSpeed < 10.8)
            return 5;
        else if (d->m_windSpeed < 13.9)
            return 6;
        else if (d->m_windSpeed < 17.2)
            return 7;
        else if (d->m_windSpeed < 20.8)
            return 8;
        else if (d->m_windSpeed < 24.5)
            return 9;
        else if (d->m_windSpeed < 28.5)
            return 10;
        else if (d->m_windSpeed < 32.7)
            return 11;
        else
            return 12;
    } else {
        mDebug() << "Wrong speed format";
        return 0;
    }
}

void WeatherData::setWindSpeed(qreal speed, WeatherData::SpeedUnit format)
{
    detach();
    if (WeatherData::mps == format) {
        d->m_windSpeed = speed;
    }
    if (WeatherData::kph == format) {
        d->m_windSpeed = speed * KPH2MPS;
    } else if (WeatherData::mph == format) {
        d->m_windSpeed = speed * MPH2MPS;
    } else if (WeatherData::knots == format) {
        d->m_windSpeed = speed * KN2MPS;
    } else if (WeatherData::beaufort == format) {
        int rounded = (int)speed;
        if (0 == rounded)
            d->m_windSpeed = 0.15;
        else if (1 == rounded)
            d->m_windSpeed = 0.95;
        else if (2 == rounded)
            d->m_windSpeed = 2.5;
        else if (3 == rounded)
            d->m_windSpeed = 4.45;
        else if (4 == rounded)
            d->m_windSpeed = 6.75;
        else if (5 == rounded)
            d->m_windSpeed = 9.4;
        else if (6 == rounded)
            d->m_windSpeed = 12.35;
        else if (7 == rounded)
            d->m_windSpeed = 15.55;
        else if (8 == rounded)
            d->m_windSpeed = 19.0;
        else if (9 == rounded)
            d->m_windSpeed = 22.65;
        else if (10 == rounded)
            d->m_windSpeed = 26.5;
        else if (11 == rounded)
            d->m_windSpeed = 30.6;
        else
            d->m_windSpeed = 34;
    } else {
        mDebug() << "Wrong speed format";
    }
}

bool WeatherData::hasValidWindSpeed() const
{
    return d->isPositiveValue(d->m_windSpeed);
}

QString WeatherData::windSpeedString(WeatherData::SpeedUnit unit) const
{
    QLocale locale = QLocale::system();
    // We round to integer.
    QString string = locale.toString(floor(windSpeed(unit) + 0.5));
    string += QLatin1Char(' ');
    switch (unit) {
    case WeatherData::kph:
        string += QObject::tr("km/h");
        break;
    case WeatherData::mph:
        string += QObject::tr("mph");
        break;
    case WeatherData::mps:
        string += QObject::tr("m/s");
        break;
    case WeatherData::knots:
        string += QObject::tr("knots");
        break;
    case WeatherData::beaufort:
        string += QObject::tr("Beaufort");
        break;
    }
    return string;
}

qreal WeatherData::temperature(WeatherData::TemperatureUnit format) const
{
    return d->fromKelvin(d->m_temperature, format);
}

void WeatherData::setTemperature(qreal temp, WeatherData::TemperatureUnit format)
{
    detach();
    d->m_temperature = d->toKelvin(temp, format);
}

bool WeatherData::hasValidTemperature() const
{
    return d->isPositiveValue(d->m_temperature);
}

QString WeatherData::temperatureString(WeatherData::TemperatureUnit format) const
{
    return d->generateTemperatureString(d->m_temperature, format);
}

qreal WeatherData::maxTemperature(WeatherData::TemperatureUnit format) const
{
    return d->fromKelvin(d->m_maxTemperature, format);
}

void WeatherData::setMaxTemperature(qreal temp, WeatherData::TemperatureUnit format)
{
    detach();
    d->m_maxTemperature = d->toKelvin(temp, format);
}

QString WeatherData::maxTemperatureString(WeatherData::TemperatureUnit format) const
{
    return d->generateTemperatureString(d->m_maxTemperature, format);
}

bool WeatherData::hasValidMaxTemperature() const
{
    return d->isPositiveValue(d->m_maxTemperature);
}

qreal WeatherData::minTemperature(WeatherData::TemperatureUnit format) const
{
    return d->fromKelvin(d->m_minTemperature, format);
}

QString WeatherData::minTemperatureString(WeatherData::TemperatureUnit format) const
{
    return d->generateTemperatureString(d->m_minTemperature, format);
}

void WeatherData::setMinTemperature(qreal temp, WeatherData::TemperatureUnit format)
{
    detach();
    d->m_minTemperature = d->toKelvin(temp, format);
}

bool WeatherData::hasValidMinTemperature() const
{
    return d->isPositiveValue(d->m_minTemperature);
}

WeatherData::Visibility WeatherData::visibility() const
{
    return d->m_visibility;
}

void WeatherData::setVisibilty(WeatherData::Visibility visibility)
{
    detach();
    d->m_visibility = visibility;
}

bool WeatherData::hasValidVisibility() const
{
    return d->m_visibility != WeatherData::VisibilityNotAvailable;
}

qreal WeatherData::pressure(WeatherData::PressureUnit format) const
{
    if (WeatherData::HectoPascal == format) {
        return d->m_pressure;
    } else if (WeatherData::KiloPascal == format) {
        return d->m_pressure * HPA2KPA;
    } else if (WeatherData::Bar == format) {
        return d->m_pressure * HPA2BAR;
    } else if (WeatherData::mmHg == format) {
        return d->m_pressure * HPA2HG;
    } else if (WeatherData::inchHg == format) {
        return d->m_pressure * HPA2IHG;
    } else {
        mDebug() << "Wrong pressure format";
        return 0;
    }
}

void WeatherData::setPressure(qreal pressure, WeatherData::PressureUnit format)
{
    detach();
    if (WeatherData::HectoPascal == format) {
        d->m_pressure = pressure;
    } else if (WeatherData::KiloPascal == format) {
        d->m_pressure = pressure * KPA2HPA;
    } else if (WeatherData::Bar == format) {
        d->m_pressure = pressure * BAR2HPA;
    } else if (WeatherData::mmHg == format) {
        d->m_pressure = pressure * HG2HPA;
    } else if (WeatherData::inchHg == format) {
        d->m_pressure = pressure * IHG2HPA;
    } else {
        mDebug() << "Wrong pressure format";
    }
}

bool WeatherData::hasValidPressure() const
{
    return d->isPositiveValue(d->m_pressure);
}

QString WeatherData::pressureString(WeatherData::PressureUnit unit) const
{
    QLocale locale = QLocale::system();
    // We round to integer.
    QString string = locale.toString(pressure(unit), 'f', 2);
    string += QLatin1Char(' ');
    switch (unit) {
    case WeatherData::HectoPascal:
        string += tr("hPa");
        break;
    case WeatherData::KiloPascal:
        string += tr("kPa");
        break;
    case WeatherData::Bar:
        string += tr("Bar");
        break;
    case WeatherData::mmHg:
        string += tr("mmHg");
        break;
    case WeatherData::inchHg:
        string += tr("inch Hg");
        break;
    }
    return string;
}

WeatherData::PressureDevelopment WeatherData::pressureDevelopment() const
{
    return d->m_pressureDevelopment;
}

void WeatherData::setPressureDevelopment(WeatherData::PressureDevelopment pressureDevelopment)
{
    detach();
    d->m_pressureDevelopment = pressureDevelopment;
}

bool WeatherData::hasValidPressureDevelopment() const
{
    return d->m_pressureDevelopment != WeatherData::PressureDevelopmentNotAvailable;
}

QString WeatherData::pressureDevelopmentString() const
{
    switch (pressureDevelopment()) {
    case Rising:
        return tr("rising", "air pressure is rising");
    case NoChange:
        return tr("steady", "air pressure has no change");
    case Falling:
        return tr("falling", "air pressure falls");
    default:
        return {};
    }
}

qreal WeatherData::humidity() const
{
    return d->m_humidity;
}

void WeatherData::setHumidity(qreal humidity)
{
    detach();
    d->m_humidity = humidity;
}

bool WeatherData::hasValidHumidity() const
{
    return d->isPositiveValue(d->m_humidity);
}

QString WeatherData::humidityString() const
{
    return QStringLiteral("%1 %").arg(humidity());
}

QString WeatherData::toHtml(WeatherData::TemperatureUnit temperatureUnit, WeatherData::SpeedUnit speedUnit, WeatherData::PressureUnit pressureUnit) const
{
    QString html;
    if (hasValidPublishingTime()) {
        html += tr("Publishing time: %1<br>").arg(publishingTime().toLocalTime().toString());
    }
    if (hasValidCondition()) {
        html += tr("Condition: %1<br>").arg(conditionString());
    }
    if (hasValidTemperature()) {
        html += tr("Temperature: %1<br>").arg(temperatureString(temperatureUnit));
    }
    if (hasValidMaxTemperature()) {
        html += tr("Max temperature: %1<br>").arg(maxTemperatureString(temperatureUnit));
    }
    if (hasValidMinTemperature()) {
        html += tr("Min temperature: %1<br>").arg(minTemperatureString(temperatureUnit));
    }
    if (hasValidWindDirection()) {
        html += tr("Wind direction: %1<br>").arg(windDirectionString());
    }
    if (hasValidWindSpeed()) {
        html += tr("Wind speed: %1<br>").arg(windSpeedString(speedUnit));
    }
    if (hasValidPressure()) {
        html += tr("Pressure: %1<br>").arg(pressureString(pressureUnit));
    }
    if (hasValidPressureDevelopment()) {
        html += tr("Pressure development: %1<br>").arg(pressureDevelopmentString());
    }
    if (hasValidHumidity()) {
        html += tr("Humidity: %1<br>").arg(humidityString());
    }

    return html;
}

WeatherData &WeatherData::operator=(const WeatherData &other)
{
    qAtomicAssign(d, other.d);
    return *this;
}

void WeatherData::detach()
{
    qAtomicDetach(d);
}

} // namespace Marble
