/******************************************************************************
 *
 * Project:  DXF Translator
 * Purpose:  Implements translation support for HATCH elements as part
 *           of the OGRDXFLayer class.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2010, Frank Warmerdam <warmerdam@pobox.com>
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.com>
 * Copyright (c) 2017, Alan Thomas <alant@outlook.com.au>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "ogr_dxf.h"
#include "cpl_conv.h"
#include "ogr_api.h"

#include <algorithm>
#include <cmath>
#include "ogrdxf_polyline_smooth.h"

/************************************************************************/
/*                           TranslateHATCH()                           */
/*                                                                      */
/*      We mostly just try to convert hatch objects as polygons or      */
/*      multipolygons representing the hatched area.  It is hard to     */
/*      preserve the actual details of the hatching.                    */
/************************************************************************/

OGRDXFFeature *OGRDXFLayer::TranslateHATCH()

{
    char szLineBuf[257];
    int nCode = 0;
    OGRDXFFeature *poFeature = new OGRDXFFeature(poFeatureDefn);

    double dfElevation = 0.0;  // Z value to be used for EVERY point
    OGRGeometryCollection oGC;
    std::string osExtendedData;

    while ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) > 0)
    {
        switch (nCode)
        {
            case 30:
                // Constant elevation.
                dfElevation = CPLAtof(szLineBuf);
                break;

            case 70:
            {
                const int nFillFlag = atoi(szLineBuf);
                poFeature->oStyleProperties["FillFlag"] =
                    nFillFlag ? "Filled" : "Pattern";
                break;
            }

            case 2:  // Hatch pattern name
                poFeature->SetField("Text", szLineBuf);
                break;

            case 91:
            {
                int nBoundaryPathCount = atoi(szLineBuf);

                for (int iBoundary = 0; iBoundary < nBoundaryPathCount;
                     iBoundary++)
                {
                    if (CollectBoundaryPath(&oGC, dfElevation) != OGRERR_NONE)
                        break;
                }
            }
            break;

            case 52:
            {
                poFeature->oStyleProperties["HatchPatternRotation"] = szLineBuf;
                break;
            }

            case 41:
            {
                poFeature->oStyleProperties["HatchPatternScale"] = szLineBuf;
                break;
            }

            case 1001:
            {
                osExtendedData = szLineBuf;
                break;
            }

            case 1071:
            {
                if (osExtendedData == "HATCHBACKGROUNDCOLOR")
                    poFeature->oStyleProperties["HatchBackgroundColor"] =
                        szLineBuf;
                break;
            }

            default:
                TranslateGenericProperty(poFeature, nCode, szLineBuf);
                break;
        }
    }
    if (nCode < 0)
    {
        DXF_LAYER_READER_ERROR();
        delete poFeature;
        return nullptr;
    }

    if (nCode == 0)
        poDS->UnreadValue();

    /* -------------------------------------------------------------------- */
    /*      Obtain a tolerance value used when building the polygon.        */
    /* -------------------------------------------------------------------- */
    double dfTolerance = poDS->HatchTolerance();
    if (dfTolerance < 0)
    {
        // If the configuration variable isn't set, compute the bounding box
        // and work out a tolerance from that
        OGREnvelope oEnvelope;
        oGC.getEnvelope(&oEnvelope);
        dfTolerance = std::max(oEnvelope.MaxX - oEnvelope.MinX,
                               oEnvelope.MaxY - oEnvelope.MinY) *
                      1e-7;
    }

    /* -------------------------------------------------------------------- */
    /*      Try to turn the set of lines into something useful.             */
    /* -------------------------------------------------------------------- */
    OGRErr eErr;

    auto poFinalGeom = std::unique_ptr<OGRGeometry>(
        OGRGeometry::FromHandle(OGRBuildPolygonFromEdges(
            OGRGeometry::ToHandle(&oGC), TRUE, TRUE, dfTolerance, &eErr)));
    if (eErr != OGRERR_NONE)
    {
        auto poMLS = std::make_unique<OGRMultiLineString>();
        for (int i = 0; i < oGC.getNumGeometries(); i++)
            poMLS->addGeometry(oGC.getGeometryRef(i));
        poFinalGeom = std::move(poMLS);
    }

    poFeature->ApplyOCSTransformer(poFinalGeom.get());
    poFeature->SetGeometry(std::move(poFinalGeom));

    PrepareBrushStyle(poFeature);

    return poFeature;
}

/************************************************************************/
/*                        CollectBoundaryPath()                         */
/************************************************************************/

OGRErr OGRDXFLayer::CollectBoundaryPath(OGRGeometryCollection *poGC,
                                        const double dfElevation)

{
    char szLineBuf[257];

    /* -------------------------------------------------------------------- */
    /*      Read the boundary path type.                                    */
    /* -------------------------------------------------------------------- */
    int nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
    if (nCode != 92)
    {
        DXF_LAYER_READER_ERROR();
        return OGRERR_FAILURE;
    }

    const int nBoundaryPathType = atoi(szLineBuf);

    /* ==================================================================== */
    /*      Handle polyline loops.                                          */
    /* ==================================================================== */
    if (nBoundaryPathType & 0x02)
        return CollectPolylinePath(poGC, dfElevation);

    /* ==================================================================== */
    /*      Handle non-polyline loops.                                      */
    /* ==================================================================== */

    /* -------------------------------------------------------------------- */
    /*      Read number of edges.                                           */
    /* -------------------------------------------------------------------- */
    nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
    if (nCode != 93)
    {
        DXF_LAYER_READER_ERROR();
        return OGRERR_FAILURE;
    }

    const int nEdgeCount = atoi(szLineBuf);

    /* -------------------------------------------------------------------- */
    /*      Loop reading edges.                                             */
    /* -------------------------------------------------------------------- */
    for (int iEdge = 0; iEdge < nEdgeCount; iEdge++)
    {
        /* --------------------------------------------------------------------
         */
        /*      Read the edge type. */
        /* --------------------------------------------------------------------
         */
        const int ET_LINE = 1;
        const int ET_CIRCULAR_ARC = 2;
        const int ET_ELLIPTIC_ARC = 3;
        const int ET_SPLINE = 4;

        nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
        if (nCode != 72)
        {
            DXF_LAYER_READER_ERROR();
            return OGRERR_FAILURE;
        }

        int nEdgeType = atoi(szLineBuf);

        /* --------------------------------------------------------------------
         */
        /*      Process a line edge. */
        /* --------------------------------------------------------------------
         */
        if (nEdgeType == ET_LINE)
        {
            double dfStartX = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 10)
                dfStartX = CPLAtof(szLineBuf);
            else
                break;

            double dfStartY = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 20)
                dfStartY = CPLAtof(szLineBuf);
            else
                break;

            double dfEndX = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 11)
                dfEndX = CPLAtof(szLineBuf);
            else
                break;

            double dfEndY = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 21)
                dfEndY = CPLAtof(szLineBuf);
            else
                break;

            OGRLineString *poLS = new OGRLineString();

            poLS->addPoint(dfStartX, dfStartY, dfElevation);
            poLS->addPoint(dfEndX, dfEndY, dfElevation);

            poGC->addGeometryDirectly(poLS);
        }
        /* --------------------------------------------------------------------
         */
        /*      Process a circular arc. */
        /* --------------------------------------------------------------------
         */
        else if (nEdgeType == ET_CIRCULAR_ARC)
        {
            double dfCenterX = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 10)
                dfCenterX = CPLAtof(szLineBuf);
            else
                break;

            double dfCenterY = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 20)
                dfCenterY = CPLAtof(szLineBuf);
            else
                break;

            double dfRadius = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 40)
                dfRadius = CPLAtof(szLineBuf);
            else
                break;

            double dfStartAngle = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 50)
                dfStartAngle = CPLAtof(szLineBuf);
            else
                break;

            double dfEndAngle = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 51)
                dfEndAngle = CPLAtof(szLineBuf);
            else
                break;

            bool bCounterClockwise = false;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 73)
                bCounterClockwise = atoi(szLineBuf) != 0;
            else if (nCode >= 0)
                poDS->UnreadValue();
            else
                break;

            if (dfStartAngle > dfEndAngle)
                dfEndAngle += 360.0;
            if (bCounterClockwise)
            {
                dfStartAngle *= -1;
                dfEndAngle *= -1;
            }

            if (fabs(dfEndAngle - dfStartAngle) <= 361.0)
            {
                OGRGeometry *poArc = OGRGeometryFactory::approximateArcAngles(
                    dfCenterX, dfCenterY, dfElevation, dfRadius, dfRadius, 0.0,
                    dfStartAngle, dfEndAngle, 0.0, poDS->InlineBlocks());

                // If the input was 2D, we assume we want to keep it that way
                if (dfElevation == 0.0)
                    poArc->flattenTo2D();

                poGC->addGeometryDirectly(poArc);
            }
            else
            {
                // TODO: emit error ?
            }
        }

        /* --------------------------------------------------------------------
         */
        /*      Process an elliptical arc. */
        /* --------------------------------------------------------------------
         */
        else if (nEdgeType == ET_ELLIPTIC_ARC)
        {
            double dfCenterX = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 10)
                dfCenterX = CPLAtof(szLineBuf);
            else
                break;

            double dfCenterY = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 20)
                dfCenterY = CPLAtof(szLineBuf);
            else
                break;

            double dfMajorX = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 11)
                dfMajorX = CPLAtof(szLineBuf);
            else
                break;

            double dfMajorY = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 21)
                dfMajorY = CPLAtof(szLineBuf);
            else
                break;

            double dfRatio = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 40)
                dfRatio = CPLAtof(szLineBuf);
            if (dfRatio == 0.0)
                break;

            double dfStartAngle = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 50)
                dfStartAngle = CPLAtof(szLineBuf);
            else
                break;

            double dfEndAngle = 0.0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 51)
                dfEndAngle = CPLAtof(szLineBuf);
            else
                break;

            bool bCounterClockwise = false;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 73)
                bCounterClockwise = atoi(szLineBuf) != 0;
            else if (nCode >= 0)
                poDS->UnreadValue();
            else
                break;

            if (dfStartAngle > dfEndAngle)
                dfEndAngle += 360.0;
            if (bCounterClockwise)
            {
                dfStartAngle *= -1;
                dfEndAngle *= -1;
            }

            const double dfMajorRadius =
                sqrt(dfMajorX * dfMajorX + dfMajorY * dfMajorY);
            const double dfMinorRadius = dfMajorRadius * dfRatio;

            const double dfRotation =
                -1 * atan2(dfMajorY, dfMajorX) * 180 / M_PI;

            // The start and end angles are stored as circular angles. However,
            // approximateArcAngles is expecting elliptical angles (what AutoCAD
            // calls "parameters"), so let's transform them.
            dfStartAngle =
                180.0 * round(dfStartAngle / 180) +
                (fabs(fmod(dfStartAngle, 180)) == 90
                     ? (std::signbit(dfStartAngle) ? 180 : -180)
                     : 0) +
                atan((1.0 / dfRatio) * tan(dfStartAngle * M_PI / 180)) * 180 /
                    M_PI;
            dfEndAngle = 180.0 * round(dfEndAngle / 180) +
                         (fabs(fmod(dfEndAngle, 180)) == 90
                              ? (std::signbit(dfEndAngle) ? 180 : -180)
                              : 0) +
                         atan((1.0 / dfRatio) * tan(dfEndAngle * M_PI / 180)) *
                             180 / M_PI;

            if (fabs(dfEndAngle - dfStartAngle) <= 361.0)
            {
                OGRGeometry *poArc = OGRGeometryFactory::approximateArcAngles(
                    dfCenterX, dfCenterY, dfElevation, dfMajorRadius,
                    dfMinorRadius, dfRotation, dfStartAngle, dfEndAngle, 0.0,
                    poDS->InlineBlocks());

                // If the input was 2D, we assume we want to keep it that way
                if (dfElevation == 0.0)
                    poArc->flattenTo2D();

                poGC->addGeometryDirectly(poArc);
            }
            else
            {
                // TODO: emit error ?
            }
        }

        /* --------------------------------------------------------------------
         */
        /*      Process an elliptical arc. */
        /* --------------------------------------------------------------------
         */
        else if (nEdgeType == ET_SPLINE)
        {
            int nDegree = 3;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 94)
                nDegree = atoi(szLineBuf);
            else
                break;

            // Skip a few things we don't care about
            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) != 73)
                break;
            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) != 74)
                break;

            int nKnots = 0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 95)
                nKnots = atoi(szLineBuf);
            else
                break;

            int nControlPoints = 0;

            if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) == 96)
                nControlPoints = atoi(szLineBuf);
            else
                break;

            std::vector<double> adfKnots(FORTRAN_INDEXING, 0.0);

            nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
            if (nCode != 40)
                break;

            while (nCode == 40)
            {
                adfKnots.push_back(CPLAtof(szLineBuf));
                nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
            }

            std::vector<double> adfControlPoints(FORTRAN_INDEXING, 0.0);
            std::vector<double> adfWeights(FORTRAN_INDEXING, 0.0);

            if (nCode != 10)
                break;

            while (nCode == 10)
            {
                adfControlPoints.push_back(CPLAtof(szLineBuf));

                if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) ==
                    20)
                {
                    adfControlPoints.push_back(CPLAtof(szLineBuf));
                }
                else
                    break;

                adfControlPoints.push_back(0.0);  // Z coordinate

                // 42 (weights) are optional
                if ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) ==
                    42)
                {
                    adfWeights.push_back(CPLAtof(szLineBuf));
                    nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
                }
            }

            // Skip past the number of fit points
            if (nCode != 97)
                break;

            // Eat the rest of this section, if present, until the next
            // boundary segment (72) or the conclusion of the boundary data (97)
            nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
            while (nCode > 0 && nCode != 72 && nCode != 97)
                nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
            if (nCode > 0)
                poDS->UnreadValue();

            auto poLS =
                InsertSplineWithChecks(nDegree, adfControlPoints,
                                       /* bHaZ = */ false, nControlPoints,
                                       adfKnots, nKnots, adfWeights);

            if (!poLS)
            {
                DXF_LAYER_READER_ERROR();
                return OGRERR_FAILURE;
            }

            poGC->addGeometryDirectly(poLS.release());
        }

        else
        {
            CPLDebug("DXF", "Unsupported HATCH boundary line type:%d",
                     nEdgeType);
            return OGRERR_UNSUPPORTED_OPERATION;
        }
    }

    if (nCode < 0)
    {
        DXF_LAYER_READER_ERROR();
        return OGRERR_FAILURE;
    }

    /* -------------------------------------------------------------------- */
    /*      Skip through source boundary objects if present.                */
    /* -------------------------------------------------------------------- */
    nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
    if (nCode != 97)
    {
        if (nCode < 0)
            return OGRERR_FAILURE;
        poDS->UnreadValue();
    }
    else
    {
        int iObj, nObjCount = atoi(szLineBuf);

        for (iObj = 0; iObj < nObjCount; iObj++)
        {
            if (poDS->ReadValue(szLineBuf, sizeof(szLineBuf)) < 0)
                return OGRERR_FAILURE;
        }
    }

    return OGRERR_NONE;
}

/************************************************************************/
/*                        CollectPolylinePath()                         */
/************************************************************************/

OGRErr OGRDXFLayer::CollectPolylinePath(OGRGeometryCollection *poGC,
                                        const double dfElevation)

{
    int nCode = 0;
    char szLineBuf[257];
    DXFSmoothPolyline oSmoothPolyline;
    double dfBulge = 0.0;
    double dfX = 0.0;
    double dfY = 0.0;
    bool bHaveX = false;
    bool bHaveY = false;
    bool bIsClosed = false;
    int nVertexCount = -1;
    bool bHaveBulges = false;

    if (dfElevation != 0)
        oSmoothPolyline.setCoordinateDimension(3);

    /* -------------------------------------------------------------------- */
    /*      Read the boundary path type.                                    */
    /* -------------------------------------------------------------------- */
    while ((nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf))) > 0)
    {
        if (nVertexCount > 0 && (int)oSmoothPolyline.size() == nVertexCount)
            break;

        switch (nCode)
        {
            case 93:
                nVertexCount = atoi(szLineBuf);
                break;

            case 72:
                bHaveBulges = CPL_TO_BOOL(atoi(szLineBuf));
                break;

            case 73:
                bIsClosed = CPL_TO_BOOL(atoi(szLineBuf));
                break;

            case 10:
                if (bHaveX && bHaveY)
                {
                    oSmoothPolyline.AddPoint(dfX, dfY, dfElevation, dfBulge);
                    dfBulge = 0.0;
                    bHaveY = false;
                }
                dfX = CPLAtof(szLineBuf);
                bHaveX = true;
                break;

            case 20:
                if (bHaveX && bHaveY)
                {
                    oSmoothPolyline.AddPoint(dfX, dfY, dfElevation, dfBulge);
                    dfBulge = 0.0;
                    bHaveX = false;
                }
                dfY = CPLAtof(szLineBuf);
                bHaveY = true;
                if (bHaveX /* && bHaveY */ && !bHaveBulges)
                {
                    oSmoothPolyline.AddPoint(dfX, dfY, dfElevation, dfBulge);
                    dfBulge = 0.0;
                    bHaveX = false;
                    bHaveY = false;
                }
                break;

            case 42:
                dfBulge = CPLAtof(szLineBuf);
                if (bHaveX && bHaveY)
                {
                    oSmoothPolyline.AddPoint(dfX, dfY, dfElevation, dfBulge);
                    dfBulge = 0.0;
                    bHaveX = false;
                    bHaveY = false;
                }
                break;

            default:
                break;
        }
    }
    if (nCode < 0)
    {
        DXF_LAYER_READER_ERROR();
        return OGRERR_FAILURE;
    }

    if (nCode != 10 && nCode != 20 && nCode != 42)
        poDS->UnreadValue();

    if (bHaveX && bHaveY)
        oSmoothPolyline.AddPoint(dfX, dfY, dfElevation, dfBulge);

    if (bIsClosed)
        oSmoothPolyline.Close();

    if (oSmoothPolyline.IsEmpty())
    {
        return OGRERR_FAILURE;
    }

    // Only process polylines with at least 2 vertices
    if (nVertexCount >= 2)
    {
        oSmoothPolyline.SetUseMaxGapWhenTessellatingArcs(poDS->InlineBlocks());
        poGC->addGeometryDirectly(oSmoothPolyline.Tessellate(false));
    }

    /* -------------------------------------------------------------------- */
    /*      Skip through source boundary objects if present.                */
    /* -------------------------------------------------------------------- */
    nCode = poDS->ReadValue(szLineBuf, sizeof(szLineBuf));
    if (nCode != 97)
    {
        if (nCode < 0)
            return OGRERR_FAILURE;
        poDS->UnreadValue();
    }
    else
    {
        int iObj, nObjCount = atoi(szLineBuf);

        for (iObj = 0; iObj < nObjCount; iObj++)
        {
            if (poDS->ReadValue(szLineBuf, sizeof(szLineBuf)) < 0)
                return OGRERR_FAILURE;
        }
    }
    return OGRERR_NONE;
}
