/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "weatherapibackend.h"

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QLoggingCategory>

Q_DECLARE_LOGGING_CATEGORY(requestsLog)

static QString niceTemperatureString(double t)
{
    return QString::number(qRound(t)) + QChar(0xB0);
}

/*
    Converts weather code to a string that will be used to show the icon.
    The possible strings are based on the icon names. The icon name is built up
    as follows:
        weather-[mystring].png
    where [mystring] is the value returned by this method.
    Check resources for the full list of available icons.
*/
static QString weatherCodeToString(int code)
{
    switch (code) {
    case 1000:
        return "sunny";
    case 1003:
        return "sunny-very-few-clouds";
    case 1006:
        return "few-clouds";
    case 1009:
        return "overcast";
    case 1030:
    case 1135:
    case 1147:
        return "fog";
    case 1063:
    case 1072:
    case 1150:
    case 1153:
    case 1168:
    case 1171:
    case 1180:
    case 1183:
    case 1186:
    case 1189:
    case 1198:
        return "showers-scattered";
    case 1066:
    case 1069:
    case 1114:
    case 1117:
    case 1210:
    case 1213:
    case 1216:
    case 1219:
    case 1222:
    case 1225:
    case 1237:
    case 1255:
    case 1258:
    case 1261:
    case 1264:
    case 1279:
    case 1282:
        return "snow";
    case 1087:
        return "storm";
    case 1192:
    case 1195:
    case 1201:
    case 1240:
    case 1243:
    case 1246:
        return "showers";
    case 1204:
    case 1207:
    case 1249:
    case 1252:
        return "sleet";
    case 1273:
    case 1276:
        return "thundershower";
    default:
        return "sunny";
    }

    return "sunny"; // default choice
}

static void parseWeatherDescription(const QJsonObject &object, WeatherInfo &info)
{
    const QJsonObject conditionObject = object.value(u"condition").toObject();
    info.m_weatherDescription = conditionObject.value(u"text").toString();
    info.m_weatherIconId = weatherCodeToString(conditionObject.value(u"code").toInt());
}

WeatherApiBackend::WeatherApiBackend(QObject *parent)
    : ProviderBackend(parent),
      m_networkManager(new QNetworkAccessManager(this)),
      m_apiKey(QStringLiteral("8edde160c63c4be6b77101828211208"))
{
}

void WeatherApiBackend::requestWeatherInfo(const QString &city)
{
    generateWeatherRequest(city, QGeoCoordinate());
}

void WeatherApiBackend::requestWeatherInfo(const QGeoCoordinate &coordinate)
{
    const QString coordinateStr =
            QString("%1,%2").arg(coordinate.latitude()).arg(coordinate.longitude());
    generateWeatherRequest(coordinateStr, coordinate);
}

void WeatherApiBackend::handleWeatherForecastReply(QNetworkReply *reply,
                                                   const QGeoCoordinate &coordinate)
{
    if (!reply) {
        emit errorOccurred();
        return;
    }
    bool parsed = false;
    if (!reply->error()) {
        QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
        const QJsonObject documentObject = document.object();
        const QJsonObject locationObject = documentObject.value(u"location").toObject();

        // extract location name
        LocationInfo location;
        location.m_name = locationObject.value(u"name").toString();
        if (coordinate.isValid())
            location.m_coordinate = coordinate;
        qCDebug(requestsLog) << "Got weather for" << location.m_name;

        // extract current weather
        WeatherInfo currentWeather;

        const QJsonObject currentWeatherObject = documentObject.value(u"current").toObject();
        const QJsonValue temperature = currentWeatherObject.value(u"temp_c");
        if (temperature.isDouble())
            currentWeather.m_temperature = niceTemperatureString(temperature.toDouble());

        parseWeatherDescription(currentWeatherObject, currentWeather);

        parsed = !location.m_name.isEmpty() && !currentWeather.m_temperature.isEmpty();

        if (parsed) {
            QList<WeatherInfo> weatherDetails;
            weatherDetails << currentWeather;

            // extract forecast details
            const QJsonObject forecastObject = documentObject.value(u"forecast").toObject();
            const QJsonArray forecastDays = forecastObject.value(u"forecastday").toArray();
            for (qsizetype i = 0; i < forecastDays.size(); ++i) {
                const QJsonObject dayInfo = forecastDays.at(i).toObject();
                WeatherInfo info;

                const QDateTime dt =
                        QDateTime::fromSecsSinceEpoch(dayInfo.value(u"date_epoch").toInteger());
                info.m_dayOfWeek = dt.toString(u"ddd");

                const QJsonObject dayObject = dayInfo.value(u"day").toObject();
                const QJsonValue minTemp = dayObject.value(u"mintemp_c");
                const QJsonValue maxTemp = dayObject.value(u"maxtemp_c");
                if (minTemp.isDouble() && maxTemp.isDouble()) {
                    info.m_temperature = niceTemperatureString(minTemp.toDouble()) + QChar('/')
                            + niceTemperatureString(maxTemp.toDouble());
                } else {
                    qCDebug(requestsLog, "Failed to parse min or max temperature.");
                }

                parseWeatherDescription(dayObject, info);

                if (!info.m_temperature.isEmpty() && !info.m_weatherIconId.isEmpty())
                    weatherDetails.push_back(info);
            }

            emit weatherInformation(location, weatherDetails);
        }
    }
    if (!parsed) {
        emit errorOccurred();
        if (reply->error())
            qCDebug(requestsLog) << reply->errorString();
        else
            qCDebug(requestsLog, "Failed to parse weather JSON.");
    }

    reply->deleteLater();
}

void WeatherApiBackend::generateWeatherRequest(const QString &locationString,
                                               const QGeoCoordinate &coordinate)
{
    QUrl url("https://api.weatherapi.com/v1/forecast.json");

    QUrlQuery query;
    query.addQueryItem(QStringLiteral("key"), m_apiKey);
    query.addQueryItem(QStringLiteral("q"), locationString);
    query.addQueryItem(QStringLiteral("days"), QStringLiteral("4"));
    query.addQueryItem(QStringLiteral("aqi"), QStringLiteral("no"));
    query.addQueryItem(QStringLiteral("alerts"), QStringLiteral("no"));

    url.setQuery(query);

    QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::finished, this,
            [this, reply, coordinate]() { handleWeatherForecastReply(reply, coordinate); });
}
