/******************************************************************************
 *
 * Project:  GDAL Utilities
 * Purpose:  GDAL Raster creation utility
 * Author:   Even Rouault <even dot rouault at spatialys.com>
 *
 ******************************************************************************
 * Copyright (c) 2020, Even Rouault <even dot rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "cpl_string.h"
#include "gdal_version.h"
#include "gdal_priv.h"
#include "gdal.h"
#include "commonutils.h"
#include "ogr_spatialref.h"

#include <cstdlib>
#include <memory>
#include <vector>

static void Usage(bool bIsError)

{
    fprintf(
        bIsError ? stderr : stdout,
        "Usage: gdal_create [--help] [--help-general]\n"
        "       [-of <format>]\n"
        "       [-outsize <xsize> <ysize>]\n"
        "       [-bands <count>]\n"
        "       [-burn <value>]...\n"
        "       [-ot "
        "{Byte/Int8/Int16/UInt16/UInt32/Int32/UInt64/Int64/Float32/Float64/\n"
        "             CInt16/CInt32/CFloat32/CFloat64}] [-strict]\n"
        "       [-a_srs <srs_def>] [-a_ullr <ulx> <uly> <lrx> <lry>] "
        "[-a_nodata <value>]\n"
        "       [-mo <META-TAG>=<VALUE>]... [-q]\n"
        "       [-co <NAME>=<VALUE>]...\n"
        "       [-if <input_dataset>]\n"
        "       <out_dataset>\n");

    exit(bIsError ? 1 : 0);
}

/************************************************************************/
/*                            ArgIsNumeric()                            */
/************************************************************************/

static bool ArgIsNumeric(const char *pszArg)

{
    char *pszEnd = nullptr;
    CPLStrtod(pszArg, &pszEnd);
    return pszEnd != nullptr && pszEnd[0] == '\0';
}

/************************************************************************/
/*                                main()                                */
/************************************************************************/

MAIN_START(argc, argv)

{
    /* Check strict compilation and runtime library version as we use C++ API */
    if (!GDAL_CHECK_VERSION(argv[0]))
        exit(1);

    EarlySetConfigOptions(argc, argv);

    /* -------------------------------------------------------------------- */
    /*      Register standard GDAL drivers, and process generic GDAL        */
    /*      command options.                                                */
    /* -------------------------------------------------------------------- */
    GDALAllRegister();
    argc = GDALGeneralCmdLineProcessor(argc, &argv, 0);
    if (argc < 1)
        exit(-argc);

    const char *pszFormat = nullptr;
    const char *pszFilename = nullptr;
    CPLStringList aosCreateOptions;
    int nPixels = 0;
    int nLines = 0;
    int nBandCount = -1;
    GDALDataType eDT = GDT_Unknown;
    double dfULX = 0;
    double dfULY = 0;
    double dfLRX = 0;
    double dfLRY = 0;
    bool bGeoTransform = false;
    const char *pszOutputSRS = nullptr;
    CPLStringList aosMetadata;
    std::vector<double> adfBurnValues;
    bool bQuiet = false;
    int bSetNoData = false;
    std::string osNoData;
    const char *pszInputFile = nullptr;
    for (int i = 1; argv != nullptr && argv[i] != nullptr; i++)
    {
        if (EQUAL(argv[i], "--utility_version"))
        {
            printf("%s was compiled against GDAL %s and is running against "
                   "GDAL %s\n",
                   argv[0], GDAL_RELEASE_NAME, GDALVersionInfo("RELEASE_NAME"));
            CSLDestroy(argv);
            return 0;
        }
        else if (EQUAL(argv[i], "--help"))
        {
            Usage(false);
        }
        else if (i < argc - 1 &&
                 (EQUAL(argv[i], "-of") || EQUAL(argv[i], "-f")))
        {
            ++i;
            pszFormat = argv[i];
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-co"))
        {
            ++i;
            aosCreateOptions.AddString(argv[i]);
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-mo"))
        {
            ++i;
            aosMetadata.AddString(argv[i]);
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-bands"))
        {
            ++i;
            nBandCount = atoi(argv[i]);
        }
        else if (i + 2 < argc && EQUAL(argv[i], "-outsize"))
        {
            ++i;
            nPixels = atoi(argv[i]);
            ++i;
            nLines = atoi(argv[i]);
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-ot"))
        {
            ++i;
            for (int iType = 1; iType < GDT_TypeCount; iType++)
            {
                if (GDALGetDataTypeName(static_cast<GDALDataType>(iType)) !=
                        nullptr &&
                    EQUAL(GDALGetDataTypeName(static_cast<GDALDataType>(iType)),
                          argv[i]))
                {
                    eDT = static_cast<GDALDataType>(iType);
                }
            }

            if (eDT == GDT_Unknown)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "Unknown output pixel type: %s.", argv[i]);
                CSLDestroy(argv);
                exit(1);
            }
        }
        else if (i + 4 < argc && EQUAL(argv[i], "-a_ullr"))
        {
            bGeoTransform = true;
            // coverity[tainted_data]
            dfULX = CPLAtofM(argv[++i]);
            // coverity[tainted_data]
            dfULY = CPLAtofM(argv[++i]);
            // coverity[tainted_data]
            dfLRX = CPLAtofM(argv[++i]);
            // coverity[tainted_data]
            dfLRY = CPLAtofM(argv[++i]);
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-a_srs"))
        {
            ++i;
            pszOutputSRS = argv[i];
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-a_nodata"))
        {
            bSetNoData = true;
            ++i;
            // coverity[tainted_data]
            osNoData = argv[i];
        }

        else if (i < argc - 1 && EQUAL(argv[i], "-burn"))
        {
            if (strchr(argv[i + 1], ' '))
            {
                ++i;
                CPLStringList aosTokens(CSLTokenizeString(argv[i]));
                for (int j = 0; j < aosTokens.size(); j++)
                {
                    adfBurnValues.push_back(CPLAtof(aosTokens[j]));
                }
            }
            else
            {
                // coverity[tainted_data]
                while (i < argc - 1 && ArgIsNumeric(argv[i + 1]))
                {
                    ++i;
                    // coverity[tainted_data]
                    adfBurnValues.push_back(CPLAtof(argv[i]));
                }
            }
        }
        else if (i < argc - 1 && EQUAL(argv[i], "-if"))
        {
            ++i;
            pszInputFile = argv[i];
        }
        else if (EQUAL(argv[i], "-q"))
        {
            bQuiet = true;
        }
        else if (argv[i][0] == '-')
        {
            CPLError(CE_Failure, CPLE_NotSupported, "Unknown option name '%s'",
                     argv[i]);
            CSLDestroy(argv);
            Usage(true);
        }
        else if (pszFilename == nullptr)
        {
            pszFilename = argv[i];
        }
        else
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Too many command options '%s'", argv[i]);
            CSLDestroy(argv);
            Usage(true);
        }
    }
    if (pszFilename == nullptr)
    {
        CSLDestroy(argv);
        Usage(true);
    }

    double adfGeoTransform[6] = {0, 1, 0, 0, 0, 1};
    if (bGeoTransform && nPixels > 0 && nLines > 0)
    {
        adfGeoTransform[0] = dfULX;
        adfGeoTransform[1] = (dfLRX - dfULX) / nPixels;
        adfGeoTransform[2] = 0;
        adfGeoTransform[3] = dfULY;
        adfGeoTransform[4] = 0;
        adfGeoTransform[5] = (dfLRY - dfULY) / nLines;
    }

    std::unique_ptr<GDALDataset> poInputDS;
    if (pszInputFile)
    {
        poInputDS.reset(GDALDataset::Open(
            pszInputFile, GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR));
        if (poInputDS == nullptr)
        {
            CSLDestroy(argv);
            GDALDestroyDriverManager();
            exit(1);
        }
        if (nPixels == 0)
        {
            nPixels = poInputDS->GetRasterXSize();
            nLines = poInputDS->GetRasterYSize();
        }
        if (nBandCount < 0)
        {
            nBandCount = poInputDS->GetRasterCount();
        }
        if (eDT == GDT_Unknown && poInputDS->GetRasterCount() > 0)
        {
            eDT = poInputDS->GetRasterBand(1)->GetRasterDataType();
        }
        if (pszOutputSRS == nullptr)
        {
            pszOutputSRS = poInputDS->GetProjectionRef();
        }
        if (!(bGeoTransform && nPixels > 0 && nLines > 0))
        {
            if (poInputDS->GetGeoTransform(adfGeoTransform) == CE_None)
            {
                bGeoTransform = true;
            }
        }
        if (!bSetNoData && poInputDS->GetRasterCount() > 0)
        {
            if (eDT == GDT_Int64)
            {
                const auto nNoDataValue =
                    poInputDS->GetRasterBand(1)->GetNoDataValueAsInt64(
                        &bSetNoData);
                if (bSetNoData)
                    osNoData = CPLSPrintf(CPL_FRMT_GIB,
                                          static_cast<GIntBig>(nNoDataValue));
            }
            else if (eDT == GDT_UInt64)
            {
                const auto nNoDataValue =
                    poInputDS->GetRasterBand(1)->GetNoDataValueAsUInt64(
                        &bSetNoData);
                if (bSetNoData)
                    osNoData = CPLSPrintf(CPL_FRMT_GUIB,
                                          static_cast<GUIntBig>(nNoDataValue));
            }
            else
            {
                const double dfNoDataValue =
                    poInputDS->GetRasterBand(1)->GetNoDataValue(&bSetNoData);
                if (bSetNoData)
                    osNoData = CPLSPrintf("%.18g", dfNoDataValue);
            }
        }
    }

    GDALDriverH hDriver = GDALGetDriverByName(
        pszFormat ? pszFormat : GetOutputDriverForRaster(pszFilename).c_str());
    if (hDriver == nullptr)
    {
        fprintf(stderr, "Output driver not found.\n");
        CSLDestroy(argv);
        GDALDestroyDriverManager();
        exit(1);
    }
    const bool bHasCreate =
        GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATE, nullptr) != nullptr;
    if (!bHasCreate &&
        GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATECOPY, nullptr) == nullptr)
    {
        fprintf(stderr, "This driver has no creation capabilities.\n");
        CSLDestroy(argv);
        GDALDestroyDriverManager();
        exit(1);
    }
    GDALDriverH hTmpDriver = GDALGetDriverByName("MEM");
    if (!bHasCreate && hTmpDriver == nullptr)
    {
        fprintf(stderr, "MEM driver not available.\n");
        CSLDestroy(argv);
        GDALDestroyDriverManager();
        exit(1);
    }

    if (nPixels != 0 && eDT == GDT_Unknown)
    {
        eDT = GDT_Byte;
    }
    if (nBandCount < 0)
    {
        nBandCount = eDT == GDT_Unknown ? 0 : 1;
    }
    GDALDatasetH hDS = GDALCreate(
        bHasCreate ? hDriver : hTmpDriver, pszFilename, nPixels, nLines,
        nBandCount, eDT, bHasCreate ? aosCreateOptions.List() : nullptr);

    if (hDS == nullptr)
    {
        GDALDestroyDriverManager();
        CSLDestroy(argv);
        exit(1);
    }

    if (pszOutputSRS && pszOutputSRS[0] != '\0' && !EQUAL(pszOutputSRS, "NONE"))
    {
        OGRSpatialReference oSRS;
        oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);

        if (oSRS.SetFromUserInput(pszOutputSRS) != OGRERR_NONE)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Failed to process SRS definition: %s", pszOutputSRS);
            CSLDestroy(argv);
            GDALDestroyDriverManager();
            exit(1);
        }

        char *pszSRS = nullptr;
        oSRS.exportToWkt(&pszSRS);

        if (GDALSetProjection(hDS, pszSRS) != CE_None)
        {
            CPLFree(pszSRS);
            GDALClose(hDS);
            CSLDestroy(argv);
            GDALDestroyDriverManager();
            exit(1);
        }
        CPLFree(pszSRS);
    }
    if (bGeoTransform)
    {
        if (nPixels == 0)
        {
            fprintf(stderr,
                    "-outsize must be specified when -a_ullr is used.\n");
            GDALClose(hDS);
            GDALDestroyDriverManager();
            exit(1);
        }
        if (GDALSetGeoTransform(hDS, adfGeoTransform) != CE_None)
        {
            GDALClose(hDS);
            CSLDestroy(argv);
            GDALDestroyDriverManager();
            exit(1);
        }
    }
    if (!aosMetadata.empty())
    {
        GDALSetMetadata(hDS, aosMetadata.List(), nullptr);
    }
    const int nBands = GDALGetRasterCount(hDS);
    if (bSetNoData)
    {
        for (int i = 0; i < nBands; i++)
        {
            auto hBand = GDALGetRasterBand(hDS, i + 1);
            if (eDT == GDT_Int64)
            {
                GDALSetRasterNoDataValueAsInt64(
                    hBand, static_cast<int64_t>(
                               std::strtoll(osNoData.c_str(), nullptr, 10)));
            }
            else if (eDT == GDT_UInt64)
            {
                GDALSetRasterNoDataValueAsUInt64(
                    hBand, static_cast<uint64_t>(
                               std::strtoull(osNoData.c_str(), nullptr, 10)));
            }
            else
            {
                GDALSetRasterNoDataValue(hBand, CPLAtofM(osNoData.c_str()));
            }
        }
    }
    if (!adfBurnValues.empty())
    {
        for (int i = 0; i < nBands; i++)
        {
            GDALFillRaster(GDALGetRasterBand(hDS, i + 1),
                           i < static_cast<int>(adfBurnValues.size())
                               ? adfBurnValues[i]
                               : adfBurnValues.back(),
                           0);
        }
    }

    bool bHasGotErr = false;
    if (!bHasCreate)
    {
        GDALDatasetH hOutDS = GDALCreateCopy(
            hDriver, pszFilename, hDS, false, aosCreateOptions.List(),
            bQuiet ? GDALDummyProgress : GDALTermProgress, nullptr);
        if (hOutDS == nullptr)
        {
            GDALClose(hDS);
            CSLDestroy(argv);
            GDALDestroyDriverManager();
            exit(1);
        }
        if (GDALClose(hOutDS) != CE_None)
        {
            bHasGotErr = true;
        }
    }

    const bool bWasFailureBefore = (CPLGetLastErrorType() == CE_Failure);
    if (GDALClose(hDS) != CE_None)
        bHasGotErr = true;
    if (!bWasFailureBefore && CPLGetLastErrorType() == CE_Failure)
    {
        bHasGotErr = true;
    }

    CSLDestroy(argv);
    return bHasGotErr ? 1 : 0;
}
MAIN_END
