/******************************************************************************
 *
 * Project:  GDAL
 * Purpose:  gdal "raster create" subcommand
 * Author:   Even Rouault <even dot rouault at spatialys.com>
 *
 ******************************************************************************
 * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "gdalalg_raster_create.h"

#include "cpl_conv.h"
#include "gdal_priv.h"
#include "gdal_utils.h"
#include "ogr_spatialref.h"

//! @cond Doxygen_Suppress

#ifndef _
#define _(x) (x)
#endif

/************************************************************************/
/*        GDALRasterCreateAlgorithm::GDALRasterCreateAlgorithm()        */
/************************************************************************/

GDALRasterCreateAlgorithm::GDALRasterCreateAlgorithm(
    bool standaloneStep) noexcept
    : GDALRasterPipelineStepAlgorithm(
          NAME, DESCRIPTION, HELP_URL,
          ConstructorOptions()
              .SetStandaloneStep(standaloneStep)
              .SetAddDefaultArguments(false)
              .SetAutoOpenInputDatasets(true)
              .SetInputDatasetHelpMsg("Template raster dataset")
              .SetInputDatasetAlias("like")
              .SetInputDatasetMetaVar("TEMPLATE-DATASET")
              .SetInputDatasetRequired(false)
              .SetInputDatasetPositional(false)
              .SetInputDatasetMaxCount(1))
{
    AddRasterInputArgs(false, false);
    if (standaloneStep)
    {
        AddProgressArg();
        AddRasterOutputArgs(false);
    }

    AddArg("size", 0, _("Output size in pixels"), &m_size)
        .SetMinCount(2)
        .SetMaxCount(2)
        .SetMinValueIncluded(0)
        .SetRepeatedArgAllowed(false)
        .SetDisplayHintAboutRepetition(false)
        .SetMetaVar("<width>,<height>");
    AddArg("band-count", 0, _("Number of bands"), &m_bandCount)
        .SetDefault(m_bandCount)
        .SetMinValueIncluded(0);
    AddOutputDataTypeArg(&m_type).SetDefault(m_type);

    AddNodataArg(&m_nodata, /* noneAllowed = */ true);

    AddArg("burn", 0, _("Burn value"), &m_burnValues);
    AddArg("crs", 0, _("Set CRS"), &m_crs)
        .AddHiddenAlias("a_srs")
        .SetIsCRSArg(/*noneAllowed=*/true);
    AddBBOXArg(&m_bbox);

    {
        auto &arg = AddArg("metadata", 0, _("Add metadata item"), &m_metadata)
                        .SetMetaVar("<KEY>=<VALUE>")
                        .SetPackedValuesAllowed(false);
        arg.AddValidationAction([this, &arg]()
                                { return ParseAndValidateKeyValue(arg); });
        arg.AddHiddenAlias("mo");
    }

    const auto inputArg = GetArg(GDAL_ARG_NAME_INPUT);
    CPLAssertNotNull(inputArg);

    AddArg("copy-metadata", 0, _("Copy metadata from input dataset"),
           &m_copyMetadata)
        .AddDirectDependency(*inputArg);
    AddArg("copy-overviews", 0,
           _("Create same overview levels as input dataset"), &m_copyOverviews)
        .AddDirectDependency(*inputArg);
}

/************************************************************************/
/*                 GDALRasterCreateAlgorithm::RunImpl()                 */
/************************************************************************/

bool GDALRasterCreateAlgorithm::RunImpl(GDALProgressFunc pfnProgress,
                                        void *pProgressData)
{
    GDALPipelineStepRunContext stepCtxt;
    stepCtxt.m_pfnProgress = pfnProgress;
    stepCtxt.m_pProgressData = pProgressData;
    return RunPreStepPipelineValidations() && RunStep(stepCtxt);
}

/************************************************************************/
/*                 GDALRasterCreateAlgorithm::RunStep()                 */
/************************************************************************/

bool GDALRasterCreateAlgorithm::RunStep(GDALPipelineStepRunContext &)
{
    CPLAssert(!m_outputDataset.GetDatasetRef());

    if (m_standaloneStep)
    {
        if (m_format.empty())
        {
            const auto aosFormats =
                CPLStringList(GDALGetOutputDriversForDatasetName(
                    m_outputDataset.GetName().c_str(), GDAL_OF_RASTER,
                    /* bSingleMatch = */ true,
                    /* bWarn = */ true));
            if (aosFormats.size() != 1)
            {
                ReportError(CE_Failure, CPLE_AppDefined,
                            "Cannot guess driver for %s",
                            m_outputDataset.GetName().c_str());
                return false;
            }
            m_format = aosFormats[0];
        }
    }
    else
    {
        m_format = "MEM";
    }

    OGRSpatialReference oSRS;

    GDALGeoTransform gt;
    bool bGTValid = false;

    CPLStringList aosCreationOptions(m_creationOptions);

    GDALDataset *poSrcDS = m_inputDataset.empty()
                               ? nullptr
                               : m_inputDataset.front().GetDatasetRef();
    if (poSrcDS)
    {
        if (m_size.empty())
        {
            m_size = std::vector<int>{poSrcDS->GetRasterXSize(),
                                      poSrcDS->GetRasterYSize()};
        }

        if (!GetArg("band-count")->IsExplicitlySet())
        {
            m_bandCount = poSrcDS->GetRasterCount();
        }

        if (!GetArg("datatype")->IsExplicitlySet())
        {
            if (m_bandCount > 0)
            {
                m_type = GDALGetDataTypeName(
                    poSrcDS->GetRasterBand(1)->GetRasterDataType());
            }
        }

        if (m_crs.empty())
        {
            if (const auto poSRS = poSrcDS->GetSpatialRef())
                oSRS = *poSRS;
        }

        if (m_bbox.empty())
        {
            bGTValid = poSrcDS->GetGeoTransform(gt) == CE_None;
        }

        if (m_nodata.empty() && m_bandCount > 0)
        {
            int bNoData = false;
            const double dfNoData =
                poSrcDS->GetRasterBand(1)->GetNoDataValue(&bNoData);
            if (bNoData)
                m_nodata = CPLSPrintf("%.17g", dfNoData);
        }

        // Replicate tiling of input datasets for a few popular output formats,
        // when compatible, and when the user hasn't specified creation options
        // affecting tiling.
        int nBlockXSize = 0, nBlockYSize = 0;
        if (m_bandCount > 0)
            poSrcDS->GetRasterBand(1)->GetBlockSize(&nBlockXSize, &nBlockYSize);

        if (EQUAL(m_format.c_str(), "GTIFF") &&
            aosCreationOptions.FetchNameValue("TILED") == nullptr &&
            aosCreationOptions.FetchNameValue("BLOCKXSIZE") == nullptr &&
            aosCreationOptions.FetchNameValue("BLOCKYSIZE") == nullptr &&
            m_bandCount > 0)
        {
            if (nBlockXSize != poSrcDS->GetRasterXSize() &&
                (nBlockXSize % 16) == 0 && (nBlockYSize % 16) == 0)
            {
                aosCreationOptions.SetNameValue("TILED", "YES");
                aosCreationOptions.SetNameValue("BLOCKXSIZE",
                                                CPLSPrintf("%d", nBlockXSize));
                aosCreationOptions.SetNameValue("BLOCKYSIZE",
                                                CPLSPrintf("%d", nBlockYSize));
            }
        }
        else if (EQUAL(m_format.c_str(), "COG") &&
                 aosCreationOptions.FetchNameValue("BLOCKSIZE") == nullptr &&
                 m_bandCount > 0)
        {
            if (nBlockXSize != poSrcDS->GetRasterXSize() &&
                nBlockXSize == nBlockYSize && nBlockXSize >= 128 &&
                (nBlockXSize % 16) == 0)
            {
                aosCreationOptions.SetNameValue("BLOCKSIZE",
                                                CPLSPrintf("%d", nBlockXSize));
            }
        }
        else if (EQUAL(m_format.c_str(), "GPKG") &&
                 aosCreationOptions.FetchNameValue("BLOCKSIZE") == nullptr &&
                 aosCreationOptions.FetchNameValue("BLOCKXSIZE") == nullptr &&
                 aosCreationOptions.FetchNameValue("BLOCKYSIZE") == nullptr &&
                 m_bandCount > 0)
        {
            if (nBlockXSize != poSrcDS->GetRasterXSize() &&
                nBlockXSize >= 256 && nBlockXSize <= 4096 &&
                nBlockYSize >= 256 && nBlockYSize <= 4096)
            {
                aosCreationOptions.SetNameValue("BLOCKXSIZE",
                                                CPLSPrintf("%d", nBlockXSize));
                aosCreationOptions.SetNameValue("BLOCKYSIZE",
                                                CPLSPrintf("%d", nBlockYSize));
            }
        }
    }

    if (m_size.empty())
    {
        ReportError(CE_Failure, CPLE_IllegalArg,
                    "Argument 'size' should be specified, or 'like' dataset "
                    "should be specified");
        return false;
    }

    if (!m_burnValues.empty() && m_burnValues.size() != 1 &&
        static_cast<int>(m_burnValues.size()) != m_bandCount)
    {
        if (m_bandCount == 1)
        {
            ReportError(CE_Failure, CPLE_IllegalArg,
                        "One value should be provided for argument "
                        "'burn', given there is one band");
        }
        else
        {
            ReportError(CE_Failure, CPLE_IllegalArg,
                        "One or %d values should be provided for argument "
                        "'burn', given there are %d bands",
                        m_bandCount, m_bandCount);
        }
        return false;
    }

    auto poDriver = GetGDALDriverManager()->GetDriverByName(m_format.c_str());
    if (!poDriver)
    {
        // shouldn't happen given checks done in GDALAlgorithm
        ReportError(CE_Failure, CPLE_AppDefined, "Cannot find driver %s",
                    m_format.c_str());
        return false;
    }

    if (m_appendRaster)
    {
        if (poDriver->GetMetadataItem(GDAL_DCAP_CREATE_SUBDATASETS) == nullptr)
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "-append option not supported for driver %s",
                        poDriver->GetDescription());
            return false;
        }
        aosCreationOptions.SetNameValue("APPEND_SUBDATASET", "YES");
    }

    auto poRetDS = std::unique_ptr<GDALDataset>(poDriver->Create(
        m_outputDataset.GetName().c_str(), m_size[0], m_size[1], m_bandCount,
        GDALGetDataTypeByName(m_type.c_str()), aosCreationOptions.List()));
    if (!poRetDS)
    {
        return false;
    }

    if (!m_crs.empty() && m_crs != "none" && m_crs != "null")
    {
        oSRS.SetFromUserInput(m_crs.c_str());
        oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
    }

    if (!oSRS.IsEmpty())
    {
        if (poRetDS->SetSpatialRef(&oSRS) != CE_None)
        {
            ReportError(CE_Failure, CPLE_AppDefined, "Setting CRS failed");
            return false;
        }
    }

    if (!m_bbox.empty())
    {
        if (poRetDS->GetRasterXSize() == 0 || poRetDS->GetRasterYSize() == 0)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Cannot set extent because one of dataset height or "
                        "width is null");
            return false;
        }
        bGTValid = true;
        gt.xorig = m_bbox[0];
        gt.xscale = (m_bbox[2] - m_bbox[0]) / poRetDS->GetRasterXSize();
        gt.xrot = 0;
        gt.yorig = m_bbox[3];
        gt.yrot = 0;
        gt.yscale = -(m_bbox[3] - m_bbox[1]) / poRetDS->GetRasterYSize();
    }
    if (bGTValid)
    {
        if (poRetDS->SetGeoTransform(gt) != CE_None)
        {
            ReportError(CE_Failure, CPLE_AppDefined, "Setting extent failed");
            return false;
        }
    }

    if (!m_nodata.empty() && !EQUAL(m_nodata.c_str(), "none"))
    {
        for (int i = 0; i < poRetDS->GetRasterCount(); ++i)
        {
            bool bCannotBeExactlyRepresented = false;
            if (poRetDS->GetRasterBand(i + 1)->SetNoDataValueAsString(
                    m_nodata.c_str(), &bCannotBeExactlyRepresented) != CE_None)
            {
                if (bCannotBeExactlyRepresented)
                {
                    ReportError(CE_Failure, CPLE_AppDefined,
                                "Setting nodata value failed as it cannot be "
                                "represented on its data type");
                }
                else
                {
                    ReportError(CE_Failure, CPLE_AppDefined,
                                "Setting nodata value failed");
                }
                return false;
            }
        }
    }

    if (m_copyMetadata)
    {

        // This should never happen because of the dependency set
        CPLAssertNotNull(poSrcDS);

        {
            const CPLStringList aosDomains(poSrcDS->GetMetadataDomainList());
            for (const char *domain : aosDomains)
            {
                if (!EQUAL(domain, "IMAGE_STRUCTURE"))
                {
                    if (poRetDS->SetMetadata(poSrcDS->GetMetadata(domain),
                                             domain) != CE_None)
                    {
                        ReportError(CE_Failure, CPLE_AppDefined,
                                    "Cannot copy '%s' metadata domain", domain);
                        return false;
                    }
                }
            }
        }
        for (int i = 0; i < m_bandCount; ++i)
        {
            const CPLStringList aosDomains(
                poSrcDS->GetRasterBand(i + 1)->GetMetadataDomainList());
            for (const char *domain : aosDomains)
            {
                if (!EQUAL(domain, "IMAGE_STRUCTURE"))
                {
                    if (poRetDS->GetRasterBand(i + 1)->SetMetadata(
                            poSrcDS->GetRasterBand(i + 1)->GetMetadata(domain),
                            domain) != CE_None)
                    {
                        ReportError(
                            CE_Failure, CPLE_AppDefined,
                            "Cannot copy '%s' metadata domain for band %d",
                            domain, i + 1);
                        return false;
                    }
                }
            }
        }
    }

    const CPLStringList aosMD(m_metadata);
    for (const auto &[key, value] : cpl::IterateNameValue(aosMD))
    {
        if (poRetDS->SetMetadataItem(key, value) != CE_None)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "SetMetadataItem('%s', '%s') failed", key, value);
            return false;
        }
    }

    if (m_copyOverviews && m_bandCount > 0)
    {
        // This should never happen because of the dependency set
        CPLAssertNotNull(poSrcDS);

        if (poSrcDS->GetRasterXSize() != poRetDS->GetRasterXSize() ||
            poSrcDS->GetRasterYSize() != poRetDS->GetRasterYSize())
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Argument 'copy-overviews' can only be set when the "
                        "input and output datasets have the same dimension");
            return false;
        }
        const int nOverviewCount =
            poSrcDS->GetRasterBand(1)->GetOverviewCount();
        std::vector<int> anLevels;
        for (int i = 0; i < nOverviewCount; ++i)
        {
            const auto poOvrBand = poSrcDS->GetRasterBand(1)->GetOverview(i);
            const int nOvrFactor = GDALComputeOvFactor(
                poOvrBand->GetXSize(), poSrcDS->GetRasterXSize(),
                poOvrBand->GetYSize(), poSrcDS->GetRasterYSize());
            anLevels.push_back(nOvrFactor);
        }
        if (poRetDS->BuildOverviews(
                "NONE", nOverviewCount, anLevels.data(),
                /* nListBands = */ 0, /* panBandList = */ nullptr,
                /* pfnProgress = */ nullptr, /* pProgressData = */ nullptr,
                /* options = */ nullptr) != CE_None)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Creating overview(s) failed");
            return false;
        }
    }

    if (!m_burnValues.empty())
    {
        for (int i = 0; i < m_bandCount; ++i)
        {
            const int burnValueIdx = m_burnValues.size() == 1 ? 0 : i;
            const auto poDstBand = poRetDS->GetRasterBand(i + 1);
            // cppcheck-suppress negativeContainerIndex
            if (poDstBand->Fill(m_burnValues[burnValueIdx]) != CE_None)
            {
                ReportError(CE_Failure, CPLE_AppDefined,
                            "Setting burn value failed");
                return false;
            }
        }
        if (poRetDS->FlushCache(false) != CE_None)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Setting burn value failed");
            return false;
        }
    }

    m_outputDataset.Set(std::move(poRetDS));

    return true;
}

GDALRasterCreateAlgorithmStandalone::~GDALRasterCreateAlgorithmStandalone() =
    default;

//! @endcond
