// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2012 Torsten Rahn <rahn@kde.org>
// SPDX-FileCopyrightText: 2012 Cezar Mocan <mocancezar@gmail.com>
// SPDX-FileCopyrightText: 2014 Abhinav Gangwar <abhgang@gmail.com>
//
// For the Natural Earth Layer providing the Default data set at 0.5 arcminute resolution should be enough.
// This fileformat allows for even better packed data than the PNT format. For detailed polygons at arcminute
// scale on average it should use only 33% of the amount used by PNT.
//
// Description of the file format
//
// In the fileformat initially a file header is provided that provides the file format version and the number
// of polygons stored inside the file. A Polygon starts with the Polygon Header which provides the feature id
// and the number of so called "absolute nodes" that are about to follow. Absolute nodes always contain
// absolute geodetic coordinates. The Polygon Header also provides a flag that allows to specify whether the
// polygon is supposed to represent a line string ("0") or a linear ring ("1"). Each absolute node can be followed
// by relative nodes: These relative nodes are always nodes that follow in correct order inside the polygon after
// "their" absolute node. Each absolute node specifies the number of relative nodes which contain relative
// coordinates in reference to their absolute node. So an absolute node provides the absolute reference for
// relative nodes across a theoretical area of 2x2 squaredegree-area (which in practice frequently might rather
// amount to 1x1 square degrees).
//
// So much of the compression works by just referencing lat/lon diffs to special "absolute nodes". Hence the
// compression will especially work well for polygons with many nodes with a high node density.
//
// The parser has to convert these relative coordinates to absolute coordinates.
//

#include "Pn2Runner.h"

#include "GeoDataDocument.h"
#include "GeoDataLinearRing.h"
#include "GeoDataMultiGeometry.h"
#include "GeoDataPlacemark.h"
#include "GeoDataPolyStyle.h"
#include "GeoDataPolygon.h"
#include "GeoDataStyle.h"
#include "MarbleDebug.h"

#include <QFile>
#include <QFileInfo>

namespace Marble
{
// Polygon header flags, representing the type of polygon
enum polygonFlagType {
    LINESTRING = 0,
    LINEARRING = 1,
    OUTERBOUNDARY = 2,
    INNERBOUNDARY = 3,
    MULTIGEOMETRY = 4
};

Pn2Runner::Pn2Runner(QObject *parent)
    : ParsingRunner(parent)
{
}

Pn2Runner::~Pn2Runner() = default;

bool Pn2Runner::errorCheckLat(qint16 lat)
{
    return !(lat >= -10800 && lat <= +10800);
}

bool Pn2Runner::errorCheckLon(qint16 lon)
{
    return !(lon >= -21600 && lon <= +21600);
}

bool Pn2Runner::importPolygon(QDataStream &stream, GeoDataLineString *linestring, quint32 nrAbsoluteNodes)
{
    qint16 lat, lon, nrRelativeNodes;
    qint8 relativeLat, relativeLon;
    bool error = false;

    for (quint32 absoluteNode = 1; absoluteNode <= nrAbsoluteNodes; absoluteNode++) {
        stream >> lat >> lon >> nrRelativeNodes;

        error = error | errorCheckLat(lat) || errorCheckLon(lon);

        qreal degLat = (1.0 * lat / 120.0);
        qreal degLon = (1.0 * lon / 120.0);

        GeoDataCoordinates coord(degLon / 180 * M_PI, degLat / 180 * M_PI);
        linestring->append(coord);

        for (qint16 relativeNode = 1; relativeNode <= nrRelativeNodes; ++relativeNode) {
            stream >> relativeLat >> relativeLon;

            qint16 currLat = relativeLat + lat;
            qint16 currLon = relativeLon + lon;

            error = error | errorCheckLat(currLat) || errorCheckLon(currLon);

            qreal currDegLat = (1.0 * currLat / 120.0);
            qreal currDegLon = (1.0 * currLon / 120.0);

            GeoDataCoordinates currCoord(currDegLon / 180 * M_PI, currDegLat / 180 * M_PI);
            linestring->append(currCoord);
        }
    }

    *linestring = linestring->optimized();

    return error;
}

GeoDataDocument *Pn2Runner::parseFile(const QString &fileName, DocumentRole role, QString &error)
{
    QFileInfo fileinfo(fileName);
    if (fileinfo.suffix().compare(QLatin1StringView("pn2"), Qt::CaseInsensitive) != 0) {
        error = QStringLiteral("File %1 does not have a pn2 suffix").arg(fileName);
        mDebug() << error;
        return nullptr;
    }

    QFile file(fileName);
    if (!file.exists()) {
        error = QStringLiteral("File %1 does not exist").arg(fileName);
        mDebug() << error;
        return nullptr;
    }

    file.open(QIODevice::ReadOnly);
    m_stream.setDevice(&file); // read the data serialized from the file

    m_stream >> m_fileHeaderVersion >> m_fileHeaderPolygons >> m_isMapColorField;

    switch (m_fileHeaderVersion) {
    case 1:
        return parseForVersion1(fileName, role);
    case 2:
        return parseForVersion2(fileName, role);
    default:
        qDebug() << "File can't be parsed. We don't have parser for file header version:" << m_fileHeaderVersion;
        break;
    }

    return nullptr;
}

GeoDataDocument *Pn2Runner::parseForVersion1(const QString &fileName, DocumentRole role)
{
    auto document = new GeoDataDocument();
    document->setDocumentRole(role);

    bool error = false;

    quint32 ID, nrAbsoluteNodes;
    quint8 flag, prevFlag = -1;

    GeoDataStyle::Ptr style;
    auto polygon = new GeoDataPolygon;

    for (quint32 currentPoly = 1; (currentPoly <= m_fileHeaderPolygons) && (!error) && (!m_stream.atEnd()); currentPoly++) {
        m_stream >> ID >> nrAbsoluteNodes >> flag;

        if (flag != INNERBOUNDARY && (prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY)) {
            auto placemark = new GeoDataPlacemark;
            placemark->setGeometry(polygon);
            if (m_isMapColorField) {
                if (style) {
                    placemark->setStyle(style);
                }
            }
            document->append(placemark);
        }

        if (flag == LINESTRING) {
            auto linestring = new GeoDataLineString;
            error = error | importPolygon(m_stream, linestring, nrAbsoluteNodes);

            auto placemark = new GeoDataPlacemark;
            placemark->setGeometry(linestring);
            document->append(placemark);
        }

        if ((flag == LINEARRING) || (flag == OUTERBOUNDARY) || (flag == INNERBOUNDARY)) {
            if (flag == OUTERBOUNDARY && m_isMapColorField) {
                quint8 colorIndex;
                m_stream >> colorIndex;
                style = GeoDataStyle::Ptr(new GeoDataStyle);
                GeoDataPolyStyle polyStyle;
                polyStyle.setColorIndex(colorIndex);
                style->setPolyStyle(polyStyle);
            }

            auto linearring = new GeoDataLinearRing;
            error = error | importPolygon(m_stream, linearring, nrAbsoluteNodes);

            if (flag == LINEARRING) {
                auto placemark = new GeoDataPlacemark;
                placemark->setGeometry(linearring);
                document->append(placemark);
            }

            if (flag == OUTERBOUNDARY) {
                polygon = new GeoDataPolygon;
                polygon->setOuterBoundary(*linearring);
            }

            if (flag == INNERBOUNDARY) {
                polygon->appendInnerBoundary(*linearring);
            }
        }

        if (flag == MULTIGEOMETRY) {
            // not implemented yet, for now elements inside a multigeometry are separated as individual geometries
        }

        prevFlag = flag;
    }

    if (prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY) {
        auto placemark = new GeoDataPlacemark;
        if (m_isMapColorField) {
            if (style) {
                placemark->setStyle(style);
            }
        }
        placemark->setGeometry(polygon);
        document->append(placemark);
    }

    if (error) {
        delete document;
        document = nullptr;
        return nullptr;
    }
    document->setFileName(fileName);
    return document;
}

GeoDataDocument *Pn2Runner::parseForVersion2(const QString &fileName, DocumentRole role)
{
    auto document = new GeoDataDocument();
    document->setDocumentRole(role);

    bool error = false;

    quint32 nrAbsoluteNodes;
    quint32 placemarkCurrentID = 1;
    quint32 placemarkPrevID = 0;
    quint8 flag, prevFlag = -1;

    auto polygon = new GeoDataPolygon;
    GeoDataStyle::Ptr style;
    GeoDataPlacemark *placemark = nullptr; // new GeoDataPlacemark;

    quint32 currentPoly;
    for (currentPoly = 1; (currentPoly <= m_fileHeaderPolygons) && (!error) && (!m_stream.atEnd()); currentPoly++) {
        m_stream >> flag >> placemarkCurrentID;

        if (flag == MULTIGEOMETRY && (prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY)) {
            if (placemark) {
                placemark->setGeometry(polygon);
            }
        }

        if (flag != MULTIGEOMETRY && flag != INNERBOUNDARY && (prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY)) {
            if (placemark) {
                placemark->setGeometry(polygon);
            }
        }
        /**
         * If the parsed placemark id @p placemarkCurrentID is different
         * from the id of previous placemark @p placemarkPrevID, it means
         * we have encountered a new placemark. So, prepare a style @p style
         * if file has color indices
         */
        if (placemarkCurrentID != placemarkPrevID) {
            placemark = new GeoDataPlacemark;

            // Handle the color index
            if (m_isMapColorField) {
                quint8 colorIndex;
                m_stream >> colorIndex;
                style = GeoDataStyle::Ptr(new GeoDataStyle);
                GeoDataPolyStyle polyStyle;
                polyStyle.setColorIndex(colorIndex);
                polyStyle.setFill(true);
                style->setPolyStyle(polyStyle);
                placemark->setStyle(style);
            }

            document->append(placemark);
        }

        placemarkPrevID = placemarkCurrentID;

        if (flag != MULTIGEOMETRY) {
            m_stream >> nrAbsoluteNodes;

            if (flag == LINESTRING) {
                auto linestring = new GeoDataLineString;
                error = error | importPolygon(m_stream, linestring, nrAbsoluteNodes);
                if (placemark) {
                    placemark->setGeometry(linestring);
                }
            }

            if ((flag == LINEARRING) || (flag == OUTERBOUNDARY) || (flag == INNERBOUNDARY)) {
                auto linearring = new GeoDataLinearRing;
                error = error || importPolygon(m_stream, linearring, nrAbsoluteNodes);

                if (flag == LINEARRING) {
                    if (placemark) {
                        placemark->setGeometry(linearring);
                    }
                } else {
                    if (flag == OUTERBOUNDARY) {
                        polygon = new GeoDataPolygon;
                        polygon->setOuterBoundary(*linearring);
                    }

                    if (flag == INNERBOUNDARY) {
                        polygon->appendInnerBoundary(*linearring);
                    }

                    delete linearring;
                }
            }
            prevFlag = flag;
        }

        else {
            quint32 placemarkCurrentIDInMulti;
            quint8 flagInMulti;
            quint8 prevFlagInMulti = -1;
            quint8 multiSize = 0;

            m_stream >> multiSize;

            auto multigeom = new GeoDataMultiGeometry;

            /**
             * Read @p multiSize GeoDataGeometry objects
             */
            for (int iter = 0; iter < multiSize; ++iter) {
                m_stream >> flagInMulti >> placemarkCurrentIDInMulti >> nrAbsoluteNodes;
                if (flagInMulti != INNERBOUNDARY && (prevFlagInMulti == INNERBOUNDARY || prevFlagInMulti == OUTERBOUNDARY)) {
                    multigeom->append(polygon);
                }

                if (flagInMulti == LINESTRING) {
                    auto linestring = new GeoDataLineString;
                    error = error || importPolygon(m_stream, linestring, nrAbsoluteNodes);
                    multigeom->append(linestring);
                }

                if ((flagInMulti == LINEARRING) || (flagInMulti == OUTERBOUNDARY) || (flagInMulti == INNERBOUNDARY)) {
                    auto linearring = new GeoDataLinearRing;
                    error = error | importPolygon(m_stream, linearring, nrAbsoluteNodes);

                    if (flagInMulti == LINEARRING) {
                        multigeom->append(linearring);
                    } else {
                        if (flagInMulti == OUTERBOUNDARY) {
                            polygon = new GeoDataPolygon;
                            polygon->setOuterBoundary(*linearring);
                        }

                        if (flagInMulti == INNERBOUNDARY) {
                            polygon->appendInnerBoundary(*linearring);
                        }

                        delete linearring;
                    }
                }
                prevFlagInMulti = flagInMulti;
            }

            if (prevFlagInMulti == INNERBOUNDARY || prevFlagInMulti == OUTERBOUNDARY) {
                multigeom->append(polygon);
            }
            if (placemark) {
                placemark->setGeometry(multigeom);
            }
            prevFlag = MULTIGEOMETRY;
        }
    }

    if ((prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY) && prevFlag != MULTIGEOMETRY) {
        placemark->setGeometry(polygon);
    }

    if (error) {
        delete document;
        document = nullptr;
        return nullptr;
    }
    document->setFileName(fileName);
    return document;
}

}

#include "moc_Pn2Runner.cpp"
