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

#include <GeoDataDocument.h>
#include <GeoDataLatLonAltBox.h>
#include <GeoDataPlacemark.h>
#include <GeoDataPolygon.h>
#include <GeoDataRelation.h>
#include <MarbleDebug.h>
#include <OsmRelation.h>
#include <StyleBuilder.h>
#include <osm/OsmObjectManager.h>

namespace Marble
{

OsmRelation::OsmMember::OsmMember()
    : reference(0)
{
    // nothing to do
}

OsmPlacemarkData &OsmRelation::osmData()
{
    return m_osmData;
}

const OsmPlacemarkData &OsmRelation::osmData() const
{
    return m_osmData;
}

void OsmRelation::parseMember(const QXmlStreamAttributes &attributes)
{
    addMember(attributes.value(QLatin1StringView("ref")).toLongLong(),
              attributes.value(QLatin1StringView("role")).toString(),
              attributes.value(QLatin1StringView("type")).toString());
}

void OsmRelation::addMember(qint64 reference, const QString &role, const QString &type)
{
    OsmMember member;
    member.reference = reference;
    member.role = role;
    member.type = type;
    m_members << member;
}

void OsmRelation::createMultipolygon(GeoDataDocument *document, OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
{
    if (!m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
        return;
    }

    QStringList const outerRoles = QStringList() << QStringLiteral("outer") << QString();
    QSet<qint64> outerWays;
    QSet<qint64> outerNodes;
    OsmRings const outer = rings(outerRoles, ways, nodes, outerNodes, outerWays);

    if (outer.isEmpty()) {
        return;
    }

    GeoDataPlacemark::GeoDataVisualCategory outerCategory = StyleBuilder::determineVisualCategory(m_osmData);
    if (outerCategory == GeoDataPlacemark::None) {
        // Try to determine the visual category from the relation members
        GeoDataPlacemark::GeoDataVisualCategory const firstCategory = StyleBuilder::determineVisualCategory(ways[*outerWays.begin()].osmData());

        bool categoriesAreSame = true;
        for (auto wayId : std::as_const(outerWays)) {
            GeoDataPlacemark::GeoDataVisualCategory const category = StyleBuilder::determineVisualCategory(ways[wayId].osmData());
            if (category != firstCategory) {
                categoriesAreSame = false;
                break;
            }
        }

        if (categoriesAreSame) {
            outerCategory = firstCategory;
        }
    }

    for (auto wayId : std::as_const(outerWays)) {
        Q_ASSERT(ways.contains(wayId));
        const auto &osmData = ways[wayId].osmData();
        GeoDataPlacemark::GeoDataVisualCategory const category = StyleBuilder::determineVisualCategory(osmData);
        if ((category == GeoDataPlacemark::None || category == outerCategory) && osmData.isEmpty()) {
            // Schedule way for removal: It's a non-styled way only used to create the outer boundary in this polygon
            usedWays << wayId;
        } // else we keep it

        for (auto nodeId : ways[wayId].references()) {
            ways[wayId].osmData().addNodeReference(nodes[nodeId].coordinates(), nodes[nodeId].osmData());
        }
    }

    QStringList const innerRoles = QStringList() << QStringLiteral("inner");
    QSet<qint64> innerWays;
    OsmRings const inner = rings(innerRoles, ways, nodes, usedNodes, innerWays);

    bool const hasMultipleOuterRings = outer.size() > 1;
    for (int i = 0, n = outer.size(); i < n; ++i) {
        auto const &outerRing = outer[i];

        auto polygon = new GeoDataPolygon;
        polygon->setOuterBoundary(outerRing.first);
        OsmPlacemarkData osmData = m_osmData;
        osmData.addMemberReference(-1, outerRing.second);

        int index = 0;
        for (auto const &innerRing : std::as_const(inner)) {
            if (innerRing.first.isEmpty() || !outerRing.first.contains(innerRing.first.first())) {
                // Simple check to see if this inner ring is inside the outer ring
                continue;
            }

            if (StyleBuilder::determineVisualCategory(innerRing.second) == GeoDataPlacemark::None) {
                // Schedule way for removal: It's a non-styled way only used to create the inner boundary in this polygon
                usedWays << innerRing.second.id();
            }
            polygon->appendInnerBoundary(innerRing.first);
            osmData.addMemberReference(index, innerRing.second);
            ++index;
        }

        if (outerCategory == GeoDataPlacemark::Bathymetry) {
            // In case of a bathymetry store elevation info since it is required during styling
            // The ele=* tag is present in the outermost way
            const QString ele = QStringLiteral("ele");
            const OsmPlacemarkData &outerWayData = outerRing.second;
            auto tagIter = outerWayData.findTag(ele);
            if (tagIter != outerWayData.tagsEnd()) {
                osmData.addTag(ele, tagIter.value());
            }
        }

        auto placemark = new GeoDataPlacemark;
        placemark->setName(m_osmData.tagValue(QStringLiteral("name")));
        placemark->setVisualCategory(outerCategory);
        placemark->setOsmData(osmData);
        placemark->setZoomLevel(StyleBuilder::minimumZoomLevel(outerCategory));
        placemark->setPopularity(StyleBuilder::popularity(placemark));
        placemark->setVisible(outerCategory != GeoDataPlacemark::None);
        placemark->setGeometry(polygon);
        if (hasMultipleOuterRings) {
            /** @todo Use a GeoDataMultiGeometry to keep the ID? */
            osmData.setId(0);
            OsmObjectManager::initializeOsmData(placemark);
        } else {
            OsmObjectManager::registerId(osmData.id());
        }
        usedNodes |= outerNodes;

        document->append(placemark);
    }
}

static OsmType stringToType(const QString &str)
{
    if (str == QStringLiteral("relation")) {
        return OsmType::Relation;
    }
    if (str == QStringLiteral("node")) {
        return OsmType::Node;
    }
    return OsmType::Way;
}

void OsmRelation::createRelation(GeoDataDocument *document, const QHash<qint64, GeoDataPlacemark *> &placemarks) const
{
    if (m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
        return;
    }

    OsmPlacemarkData osmData = m_osmData;
    auto relation = new GeoDataRelation;

    relation->setName(osmData.tagValue(QStringLiteral("name")));
    if (relation->name().isEmpty()) {
        relation->setName(osmData.tagValue(QStringLiteral("ref")));
    }
    relation->osmData() = osmData;

    for (auto const &member : m_members) {
        auto const iter = placemarks.find(member.reference);
        if (iter != placemarks.constEnd()) {
            relation->addMember(*iter, member.reference, stringToType(member.type), member.role);
        }
    }

    if (relation->members().isEmpty()) {
        delete relation;
        return;
    }

    OsmObjectManager::registerId(osmData.id());
    relation->setVisible(false);
    document->append(relation);
}

OsmRelation::OsmRings
OsmRelation::rings(const QStringList &roles, const OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
{
    QSet<qint64> currentWays;
    QSet<qint64> currentNodes;
    QList<qint64> roleMembers;
    for (auto const &member : m_members) {
        if (roles.contains(member.role)) {
            if (!ways.contains(member.reference)) {
                // A way is missing. Return nothing.
                return {};
            }
            roleMembers << member.reference;
        }
    }

    OsmRings result;
    QList<OsmWay> unclosedWays;
    for (auto wayId : std::as_const(roleMembers)) {
        GeoDataLinearRing ring;
        OsmWay const &way = ways[wayId];
        if (way.references().isEmpty()) {
            continue;
        }
        if (way.references().first() != way.references().last()) {
            unclosedWays.append(way);
            continue;
        }

        OsmPlacemarkData placemarkData = way.osmData();
        for (auto id : way.references()) {
            if (!nodes.contains(id)) {
                // A node is missing. Return nothing.
                return {};
            }
            const auto &node = nodes[id];
            ring << node.coordinates();
            placemarkData.addNodeReference(node.coordinates(), node.osmData());
        }
        Q_ASSERT(ways.contains(wayId));
        currentWays << wayId;
        result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
    }

    if (!unclosedWays.isEmpty()) {
        // mDebug() << "Trying to merge non-trivial polygon boundary in relation " << m_osmData.id();
        while (unclosedWays.length() > 0) {
            GeoDataLinearRing ring;
            qint64 firstReference = unclosedWays.first().references().first();
            qint64 lastReference = firstReference;
            OsmPlacemarkData placemarkData;
            bool ok = true;
            while (ok) {
                ok = false;
                for (int i = 0; i < unclosedWays.length();) {
                    const OsmWay &nextWay = unclosedWays.at(i);
                    if (nextWay.references().first() == lastReference || nextWay.references().last() == lastReference) {
                        bool isReversed = nextWay.references().last() == lastReference;
                        QList<qint64> v = nextWay.references();
                        while (!v.isEmpty()) {
                            qint64 id = isReversed ? v.takeLast() : v.takeFirst();
                            if (!nodes.contains(id)) {
                                // A node is missing. Return nothing.
                                return {};
                            }
                            if (id != lastReference) {
                                const auto &node = nodes[id];
                                ring << node.coordinates();
                                placemarkData.addNodeReference(node.coordinates(), node.osmData());
                                currentNodes << id;
                            }
                        }
                        lastReference = isReversed ? nextWay.references().first() : nextWay.references().last();
                        Q_ASSERT(ways.contains(nextWay.osmData().id()));
                        currentWays << nextWay.osmData().id();
                        unclosedWays.removeAt(i);
                        ok = true;
                        break;
                    } else {
                        ++i;
                    }
                }
            }

            if (lastReference != firstReference) {
                return {};
            } else {
                /** @todo Merge tags common to all rings into the new osm data? */
                result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
            }
        }
    }

    usedWays |= currentWays;
    usedNodes |= currentNodes;
    return result;
}

}
