// Copyright (C) 2015 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qgeocodejsonparser.h"

#include <QtPositioning/QGeoShape>
#include <QtPositioning/QGeoRectangle>
#include <QtPositioning/QGeoAddress>
#include <QtPositioning/QGeoCoordinate>

#include <QtCore/QThreadPool>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonParseError>
#include <QtCore/QVariantMap>

#include <QtDebug>

QT_BEGIN_NAMESPACE

namespace {

/*
    Checks that the given Location object contains the information
    we need and is not malformed in any way.  We expect a Location
    object of the following form:

    "Location": {
        "Address": {
            "AdditionalData": [
                {
                    "key": "CountryName",
                    "value": "Australia"
                },
                {
                    "key": "StateName",
                    "value": "New South Wales"
                }
            ],
            "City": "Sydney",
            "Country": "AUS",
            "District": "Casula",
            "Label": "Casula, Sydney, NSW, Australia",
            "PostalCode": "2170",
            "State": "NSW"
        },
        "DisplayPosition": {
            "Latitude": -33.949509999999997,
            "Longitude": 150.90386000000001
        },
        "LocationId": "NT_5UQ89lKoiI4DIYbOrIR0-D",
        "LocationType": "area",
        "MapReference": {
            "CityId": "1469266800",
            "CountryId": "1469256839",
            "DistrictId": "1469267758",
            "MapId": "NXAM16130",
            "MapReleaseDate": "2016-10-05",
            "MapVersion": "Q1/2016",
            "ReferenceId": "868383156",
            "SideOfStreet": "neither",
            "StateId": "1469256831"
        },
        "MapView": {
            "BottomRight": {
                "Latitude": -33.966839999999998,
                "Longitude": 150.91875999999999
            },
            "TopLeft": {
                "Latitude": -33.937440000000002,
                "Longitude": 150.87457000000001
            }
        }
    }

*/
bool checkLocation(const QJsonObject &loc, QString *errorString)
{
    QJsonObject::const_iterator ait = loc.constFind(QLatin1String("Address"));
    if (ait == loc.constEnd()) {
        *errorString = QLatin1String("Expected Address element within Location object");
        return false;
    } else if (!ait.value().isObject()) {
        *errorString = QLatin1String("Expected Address object within Location object");
        return false;
    }

    QJsonObject::const_iterator dpit = loc.constFind(QLatin1String("DisplayPosition"));
    if (dpit == loc.constEnd()) {
        *errorString = QLatin1String("Expected DisplayPosition element within Location object");
        return false;
    } else if (!dpit.value().isObject()) {
        *errorString = QLatin1String("Expected DisplayPosition object within Location object");
        return false;
    }
    QJsonObject displayPosition = dpit.value().toObject();
    QJsonObject::const_iterator latit = displayPosition.constFind(QLatin1String("Latitude"));
    if (latit == displayPosition.constEnd()) {
        *errorString = QLatin1String("Expected Latitude element within Location.DisplayPosition object");
        return false;
    } else if (!latit.value().isDouble()) {
        *errorString = QLatin1String("Expected Latitude double within Location.DisplayPosition object");
        return false;
    }
    QJsonObject::const_iterator lonit = displayPosition.constFind(QLatin1String("Longitude"));
    if (lonit == displayPosition.constEnd()) {
        *errorString = QLatin1String("Expected Longitude element within Location.DisplayPosition object");
        return false;
    } else if (!lonit.value().isDouble()) {
        *errorString = QLatin1String("Expected Longitude double within Location.DisplayPosition object");
        return false;
    }

    QJsonObject::const_iterator mvit = loc.constFind(QLatin1String("MapView"));
    if (mvit == loc.constEnd()) {
        *errorString = QLatin1String("Expected MapView element within Location object");
        return false;
    } else if (!mvit.value().isObject()) {
        *errorString = QLatin1String("Expected MapView object within Location object");
        return false;
    }
    QJsonObject mapView = mvit.value().toObject();
    QJsonObject::const_iterator brit = mapView.constFind(QLatin1String("BottomRight"));
    if (brit == mapView.constEnd()) {
        *errorString = QLatin1String("Expected BottomRight element within Location.MapView object");
        return false;
    } else if (!brit.value().isObject()) {
        *errorString = QLatin1String("Expected BottomRight object within Location.MapView object");
        return false;
    }
    QJsonObject bottomRight = brit.value().toObject();
    QJsonObject::const_iterator brlatit = bottomRight.constFind(QLatin1String("Latitude"));
    if (brlatit == bottomRight.constEnd()) {
        *errorString = QLatin1String("Expected Latitude element within Location.MapView.BottomRight object");
        return false;
    } else if (!brlatit.value().isDouble()) {
        *errorString = QLatin1String("Expected Latitude double within Location.MapView.BottomRight object");
        return false;
    }
    QJsonObject::const_iterator brlonit = bottomRight.constFind(QLatin1String("Longitude"));
    if (brlonit == bottomRight.constEnd()) {
        *errorString = QLatin1String("Expected Longitude element within Location.MapView.BottomRight object");
        return false;
    } else if (!brlonit.value().isDouble()) {
        *errorString = QLatin1String("Expected Longitude double within Location.MapView.BottomRight object");
        return false;
    }
    QJsonObject::const_iterator tlit = mapView.constFind(QLatin1String("TopLeft"));
    if (tlit == mapView.constEnd()) {
        *errorString = QLatin1String("Expected TopLeft element within Location.MapView object");
        return false;
    } else if (!tlit.value().isObject()) {
        *errorString = QLatin1String("Expected TopLeft object within Location.MapView object");
        return false;
    }
    QJsonObject topLeft = tlit.value().toObject();
    QJsonObject::const_iterator tllatit = topLeft.constFind(QLatin1String("Latitude"));
    if (tllatit == topLeft.constEnd()) {
        *errorString = QLatin1String("Expected Latitude element within Location.MapView.TopLeft object");
        return false;
    } else if (!tllatit.value().isDouble()) {
        *errorString = QLatin1String("Expected Latitude double within Location.MapView.TopLeft object");
        return false;
    }
    QJsonObject::const_iterator tllonit = topLeft.constFind(QLatin1String("Longitude"));
    if (tllonit == bottomRight.constEnd()) {
        *errorString = QLatin1String("Expected Longitude element within Location.MapView.TopLeft object");
        return false;
    } else if (!tllonit.value().isDouble()) {
        *errorString = QLatin1String("Expected Longitude double within Location.MapView.TopLeft object");
        return false;
    }

    return true;
}

/*
    Checks that the given document contains the required information
    and is not malformed in any way.  We expect a document like the
    following:

    {
        "Response": {
            "MetaInfo": {
                "Timestamp": "2016-10-18T08:42:04.369+0000"
            },
            "View": [
                {
                    "ViewId": 0,
                    "_type": "SearchResultsViewType",
                    "Result": [
                        {
                            "Direction": 72.099999999999994,
                            "Distance": -1885.2,
                            "Location": {
                                // OMITTED FOR BREVITY
                            },
                            "MatchLevel": "district",
                            "MatchQuality": {
                                "City": 1,
                                "Country": 1,
                                "District": 1,
                                "PostalCode": 1,
                                "State": 1
                            },
                            "Relevance": 1
                        }
                    ]
                }
            ]
        }
    }
*/
bool checkDocument(const QJsonDocument &doc, QString *errorString)
{
    if (!doc.isObject()) {
        *errorString = QLatin1String("Expected JSON document containing object");
        return false;
    }

    QJsonObject rootObject = doc.object();
    QJsonObject::const_iterator it = rootObject.constFind(QLatin1String("Response"));
    if (it == rootObject.constEnd()) {
        *errorString = QLatin1String("Expected Response element within root object");
        return false;
    } else if (!it.value().isObject()) {
        *errorString = QLatin1String("Expected Response object within root object");
        return false;
    }

    QJsonObject response = it.value().toObject();
    QJsonObject::const_iterator rit = response.constFind(QLatin1String("View"));
    if (rit == response.constEnd()) {
        *errorString = QLatin1String("Expected View element within Response object");
        return false;
    } else if (!rit.value().isArray()) {
        *errorString = QLatin1String("Expected View array within Response object");
        return false;
    }

    const QJsonArray view = rit.value().toArray();
    for (const QJsonValueConstRef viewElement : view) {
        if (!viewElement.isObject()) {
            *errorString = QLatin1String("Expected View array element to be object");
            return false;
        }

        QJsonObject viewObject = viewElement.toObject();
        QJsonObject::const_iterator voit = viewObject.constFind(QLatin1String("Result"));
        if (voit == viewObject.constEnd()) {
            *errorString = QLatin1String("Expected Result element within View array object element");
            return false;
        } else if (!voit.value().isArray()) {
            *errorString = QLatin1String("Expected Result array within View array object element");
            return false;
        }

        const QJsonArray result = voit.value().toArray();
        for (const QJsonValueConstRef resultElement : result) {
            if (!resultElement.isObject()) {
                *errorString = QLatin1String("Expected Result array element to be object");
                return false;
            }

            QJsonObject resultObject = resultElement.toObject();
            QJsonObject::const_iterator roit = resultObject.constFind("Location");
            if (roit == resultObject.constEnd()) {
                *errorString = QLatin1String("Expected Location element in Result array element object");
                return false;
            } else if (!roit.value().isObject()) {
                *errorString = QLatin1String("Expected Location object in Result array element object");
                return false;
            }

            QJsonObject location = roit.value().toObject();
            if (!checkLocation(location, errorString)) {
                return false;
            }
        }
    }

    return true;
}

bool parseLocation(const QJsonObject &obj, const QGeoShape &bounds, QGeoLocation *loc)
{
    QJsonObject displayPosition = obj.value("DisplayPosition").toObject();
    QGeoCoordinate coordinate = QGeoCoordinate(displayPosition.value("Latitude").toDouble(), displayPosition.value("Longitude").toDouble());
    if (bounds.isValid() && !bounds.contains(coordinate)) {
        // manual bounds check failed, location can be omitted from results.
        return false;
    }

    QGeoAddress address;
    QJsonObject addr = obj.value("Address").toObject();
    address.setCountryCode(addr.value("Country").toString());
    address.setState(addr.value("State").toString());
    address.setCounty(addr.value("County").toString());
    address.setCity(addr.value("City").toString());
    address.setDistrict(addr.value("District").toString());
    QString houseNumber = addr.value("HouseNumber").toString();
    QString street = addr.value("Street").toString();
    address.setStreet(houseNumber.isEmpty() ? street : QString("%1 %2").arg(houseNumber, street));
    address.setPostalCode(addr.value("PostalCode").toString());
    QString label = addr.value("Label").toString().trimmed();
    if (!label.isEmpty()) {
        address.setText(label);
    }
    const QJsonArray additionalData = addr.value("AdditionalData").toArray();
    for (const QJsonValueConstRef adv : additionalData) {
        if (adv.isObject()) {
            const QJsonObject &ado(adv.toObject());
            if (ado.value("key").toString() == QLatin1String("CountryName")) {
                address.setCountry(ado.value("value").toString());
            }
        }
    }

    QGeoRectangle boundingBox;
    QJsonObject mapView = obj.value("MapView").toObject();
    QJsonObject bottomRight = mapView.value("BottomRight").toObject();
    QJsonObject topLeft = mapView.value("TopLeft").toObject();
    boundingBox.setBottomRight(QGeoCoordinate(bottomRight.value("Latitude").toDouble(), bottomRight.value("Longitude").toDouble()));
    boundingBox.setTopLeft(QGeoCoordinate(topLeft.value("Latitude").toDouble(), topLeft.value("Longitude").toDouble()));

    loc->setAddress(address);
    loc->setCoordinate(coordinate);
    loc->setBoundingShape(boundingBox);

    return true;
}

void parseDocument(const QJsonDocument &doc, const QGeoShape &bounds, QList<QGeoLocation> *locs)
{
    QJsonArray view = doc.object().value("Response").toObject().value("View").toArray();
    for (const QJsonValueRef viewElement : view) {
        QJsonArray result = viewElement.toObject().value("Result").toArray();
        for (const QJsonValueRef resultElement : result) {
            QGeoLocation location;
            if (parseLocation(resultElement.toObject().value("Location").toObject(), bounds, &location)) {
                locs->append(location);
            }
        }
    }
}

} // namespace

void QGeoCodeJsonParser::setBounds(const QGeoShape &bounds)
{
    m_bounds = bounds;
}

void QGeoCodeJsonParser::parse(const QByteArray &data)
{
    m_data = data;
    QThreadPool::globalInstance()->start(this);
}

void QGeoCodeJsonParser::run()
{
    // parse the document.
    QJsonParseError perror;
    m_document = QJsonDocument::fromJson(m_data, &perror);
    if (perror.error != QJsonParseError::NoError) {
        m_errorString = perror.errorString();
    } else {
        // ensure that the response is valid and contains the information we need.
        if (checkDocument(m_document, &m_errorString)) {
            // extract the location results from the response.
            parseDocument(m_document, m_bounds, &m_results);
            emit results(m_results);
            return;
        }
    }

    emit errorOccurred(m_errorString);
}

QT_END_NAMESPACE
