/******************************************************************************
 *
 * Project:  GDAL
 * Purpose:  Vector polygon rasterization code.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2000, Frank Warmerdam <warmerdam@pobox.com>
 * Copyright (c) 2011, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "gdal_alg_priv.h"

#include <cmath>
#include <cstdlib>
#include <cstring>

#include <algorithm>
#include <set>
#include <utility>
#include <vector>

#include "gdal_alg.h"

/************************************************************************/
/*                       dllImageFilledPolygon()                        */
/*                                                                      */
/*      Perform scanline conversion of the passed multi-ring            */
/*      polygon.  Note the polygon does not need to be explicitly       */
/*      closed.  The scanline function will be called with              */
/*      horizontal scanline chunks which may not be entirely            */
/*      contained within the valid raster area (in the X                */
/*      direction).                                                     */
/*                                                                      */
/*      NEW: Nodes' coordinate are kept as double  in order             */
/*      to compute accurately the intersections with the lines          */
/*                                                                      */
/*      A pixel is considered inside a polygon if its center            */
/*      falls inside the polygon. This is robust unless                 */
/*      the nodes are placed in the center of the pixels in which       */
/*      case, due to numerical inaccuracies, it's hard to predict       */
/*      if the pixel will be considered inside or outside the shape.    */
/************************************************************************/

/*
 * NOTE: This code was originally adapted from the gdImageFilledPolygon()
 * function in libgd.
 *
 * http://www.boutell.com/gd/
 *
 * It was later adapted for direct inclusion in GDAL and relicensed under
 * the GDAL MIT license (pulled from the OpenEV distribution).
 */

void GDALdllImageFilledPolygon(int nRasterXSize, int nRasterYSize,
                               int nPartCount, const int *panPartSize,
                               const double *padfX, const double *padfY,
                               const double *dfVariant,
                               llScanlineFunc pfnScanlineFunc,
                               GDALRasterizeInfo *pCBData,
                               bool bAvoidBurningSamePoints)
{
    if (!nPartCount)
    {
        return;
    }

    int n = 0;
    for (int part = 0; part < nPartCount; part++)
        n += panPartSize[part];

    std::vector<int> polyInts(n);
    std::vector<int> polyInts2;
    if (bAvoidBurningSamePoints)
        polyInts2.resize(n);

    double dminy = padfY[0];
    double dmaxy = padfY[0];
    for (int i = 1; i < n; i++)
    {
        if (padfY[i] < dminy)
        {
            dminy = padfY[i];
        }
        else if (padfY[i] > dmaxy)
        {
            dmaxy = padfY[i];
        }
    }
    const int miny = static_cast<int>(std::max(0.0, dminy));
    const int maxy =
        static_cast<int>(std::min<double>(dmaxy, nRasterYSize - 1));

    constexpr int minx = 0;
    const int maxx = nRasterXSize - 1;

    // Fix in 1.3: count a vertex only once.
    for (int y = miny; y <= maxy; y++)
    {
        int partoffset = 0;

        const double dy = y + 0.5;  // Center height of line.

        int part = 0;
        int ints = 0;
        int ints2 = 0;

        for (int i = 0; i < n; i++)
        {
            if (i == partoffset + panPartSize[part])
            {
                partoffset += panPartSize[part];
                part++;
            }

            int ind1 = 0;
            int ind2 = 0;
            if (i == partoffset)
            {
                ind1 = partoffset + panPartSize[part] - 1;
                ind2 = partoffset;
            }
            else
            {
                ind1 = i - 1;
                ind2 = i;
            }

            double dy1 = padfY[ind1];
            double dy2 = padfY[ind2];

            if ((dy1 < dy && dy2 < dy) || (dy1 > dy && dy2 > dy))
                continue;

            double dx1 = 0.0;
            double dx2 = 0.0;
            if (dy1 < dy2)
            {
                dx1 = padfX[ind1];
                dx2 = padfX[ind2];
            }
            else if (dy1 > dy2)
            {
                std::swap(dy1, dy2);
                dx2 = padfX[ind1];
                dx1 = padfX[ind2];
            }
            else  // if( fabs(dy1-dy2) < 1.e-6 )
            {

                // AE: DO NOT skip bottom horizontal segments
                // -Fill them separately-
                if (padfX[ind1] > padfX[ind2])
                {
                    const double dhorizontal_x1 = floor(padfX[ind2] + 0.5);
                    const double dhorizontal_x2 = floor(padfX[ind1] + 0.5);

                    if ((dhorizontal_x1 > static_cast<double>(maxx)) ||
                        (dhorizontal_x2 <= 0))
                        continue;
                    const int horizontal_x1 =
                        static_cast<int>(std::max(dhorizontal_x1, 0.0));
                    const int horizontal_x2 = static_cast<int>(
                        std::min<double>(dhorizontal_x2, nRasterXSize));

                    // Fill the horizontal segment (separately from the rest).
                    if (bAvoidBurningSamePoints)
                    {
                        polyInts2[ints2++] = horizontal_x1;
                        polyInts2[ints2++] = horizontal_x2;
                    }
                    else
                    {
                        pfnScanlineFunc(
                            pCBData, y, horizontal_x1, horizontal_x2 - 1,
                            dfVariant == nullptr ? 0 : dfVariant[0]);
                    }
                }
                // else: Skip top horizontal segments.
                // They are already filled in the regular loop.
                continue;
            }

            if (dy < dy2 && dy >= dy1)
            {
                const double intersect = std::clamp<double>(
                    (dy - dy1) * (dx2 - dx1) / (dy2 - dy1) + dx1, INT_MIN,
                    INT_MAX);

                polyInts[ints++] = static_cast<int>(floor(intersect + 0.5));
            }
        }

        std::sort(polyInts.begin(), polyInts.begin() + ints);
        std::sort(polyInts2.begin(), polyInts2.begin() + ints2);

        for (int i = 0; i + 1 < ints; i += 2)
        {
            if (polyInts[i] <= maxx && polyInts[i + 1] > minx)
            {
                pfnScanlineFunc(pCBData, y, polyInts[i], polyInts[i + 1] - 1,
                                dfVariant == nullptr ? 0 : dfVariant[0]);
            }
        }

        for (int i2 = 0, i = 0; i2 + 1 < ints2; i2 += 2)
        {
            if (polyInts2[i2] <= maxx && polyInts2[i2 + 1] > minx)
            {
                // "synchronize" polyInts[i] with polyInts2[i2]
                while (i + 1 < ints && polyInts[i] < polyInts2[i2])
                    i += 2;
                // Only burn if we don't have a common segment between
                // polyInts[] and polyInts2[]
                if (i + 1 >= ints || polyInts[i] != polyInts2[i2])
                {
                    pfnScanlineFunc(pCBData, y, polyInts2[i2],
                                    polyInts2[i2 + 1] - 1,
                                    dfVariant == nullptr ? 0 : dfVariant[0]);
                }
            }
        }
    }
}

/************************************************************************/
/*                         GDALdllImagePoint()                          */
/************************************************************************/

void GDALdllImagePoint(int nRasterXSize, int nRasterYSize, int nPartCount,
                       const int * /*panPartSize*/, const double *padfX,
                       const double *padfY, const double *padfVariant,
                       llPointFunc pfnPointFunc, GDALRasterizeInfo *pCBData)
{
    for (int i = 0; i < nPartCount; i++)
    {
        const double dfX = padfX[i];
        const double dfY = padfY[i];
        double dfVariant = 0.0;
        if (padfVariant != nullptr)
            dfVariant = padfVariant[i];

        if (0 <= dfX && dfX < nRasterXSize && 0 <= dfY && dfY < nRasterYSize)
            pfnPointFunc(pCBData, static_cast<int>(dfY), static_cast<int>(dfX),
                         dfVariant);
    }
}

/************************************************************************/
/*                         GDALdllImageLine()                           */
/************************************************************************/

void GDALdllImageLine(int nRasterXSize, int nRasterYSize, int nPartCount,
                      const int *panPartSize, const double *padfX,
                      const double *padfY, const double *padfVariant,
                      llPointFunc pfnPointFunc, GDALRasterizeInfo *pCBData)
{
    if (!nPartCount)
        return;

    for (int i = 0, n = 0; i < nPartCount; n += panPartSize[i++])
    {
        for (int j = 1; j < panPartSize[i]; j++)
        {
            double dfX = padfX[n + j - 1];
            double dfY = padfY[n + j - 1];
            double dfXEnd = padfX[n + j];
            double dfYEnd = padfY[n + j];

            // Skip segments that are off the target region.
            if ((dfY < 0.0 && dfYEnd < 0.0) ||
                (dfY > nRasterYSize && dfYEnd > nRasterYSize) ||
                (dfX < 0.0 && dfXEnd < 0.0) ||
                (dfX > nRasterXSize && dfXEnd > nRasterXSize))
                continue;

            // TODO: clamp coordinates to [0, nRasterXSize] * [0, nRasterYSize]
            if (!(dfX >= INT_MIN && dfX <= INT_MAX && dfY >= INT_MIN &&
                  dfY <= INT_MAX && dfXEnd >= INT_MIN && dfXEnd <= INT_MAX &&
                  dfYEnd >= INT_MIN && dfYEnd <= INT_MAX))
            {
                CPLErrorOnce(
                    CE_Warning, CPLE_AppDefined,
                    "GDALdllImageLine(): coordinates outside of int range");
                continue;
            }

            int iX = static_cast<int>(floor(dfX));
            int iY = static_cast<int>(floor(dfY));

            const int iX1 = static_cast<int>(floor(dfXEnd));
            const int iY1 = static_cast<int>(floor(dfYEnd));

            double dfVariant = 0.0;
            double dfVariant1 = 0.0;
            if (padfVariant != nullptr &&
                pCBData->eBurnValueSource != GBV_UserBurnValue)
            {
                dfVariant = padfVariant[n + j - 1];
                dfVariant1 = padfVariant[n + j];
            }

            int nDeltaX = std::abs(iX1 - iX);
            int nDeltaY = std::abs(iY1 - iY);

            // Step direction depends on line direction.
            const int nXStep = (iX > iX1) ? -1 : 1;
            const int nYStep = (iY > iY1) ? -1 : 1;

            // Determine the line slope.
            if (nDeltaX >= nDeltaY)
            {
                const int nXError = nDeltaY << 1;
                const int nYError = nXError - (nDeltaX << 1);
                int nError = nXError - nDeltaX;
                // == 0 makes clang -fcatch-undefined-behavior -ftrapv happy,
                // but if it is == 0, dfDeltaVariant is not really used, so any
                // value is okay.
                const double dfDeltaVariant =
                    nDeltaX == 0 ? 0.0 : (dfVariant1 - dfVariant) / nDeltaX;

                // Do not burn the end point, unless we are in the last
                // segment. This is to avoid burning twice intermediate points,
                // which causes artifacts in Add mode
                if (j != panPartSize[i] - 1)
                {
                    nDeltaX--;
                }

                while (nDeltaX-- >= 0)
                {
                    if (0 <= iX && iX < nRasterXSize && 0 <= iY &&
                        iY < nRasterYSize)
                        pfnPointFunc(pCBData, iY, iX, dfVariant);

                    dfVariant += dfDeltaVariant;
                    iX += nXStep;
                    if (nError > 0)
                    {
                        iY += nYStep;
                        nError += nYError;
                    }
                    else
                    {
                        nError += nXError;
                    }
                }
            }
            else
            {
                const int nXError = nDeltaX << 1;
                const int nYError = nXError - (nDeltaY << 1);
                int nError = nXError - nDeltaY;
                // == 0 makes clang -fcatch-undefined-behavior -ftrapv happy,
                // but if it is == 0, dfDeltaVariant is not really used, so any
                // value is okay.
                double dfDeltaVariant =
                    nDeltaY == 0 ? 0.0 : (dfVariant1 - dfVariant) / nDeltaY;

                // Do not burn the end point, unless we are in the last
                // segment. This is to avoid burning twice intermediate points,
                // which causes artifacts in Add mode
                if (j != panPartSize[i] - 1)
                {
                    nDeltaY--;
                }

                while (nDeltaY-- >= 0)
                {
                    if (0 <= iX && iX < nRasterXSize && 0 <= iY &&
                        iY < nRasterYSize)
                        pfnPointFunc(pCBData, iY, iX, dfVariant);

                    dfVariant += dfDeltaVariant;
                    iY += nYStep;
                    if (nError > 0)
                    {
                        iX += nXStep;
                        nError += nYError;
                    }
                    else
                    {
                        nError += nXError;
                    }
                }
            }
        }
    }
}

/************************************************************************/
/*                     GDALdllImageLineAllTouched()                     */
/*                                                                      */
/*      This alternate line drawing algorithm attempts to ensure        */
/*      that every pixel touched at all by the line will get set.       */
/*      @param padfVariant should contain the values that are to be     */
/*      added to the burn value.  The values along the line between the */
/*      points will be linearly interpolated. These values are used     */
/*      only if pCBData->eBurnValueSource is set to something other     */
/*      than GBV_UserBurnValue. If NULL is passed, a monotonous line    */
/*      will be drawn with the burn value.                              */
/************************************************************************/

void GDALdllImageLineAllTouched(
    int nRasterXSize, int nRasterYSize, int nPartCount, const int *panPartSize,
    const double *padfX, const double *padfY, const double *padfVariant,
    llPointFunc pfnPointFunc, GDALRasterizeInfo *pCBData,
    bool bAvoidBurningSamePoints, bool bIntersectOnly)

{
    // This is an epsilon to detect geometries that are aligned with pixel
    // coordinates. Hard to find the right value. We put it to that value
    // to satisfy the scenarios of https://github.com/OSGeo/gdal/issues/7523
    // and https://github.com/OSGeo/gdal/issues/6414
    constexpr double EPSILON_INTERSECT_ONLY = 1e-4;

    if (!nPartCount)
        return;

    for (int i = 0, n = 0; i < nPartCount; n += panPartSize[i++])
    {
        std::set<std::pair<int, int>> lastBurntPoints;
        std::set<std::pair<int, int>> newBurntPoints;

        for (int j = 1; j < panPartSize[i]; j++)
        {
            lastBurntPoints = std::move(newBurntPoints);
            newBurntPoints.clear();

            double dfX = padfX[n + j - 1];
            double dfY = padfY[n + j - 1];

            double dfXEnd = padfX[n + j];
            double dfYEnd = padfY[n + j];

            // Skip segments that are off the target region.
            if ((dfY < 0.0 && dfYEnd < 0.0) ||
                (dfY > nRasterYSize && dfYEnd > nRasterYSize) ||
                (dfX < 0.0 && dfXEnd < 0.0) ||
                (dfX > nRasterXSize && dfXEnd > nRasterXSize))
                continue;

            // TODO: clamp coordinates to [0, nRasterXSize] * [0, nRasterYSize]
            if (!(dfX >= INT_MIN && dfX <= INT_MAX && dfY >= INT_MIN &&
                  dfY <= INT_MAX && dfXEnd >= INT_MIN && dfXEnd <= INT_MAX &&
                  dfYEnd >= INT_MIN && dfYEnd <= INT_MAX))
            {
                CPLErrorOnce(CE_Warning, CPLE_AppDefined,
                             "GDALdllImageLineAllTouched(): coordinates "
                             "outside of int range");
                continue;
            }

            double dfVariant = 0.0;
            double dfVariantEnd = 0.0;
            if (padfVariant != nullptr &&
                static_cast<GDALRasterizeInfo *>(pCBData)->eBurnValueSource !=
                    GBV_UserBurnValue)
            {
                dfVariant = padfVariant[n + j - 1];
                dfVariantEnd = padfVariant[n + j];
            }

            // Swap if needed so we can proceed from left2right (X increasing)
            if (dfX > dfXEnd)
            {
                std::swap(dfX, dfXEnd);
                std::swap(dfY, dfYEnd);
                std::swap(dfVariant, dfVariantEnd);
            }

            // Special case for vertical lines.

            if (fabs(dfX - dfXEnd) < .01)
            {
                if (bIntersectOnly)
                {
                    if (std::abs(dfX - std::round(dfX)) <
                            EPSILON_INTERSECT_ONLY &&
                        std::abs(dfXEnd - std::round(dfXEnd)) <
                            EPSILON_INTERSECT_ONLY)
                        continue;
                }

                if (dfYEnd < dfY)
                {
                    std::swap(dfY, dfYEnd);
                    std::swap(dfVariant, dfVariantEnd);
                }

                const int iX = static_cast<int>(floor(dfXEnd));
                int iY = static_cast<int>(floor(dfY));
                int iYEnd =
                    static_cast<int>(floor(dfYEnd - EPSILON_INTERSECT_ONLY));

                if (iX < 0 || iX >= nRasterXSize)
                    continue;

                double dfDeltaVariant = 0.0;
                if (dfYEnd - dfY > 0.0)
                    dfDeltaVariant = (dfVariantEnd - dfVariant) /
                                     (dfYEnd - dfY);  // Per unit change in iY.

                // Clip to the borders of the target region.
                if (iY < 0)
                    iY = 0;
                if (iYEnd >= nRasterYSize)
                    iYEnd = nRasterYSize - 1;
                dfVariant += dfDeltaVariant * (iY - dfY);

                if (padfVariant == nullptr)
                {
                    for (; iY <= iYEnd; iY++)
                    {
                        if (bAvoidBurningSamePoints)
                        {
                            auto yx = std::pair<int, int>(iY, iX);
                            if (lastBurntPoints.find(yx) !=
                                lastBurntPoints.end())
                            {
                                continue;
                            }
                            newBurntPoints.insert(yx);
                        }
                        pfnPointFunc(pCBData, iY, iX, 0.0);
                    }
                }
                else
                {
                    for (; iY <= iYEnd; iY++, dfVariant += dfDeltaVariant)
                    {
                        if (bAvoidBurningSamePoints)
                        {
                            auto yx = std::pair<int, int>(iY, iX);
                            if (lastBurntPoints.find(yx) !=
                                lastBurntPoints.end())
                            {
                                continue;
                            }
                            newBurntPoints.insert(yx);
                        }
                        pfnPointFunc(pCBData, iY, iX, dfVariant);
                    }
                }

                continue;  // Next segment.
            }

            const double dfDeltaVariant =
                (dfVariantEnd - dfVariant) /
                (dfXEnd - dfX);  // Per unit change in iX.

            // Special case for horizontal lines.
            if (fabs(dfY - dfYEnd) < .01)
            {
                if (bIntersectOnly)
                {
                    if (std::abs(dfY - std::round(dfY)) <
                            EPSILON_INTERSECT_ONLY &&
                        std::abs(dfYEnd - std::round(dfYEnd)) <
                            EPSILON_INTERSECT_ONLY)
                        continue;
                }

                if (dfXEnd < dfX)
                {
                    std::swap(dfX, dfXEnd);
                    std::swap(dfVariant, dfVariantEnd);
                }

                int iX = static_cast<int>(floor(dfX));
                const int iY = static_cast<int>(floor(dfY));
                int iXEnd =
                    static_cast<int>(floor(dfXEnd - EPSILON_INTERSECT_ONLY));

                if (iY < 0 || iY >= nRasterYSize)
                    continue;

                // Clip to the borders of the target region.
                if (iX < 0)
                    iX = 0;
                if (iXEnd >= nRasterXSize)
                    iXEnd = nRasterXSize - 1;
                dfVariant += dfDeltaVariant * (iX - dfX);

                if (padfVariant == nullptr)
                {
                    for (; iX <= iXEnd; iX++)
                    {
                        if (bAvoidBurningSamePoints)
                        {
                            auto yx = std::pair<int, int>(iY, iX);
                            if (lastBurntPoints.find(yx) !=
                                lastBurntPoints.end())
                            {
                                continue;
                            }
                            newBurntPoints.insert(yx);
                        }
                        pfnPointFunc(pCBData, iY, iX, 0.0);
                    }
                }
                else
                {
                    for (; iX <= iXEnd; iX++, dfVariant += dfDeltaVariant)
                    {
                        if (bAvoidBurningSamePoints)
                        {
                            auto yx = std::pair<int, int>(iY, iX);
                            if (lastBurntPoints.find(yx) !=
                                lastBurntPoints.end())
                            {
                                continue;
                            }
                            newBurntPoints.insert(yx);
                        }
                        pfnPointFunc(pCBData, iY, iX, dfVariant);
                    }
                }

                continue;  // Next segment.
            }

            /* --------------------------------------------------------------------
             */
            /*      General case - left to right sloped. */
            /* --------------------------------------------------------------------
             */
            const double dfSlope = (dfYEnd - dfY) / (dfXEnd - dfX);

            // Clip segment in X.
            if (dfXEnd > nRasterXSize)
            {
                dfYEnd -= (dfXEnd - nRasterXSize) * dfSlope;
                dfXEnd = nRasterXSize;
            }
            if (dfX < 0.0)
            {
                dfY += (0.0 - dfX) * dfSlope;
                dfVariant += dfDeltaVariant * (0.0 - dfX);
                dfX = 0.0;
            }

            // Clip segment in Y.
            if (dfYEnd > dfY)
            {
                if (dfY < 0.0)
                {
                    const double dfDiffX = (0.0 - dfY) / dfSlope;
                    dfX += dfDiffX;
                    dfVariant += dfDeltaVariant * dfDiffX;
                    dfY = 0.0;
                }
                if (dfYEnd >= nRasterYSize)
                {
                    dfXEnd += (dfYEnd - nRasterYSize) / dfSlope;
                    if (dfXEnd > nRasterXSize)
                        dfXEnd = nRasterXSize;
                    // dfYEnd is no longer used afterwards, but for
                    // consistency it should be:
                    // dfYEnd = nRasterXSize;
                }
            }
            else
            {
                if (dfY >= nRasterYSize)
                {
                    const double dfDiffX = (nRasterYSize - dfY) / dfSlope;
                    dfX += dfDiffX;
                    dfVariant += dfDeltaVariant * dfDiffX;
                    dfY = nRasterYSize;
                }
                if (dfYEnd < 0.0)
                {
                    dfXEnd -= (dfYEnd - 0) / dfSlope;
                    // dfYEnd is no longer used afterwards, but for
                    // consistency it should be:
                    // dfYEnd = 0.0;
                }
            }

            // Step from pixel to pixel.
            while (dfX >= 0.0 && dfX < dfXEnd)
            {
                const int iX = static_cast<int>(floor(dfX));
                const int iY = static_cast<int>(floor(dfY));

                // Burn in the current point.
                // We should be able to drop the Y check because we clipped
                // in Y, but there may be some error with all the small steps.
                if (iY >= 0 && iY < nRasterYSize)
                {
                    if (bAvoidBurningSamePoints)
                    {
                        auto yx = std::pair<int, int>(iY, iX);
                        if (lastBurntPoints.find(yx) == lastBurntPoints.end() &&
                            newBurntPoints.find(yx) == newBurntPoints.end())
                        {
                            newBurntPoints.insert(yx);
                            pfnPointFunc(pCBData, iY, iX, dfVariant);
                        }
                    }
                    else
                    {
                        pfnPointFunc(pCBData, iY, iX, dfVariant);
                    }
                }

                double dfStepX = floor(dfX + 1.0) - dfX;
                double dfStepY = dfStepX * dfSlope;

                // Step to right pixel without changing scanline?
                if (static_cast<int>(floor(dfY + dfStepY)) == iY)
                {
                    dfX += dfStepX;
                    dfY += dfStepY;
                    dfVariant += dfDeltaVariant * dfStepX;
                }
                else if (dfSlope < 0)
                {
                    dfStepY = iY - dfY;
                    if (dfStepY > -0.000000001)
                        dfStepY = -0.000000001;

                    dfStepX = dfStepY / dfSlope;
                    dfX += dfStepX;
                    dfY += dfStepY;
                    dfVariant += dfDeltaVariant * dfStepX;
                }
                else
                {
                    dfStepY = (iY + 1) - dfY;
                    if (dfStepY < 0.000000001)
                        dfStepY = 0.000000001;

                    dfStepX = dfStepY / dfSlope;
                    dfX += dfStepX;
                    dfY += dfStepY;
                    dfVariant += dfDeltaVariant * dfStepX;
                }
            }  // Next step along segment.
        }      // Next segment.
    }          // Next part.
}
