/******************************************************************************
 *
 * Project:  MapServer
 * Purpose:  Commandline App to build tile index for raster files.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2001, Frank Warmerdam, DM Solutions Group Inc
 * Copyright (c) 2007-2023, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "cpl_conv.h"
#include "cpl_minixml.h"
#include "cpl_string.h"
#include "gdal_utils.h"
#include "gdal_priv.h"
#include "gdal_utils_priv.h"
#include "ogr_api.h"
#include "ogrsf_frmts.h"
#include "ogr_spatialref.h"
#include "commonutils.h"
#include "gdalargumentparser.h"

#include <ctype.h>

#include <algorithm>
#include <cmath>
#include <limits>
#include <set>

typedef enum
{
    FORMAT_AUTO,
    FORMAT_WKT,
    FORMAT_EPSG,
    FORMAT_PROJ
} SrcSRSFormat;

/************************************************************************/
/*                        GDALTileIndexRasterMetadata                   */
/************************************************************************/

struct GDALTileIndexRasterMetadata
{
    OGRFieldType eType = OFTString;
    std::string osFieldName{};
    std::string osRasterItemName{};
};

/************************************************************************/
/*                          GDALTileIndexOptions                        */
/************************************************************************/

struct GDALTileIndexOptions
{
    bool bOverwrite = false;
    std::string osFormat{};
    std::string osIndexLayerName{};
    std::string osLocationField = "location";
    CPLStringList aosLCO{};
    std::string osTargetSRS{};
    bool bWriteAbsolutePath = false;
    bool bSkipDifferentProjection = false;
    std::string osSrcSRSFieldName{};
    SrcSRSFormat eSrcSRSFormat = FORMAT_AUTO;
    double xres = std::numeric_limits<double>::quiet_NaN();
    double yres = std::numeric_limits<double>::quiet_NaN();
    double xmin = std::numeric_limits<double>::quiet_NaN();
    double ymin = std::numeric_limits<double>::quiet_NaN();
    double xmax = std::numeric_limits<double>::quiet_NaN();
    double ymax = std::numeric_limits<double>::quiet_NaN();
    std::string osBandCount{};
    std::string osNodata{};
    std::string osColorInterp{};
    std::string osDataType{};
    bool bMaskBand = false;
    std::vector<std::string> aosMetadata{};
    std::string osGTIFilename{};
    bool bRecursive = false;
    double dfMinPixelSize = std::numeric_limits<double>::quiet_NaN();
    double dfMaxPixelSize = std::numeric_limits<double>::quiet_NaN();
    std::vector<GDALTileIndexRasterMetadata> aoFetchMD{};
    std::set<std::string> oSetFilenameFilters{};
};

/************************************************************************/
/*                               PatternMatch()                         */
/************************************************************************/

static bool PatternMatch(const char *input, const char *pattern)

{
    while (*input != '\0')
    {
        if (*pattern == '\0')
            return false;

        else if (*pattern == '?')
        {
            pattern++;
            if (static_cast<unsigned int>(*input) > 127)
            {
                // Continuation bytes of such characters are of the form
                // 10xxxxxx (0x80), whereas single-byte are 0xxxxxxx
                // and the start of a multi-byte is 11xxxxxx
                do
                {
                    input++;
                } while (static_cast<unsigned int>(*input) > 127);
            }
            else
            {
                input++;
            }
        }
        else if (*pattern == '*')
        {
            if (pattern[1] == '\0')
                return true;

            // Try eating varying amounts of the input till we get a positive.
            for (int eat = 0; input[eat] != '\0'; eat++)
            {
                if (PatternMatch(input + eat, pattern + 1))
                    return true;
            }

            return false;
        }
        else
        {
            if (CPLTolower(*pattern) != CPLTolower(*input))
            {
                return false;
            }
            else
            {
                input++;
                pattern++;
            }
        }
    }

    if (*pattern != '\0' && strcmp(pattern, "*") != 0)
        return false;
    else
        return true;
}

/************************************************************************/
/*                     GDALTileIndexAppOptionsGetParser()               */
/************************************************************************/

static std::unique_ptr<GDALArgumentParser> GDALTileIndexAppOptionsGetParser(
    GDALTileIndexOptions *psOptions,
    GDALTileIndexOptionsForBinary *psOptionsForBinary)
{
    auto argParser = std::make_unique<GDALArgumentParser>(
        "gdaltindex", /* bForBinary=*/psOptionsForBinary != nullptr);

    argParser->add_description(
        _("Build a tile index from a list of datasets."));

    argParser->add_epilog(
        _("For more details, see the full documentation for gdaltindex at\n"
          "https://gdal.org/programs/gdaltindex.html"));

    argParser->add_argument("-overwrite")
        .flag()
        .store_into(psOptions->bOverwrite)
        .help(_("Overwrite the output tile index file if it already exists."));

    argParser->add_argument("-recursive")
        .flag()
        .store_into(psOptions->bRecursive)
        .help(_("Whether directories specified in <file_or_dir> should be "
                "explored recursively."));

    argParser->add_argument("-filename_filter")
        .metavar("<val>")
        .append()
        .store_into(psOptions->oSetFilenameFilters)
        .help(_("Pattern that the filenames contained in directories pointed "
                "by <file_or_dir> should follow."));

    argParser->add_argument("-min_pixel_size")
        .metavar("<val>")
        .store_into(psOptions->dfMinPixelSize)
        .help(_("Minimum pixel size in term of geospatial extent per pixel "
                "(resolution) that a raster should have to be selected."));

    argParser->add_argument("-max_pixel_size")
        .metavar("<val>")
        .store_into(psOptions->dfMaxPixelSize)
        .help(_("Maximum pixel size in term of geospatial extent per pixel "
                "(resolution) that a raster should have to be selected."));

    argParser->add_output_format_argument(psOptions->osFormat);

    argParser->add_argument("-tileindex")
        .metavar("<field_name>")
        .store_into(psOptions->osLocationField)
        .help(_("Name of the layer in the tile index file."));

    argParser->add_argument("-write_absolute_path")
        .flag()
        .store_into(psOptions->bWriteAbsolutePath)
        .help(_("Write the absolute path of the raster files in the tile index "
                "file."));

    argParser->add_argument("-skip_different_projection")
        .flag()
        .store_into(psOptions->bSkipDifferentProjection)
        .help(_(
            "Only files with the same projection as files already inserted in "
            "the tile index will be inserted (unless -t_srs is specified)."));

    argParser->add_argument("-t_srs")
        .metavar("<srs_def>")
        .store_into(psOptions->osTargetSRS)
        .help(_("Geometries of input files will be transformed to the desired "
                "target coordinate reference system."));

    argParser->add_argument("-src_srs_name")
        .metavar("<field_name>")
        .store_into(psOptions->osSrcSRSFieldName)
        .help(_("Name of the field in the tile index file where the source SRS "
                "will be stored."));

    argParser->add_argument("-src_srs_format")
        .metavar("{AUTO|WKT|EPSG|PROJ}")
        .choices("AUTO", "WKT", "EPSG", "PROJ")
        .action(
            [psOptions](const auto &f)
            {
                if (f == "WKT")
                    psOptions->eSrcSRSFormat = FORMAT_WKT;
                else if (f == "EPSG")
                    psOptions->eSrcSRSFormat = FORMAT_EPSG;
                else if (f == "PROJ")
                    psOptions->eSrcSRSFormat = FORMAT_PROJ;
                else
                    psOptions->eSrcSRSFormat = FORMAT_AUTO;
            })
        .help(_("Format of the source SRS to store in the tile index file."));

    argParser->add_argument("-lyr_name")
        .metavar("<name>")
        .store_into(psOptions->osIndexLayerName)
        .help(_("Name of the layer in the tile index file."));

    argParser->add_layer_creation_options_argument(psOptions->aosLCO);

    // GTI driver options

    argParser->add_argument("-gti_filename")
        .metavar("<filename>")
        .store_into(psOptions->osGTIFilename)
        .help(_("Filename of the XML Virtual Tile Index file to generate."));

    // NOTE: no store_into
    argParser->add_argument("-tr")
        .metavar("<xres> <yres>")
        .nargs(2)
        .scan<'g', double>()
        .help(_("Set target resolution."));

    // NOTE: no store_into
    argParser->add_argument("-te")
        .metavar("<xmin> <ymin> <xmax> <ymax>")
        .nargs(4)
        .scan<'g', double>()
        .help(_("Set target extent in SRS unit."));

    argParser->add_argument("-ot")
        .metavar("<datatype>")
        .store_into(psOptions->osDataType)
        .help(_("Output data type."));

    argParser->add_argument("-bandcount")
        .metavar("<val>")
        .store_into(psOptions->osBandCount)
        .help(_("Number of bands of the tiles of the tile index."));

    argParser->add_argument("-nodata")
        .metavar("<val>")
        .append()
        .store_into(psOptions->osNodata)
        .help(_("Nodata value of the tiles of the tile index."));

    // Should we use choices here?
    argParser->add_argument("-colorinterp")
        .metavar("<val>")
        .append()
        .store_into(psOptions->osColorInterp)
        .help(_("Color interpretation of of the tiles of the tile index: red, "
                "green, blue, alpha, gray, undefined."));

    argParser->add_argument("-mask")
        .flag()
        .store_into(psOptions->bMaskBand)
        .help(_("Add a mask band to the tiles of the tile index."));

    argParser->add_argument("-mo")
        .metavar("<name>=<value>")
        .append()
        .store_into(psOptions->aosMetadata)
        .help(_("Write an arbitrary layer metadata item, for formats that "
                "support layer metadata."));

    // NOTE: no store_into
    argParser->add_argument("-fetch_md")
        .nargs(3)
        .metavar("<gdal_md_name> <fld_name> <fld_type>")
        .append()
        .help("Fetch a metadata item from the raster tile and write it as a "
              "field in the tile index.");

    if (psOptionsForBinary)
    {
        argParser->add_quiet_argument(&psOptionsForBinary->bQuiet);

        argParser->add_argument("index_file")
            .metavar("<index_file>")
            .store_into(psOptionsForBinary->osDest)
            .help(_("The name of the output file to create/append to."));

        argParser->add_argument("file_or_dir")
            .metavar("<file_or_dir>")
            .nargs(argparse::nargs_pattern::at_least_one)
            .action([psOptionsForBinary](const std::string &s)
                    { psOptionsForBinary->aosSrcFiles.AddString(s.c_str()); })
            .help(_(
                "The input GDAL raster files or directory, can be multiple "
                "locations separated by spaces. Wildcards may also be used."));
    }

    return argParser;
}

/************************************************************************/
/*                  GDALTileIndexAppGetParserUsage()                    */
/************************************************************************/

std::string GDALTileIndexAppGetParserUsage()
{
    try
    {
        GDALTileIndexOptions sOptions;
        GDALTileIndexOptionsForBinary sOptionsForBinary;
        auto argParser =
            GDALTileIndexAppOptionsGetParser(&sOptions, &sOptionsForBinary);
        return argParser->usage();
    }
    catch (const std::exception &err)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Unexpected exception: %s",
                 err.what());
        return std::string();
    }
}

/************************************************************************/
/*                        GDALTileIndexTileIterator                     */
/************************************************************************/

struct GDALTileIndexTileIterator
{
    const GDALTileIndexOptions *psOptions = nullptr;
    int nSrcCount = 0;
    const char *const *papszSrcDSNames = nullptr;
    std::string osCurDir{};
    int iCurSrc = 0;
    VSIDIR *psDir = nullptr;

    CPL_DISALLOW_COPY_ASSIGN(GDALTileIndexTileIterator)

    GDALTileIndexTileIterator(const GDALTileIndexOptions *psOptionsIn,
                              int nSrcCountIn,
                              const char *const *papszSrcDSNamesIn)
        : psOptions(psOptionsIn), nSrcCount(nSrcCountIn),
          papszSrcDSNames(papszSrcDSNamesIn)
    {
    }

    void reset()
    {
        if (psDir)
            VSICloseDir(psDir);
        psDir = nullptr;
        iCurSrc = 0;
    }

    std::string next()
    {
        while (true)
        {
            if (!psDir)
            {
                if (iCurSrc == nSrcCount)
                {
                    break;
                }

                VSIStatBufL sStatBuf;
                const std::string osCurName = papszSrcDSNames[iCurSrc++];
                if (VSIStatL(osCurName.c_str(), &sStatBuf) == 0 &&
                    VSI_ISDIR(sStatBuf.st_mode))
                {
                    auto poSrcDS = std::unique_ptr<GDALDataset>(
                        GDALDataset::Open(osCurName.c_str(), GDAL_OF_RASTER,
                                          nullptr, nullptr, nullptr));
                    if (poSrcDS)
                        return osCurName;

                    osCurDir = osCurName;
                    psDir = VSIOpenDir(
                        osCurDir.c_str(),
                        /*nDepth=*/psOptions->bRecursive ? -1 : 0, nullptr);
                    if (!psDir)
                    {
                        CPLError(CE_Failure, CPLE_AppDefined,
                                 "Cannot open directory %s", osCurDir.c_str());
                        return std::string();
                    }
                }
                else
                {
                    return osCurName;
                }
            }

            auto psEntry = VSIGetNextDirEntry(psDir);
            if (!psEntry)
            {
                VSICloseDir(psDir);
                psDir = nullptr;
                continue;
            }

            if (!psOptions->oSetFilenameFilters.empty())
            {
                bool bMatchFound = false;
                const std::string osFilenameOnly =
                    CPLGetFilename(psEntry->pszName);
                for (const auto &osFilter : psOptions->oSetFilenameFilters)
                {
                    if (PatternMatch(osFilenameOnly.c_str(), osFilter.c_str()))
                    {
                        bMatchFound = true;
                        break;
                    }
                }
                if (!bMatchFound)
                    continue;
            }

            const std::string osFilename =
                CPLFormFilename(osCurDir.c_str(), psEntry->pszName, nullptr);
            if (VSI_ISDIR(psEntry->nMode))
            {
                auto poSrcDS = std::unique_ptr<GDALDataset>(
                    GDALDataset::Open(osFilename.c_str(), GDAL_OF_RASTER,
                                      nullptr, nullptr, nullptr));
                if (poSrcDS)
                    return osFilename;
                continue;
            }

            return osFilename;
        }
        return std::string();
    }
};

/************************************************************************/
/*                           GDALTileIndex()                            */
/************************************************************************/

/* clang-format off */
/**
 * Build a tile index from a list of datasets.
 *
 * This is the equivalent of the
 * <a href="/programs/gdaltindex.html">gdaltindex</a> utility.
 *
 * GDALTileIndexOptions* must be allocated and freed with
 * GDALTileIndexOptionsNew() and GDALTileIndexOptionsFree() respectively.
 *
 * @param pszDest the destination dataset path.
 * @param nSrcCount the number of input datasets.
 * @param papszSrcDSNames the list of input dataset names
 * @param psOptionsIn the options struct returned by GDALTileIndexOptionsNew() or
 * NULL.
 * @param pbUsageError pointer to a integer output variable to store if any
 * usage error has occurred.
 * @return the output dataset (new dataset that must be closed using
 * GDALClose()) or NULL in case of error.
 *
 * @since GDAL3.9
 */
/* clang-format on */

GDALDatasetH GDALTileIndex(const char *pszDest, int nSrcCount,
                           const char *const *papszSrcDSNames,
                           const GDALTileIndexOptions *psOptionsIn,
                           int *pbUsageError)
{
    if (nSrcCount == 0)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "No input dataset specified.");

        if (pbUsageError)
            *pbUsageError = TRUE;
        return nullptr;
    }

    auto psOptions = psOptionsIn
                         ? std::make_unique<GDALTileIndexOptions>(*psOptionsIn)
                         : std::make_unique<GDALTileIndexOptions>();

    GDALTileIndexTileIterator oGDALTileIndexTileIterator(
        psOptions.get(), nSrcCount, papszSrcDSNames);

    /* -------------------------------------------------------------------- */
    /*      Create and validate target SRS if given.                        */
    /* -------------------------------------------------------------------- */
    OGRSpatialReference oTargetSRS;
    if (!psOptions->osTargetSRS.empty())
    {
        if (psOptions->bSkipDifferentProjection)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "-skip_different_projections does not apply "
                     "when -t_srs is requested.");
        }
        oTargetSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
        // coverity[tainted_data]
        oTargetSRS.SetFromUserInput(psOptions->osTargetSRS.c_str());
    }

    /* -------------------------------------------------------------------- */
    /*      Open or create the target datasource                            */
    /* -------------------------------------------------------------------- */

    if (psOptions->bOverwrite)
    {
        CPLPushErrorHandler(CPLQuietErrorHandler);
        auto hDriver = GDALIdentifyDriver(pszDest, nullptr);
        if (hDriver)
            GDALDeleteDataset(hDriver, pszDest);
        else
            VSIUnlink(pszDest);
        CPLPopErrorHandler();
    }

    auto poTileIndexDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
        pszDest, GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr, nullptr, nullptr));
    OGRLayer *poLayer = nullptr;
    std::string osFormat;
    int nMaxFieldSize = 254;
    bool bExistingLayer = false;

    if (poTileIndexDS != nullptr)
    {
        auto poDriver = poTileIndexDS->GetDriver();
        if (poDriver)
            osFormat = poDriver->GetDescription();

        if (poTileIndexDS->GetLayerCount() == 1)
        {
            poLayer = poTileIndexDS->GetLayer(0);
        }
        else
        {
            if (psOptions->osIndexLayerName.empty())
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "-lyr_name must be specified.");
                if (pbUsageError)
                    *pbUsageError = true;
                return nullptr;
            }
            CPLPushErrorHandler(CPLQuietErrorHandler);
            poLayer = poTileIndexDS->GetLayerByName(
                psOptions->osIndexLayerName.c_str());
            CPLPopErrorHandler();
        }
    }
    else
    {
        if (psOptions->osFormat.empty())
        {
            const auto aoDrivers = GetOutputDriversFor(pszDest, GDAL_OF_VECTOR);
            if (aoDrivers.empty())
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Cannot guess driver for %s", pszDest);
                return nullptr;
            }
            else
            {
                if (aoDrivers.size() > 1)
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Several drivers matching %s extension. Using %s",
                             CPLGetExtension(pszDest), aoDrivers[0].c_str());
                }
                osFormat = aoDrivers[0];
            }
        }
        else
        {
            osFormat = psOptions->osFormat;
        }
        if (!EQUAL(osFormat.c_str(), "ESRI Shapefile"))
            nMaxFieldSize = 0;

        auto poDriver =
            GetGDALDriverManager()->GetDriverByName(osFormat.c_str());
        if (poDriver == nullptr)
        {
            CPLError(CE_Warning, CPLE_AppDefined, "%s driver not available.",
                     osFormat.c_str());
            return nullptr;
        }

        poTileIndexDS.reset(
            poDriver->Create(pszDest, 0, 0, 0, GDT_Unknown, nullptr));
        if (!poTileIndexDS)
            return nullptr;
    }

    if (poLayer)
    {
        bExistingLayer = true;
    }
    else
    {
        std::string osLayerName;
        if (psOptions->osIndexLayerName.empty())
        {
            VSIStatBuf sStat;
            if (EQUAL(osFormat.c_str(), "ESRI Shapefile") ||
                VSIStat(pszDest, &sStat) == 0)
            {
                osLayerName = CPLGetBasename(pszDest);
            }
            else
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "-lyr_name must be specified.");
                if (pbUsageError)
                    *pbUsageError = true;
                return nullptr;
            }
        }
        else
        {
            if (psOptions->bOverwrite)
            {
                for (int i = 0; i < poTileIndexDS->GetLayerCount(); ++i)
                {
                    auto poExistingLayer = poTileIndexDS->GetLayer(i);
                    if (poExistingLayer && poExistingLayer->GetName() ==
                                               psOptions->osIndexLayerName)
                    {
                        poTileIndexDS->DeleteLayer(i);
                        break;
                    }
                }
            }

            osLayerName = psOptions->osIndexLayerName;
        }

        /* get spatial reference for output file from target SRS (if set) */
        /* or from first input file */
        OGRSpatialReference oSRS;
        if (!oTargetSRS.IsEmpty())
        {
            oSRS = oTargetSRS;
        }
        else
        {
            std::string osFilename = oGDALTileIndexTileIterator.next();
            if (osFilename.empty())
            {
                CPLError(CE_Failure, CPLE_AppDefined, "Cannot find any tile");
                return nullptr;
            }
            oGDALTileIndexTileIterator.reset();
            auto poSrcDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
                osFilename.c_str(), GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR,
                nullptr, nullptr, nullptr));
            if (!poSrcDS)
                return nullptr;

            auto poSrcSRS = poSrcDS->GetSpatialRef();
            if (poSrcSRS)
                oSRS = *poSrcSRS;
        }

        poLayer = poTileIndexDS->CreateLayer(
            osLayerName.c_str(), oSRS.IsEmpty() ? nullptr : &oSRS, wkbPolygon,
            psOptions->aosLCO.List());
        if (!poLayer)
            return nullptr;

        OGRFieldDefn oLocationField(psOptions->osLocationField.c_str(),
                                    OFTString);
        oLocationField.SetWidth(nMaxFieldSize);
        if (poLayer->CreateField(&oLocationField) != OGRERR_NONE)
            return nullptr;

        if (!psOptions->osSrcSRSFieldName.empty())
        {
            OGRFieldDefn oSrcSRSField(psOptions->osSrcSRSFieldName.c_str(),
                                      OFTString);
            oSrcSRSField.SetWidth(nMaxFieldSize);
            if (poLayer->CreateField(&oSrcSRSField) != OGRERR_NONE)
                return nullptr;
        }
    }

    auto poLayerDefn = poLayer->GetLayerDefn();

    for (const auto &oFetchMD : psOptions->aoFetchMD)
    {
        if (poLayerDefn->GetFieldIndex(oFetchMD.osFieldName.c_str()) < 0)
        {
            OGRFieldDefn oField(oFetchMD.osFieldName.c_str(), oFetchMD.eType);
            if (poLayer->CreateField(&oField) != OGRERR_NONE)
                return nullptr;
        }
    }

    if (!psOptions->osGTIFilename.empty())
    {
        if (!psOptions->aosMetadata.empty())
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "-mo is not supported when -gti_filename is used");
            return nullptr;
        }
        CPLXMLNode *psRoot =
            CPLCreateXMLNode(nullptr, CXT_Element, "GDALTileIndexDataset");
        CPLCreateXMLElementAndValue(psRoot, "IndexDataset", pszDest);
        CPLCreateXMLElementAndValue(psRoot, "IndexLayer", poLayer->GetName());
        CPLCreateXMLElementAndValue(psRoot, "LocationField",
                                    psOptions->osLocationField.c_str());
        if (!std::isnan(psOptions->xres))
        {
            CPLCreateXMLElementAndValue(psRoot, "ResX",
                                        CPLSPrintf("%.18g", psOptions->xres));
            CPLCreateXMLElementAndValue(psRoot, "ResY",
                                        CPLSPrintf("%.18g", psOptions->yres));
        }
        if (!std::isnan(psOptions->xmin))
        {
            CPLCreateXMLElementAndValue(psRoot, "MinX",
                                        CPLSPrintf("%.18g", psOptions->xmin));
            CPLCreateXMLElementAndValue(psRoot, "MinY",
                                        CPLSPrintf("%.18g", psOptions->ymin));
            CPLCreateXMLElementAndValue(psRoot, "MaxX",
                                        CPLSPrintf("%.18g", psOptions->xmax));
            CPLCreateXMLElementAndValue(psRoot, "MaxY",
                                        CPLSPrintf("%.18g", psOptions->ymax));
        }

        int nBandCount = 0;
        if (!psOptions->osBandCount.empty())
        {
            nBandCount = atoi(psOptions->osBandCount.c_str());
        }
        else
        {
            if (!psOptions->osDataType.empty())
            {
                nBandCount = std::max(
                    nBandCount,
                    CPLStringList(CSLTokenizeString2(
                                      psOptions->osDataType.c_str(), ", ", 0))
                        .size());
            }
            if (!psOptions->osNodata.empty())
            {
                nBandCount = std::max(
                    nBandCount,
                    CPLStringList(CSLTokenizeString2(
                                      psOptions->osNodata.c_str(), ", ", 0))
                        .size());
            }
            if (!psOptions->osColorInterp.empty())
            {
                nBandCount =
                    std::max(nBandCount,
                             CPLStringList(
                                 CSLTokenizeString2(
                                     psOptions->osColorInterp.c_str(), ", ", 0))
                                 .size());
            }
        }

        for (int i = 0; i < nBandCount; ++i)
        {
            auto psBand = CPLCreateXMLNode(psRoot, CXT_Element, "Band");
            CPLAddXMLAttributeAndValue(psBand, "band", CPLSPrintf("%d", i + 1));
            if (!psOptions->osDataType.empty())
            {
                const CPLStringList aosTokens(
                    CSLTokenizeString2(psOptions->osDataType.c_str(), ", ", 0));
                if (aosTokens.size() == 1)
                    CPLAddXMLAttributeAndValue(psBand, "dataType",
                                               aosTokens[0]);
                else if (i < aosTokens.size())
                    CPLAddXMLAttributeAndValue(psBand, "dataType",
                                               aosTokens[i]);
            }
            if (!psOptions->osNodata.empty())
            {
                const CPLStringList aosTokens(
                    CSLTokenizeString2(psOptions->osNodata.c_str(), ", ", 0));
                if (aosTokens.size() == 1)
                    CPLCreateXMLElementAndValue(psBand, "NoDataValue",
                                                aosTokens[0]);
                else if (i < aosTokens.size())
                    CPLCreateXMLElementAndValue(psBand, "NoDataValue",
                                                aosTokens[i]);
            }
            if (!psOptions->osColorInterp.empty())
            {
                const CPLStringList aosTokens(CSLTokenizeString2(
                    psOptions->osColorInterp.c_str(), ", ", 0));
                if (aosTokens.size() == 1)
                    CPLCreateXMLElementAndValue(psBand, "ColorInterp",
                                                aosTokens[0]);
                else if (i < aosTokens.size())
                    CPLCreateXMLElementAndValue(psBand, "ColorInterp",
                                                aosTokens[i]);
            }
        }

        if (psOptions->bMaskBand)
        {
            CPLCreateXMLElementAndValue(psRoot, "MaskBand", "true");
        }
        int res =
            CPLSerializeXMLTreeToFile(psRoot, psOptions->osGTIFilename.c_str());
        CPLDestroyXMLNode(psRoot);
        if (!res)
            return nullptr;
    }
    else
    {
        poLayer->SetMetadataItem("LOCATION_FIELD",
                                 psOptions->osLocationField.c_str());
        if (!std::isnan(psOptions->xres))
        {
            poLayer->SetMetadataItem("RESX",
                                     CPLSPrintf("%.18g", psOptions->xres));
            poLayer->SetMetadataItem("RESY",
                                     CPLSPrintf("%.18g", psOptions->yres));
        }
        if (!std::isnan(psOptions->xmin))
        {
            poLayer->SetMetadataItem("MINX",
                                     CPLSPrintf("%.18g", psOptions->xmin));
            poLayer->SetMetadataItem("MINY",
                                     CPLSPrintf("%.18g", psOptions->ymin));
            poLayer->SetMetadataItem("MAXX",
                                     CPLSPrintf("%.18g", psOptions->xmax));
            poLayer->SetMetadataItem("MAXY",
                                     CPLSPrintf("%.18g", psOptions->ymax));
        }
        if (!psOptions->osBandCount.empty())
        {
            poLayer->SetMetadataItem("BAND_COUNT",
                                     psOptions->osBandCount.c_str());
        }
        if (!psOptions->osDataType.empty())
        {
            poLayer->SetMetadataItem("DATA_TYPE",
                                     psOptions->osDataType.c_str());
        }
        if (!psOptions->osNodata.empty())
        {
            poLayer->SetMetadataItem("NODATA", psOptions->osNodata.c_str());
        }
        if (!psOptions->osColorInterp.empty())
        {
            poLayer->SetMetadataItem("COLOR_INTERPRETATION",
                                     psOptions->osColorInterp.c_str());
        }
        if (psOptions->bMaskBand)
        {
            poLayer->SetMetadataItem("MASK_BAND", "YES");
        }
        const CPLStringList aosMetadata(psOptions->aosMetadata);
        for (const auto &[pszKey, pszValue] :
             cpl::IterateNameValue(aosMetadata))
        {
            poLayer->SetMetadataItem(pszKey, pszValue);
        }
    }

    const int ti_field =
        poLayerDefn->GetFieldIndex(psOptions->osLocationField.c_str());
    if (ti_field < 0)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to find field `%s' in file `%s'.",
                 psOptions->osLocationField.c_str(), pszDest);
        return nullptr;
    }

    int i_SrcSRSName = -1;
    if (!psOptions->osSrcSRSFieldName.empty())
    {
        i_SrcSRSName =
            poLayerDefn->GetFieldIndex(psOptions->osSrcSRSFieldName.c_str());
        if (i_SrcSRSName < 0)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Unable to find field `%s' in file `%s'.",
                     psOptions->osSrcSRSFieldName.c_str(), pszDest);
            return nullptr;
        }
    }

    // Load in memory existing file names in tile index.
    std::set<std::string> oSetExistingFiles;
    OGRSpatialReference oAlreadyExistingSRS;
    if (bExistingLayer)
    {
        for (auto &&poFeature : poLayer)
        {
            if (poFeature->IsFieldSetAndNotNull(ti_field))
            {
                if (oSetExistingFiles.empty())
                {
                    auto poSrcDS =
                        std::unique_ptr<GDALDataset>(GDALDataset::Open(
                            poFeature->GetFieldAsString(ti_field),
                            GDAL_OF_RASTER, nullptr, nullptr, nullptr));
                    if (poSrcDS)
                    {
                        auto poSrcSRS = poSrcDS->GetSpatialRef();
                        if (poSrcSRS)
                            oAlreadyExistingSRS = *poSrcSRS;
                    }
                }
                oSetExistingFiles.insert(poFeature->GetFieldAsString(ti_field));
            }
        }
    }

    std::string osCurrentPath;
    if (psOptions->bWriteAbsolutePath)
    {
        char *pszCurrentPath = CPLGetCurrentDir();
        if (pszCurrentPath)
        {
            osCurrentPath = pszCurrentPath;
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "This system does not support the CPLGetCurrentDir call. "
                     "The option -bWriteAbsolutePath will have no effect.");
        }
        CPLFree(pszCurrentPath);
    }

    const bool bIsGTIContext =
        !std::isnan(psOptions->xres) || !std::isnan(psOptions->xmin) ||
        !psOptions->osBandCount.empty() || !psOptions->osNodata.empty() ||
        !psOptions->osColorInterp.empty() || !psOptions->osDataType.empty() ||
        psOptions->bMaskBand || !psOptions->aosMetadata.empty() ||
        !psOptions->osGTIFilename.empty();

    /* -------------------------------------------------------------------- */
    /*      loop over GDAL files, processing.                               */
    /* -------------------------------------------------------------------- */
    while (true)
    {
        const std::string osSrcFilename = oGDALTileIndexTileIterator.next();
        if (osSrcFilename.empty())
            break;

        std::string osFileNameToWrite;
        VSIStatBuf sStatBuf;

        // Make sure it is a file before building absolute path name.
        if (!osCurrentPath.empty() &&
            CPLIsFilenameRelative(osSrcFilename.c_str()) &&
            VSIStat(osSrcFilename.c_str(), &sStatBuf) == 0)
        {
            osFileNameToWrite = CPLProjectRelativeFilename(
                osCurrentPath.c_str(), osSrcFilename.c_str());
        }
        else
        {
            osFileNameToWrite = osSrcFilename.c_str();
        }

        // Checks that file is not already in tileindex.
        if (oSetExistingFiles.find(osFileNameToWrite) !=
            oSetExistingFiles.end())
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "File %s is already in tileindex. Skipping it.",
                     osFileNameToWrite.c_str());
            continue;
        }

        auto poSrcDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
            osSrcFilename.c_str(), GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR,
            nullptr, nullptr, nullptr));
        if (poSrcDS == nullptr)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "Unable to open %s, skipping.", osSrcFilename.c_str());
            continue;
        }

        double adfGeoTransform[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
        if (poSrcDS->GetGeoTransform(adfGeoTransform) != CE_None)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "It appears no georeferencing is available for\n"
                     "`%s', skipping.",
                     osSrcFilename.c_str());
            continue;
        }

        auto poSrcSRS = poSrcDS->GetSpatialRef();
        // If not set target srs, test that the current file uses same
        // projection as others.
        if (oTargetSRS.IsEmpty())
        {
            if (!oAlreadyExistingSRS.IsEmpty())
            {
                if (poSrcSRS == nullptr ||
                    !poSrcSRS->IsSame(&oAlreadyExistingSRS))
                {
                    CPLError(
                        CE_Warning, CPLE_AppDefined,
                        "%s is not using the same projection system "
                        "as other files in the tileindex.\n"
                        "This may cause problems when using it in MapServer "
                        "for example.\n"
                        "Use -t_srs option to set target projection system. %s",
                        osSrcFilename.c_str(),
                        psOptions->bSkipDifferentProjection
                            ? "Skipping this file."
                            : "");
                    if (psOptions->bSkipDifferentProjection)
                    {
                        continue;
                    }
                }
            }
            else
            {
                if (poSrcSRS)
                    oAlreadyExistingSRS = *poSrcSRS;
            }
        }

        const int nXSize = poSrcDS->GetRasterXSize();
        const int nYSize = poSrcDS->GetRasterYSize();
        if (nXSize == 0 || nYSize == 0)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "%s has 0 width or height. Skipping",
                     osSrcFilename.c_str());
            continue;
        }

        double adfX[5] = {0.0, 0.0, 0.0, 0.0, 0.0};
        double adfY[5] = {0.0, 0.0, 0.0, 0.0, 0.0};
        adfX[0] = adfGeoTransform[0] + 0 * adfGeoTransform[1] +
                  0 * adfGeoTransform[2];
        adfY[0] = adfGeoTransform[3] + 0 * adfGeoTransform[4] +
                  0 * adfGeoTransform[5];

        adfX[1] = adfGeoTransform[0] + nXSize * adfGeoTransform[1] +
                  0 * adfGeoTransform[2];
        adfY[1] = adfGeoTransform[3] + nXSize * adfGeoTransform[4] +
                  0 * adfGeoTransform[5];

        adfX[2] = adfGeoTransform[0] + nXSize * adfGeoTransform[1] +
                  nYSize * adfGeoTransform[2];
        adfY[2] = adfGeoTransform[3] + nXSize * adfGeoTransform[4] +
                  nYSize * adfGeoTransform[5];

        adfX[3] = adfGeoTransform[0] + 0 * adfGeoTransform[1] +
                  nYSize * adfGeoTransform[2];
        adfY[3] = adfGeoTransform[3] + 0 * adfGeoTransform[4] +
                  nYSize * adfGeoTransform[5];

        adfX[4] = adfGeoTransform[0] + 0 * adfGeoTransform[1] +
                  0 * adfGeoTransform[2];
        adfY[4] = adfGeoTransform[3] + 0 * adfGeoTransform[4] +
                  0 * adfGeoTransform[5];

        // If set target srs, do the forward transformation of all points.
        if (!oTargetSRS.IsEmpty() && poSrcSRS)
        {
            if (!poSrcSRS->IsSame(&oTargetSRS))
            {
                auto poCT = std::unique_ptr<OGRCoordinateTransformation>(
                    OGRCreateCoordinateTransformation(poSrcSRS, &oTargetSRS));
                if (!poCT || !poCT->Transform(5, adfX, adfY, nullptr))
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "unable to transform points from source "
                             "SRS `%s' to target SRS `%s' for file `%s' - file "
                             "skipped",
                             poSrcDS->GetProjectionRef(),
                             psOptions->osTargetSRS.c_str(),
                             osFileNameToWrite.c_str());
                    continue;
                }
            }
        }
        else if (bIsGTIContext && !oAlreadyExistingSRS.IsEmpty() &&
                 (poSrcSRS == nullptr ||
                  !poSrcSRS->IsSame(&oAlreadyExistingSRS)))
        {
            CPLError(
                CE_Failure, CPLE_AppDefined,
                "%s is not using the same projection system "
                "as other files in the tileindex. This is not compatible of "
                "GTI use. Use -t_srs option to reproject tile extents "
                "to a common SRS.",
                osSrcFilename.c_str());
            return nullptr;
        }

        const double dfMinX =
            std::min(std::min(adfX[0], adfX[1]), std::min(adfX[2], adfX[3]));
        const double dfMinY =
            std::min(std::min(adfY[0], adfY[1]), std::min(adfY[2], adfY[3]));
        const double dfMaxX =
            std::max(std::max(adfX[0], adfX[1]), std::max(adfX[2], adfX[3]));
        const double dfMaxY =
            std::max(std::max(adfY[0], adfY[1]), std::max(adfY[2], adfY[3]));
        const double dfRes =
            (dfMaxX - dfMinX) * (dfMaxY - dfMinY) / nXSize / nYSize;
        if (!std::isnan(psOptions->dfMinPixelSize) &&
            dfRes < psOptions->dfMinPixelSize)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "%s has %f as pixel size (< %f). Skipping",
                     osSrcFilename.c_str(), dfRes, psOptions->dfMinPixelSize);
            continue;
        }
        if (!std::isnan(psOptions->dfMaxPixelSize) &&
            dfRes > psOptions->dfMaxPixelSize)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "%s has %f as pixel size (> %f). Skipping",
                     osSrcFilename.c_str(), dfRes, psOptions->dfMaxPixelSize);
            continue;
        }

        auto poFeature = std::make_unique<OGRFeature>(poLayerDefn);
        poFeature->SetField(ti_field, osFileNameToWrite.c_str());

        if (i_SrcSRSName >= 0 && poSrcSRS)
        {
            const char *pszAuthorityCode = poSrcSRS->GetAuthorityCode(nullptr);
            const char *pszAuthorityName = poSrcSRS->GetAuthorityName(nullptr);
            if (psOptions->eSrcSRSFormat == FORMAT_AUTO)
            {
                if (pszAuthorityName != nullptr && pszAuthorityCode != nullptr)
                {
                    poFeature->SetField(i_SrcSRSName,
                                        CPLSPrintf("%s:%s", pszAuthorityName,
                                                   pszAuthorityCode));
                }
                else if (nMaxFieldSize == 0 ||
                         strlen(poSrcDS->GetProjectionRef()) <=
                             static_cast<size_t>(nMaxFieldSize))
                {
                    poFeature->SetField(i_SrcSRSName,
                                        poSrcDS->GetProjectionRef());
                }
                else
                {
                    char *pszProj4 = nullptr;
                    if (poSrcSRS->exportToProj4(&pszProj4) == OGRERR_NONE)
                    {
                        poFeature->SetField(i_SrcSRSName, pszProj4);
                    }
                    else
                    {
                        poFeature->SetField(i_SrcSRSName,
                                            poSrcDS->GetProjectionRef());
                    }
                    CPLFree(pszProj4);
                }
            }
            else if (psOptions->eSrcSRSFormat == FORMAT_WKT)
            {
                if (nMaxFieldSize == 0 ||
                    strlen(poSrcDS->GetProjectionRef()) <=
                        static_cast<size_t>(nMaxFieldSize))
                {
                    poFeature->SetField(i_SrcSRSName,
                                        poSrcDS->GetProjectionRef());
                }
                else
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Cannot write WKT for file %s as it is too long!",
                             osFileNameToWrite.c_str());
                }
            }
            else if (psOptions->eSrcSRSFormat == FORMAT_PROJ)
            {
                char *pszProj4 = nullptr;
                if (poSrcSRS->exportToProj4(&pszProj4) == OGRERR_NONE)
                {
                    poFeature->SetField(i_SrcSRSName, pszProj4);
                }
                CPLFree(pszProj4);
            }
            else if (psOptions->eSrcSRSFormat == FORMAT_EPSG)
            {
                if (pszAuthorityName != nullptr && pszAuthorityCode != nullptr)
                    poFeature->SetField(i_SrcSRSName,
                                        CPLSPrintf("%s:%s", pszAuthorityName,
                                                   pszAuthorityCode));
            }
        }

        for (const auto &oFetchMD : psOptions->aoFetchMD)
        {
            if (EQUAL(oFetchMD.osRasterItemName.c_str(), "{PIXEL_SIZE}"))
            {
                poFeature->SetField(oFetchMD.osFieldName.c_str(), dfRes);
                continue;
            }

            const char *pszMD =
                poSrcDS->GetMetadataItem(oFetchMD.osRasterItemName.c_str());
            if (pszMD)
            {
                if (EQUAL(oFetchMD.osRasterItemName.c_str(),
                          "TIFFTAG_DATETIME"))
                {
                    int nYear, nMonth, nDay, nHour, nMin, nSec;
                    if (sscanf(pszMD, "%04d:%02d:%02d %02d:%02d:%02d", &nYear,
                               &nMonth, &nDay, &nHour, &nMin, &nSec) == 6)
                    {
                        poFeature->SetField(
                            oFetchMD.osFieldName.c_str(),
                            CPLSPrintf("%04d/%02d/%02d %02d:%02d:%02d", nYear,
                                       nMonth, nDay, nHour, nMin, nSec));
                        continue;
                    }
                }
                poFeature->SetField(oFetchMD.osFieldName.c_str(), pszMD);
            }
        }

        auto poPoly = std::make_unique<OGRPolygon>();
        auto poRing = std::make_unique<OGRLinearRing>();
        for (int k = 0; k < 5; k++)
            poRing->addPoint(adfX[k], adfY[k]);
        poPoly->addRingDirectly(poRing.release());
        poFeature->SetGeometryDirectly(poPoly.release());

        if (poLayer->CreateFeature(poFeature.get()) != OGRERR_NONE)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Failed to create feature in tile index.");
            return nullptr;
        }
    }

    return GDALDataset::ToHandle(poTileIndexDS.release());
}

/************************************************************************/
/*                             SanitizeSRS                              */
/************************************************************************/

static char *SanitizeSRS(const char *pszUserInput)

{
    OGRSpatialReferenceH hSRS;
    char *pszResult = nullptr;

    CPLErrorReset();

    hSRS = OSRNewSpatialReference(nullptr);
    if (OSRSetFromUserInput(hSRS, pszUserInput) == OGRERR_NONE)
        OSRExportToWkt(hSRS, &pszResult);
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Translating SRS failed:\n%s",
                 pszUserInput);
    }

    OSRDestroySpatialReference(hSRS);

    return pszResult;
}

/************************************************************************/
/*                          GDALTileIndexOptionsNew()                   */
/************************************************************************/

/**
 * Allocates a GDALTileIndexOptions struct.
 *
 * @param papszArgv NULL terminated list of options (potentially including
 * filename and open options too), or NULL. The accepted options are the ones of
 * the <a href="/programs/gdaltindex.html">gdaltindex</a> utility.
 * @param psOptionsForBinary (output) may be NULL (and should generally be
 * NULL), otherwise (gdaltindex_bin.cpp use case) must be allocated with
 * GDALTileIndexOptionsForBinaryNew() prior to this function. Will be filled
 * with potentially present filename, open options,...
 * @return pointer to the allocated GDALTileIndexOptions struct. Must be freed
 * with GDALTileIndexOptionsFree().
 *
 * @since GDAL 3.9
 */

GDALTileIndexOptions *
GDALTileIndexOptionsNew(char **papszArgv,
                        GDALTileIndexOptionsForBinary *psOptionsForBinary)
{
    auto psOptions = std::make_unique<GDALTileIndexOptions>();

    /* -------------------------------------------------------------------- */
    /*      Parse arguments.                                                */
    /* -------------------------------------------------------------------- */

    CPLStringList aosArgv;

    if (papszArgv)
    {
        const int nArgc = CSLCount(papszArgv);
        for (int i = 0; i < nArgc; i++)
        {
            aosArgv.AddString(papszArgv[i]);
        }
    }

    try
    {
        auto argParser = GDALTileIndexAppOptionsGetParser(psOptions.get(),
                                                          psOptionsForBinary);
        argParser->parse_args_without_binary_name(aosArgv.List());

        // Check all no store_into args
        if (auto oTr = argParser->present<std::vector<double>>("-tr"))
        {
            psOptions->xres = (*oTr)[0];
            psOptions->yres = (*oTr)[1];
        }

        if (auto oTargetExtent = argParser->present<std::vector<double>>("-te"))
        {
            psOptions->xmin = (*oTargetExtent)[0];
            psOptions->ymin = (*oTargetExtent)[1];
            psOptions->xmax = (*oTargetExtent)[2];
            psOptions->ymax = (*oTargetExtent)[3];
        }

        if (auto fetchMd =
                argParser->present<std::vector<std::string>>("-fetch_md"))
        {

            CPLAssert(fetchMd->size() % 3 == 0);

            // Loop
            for (size_t i = 0; i < fetchMd->size(); i += 3)
            {
                OGRFieldType type;
                const auto &typeName{fetchMd->at(i + 2)};
                if (typeName == "String")
                {
                    type = OFTString;
                }
                else if (typeName == "Integer")
                {
                    type = OFTInteger;
                }
                else if (typeName == "Integer64")
                {
                    type = OFTInteger64;
                }
                else if (typeName == "Real")
                {
                    type = OFTReal;
                }
                else if (typeName == "Date")
                {
                    type = OFTDate;
                }
                else if (typeName == "DateTime")
                {
                    type = OFTDateTime;
                }
                else
                {
                    CPLError(CE_Failure, CPLE_AppDefined,
                             "-fetch_md requires a valid type name as third "
                             "argument: %s was given.",
                             fetchMd->at(i).c_str());
                    return nullptr;
                }

                const GDALTileIndexRasterMetadata oMD{type, fetchMd->at(i + 1),
                                                      fetchMd->at(i)};
                psOptions->aoFetchMD.push_back(oMD);
            }
        }

        // Check -t_srs
        if (!psOptions->osTargetSRS.empty())
        {
            auto sanitized{SanitizeSRS(psOptions->osTargetSRS.c_str())};
            if (sanitized)
            {
                psOptions->osTargetSRS = sanitized;
                CPLFree(sanitized);
            }
            else
            {
                // Error was already reported by SanitizeSRS, just return nullptr
                psOptions->osTargetSRS.clear();
                return nullptr;
            }
        }
    }
    catch (const std::exception &error)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "%s", error.what());
        return nullptr;
    }

    return psOptions.release();
}

/************************************************************************/
/*                        GDALTileIndexOptionsFree()                    */
/************************************************************************/

/**
 * Frees the GDALTileIndexOptions struct.
 *
 * @param psOptions the options struct for GDALTileIndex().
 *
 * @since GDAL 3.9
 */

void GDALTileIndexOptionsFree(GDALTileIndexOptions *psOptions)
{
    delete psOptions;
}

#undef CHECK_HAS_ENOUGH_ADDITIONAL_ARGS
