/****************************************************************************
 *
 * MODULE:       r.out.gdal
 * AUTHOR(S):    Vytautas Vebra <olivership@gmail.com>, Markus Metz
 * PURPOSE:      Exports GRASS raster to GDAL supported formats;
 *               based on GDAL library.
 *               Replaces r.out.gdal.sh script which used the gdal_translate
 *               executable and GDAL grass-format plugin.
 * COPYRIGHT:    (C) 2006-2009 by the GRASS Development Team
 *
 *               This program is free software under the GNU General Public
 *               License (>=v2). Read the file COPYING that comes with GRASS
 *               for details.
 *
 *****************************************************************************/

/* Undefine this if you do not want any extra function calls before G_parse() */
#define __ALLOW_DYNAMIC_OPTIONS__

#include <stdlib.h>
#include <unistd.h>
#include <float.h>
#include <math.h>
#include <string.h>
#include <grass/gis.h>
#include <grass/raster.h>
#include <grass/imagery.h>
#include <grass/gprojects.h>
#include <grass/glocale.h>
#include <grass/dbmi.h>

#include <cpl_string.h>
#include <cpl_port.h>

#include "local_proto.h"

int range_check(double, double, GDALDataType);
int nodataval_check(double, GDALDataType);
double set_default_nodata_value(GDALDataType, double, double);

void supported_formats(const char **formats)
{
    /* Code taken from r.in.gdal */

    int iDr;
    dbString gdal_formats;

    db_init_string(&gdal_formats);

    if (*formats)
        fprintf(stdout, _("Supported formats:\n"));

    for (iDr = 0; iDr < GDALGetDriverCount(); iDr++) {

        GDALDriverH hDriver = GDALGetDriver(iDr);
        const char *pszRWFlag;

#ifdef GDAL_DCAP_RASTER
        /* Starting with GDAL 2.0, vector drivers can also be returned */
        /* Only keep raster drivers */
        if (!GDALGetMetadataItem(hDriver, GDAL_DCAP_RASTER, NULL))
            continue;
#endif

        if (GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATE, NULL))
            pszRWFlag = "rw+";
        else if (GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATECOPY, NULL))
            pszRWFlag = "rw";
        else
            continue;

        if (*formats)
            fprintf(stdout, "  %s (%s): %s\n", GDALGetDriverShortName(hDriver),
                    pszRWFlag, GDALGetDriverLongName(hDriver));
        else {
            if (GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATE, NULL) ||
                GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATECOPY, NULL)) {
                if (db_sizeof_string(&gdal_formats) > 0)
                    db_append_string(&gdal_formats, ",");

                db_append_string(&gdal_formats,
                                 (char *)GDALGetDriverShortName(hDriver));
            }
        }
    }

    if (db_sizeof_string(&gdal_formats) > 0)
        *formats = G_store(db_get_string(&gdal_formats));

    return;
}

static void AttachMetadata(GDALDatasetH hDS, char **papszMetadataOptions)
/* function taken from gdal_translate */
{
    int nCount = CSLCount(papszMetadataOptions);
    int i;

    for (i = 0; i < nCount; i++) {
        char *pszKey = NULL;
        const char *pszValue;

        pszValue = CPLParseNameValue(papszMetadataOptions[i], &pszKey);
        GDALSetMetadataItem(hDS, pszKey, pszValue, NULL);
        CPLFree(pszKey);
    }

    CSLDestroy(papszMetadataOptions);
}

int main(int argc, char *argv[])
{

    struct GModule *module;
    struct Flag *flag_l, *flag_c, *flag_m, *flag_f, *flag_t;
    struct Option *input, *format, *type, *output, *createopt, *metaopt,
        *nodataopt, *overviewopt, *offsetopt, *scaleopt;

    struct Cell_head cellhead;
    struct Ref ref;
    const char *mapset, *gdal_formats = NULL;
    RASTER_MAP_TYPE maptype, testmaptype;
    int bHaveMinMax;
    double dfCellMin, export_min;
    double dfCellMax, export_max;
    struct FPRange sRange;
    int retval = 0;
    int n_overviews = 0;

    G_gisinit(argv[0]);

    module = G_define_module();
    module->description =
        _("Exports GRASS raster maps into GDAL supported formats.");
    G_add_keyword(_("raster"));
    G_add_keyword(_("export"));
    G_add_keyword(_("output"));

    flag_l = G_define_flag();
    flag_l->key = 'l';
    flag_l->description = _("List supported output formats");
    flag_l->guisection = _("Print");
    flag_l->suppress_required = YES;

    flag_c = G_define_flag();
    flag_c->key = 'c';
    flag_c->label = _("Do not write GDAL standard colortable");
    flag_c->description = _("Only applicable to Byte or UInt16 data types");
    flag_c->guisection = _("Creation");

    flag_m = G_define_flag();
    flag_m->key = 'm';
    flag_m->label = _("Do not write non-standard metadata");
    flag_m->description = _("Enhances compatibility with other GIS software");
    flag_m->guisection = _("Creation");

    flag_t = G_define_flag();
    flag_t->key = 't';
    flag_t->label = _("Write raster attribute table");
    flag_t->description = _("Some export formats may not be supported");

    flag_f = G_define_flag();
    flag_f->key = 'f';
    flag_f->label = _("Force raster export despite any warnings of data loss");
    flag_f->description = _("Overrides nodata safety check");

    input = G_define_standard_option(G_OPT_R_INPUT);
    input->description = _("Name of raster map (or group) to export");

    output = G_define_standard_option(G_OPT_F_OUTPUT);
    output->description = _("Name for output raster file");

    format = G_define_option();
    format->key = "format";
    format->type = TYPE_STRING;
    format->description =
        _("Raster data format to write (case sensitive, see also -l flag)");

#ifdef __ALLOW_DYNAMIC_OPTIONS__
    /* Init GDAL */
    GDALAllRegister();
    supported_formats(&gdal_formats);
    if (gdal_formats)
        format->options = G_store(gdal_formats);
        /* else
         * G_fatal_error (_("Unknown GIS formats")); */
#else
    gdal_formats = "AAIGrid,BMP,BSB,DTED,ELAS,ENVI,FIT,GIF,GTiff,HFA,JPEG,MEM,"
                   "MFF,MFF2,NITF,PAux,PNG,PNM,VRT,XPM";
    format->options = gdal_formats;
#endif
    format->answer = "GTiff";
    format->required = YES;

    type = G_define_option();
    type->key = "type";
    type->type = TYPE_STRING;
    type->description = _("Data type");
    type->options = "Byte,Int16,UInt16,Int32,UInt32,Float32,Float64,CInt16,"
                    "CInt32,CFloat32,CFloat64";
    type->required = NO;
    type->guisection = _("Creation");

    createopt = G_define_option();
    createopt->key = "createopt";
    createopt->type = TYPE_STRING;
    createopt->label =
        _("Creation option(s) to pass to the output format driver");
    createopt->description = _("In the form of \"NAME=VALUE\", separate "
                               "multiple entries with a comma");
    createopt->multiple = YES;
    createopt->required = NO;
    createopt->guisection = _("Creation");

    metaopt = G_define_option();
    metaopt->key = "metaopt";
    metaopt->type = TYPE_STRING;
    metaopt->label = _("Metadata key(s) and value(s) to include");
    metaopt->description =
        _("In the form of \"META-TAG=VALUE\", separate multiple entries "
          "with a comma. Not supported by all output format drivers.");
    metaopt->multiple = YES;
    metaopt->required = NO;
    metaopt->guisection = _("Creation");

    nodataopt = G_define_option();
    nodataopt->key = "nodata";
    nodataopt->type = TYPE_DOUBLE;
    nodataopt->label = _("Assign a specified nodata value to output bands");
    nodataopt->description =
        _("If given, the nodata value is always written to metadata "
          "even if there are no NULL cells in the input band "
          "(enhances output compatibility).");
    nodataopt->multiple = NO;
    nodataopt->required = NO;
    nodataopt->guisection = _("Creation");

    overviewopt = G_define_option();
    overviewopt->key = "overviews";
    overviewopt->type = TYPE_INTEGER;
    overviewopt->options = "0-5";
    overviewopt->answer = "0";
    overviewopt->label =
        _("Number of overviews to create for the output dataset");
    overviewopt->multiple = NO;
    overviewopt->required = NO;
    overviewopt->guisection = _("Creation");

    offsetopt = G_define_option();
    offsetopt->key = "offset";
    offsetopt->type = TYPE_DOUBLE;
    offsetopt->description =
        _("Assign a specified offset value to output bands");
    offsetopt->multiple = NO;
    offsetopt->required = NO;
    offsetopt->guisection = _("Creation");

    scaleopt = G_define_option();
    scaleopt->key = "scale";
    scaleopt->type = TYPE_DOUBLE;
    scaleopt->description = _("Assign a specified scale value to output bands");
    scaleopt->multiple = NO;
    scaleopt->required = NO;
    scaleopt->guisection = _("Creation");

    if (G_parser(argc, argv))
        exit(EXIT_FAILURE);

#ifndef __ALLOW_DYNAMIC_OPTIONS__
    /* Init GDAL */
    GDALAllRegister();
#endif

    if (flag_l->answer) {
        supported_formats(&gdal_formats);
        exit(EXIT_SUCCESS);
    }

    /* Find input GRASS raster.. */
    mapset = G_find_raster2(input->answer, "");

    if (mapset != NULL) {
        /* Add input to "group". "Group" with 1 raster (band) will exist only in
         * memory. */
        I_init_group_ref(&ref);
        I_add_file_to_group_ref(input->answer, mapset, &ref);
    }
    else {
        /* Maybe input is group. Try to read group file */
        if (I_get_group_ref(input->answer, &ref) != 1)
            G_fatal_error(_("Raster map or group <%s> not found"),
                          input->answer);
    }
    if (ref.nfiles == 0)
        G_fatal_error(_("No raster maps in group <%s>"), input->answer);

    /* Read project and region data */
    struct Key_Value *projinfo = G_get_projinfo();
    struct Key_Value *projunits = G_get_projunits();
    struct Key_Value *projepsg = G_get_projepsg();
    char *srswkt = NULL;

#if GDAL_VERSION_MAJOR >= 3 && PROJ_VERSION_MAJOR >= 6
    char *indef;

    if ((indef = G_get_projsrid())) {
        PJ *obj = NULL;

        if ((obj = proj_create(NULL, indef))) {
            srswkt = G_store(proj_as_wkt(NULL, obj, PJ_WKT2_LATEST, NULL));

            if (srswkt && !*srswkt) {
                G_free(srswkt);
                srswkt = NULL;
            }
            proj_destroy(obj);
        }
    }
    if (!srswkt && (indef = G_get_projwkt())) {
        OGRSpatialReferenceH hSRS;
        char **papszOptions = NULL;

        hSRS = OSRNewSpatialReference(indef);
        papszOptions = G_calloc(2, sizeof(char *));
        papszOptions[0] = G_store("FORMAT=WKT2");
        OSRExportToWktEx(hSRS, &srswkt, (const char **)papszOptions);
        G_free(papszOptions[0]);
        G_free(papszOptions);
        srswkt = G_store(srswkt);
        if (srswkt && !*srswkt) {
            G_free(srswkt);
            srswkt = NULL;
        }
        OSRDestroySpatialReference(hSRS);
    }
#endif
    if (!srswkt) {
        srswkt = GPJ_grass_to_wkt2(projinfo, projunits, projepsg, 0, 0);

#if GDAL_VERSION_MAJOR >= 3 && PROJ_VERSION_MAJOR >= 6
        /* convert bound CRS */
        if (srswkt && *srswkt) {
            PJ *obj = NULL;

            indef = srswkt;
            if ((obj = proj_create(NULL, indef))) {
                if (proj_get_type(obj) == PJ_TYPE_BOUND_CRS) {
                    PJ *source_crs;

                    G_debug(1, "found bound crs");
                    source_crs = proj_get_source_crs(NULL, obj);
                    if (source_crs) {
                        srswkt = G_store(proj_as_wkt(NULL, source_crs,
                                                     PJ_WKT2_LATEST, NULL));
                        if (srswkt && !*srswkt) {
                            G_free(srswkt);
                            srswkt = NULL;
                        }
                        proj_destroy(source_crs);
                    }
                }
                proj_destroy(obj);
            }
        }
#endif
    }

    G_get_window(&cellhead);

    /* Try to create raster data drivers. If failed - exit. */
    GDALDriverH hDriver = NULL, hMEMDriver = NULL;

    hDriver = GDALGetDriverByName(format->answer);
    if (hDriver == NULL)
        G_fatal_error(_("Unable to get <%s> driver"), format->answer);
    /* Does driver support GDALCreate ? */
    if (GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATE, NULL) == NULL) {
        /* If not - create MEM driver for intermediate dataset.
         * Check if raster can be created at all (with GDALCreateCopy) */
        if (GDALGetMetadataItem(hDriver, GDAL_DCAP_CREATECOPY, NULL)) {
            G_message(_("Driver <%s> does not support direct writing. "
                        "Using MEM driver for intermediate dataset."),
                      format->answer);
            hMEMDriver = GDALGetDriverByName("MEM");
            if (hMEMDriver == NULL)
                G_fatal_error(_("Unable to get in-memory raster driver"));
        }
        else
            G_fatal_error(_("Driver <%s> does not support creating rasters"),
                          format->answer);
    }

    /* Determine GDAL data type */
    GDALDataType datatype = GDT_Unknown;

    maptype = CELL_TYPE;

    if (type->answer) {
        /* reduce number of strcmps ... */
        if (type->answer[0] == 'B') {
            datatype = GDT_Byte;
            maptype = CELL_TYPE;
        }
        else if (type->answer[0] == 'I') {
            if (strcmp(type->answer, "Int16") == 0) {
                datatype = GDT_Int16;
                maptype = CELL_TYPE;
            }
            else if (strcmp(type->answer, "Int32") == 0) {
                datatype = GDT_Int32;
                maptype = CELL_TYPE;
            }
        }
        else if (type->answer[0] == 'U') {
            if (strcmp(type->answer, "UInt16") == 0) {
                datatype = GDT_UInt16;
                maptype = CELL_TYPE;
            }
            else if (strcmp(type->answer, "UInt32") == 0) {
                datatype = GDT_UInt32;
                maptype = DCELL_TYPE;
            }
        }
        else if (type->answer[0] == 'F') {
            if (strcmp(type->answer, "Float32") == 0) {
                datatype = GDT_Float32;
                maptype = FCELL_TYPE;
            }
            else if (strcmp(type->answer, "Float64") == 0) {
                datatype = GDT_Float64;
                maptype = DCELL_TYPE;
            }
        }
        else if (type->answer[0] == 'C') {
            if (strcmp(type->answer, "CInt16") == 0) {
                datatype = GDT_CInt16;
                maptype = CELL_TYPE;
            }
            else if (strcmp(type->answer, "CInt32") == 0) {
                datatype = GDT_CInt32;
                maptype = CELL_TYPE;
            }
            else if (strcmp(type->answer, "CFloat32") == 0) {
                datatype = GDT_CFloat32;
                maptype = FCELL_TYPE;
            }
            else if (strcmp(type->answer, "CFloat64") == 0) {
                datatype = GDT_CFloat64;
                maptype = DCELL_TYPE;
            }
        }
    }

    /* get min/max values */
    int band;

    bHaveMinMax = TRUE;
    export_min = TYPE_FLOAT64_MIN;
    export_max = TYPE_FLOAT64_MAX;
    for (band = 0; band < ref.nfiles; band++) {
        if (Rast_read_fp_range(ref.file[band].name, ref.file[band].mapset,
                               &sRange) == -1) {
            bHaveMinMax = FALSE;
            G_warning(_("Could not read data range of raster <%s>"),
                      ref.file[band].name);
        }
        else {
            Rast_get_fp_range_min_max(&sRange, &dfCellMin, &dfCellMax);
            if (band == 0) {
                export_min = dfCellMin;
                export_max = dfCellMax;
            }
            else {
                if (export_min > dfCellMin)
                    export_min = dfCellMin;
                if (export_max < dfCellMax)
                    export_max = dfCellMax;
            }
        }
        G_debug(3, "Range of <%s>: min: %f, max: %f", ref.file[band].name,
                dfCellMin, dfCellMax);
    }
    if (bHaveMinMax == FALSE) {
        export_min = TYPE_FLOAT64_MIN;
        export_max = TYPE_FLOAT64_MAX;
    }
    G_debug(3, "Total range: min: %f, max: %f", export_min, export_max);

    /* GDAL datatype not set by user, determine suitable datatype */
    if (datatype == GDT_Unknown) {
        /* Use raster data type from first GRASS raster in a group */
        maptype = Rast_map_type(ref.file[0].name, ref.file[0].mapset);
        if (maptype == FCELL_TYPE) {
            datatype = GDT_Float32;
        }
        else if (maptype == DCELL_TYPE) {
            datatype = GDT_Float64;
        }
        else {
            /* Special tricks for GeoTIFF color table support and such */
            if (export_min >= TYPE_BYTE_MIN && export_max <= TYPE_BYTE_MAX) {
                datatype = GDT_Byte;
            }
            else {
                if (export_min >= TYPE_UINT16_MIN &&
                    export_max <= TYPE_UINT16_MAX) {
                    datatype = GDT_UInt16;
                }
                else if (export_min >= TYPE_INT16_MIN &&
                         export_max <= TYPE_INT16_MAX) {
                    datatype = GDT_Int16;
                }
                else {
                    datatype = GDT_Int32; /* need to fine tune this more? */
                }
            }
        }
    }

    /* got a GDAL datatype, report to user */
    G_verbose_message(_("Exporting to GDAL data type: %s"),
                      GDALGetDataTypeName(datatype));

    G_debug(3, "Input map datatype=%s\n",
            (maptype == CELL_TYPE
                 ? "CELL"
                 : (maptype == DCELL_TYPE
                        ? "DCELL"
                        : (maptype == FCELL_TYPE ? "FCELL" : "??"))));

    /* if GDAL datatype set by user, do checks */
    if (type->answer) {

        /* Check if raster data range is outside of the range of
         * given GDAL datatype, not even overlapping */
        if (range_check(export_min, export_max, datatype))
            G_fatal_error(_(
                "Raster export would result in complete data loss, aborting."));

        /* Precision tests */
        for (band = 0; band < ref.nfiles; band++) {
            testmaptype =
                Rast_map_type(ref.file[band].name, ref.file[band].mapset);
            /* Exporting floating point rasters to some integer type ? */
            if ((testmaptype == FCELL_TYPE || testmaptype == DCELL_TYPE) &&
                (datatype == GDT_Byte || datatype == GDT_Int16 ||
                 datatype == GDT_UInt16 || datatype == GDT_Int32 ||
                 datatype == GDT_UInt32)) {
                G_warning(_("Precision loss: Raster map <%s> of type %s to be "
                            "exported as %s. "
                            "This can be avoided by using %s."),
                          ref.file[band].name,
                          (maptype == FCELL_TYPE ? "FCELL" : "DCELL"),
                          GDALGetDataTypeName(datatype),
                          (maptype == FCELL_TYPE ? "Float32" : "Float64"));
                retval = -1;
            }
            /* Exporting CELL with large values to GDT_Float32 ? Cap is 2^24 */
            if (testmaptype == CELL_TYPE && datatype == GDT_Float32 &&
                (dfCellMin < -16777216 || dfCellMax > 16777216)) {
                G_warning(
                    _("Precision loss: The range of <%s> can not be "
                      "accurately preserved with GDAL datatype Float32. "
                      "This can be avoided by exporting to Int32 or Float64."),
                    ref.file[band].name);
                retval = -1;
            }
            /* Exporting DCELL to GDT_Float32 ? */
            if (testmaptype == DCELL_TYPE && datatype == GDT_Float32) {
                G_warning(_("Precision loss: Float32 can not preserve the "
                            "DCELL precision of raster <%s>. "
                            "This can be avoided by using Float64"),
                          ref.file[band].name);
                retval = -1;
            }
        }
        if (retval == -1) {
            if (flag_f->answer)
                G_warning(_("Forcing raster export"));
            else
                G_fatal_error(
                    _("Raster export aborted. "
                      "To override data loss check, use the -%c flag"),
                    flag_f->key);
        }
    }

    /* Nodata value */
    double nodataval;
    int default_nodataval = 1;

    /* User-specified nodata-value ? */
    if (nodataopt->answer) {
        nodataval = atof(nodataopt->answer);
        default_nodataval = 0;
        /* Check if given nodata value can be represented by selected GDAL
         * datatype */
        if (nodataval_check(nodataval, datatype)) {
            G_fatal_error(_("Raster export aborted."));
        }
    }
    /* Set reasonable default nodata value */
    else {
        nodataval = set_default_nodata_value(datatype, export_min, export_max);
    }

    /* Offset value */
    double offsetval = 0; /* not defined */

    if (offsetopt->answer != NULL) {
        offsetval = atof(offsetopt->answer);
    }

    /* Scale value */
    double scaleval = 1; /* not defined */

    if (scaleopt->answer != NULL) {
        scaleval = atof(scaleopt->answer);
    }

    /* exact range and nodata checks for each band */
    G_message(_("Checking GDAL data type and nodata value..."));
    for (band = 0; band < ref.nfiles; band++) {
        if (ref.nfiles > 1) {
            G_verbose_message(
                _("Checking options for raster map <%s> (band %d)..."),
                G_fully_qualified_name(ref.file[band].name,
                                       ref.file[band].mapset),
                band + 1);
        }

        retval = exact_checks(datatype, ref.file[band].name,
                              ref.file[band].mapset, &cellhead, maptype,
                              nodataval, nodataopt->key, default_nodataval);

        /* nodata value is present in the data to be exported */
        if (retval == -1) {
            if (flag_f->answer)
                G_warning(_("Forcing raster export."));
            else
                G_fatal_error(_("Raster export aborted."));
        }
        /* data don't fit into range of GDAL datatype */
        else if (retval == -2) {
            G_fatal_error(_("Raster export aborted."));
        }
    }

    /* Create dataset for output with target driver or, if needed, with
     * in-memory driver */
    char **papszOptions = NULL;

    /* Parse dataset creation options */
    if (createopt->answer) {
        int i;

        i = 0;
        while (createopt->answers[i]) {
            papszOptions = CSLAddString(papszOptions, createopt->answers[i]);
            i++;
        }
    }

    GDALDatasetH hCurrDS = NULL, hMEMDS = NULL, hDstDS = NULL;

    if (hMEMDriver) {
        hMEMDS = GDALCreate(hMEMDriver, "", cellhead.cols, cellhead.rows,
                            ref.nfiles, datatype, papszOptions);
        if (hMEMDS == NULL)
            G_fatal_error(_("Unable to create dataset using "
                            "memory raster driver"));
        hCurrDS = hMEMDS;
    }
    else {
        hDstDS = GDALCreate(hDriver, output->answer, cellhead.cols,
                            cellhead.rows, ref.nfiles, datatype, papszOptions);
        if (hDstDS == NULL)
            G_fatal_error(_("Unable to create <%s> dataset using <%s> driver"),
                          output->answer, format->answer);
        hCurrDS = hDstDS;
    }

    /* Set Geo Transform  */
    double adfGeoTransform[6];

    adfGeoTransform[0] = cellhead.west;
    adfGeoTransform[1] = cellhead.ew_res;
    adfGeoTransform[2] = 0.0;
    adfGeoTransform[3] = cellhead.north;
    adfGeoTransform[4] = 0.0;
    adfGeoTransform[5] = -1 * cellhead.ns_res;
    if (GDALSetGeoTransform(hCurrDS, adfGeoTransform) >= CE_Failure)
        G_warning(_("Unable to set geo transform"));

    /* Set Projection  */
    CPLErr ret = CE_None;

    if (srswkt && *srswkt)
        ret = GDALSetProjection(hCurrDS, srswkt);
    if (!srswkt || ret == CE_Failure)
        G_warning(_("Unable to set projection"));

    /* Add metadata */
    AttachMetadata(hCurrDS, metaopt->answers);
    if (strcmp(format->answer, "GTiff") == 0) {
        char *pszKey;
        char *pszValue;

        pszKey = "TIFFTAG_SOFTWARE";
        G_asprintf(&pszValue, "GRASS GIS %s with GDAL %d.%d.%d",
                   GRASS_VERSION_NUMBER, GDAL_VERSION_MAJOR, GDAL_VERSION_MINOR,
                   GDAL_VERSION_REV);
        GDALSetMetadataItem(hCurrDS, pszKey, pszValue, NULL);
        G_free(pszValue);
    }
    else if (!flag_m->answer) {
        char *pszKey;
        char *pszValue;

        pszKey = "SOFTWARE";
        G_asprintf(&pszValue, "GRASS GIS %s with GDAL %d.%d.%d",
                   GRASS_VERSION_NUMBER, GDAL_VERSION_MAJOR, GDAL_VERSION_MINOR,
                   GDAL_VERSION_REV);
        GDALSetMetadataItem(hCurrDS, pszKey, pszValue, NULL);
        G_free(pszValue);
    }

    /* Export to GDAL raster */
    G_message(_("Exporting raster data to %s format..."), format->answer);
    for (band = 0; band < ref.nfiles; band++) {
        if (ref.nfiles > 1) {
            G_verbose_message(_("Exporting raster map <%s> (band %d)..."),
                              G_fully_qualified_name(ref.file[band].name,
                                                     ref.file[band].mapset),
                              band + 1);
        }

        retval = export_band(hCurrDS, band + 1, ref.file[band].name,
                             ref.file[band].mapset, &cellhead, maptype,
                             nodataval, flag_c->answer, flag_m->answer,
                             (nodataopt->answer != NULL), offsetval, scaleval);

        /* read/write error */
        if (retval == -1) {
            G_warning(_("Unable to export raster map <%s>"),
                      ref.file[band].name);
        }
        else if (flag_t->answer) {
            retval = export_attr(hCurrDS, band + 1, ref.file[band].name,
                                 ref.file[band].mapset, maptype);
        }
    }

    /* overviews */
    if (overviewopt->answer) {
        n_overviews = atoi(overviewopt->answer);
        if (n_overviews < 0 || n_overviews > 5) {
            G_warning(_("Number of overviews must be between 0 and 5"));
            n_overviews = 0;
        }
    }
    if (n_overviews) {
        int i, oi, *ol;

        G_message(_("Building overviews ..."));

        ol = G_malloc(n_overviews * sizeof(int));
        oi = 2;
        for (i = 0; i < n_overviews; i++) {
            ol[i] = oi;
            oi *= 2;
        }
        if (GDALBuildOverviews(hCurrDS, "NEAREST", n_overviews, ol, 0, NULL,
                               NULL, NULL) != CE_None) {
            G_warning(_("Unable to build overviews"));
        }
    }

    /* Finally create user requested raster format from memory raster
     * if in-memory driver was used */
    if (hMEMDS) {
        hDstDS = GDALCreateCopy(hDriver, output->answer, hMEMDS, FALSE,
                                papszOptions, NULL, NULL);
        if (hDstDS == NULL)
            G_fatal_error(
                _("Unable to create raster map <%s> using driver <%s>"),
                output->answer, format->answer);
    }

    GDALClose(hDstDS);

    if (hMEMDS)
        GDALClose(hMEMDS);

    CSLDestroy(papszOptions);

    G_done_msg("File <%s> created.", output->answer);
    exit(EXIT_SUCCESS);
}

int range_check(double min, double max, GDALDataType datatype)
{
    /* what accuracy to use to print min max for FLOAT32 and FLOAT64? %g enough?
     */

    switch (datatype) {
    case GDT_Byte:
        if (max < TYPE_BYTE_MIN || min > TYPE_BYTE_MAX) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_BYTE_MIN,
                      TYPE_BYTE_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_UInt16:
        if (max < TYPE_UINT16_MIN || min > TYPE_UINT16_MAX) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_UINT16_MIN,
                      TYPE_UINT16_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_Int16:
    case GDT_CInt16:
        if (max < TYPE_INT16_MIN || min > TYPE_INT16_MAX) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_INT16_MIN,
                      TYPE_INT16_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_Int32:
    case GDT_CInt32:
        if (max < TYPE_INT32_MIN || min > TYPE_INT32_MAX) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_INT32_MIN,
                      TYPE_INT32_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_UInt32:
        if (max < TYPE_UINT32_MIN || min > TYPE_UINT32_MAX) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %u - %u"),
                      GDALGetDataTypeName(datatype), TYPE_UINT32_MIN,
                      TYPE_UINT32_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_Float32:
    case GDT_CFloat32:
        if ((!CPLIsInf(max) && max < TYPE_FLOAT32_MIN) ||
            (!CPLIsInf(min) && min > TYPE_FLOAT32_MAX)) {
            G_warning(_("Selected GDAL datatype does not cover data range."));
            G_warning(_("GDAL datatype: %s, range: %.7g - %.7g"),
                      GDALGetDataTypeName(datatype), TYPE_FLOAT32_MIN,
                      TYPE_FLOAT32_MAX);
            G_warning(_("Range to be exported: %g - %g"), min, max);
            return 1;
        }
        else
            return 0;

    case GDT_Float64:
    case GDT_CFloat64:
        /* not needed because FLOAT64 should always cover the data range */
        return 0;

    default:
        return 0;
    }
}

int nodataval_check(double nodataval, GDALDataType datatype)
{

    switch (datatype) {
    case GDT_Byte:
        /* the additional cast to CELL is what happens in export_band()
         * accordingly below for the other GDAL types */
        if (nodataval != (double)(GByte)(CELL)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %d by "
                        "selected GDAL datatype."),
                      nodataval, (GByte)(CELL)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_BYTE_MIN,
                      TYPE_BYTE_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_UInt16:
        if (nodataval != (double)(GUInt16)(CELL)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %d by "
                        "selected GDAL datatype."),
                      nodataval, (GUInt16)(CELL)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %u - %u"),
                      GDALGetDataTypeName(datatype), TYPE_UINT16_MIN,
                      TYPE_UINT16_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_Int16:
    case GDT_CInt16:
        if (nodataval != (double)(GInt16)(CELL)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %d by "
                        "selected GDAL datatype."),
                      nodataval, (GInt16)(CELL)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_INT16_MIN,
                      TYPE_INT16_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_UInt32:
        if (nodataval != (double)(GUInt32)(DCELL)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %d by "
                        "selected GDAL datatype."),
                      nodataval, (GUInt32)(DCELL)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %u - %u"),
                      GDALGetDataTypeName(datatype), TYPE_UINT32_MIN,
                      TYPE_UINT32_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_Int32:
    case GDT_CInt32:
        /* GInt32 is equal to CELL, but that may change in the future */
        if (nodataval != (double)(GInt32)(CELL)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %d by "
                        "selected GDAL datatype."),
                      nodataval, (GInt32)(CELL)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %d - %d"),
                      GDALGetDataTypeName(datatype), TYPE_INT32_MIN,
                      TYPE_INT32_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_Float32:
    case GDT_CFloat32:
        if (!CPLIsNan(nodataval) && nodataval != (double)(float)nodataval) {
            G_warning(_("Mismatch between metadata nodata value and actual "
                        "nodata value in exported raster: "
                        "specified nodata value %g gets converted to %g by "
                        "selected GDAL datatype."),
                      nodataval, (float)nodataval);
            G_warning(_("GDAL datatype: %s, valid range: %g - %g"),
                      GDALGetDataTypeName(datatype), TYPE_FLOAT32_MIN,
                      TYPE_FLOAT32_MAX);
            return 1;
        }
        else
            return 0;

    case GDT_Float64:
    case GDT_CFloat64:
        /* not needed because FLOAT64 is equal to double */
        return 0;

    default:
        return 0;
    }
}

double set_default_nodata_value(GDALDataType datatype, double min, double max)
{
    switch (datatype) {
    case GDT_Byte:
        if (max < TYPE_BYTE_MAX)
            return (double)TYPE_BYTE_MAX;
        else if (min > TYPE_BYTE_MIN)
            return (double)TYPE_BYTE_MIN;
        else
            return (double)TYPE_BYTE_MAX;

    case GDT_UInt16:
        if (max < TYPE_UINT16_MAX)
            return (double)TYPE_UINT16_MAX;
        else if (min > TYPE_UINT16_MIN)
            return (double)TYPE_UINT16_MIN;
        else
            return (double)TYPE_UINT16_MAX;

    case GDT_Int16:
    case GDT_CInt16:
        if (min > TYPE_INT16_MIN)
            return (double)TYPE_INT16_MIN;
        else if (max < TYPE_INT16_MAX)
            return (double)TYPE_INT16_MAX;
        else
            return (double)TYPE_INT16_MIN;

    case GDT_UInt32:
        if (max < TYPE_UINT32_MAX)
            return (double)TYPE_UINT32_MAX;
        else if (min > TYPE_UINT32_MIN)
            return (double)TYPE_UINT32_MIN;
        else
            return (double)TYPE_UINT32_MAX;

    case GDT_Int32:
    case GDT_CInt32:
        if (min > TYPE_INT32_MIN)
            return (double)TYPE_INT32_MIN;
        else if (max < TYPE_INT32_MAX)
            return (double)TYPE_INT32_MAX;
        else
            return (double)TYPE_INT32_MIN;

    case GDT_Float32:
    case GDT_CFloat32:
        return NAN;

    case GDT_Float64:
    case GDT_CFloat64:
        return NAN;

    default:
        return 0;
    }
}
