/******************************************************************************
 *
 * Project:  GTA read/write Driver
 * Purpose:  GDAL bindings over GTA library.
 * Author:   Martin Lambers, marlam@marlam.de
 *
 ******************************************************************************
 * Copyright (c) 2010, 2011, Martin Lambers <marlam@marlam.de>
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at mines-paris dot org>
 *
 * 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.
 ****************************************************************************/

/*
 * This driver supports reading and writing GTAs (Generic Tagged Arrays). See
 * http://www.nongnu.org/gta/ for details on this format.
 *
 * Supported Features:
 * - CreateCopy().
 * - GTA compression can be set.
 * - Raster data is updatable for uncompressed GTAs.
 * - All input/output is routed through the VSIF*L functions
 *   (GDAL_DCAP_VIRTUALIO is set to "YES").
 * - All kinds of metadata are supported (see tag list below).
 *
 * Limitations:
 * - Only uncompressed GTAs can be updated.
 * - Only raster data updates are possible; metadata cannot be changed.
 * - Color palettes are not supported.
 * - CInt16 is stored as gta::cfloat32, and CInt32 as gta::cfloat64.
 * - GDAL metadata is assumed to be in UTF-8 encoding, so that no conversion is
 *   necessary to store it in GTA tags. I'm not sure that this is correct, but
 *   since some metadata might not be representable in the local encoding (e.g.
 *   a Chinese description in latin1), using UTF-8 seems reasonable.
 *
 * The following could be implemented, but currently is not:
 * - Allow metadata updates by using a special GDAL/METADATA_BUFFER tag that
 *   contains a number of spaces as a placeholder for additional metadata, so
 *   that the header size on disk can be kept constant.
 * - Implement Create().
 * - Implement AddBand() for uncompressed GTAs. But this would be inefficient:
 *   the old data would need to be copied to a temporary file, and then copied
 *   back while extending it with the new band.
 * - When strict conversion is requested, store CInt16 in 2 x gta::int16 and
 *   CInt32 in 2 x gta::int32, and mark these components with special flags so
 *   that this is reverted when opening the GTA.
 * - Support color palettes by storing the palette in special tags.
 *
 * This driver supports the following standard GTA tags:
 * DESCRIPTION
 * INTERPRETATION
 * NO_DATA_VALUE
 * MIN_VALUE
 * MAX_VALUE
 * UNIT
 *
 * Additionally, the following tags are used for GDAL-specific metadata:
 * GDAL/PROJECTION      (WKT)
 * GDAL/GEO_TRANSFORM   (6 doubles)
 * GDAL/OFFSET          (1 double)
 * GDAL/SCALE           (1 double)
 * GDAL/GCP_PROJECTION  (WKT)
 * GDAL/GCP_COUNT       (1 int > 0)
 * GDAL/GCP%d           (5 doubles)
 * GDAL/GCP%d_INFO      (String)
 * GDAL/CATEGORY_COUNT  (1 int > 0)
 * GDAL/CATEGORY%d      (String)
 * GDAL/META/DEFAULT/%s (String)
 * GDAL/META/RCP/%s     (String)
 */

#include <limits.h>
#include "cpl_port.h" // for snprintf for MSVC
#include "gdal_frmts.h"
#include "gdal_pam.h"
#include "gta_headers.h"

CPL_CVSID("$Id: gtadataset.cpp 8e5eeb35bf76390e3134a4ea7076dab7d478ea0e 2018-11-14 22:55:13 +0100 Even Rouault $")

/************************************************************************/
/* Helper functions                                                     */
/************************************************************************/

static void ScanDoubles( const char *pszString, double *padfDoubles, int nCount )

{
    char *pszRemainingString = (char *)pszString;
    for( int i = 0; i < nCount; i++ )
    {
        padfDoubles[i] = 0.0;   // fallback value
        padfDoubles[i] = CPLStrtod( pszRemainingString, &pszRemainingString );
    }
}

static CPLString PrintDoubles( const double *padfDoubles, int nCount )

{
    CPLString oString;
    for( int i = 0; i < nCount; i++ )
    {
        oString.FormatC( padfDoubles[i], "%.16g" );
        if( i < nCount - 1)
        {
            oString += ' ';
        }
    }
    return oString;
}

/************************************************************************/
/* ==================================================================== */
/* GTA custom IO class using GDAL's IO abstraction layer                */
/* ==================================================================== */
/************************************************************************/

class GTAIO : public gta::custom_io
{
  private:
    VSILFILE *fp;

  public:
    GTAIO( ) throw ()
        : fp( nullptr )
    {
    }

    ~GTAIO( )
    {
        close( );
    }

    int open( const char *pszFilename, const char *pszMode )
    {
        fp = VSIFOpenL( pszFilename, pszMode );
        return fp == nullptr ? -1 : 0;
    }

    void close( )
    {
        if( fp != nullptr )
        {
            VSIFCloseL( fp );
            fp = nullptr;
        }
    }

    vsi_l_offset tell( )
    {
        return VSIFTellL( fp );
    }

    virtual size_t read(void *buffer, size_t size, bool *error) throw () override
    {
        size_t s;
        s = VSIFReadL( buffer, 1, size, fp );
        if( s != size )
        {
            errno = EIO;
            *error = true;
        }
        return size;
    }

    virtual size_t write(const void *buffer, size_t size, bool *error) throw () override
    {
        size_t s;
        s = VSIFWriteL( buffer, 1, size, fp );
        if( s != size )
        {
            errno = EIO;
            *error = true;
        }
        return size;
    }

    virtual bool seekable() throw () override
    {
        return true;
    }

    virtual void seek(intmax_t offset, int whence, bool *error) throw () override
    {
        int r;
        r = VSIFSeekL( fp, offset, whence );
        if( r != 0 )
        {
            errno = EIO;
            *error = true;
        }
    }
};

/************************************************************************/
/* ==================================================================== */
/*                              GTADataset                              */
/* ==================================================================== */
/************************************************************************/

class GTARasterBand;

class GTADataset : public GDALPamDataset
{
    friend class GTARasterBand;

  private:
    // GTA input/output via VSIF*L functions
    GTAIO       oGTAIO;
    // GTA information
    gta::header oHeader;
    vsi_l_offset DataOffset;
    // Metadata
    bool        bHaveGeoTransform;
    double      adfGeoTransform[6];
    int         nGCPs;
    char        *pszGCPProjection;
    GDAL_GCP    *pasGCPs;
    // Cached data block for block-based input/output
    int         nLastBlockXOff, nLastBlockYOff;
    void        *pBlock;

    // Block-based input/output of all bands at once. This is used
    // by the GTARasterBand input/output functions.
    CPLErr      ReadBlock( int, int );
    CPLErr      WriteBlock( );

  public:
                GTADataset();
                ~GTADataset();

    static GDALDataset *Open( GDALOpenInfo * );

    CPLErr      GetGeoTransform( double * padfTransform ) override;
    CPLErr      SetGeoTransform( double * padfTransform ) override;

    const char *_GetProjectionRef( ) override;
    const OGRSpatialReference* GetSpatialRef() const override {
        return GetSpatialRefFromOldGetProjectionRef();
    }
    CPLErr      _SetProjection( const char *pszProjection ) override;
    CPLErr SetSpatialRef(const OGRSpatialReference* poSRS) override {
        return OldSetProjectionFromSetSpatialRef(poSRS);
    }

    int         GetGCPCount( ) override;
    const char *_GetGCPProjection( ) override;
    const OGRSpatialReference* GetGCPSpatialRef() const override {
        return GetGCPSpatialRefFromOldGetGCPProjection();
    }
    const GDAL_GCP *GetGCPs( ) override;
    CPLErr      _SetGCPs( int, const GDAL_GCP *, const char * ) override;
    using GDALPamDataset::SetGCPs;
    CPLErr SetGCPs( int nGCPCount, const GDAL_GCP *pasGCPList,
                    const OGRSpatialReference* poSRS ) override {
        return OldSetGCPsFromNew(nGCPCount, pasGCPList, poSRS);
    }

};

/************************************************************************/
/* ==================================================================== */
/*                            GTARasterBand                             */
/* ==================================================================== */
/************************************************************************/

class GTARasterBand : public GDALPamRasterBand
{
    friend class GTADataset;
  private:
    // Size of the component represented by this band
    size_t      sComponentSize;
    // Offset of the component represented by this band inside a GTA element
    size_t      sComponentOffset;
    // StringList for category names
    char      **papszCategoryNames;
    // StringList for metadata
    char      **papszMetaData;

  public:
                GTARasterBand( GTADataset *, int );
                ~GTARasterBand( );

    CPLErr      IReadBlock( int, int, void * ) override;
    CPLErr      IWriteBlock( int, int, void * ) override;

    char      **GetCategoryNames( ) override;
    CPLErr      SetCategoryNames( char ** ) override;

    double      GetMinimum( int * ) override;
    double      GetMaximum( int * ) override;

    double      GetNoDataValue( int * ) override;
    CPLErr      SetNoDataValue( double ) override;
    double      GetOffset( int * ) override;
    CPLErr      SetOffset( double ) override;
    double      GetScale( int * ) override;
    CPLErr      SetScale( double ) override;
    const char *GetUnitType( ) override;
    CPLErr      SetUnitType( const char * ) override;
    GDALColorInterp GetColorInterpretation( ) override;
    CPLErr      SetColorInterpretation( GDALColorInterp ) override;
};

/************************************************************************/
/*                           GTARasterBand()                            */
/************************************************************************/

GTARasterBand::GTARasterBand( GTADataset *poDSIn, int nBandIn )

{
    this->poDS = poDSIn;
    this->nBand = nBandIn;

    // Data type
    switch( poDSIn->oHeader.component_type( nBand-1 ) )
    {
    case gta::int8:
        eDataType = GDT_Byte;
        SetMetadataItem("PIXELTYPE", "SIGNEDBYTE", "IMAGE_STRUCTURE");
        break;
    case gta::uint8:
        eDataType = GDT_Byte;
        break;
    case gta::int16:
        eDataType = GDT_Int16;
        break;
    case gta::uint16:
        eDataType = GDT_UInt16;
        break;
    case gta::int32:
        eDataType = GDT_Int32;
        break;
    case gta::uint32:
        eDataType = GDT_UInt32;
        break;
    case gta::float32:
        eDataType = GDT_Float32;
        break;
    case gta::float64:
        eDataType = GDT_Float64;
        break;
    case gta::cfloat32:
        eDataType = GDT_CFloat32;
        break;
    case gta::cfloat64:
        eDataType = GDT_CFloat64;
        break;
    default:
        // cannot happen because we checked this in GTADataset::Open()
        break;
    }

    // Block size
    nBlockXSize = poDS->GetRasterXSize();
    nBlockYSize = 1;

    // Component information
    sComponentSize = static_cast<size_t>(poDSIn->oHeader.component_size( nBand-1 ));
    sComponentOffset = 0;
    for( int i = 0; i < nBand-1; i++ )
    {
        sComponentOffset += poDSIn->oHeader.component_size( i );
    }

    // Metadata
    papszCategoryNames = nullptr;
    papszMetaData = nullptr;
    if( poDSIn->oHeader.component_taglist( nBand-1 ).get( "DESCRIPTION" ) )
    {
        SetDescription( poDSIn->oHeader.component_taglist( nBand-1 ).get( "DESCRIPTION" ) );
    }
    for( uintmax_t i = 0; i < poDSIn->oHeader.component_taglist( nBand-1 ).tags(); i++)
    {
        const char *pszTagName = poDSIn->oHeader.component_taglist( nBand-1 ).name( i );
        if( STARTS_WITH(pszTagName, "GDAL/META/") )
        {
            const char *pDomainEnd = strchr( pszTagName + 10, '/' );
            if( pDomainEnd && pDomainEnd - (pszTagName + 10) > 0 )
            {
                char *pszDomain = (char *)VSIMalloc( pDomainEnd - (pszTagName + 10) + 1 );
                if( !pszDomain )
                {
                    continue;
                }
                int j;
                for( j = 0; j < pDomainEnd - (pszTagName + 10); j++ )
                {
                    pszDomain[j] = pszTagName[10 + j];
                }
                pszDomain[j] = '\0';
                const char *pszName = pszTagName + 10 + j + 1;
                const char *pszValue = poDSIn->oHeader.component_taglist( nBand-1 ).value( i );
                SetMetadataItem( pszName, pszValue,
                        strcmp( pszDomain, "DEFAULT" ) == 0 ? nullptr : pszDomain );
                VSIFree( pszDomain );
            }
        }
    }
}

/************************************************************************/
/*                           ~GTARasterBand()                           */
/************************************************************************/

GTARasterBand::~GTARasterBand( )

{
    CSLDestroy( papszCategoryNames );
    CSLDestroy( papszMetaData );
}

/************************************************************************/
/*                             GetCategoryNames()                       */
/************************************************************************/

char **GTARasterBand::GetCategoryNames( )

{
    if( !papszCategoryNames )
    {
        GTADataset *poGDS = (GTADataset *) poDS;
        const char *pszCatCount = poGDS->oHeader.component_taglist( nBand-1 ).get( "GDAL/CATEGORY_COUNT" );
        int nCatCount = 0;
        if( pszCatCount )
        {
            nCatCount = atoi( pszCatCount );
        }
        if( nCatCount > 0 )
        {
            for( int i = 0; i < nCatCount; i++ )
            {
                const char *pszCatName = poGDS->oHeader.component_taglist( nBand-1 ).get(
                        CPLSPrintf( "GDAL/CATEGORY%d", i ) );
                papszCategoryNames = CSLAddString( papszCategoryNames, pszCatName ? pszCatName : "" );
            }
        }
    }
    return papszCategoryNames;
}

/************************************************************************/
/*                             SetCategoryName()                        */
/************************************************************************/

CPLErr GTARasterBand::SetCategoryNames( char ** )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             GetMinimum()                             */
/************************************************************************/

double GTARasterBand::GetMinimum( int *pbSuccess )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "MIN_VALUE" );
    if( pszValue )
    {
        if( pbSuccess )
            *pbSuccess = true;
        return CPLAtof( pszValue );
    }
    else
    {
        return GDALRasterBand::GetMinimum( pbSuccess );
    }
}

/************************************************************************/
/*                             GetMaximum()                             */
/************************************************************************/

double GTARasterBand::GetMaximum( int *pbSuccess  )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "MAX_VALUE" );
    if( pszValue )
    {
        if( pbSuccess )
            *pbSuccess = true;
        return CPLAtof( pszValue );
    }
    else
    {
        return GDALRasterBand::GetMaximum( pbSuccess );
    }
}

/************************************************************************/
/*                             GetNoDataValue()                         */
/************************************************************************/

double GTARasterBand::GetNoDataValue( int *pbSuccess )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "NO_DATA_VALUE" );
    if( pszValue )
    {
        if( pbSuccess )
            *pbSuccess = true;
        return CPLAtof( pszValue );
    }
    else
    {
        return GDALRasterBand::GetNoDataValue( pbSuccess );
    }
}

/************************************************************************/
/*                             SetNoDataValue()                         */
/************************************************************************/

CPLErr GTARasterBand::SetNoDataValue( double )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             GetOffset()                              */
/************************************************************************/

double GTARasterBand::GetOffset( int *pbSuccess )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "GDAL/OFFSET" );
    if( pszValue )
    {
        if( pbSuccess )
            *pbSuccess = true;
        return CPLAtof( pszValue );
    }
    else
    {
        return GDALRasterBand::GetOffset( pbSuccess );
    }
}

/************************************************************************/
/*                             SetOffset()                              */
/************************************************************************/

CPLErr GTARasterBand::SetOffset( double )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             GetScale()                               */
/************************************************************************/

double GTARasterBand::GetScale( int *pbSuccess )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "GDAL/SCALE" );
    if( pszValue )
    {
        if( pbSuccess )
            *pbSuccess = true;
        return CPLAtof( pszValue );
    }
    else
    {
        return GDALRasterBand::GetScale( pbSuccess );
    }
}

/************************************************************************/
/*                             SetScale()                               */
/************************************************************************/

CPLErr GTARasterBand::SetScale( double )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             GetUnitType()                            */
/************************************************************************/

const char *GTARasterBand::GetUnitType( )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszValue = poGDS->oHeader.component_taglist( nBand-1 ).get( "UNIT" );
    return pszValue ? pszValue : "";
}

/************************************************************************/
/*                             SetUnitType()                            */
/************************************************************************/

CPLErr GTARasterBand::SetUnitType( const char * )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             GetColorInterpretation()                 */
/************************************************************************/

GDALColorInterp GTARasterBand::GetColorInterpretation( )

{
    GTADataset *poGDS = (GTADataset *) poDS;
    const char *pszColorInterpretation =
        poGDS->oHeader.component_taglist( nBand-1 ).get(
                "INTERPRETATION" );
    if( pszColorInterpretation )
    {
        if( EQUAL( pszColorInterpretation, "GRAY" ) )
            return GCI_GrayIndex ;
        else if ( EQUAL( pszColorInterpretation, "RED" ) )
            return GCI_RedBand ;
        else if ( EQUAL( pszColorInterpretation, "GREEN" ) )
            return GCI_GreenBand ;
        else if ( EQUAL( pszColorInterpretation, "BLUE" ) )
            return GCI_BlueBand ;
        else if ( EQUAL( pszColorInterpretation, "ALPHA" ) )
            return GCI_AlphaBand ;
        else if ( EQUAL( pszColorInterpretation, "HSL/H" ) )
            return GCI_HueBand ;
        else if ( EQUAL( pszColorInterpretation, "HSL/S" ) )
            return GCI_SaturationBand ;
        else if ( EQUAL( pszColorInterpretation, "HSL/L" ) )
            return GCI_LightnessBand ;
        else if ( EQUAL( pszColorInterpretation, "CMYK/C" ) )
            return GCI_CyanBand ;
        else if ( EQUAL( pszColorInterpretation, "CMYK/M" ) )
            return GCI_MagentaBand ;
        else if ( EQUAL( pszColorInterpretation, "CMYK/Y" ) )
            return GCI_YellowBand ;
        else if ( EQUAL( pszColorInterpretation, "CMYK/K" ) )
            return GCI_BlackBand ;
        else if ( EQUAL( pszColorInterpretation, "YCBCR/Y" ) )
            return GCI_YCbCr_YBand;
        else if ( EQUAL( pszColorInterpretation, "YCBCR/CB" ) )
            return GCI_YCbCr_CbBand;
        else if ( EQUAL( pszColorInterpretation, "YCBCR/CR" ) )
            return GCI_YCbCr_CrBand;
    }
    return GCI_Undefined;
}

/************************************************************************/
/*                             SetColorInterpretation()                 */
/************************************************************************/

CPLErr GTARasterBand::SetColorInterpretation( GDALColorInterp )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GTARasterBand::IReadBlock( int nBlockXOff, int nBlockYOff,
                                  void * pImage )

{
    GTADataset *poGDS = (GTADataset *) poDS;

    // Read and cache block containing all bands at once
    if( poGDS->ReadBlock( nBlockXOff, nBlockYOff ) != CE_None )
    {
        return CE_Failure;
    }

    char *pBlock = (char *)poGDS->pBlock;
    if( poGDS->oHeader.compression() != gta::none )
    {
        // pBlock contains the complete data set. Add the offset into the
        // requested block. This assumes that nBlockYSize == 1 and
        // nBlockXSize == nRasterXSize.
        pBlock += nBlockYOff * nBlockXSize * poGDS->oHeader.element_size();
    }

    // Copy the data for this band from the cached block
    for( int i = 0; i < nBlockXSize; i++ )
    {
        char *pSrc = pBlock + i * poGDS->oHeader.element_size() + sComponentOffset;
        char *pDst = (char *) pImage + i * sComponentSize;
        memcpy( (void *) pDst, (void *) pSrc, sComponentSize );
    }

    return CE_None;
}

/************************************************************************/
/*                             IWriteBlock()                            */
/************************************************************************/

CPLErr GTARasterBand::IWriteBlock( int nBlockXOff, int nBlockYOff,
                                  void * pImage )

{
    GTADataset *poGDS = (GTADataset *) poDS;

    if( poGDS->oHeader.compression() != gta::none )
    {
        CPLError( CE_Warning, CPLE_NotSupported,
                "The GTA driver cannot update compressed GTAs.\n" );
        return CE_Failure;
    }

    // Read and cache block containing all bands at once
    if( poGDS->ReadBlock( nBlockXOff, nBlockYOff ) != CE_None )
    {
        return CE_Failure;
    }
    char *pBlock = (char *)poGDS->pBlock;

    // Copy the data for this band into the cached block
    for( int i = 0; i < nBlockXSize; i++ )
    {
        char *pSrc = (char *) pImage + i * sComponentSize;
        char *pDst = pBlock + i * poGDS->oHeader.element_size() + sComponentOffset;
        memcpy( (void *) pDst, (void *) pSrc, sComponentSize );
    }

    // Write the block that contains all bands at once
    if( poGDS->WriteBlock( ) != CE_None )
    {
        return CE_Failure;
    }

    return CE_None;
}

/************************************************************************/
/* ==================================================================== */
/*                              GTADataset                              */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            GTADataset()                              */
/************************************************************************/

GTADataset::GTADataset()

{
    // Initialize Metadata
    bHaveGeoTransform = false;
    nGCPs = 0;
    pszGCPProjection = nullptr;
    pasGCPs = nullptr;
    // Initialize block-based input/output
    nLastBlockXOff = -1;
    nLastBlockYOff = -1;
    pBlock = nullptr;
    DataOffset = 0;
    memset( adfGeoTransform, 0, sizeof(adfGeoTransform) );
}

/************************************************************************/
/*                            ~GTADataset()                             */
/************************************************************************/

GTADataset::~GTADataset()

{
    FlushCache();
    VSIFree( pszGCPProjection );
    for( int i = 0; i < nGCPs; i++ )
    {
        VSIFree( pasGCPs[i].pszId );
        VSIFree( pasGCPs[i].pszInfo );
    }
    VSIFree( pasGCPs );
    VSIFree( pBlock );
}

/************************************************************************/
/*                             ReadBlock()                              */
/************************************************************************/

CPLErr GTADataset::ReadBlock( int nBlockXOff, int nBlockYOff )

{
    /* Compressed data sets must be read into memory completely.
     * Uncompressed data sets are read block-wise. */

    if( oHeader.compression() != gta::none )
    {
        if( pBlock == nullptr )
        {
            if( oHeader.data_size() > (size_t)(-1)
                    || ( pBlock = VSI_MALLOC_VERBOSE( static_cast<size_t>(oHeader.data_size()) ) ) == nullptr )
            {
                CPLError( CE_Failure, CPLE_OutOfMemory,
                        "Cannot allocate buffer for the complete data set.\n"
                        "Try to uncompress the data set to allow block-wise "
                        "reading.\n" );
                return CE_Failure;
            }

            try
            {
                oHeader.read_data( oGTAIO, pBlock );
            }
            catch( gta::exception &e )
            {
                CPLError( CE_Failure, CPLE_FileIO, "GTA error: %s\n", e.what() );
                return CE_Failure;
            }
        }
    }
    else
    {
        // This has to be the same as in the RasterBand constructor!
        int nBlockXSize = GetRasterXSize();
        int nBlockYSize = 1;

        if( nLastBlockXOff == nBlockXOff && nLastBlockYOff == nBlockYOff )
            return CE_None;

        if( pBlock == nullptr )
        {
            pBlock = VSI_MALLOC2_VERBOSE( static_cast<size_t>(oHeader.element_size()), nBlockXSize );
            if( pBlock == nullptr )
            {
                return CE_Failure;
            }
        }

        try
        {
            uintmax_t lo[2] = { (uintmax_t)nBlockXOff * nBlockXSize, (uintmax_t)nBlockYOff * nBlockYSize};
            uintmax_t hi[2] = { lo[0] + nBlockXSize - 1, lo[1] + nBlockYSize - 1 };
            oHeader.read_block( oGTAIO, DataOffset, lo, hi, pBlock );
        }
        catch( gta::exception &e )
        {
            CPLError( CE_Failure, CPLE_FileIO, "GTA error: %s\n", e.what() );
            return CE_Failure;
        }

        nLastBlockXOff = nBlockXOff;
        nLastBlockYOff = nBlockYOff;
    }
    return CE_None;
}

/************************************************************************/
/*                             WriteBlock()                             */
/************************************************************************/

CPLErr GTADataset::WriteBlock( )

{
    // This has to be the same as in the RasterBand constructor!
    int nBlockXSize = GetRasterXSize();
    int nBlockYSize = 1;

    // Write the block (nLastBlockXOff, nLastBlockYOff) stored in pBlock.
    try
    {
        uintmax_t lo[2] = { (uintmax_t)nLastBlockXOff * nBlockXSize, (uintmax_t)nLastBlockYOff * nBlockYSize};
        uintmax_t hi[2] = { lo[0] + nBlockXSize - 1, lo[1] + nBlockYSize - 1 };
        oHeader.write_block( oGTAIO, DataOffset, lo, hi, pBlock );
    }
    catch( gta::exception &e )
    {
        CPLError( CE_Failure, CPLE_FileIO, "GTA error: %s\n", e.what() );
        return CE_Failure;
    }

    return CE_None;
}

/************************************************************************/
/*                          GetGeoTransform()                           */
/************************************************************************/

CPLErr GTADataset::GetGeoTransform( double * padfTransform )

{
    if( bHaveGeoTransform )
    {
        memcpy( padfTransform, adfGeoTransform, 6*sizeof(double) );
        return CE_None;
    }
    else
    {
        return CE_Failure;
    }
}

/************************************************************************/
/*                          SetGeoTransform()                           */
/************************************************************************/

CPLErr GTADataset::SetGeoTransform( double * )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                          GetProjectionRef()                          */
/************************************************************************/

const char *GTADataset::_GetProjectionRef()

{
    const char *p = oHeader.global_taglist().get("GDAL/PROJECTION");
    return p ? p : "";
}

/************************************************************************/
/*                          SetProjection()                             */
/************************************************************************/

CPLErr GTADataset::_SetProjection( const char * )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                          GetGCPCount()                               */
/************************************************************************/

int GTADataset::GetGCPCount( )

{
    return nGCPs;
}

/************************************************************************/
/*                          GetGCPProjection()                          */
/************************************************************************/

const char * GTADataset::_GetGCPProjection( )

{
    return pszGCPProjection ? pszGCPProjection : "";
}

/************************************************************************/
/*                          GetGCPs()                                   */
/************************************************************************/

const GDAL_GCP * GTADataset::GetGCPs( )

{
    return pasGCPs;
}

/************************************************************************/
/*                          SetGCPs()                                   */
/************************************************************************/

CPLErr GTADataset::_SetGCPs( int, const GDAL_GCP *, const char * )

{
    CPLError( CE_Warning, CPLE_NotSupported,
            "The GTA driver does not support metadata updates.\n" );
    return CE_Failure;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *GTADataset::Open( GDALOpenInfo * poOpenInfo )

{
    if( poOpenInfo->nHeaderBytes < 5 )
        return nullptr;

    if( !STARTS_WITH_CI((char *)poOpenInfo->pabyHeader, "GTA") )
        return nullptr;

/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    GTADataset *poDS = new GTADataset();

    if( poDS->oGTAIO.open( poOpenInfo->pszFilename,
            poOpenInfo->eAccess == GA_Update ? "r+" : "r" ) != 0 )
    {
        CPLError( CE_Failure, CPLE_OpenFailed, "Cannot open file.\n" );
        delete poDS;
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Read the header.                                                */
/* -------------------------------------------------------------------- */

    try
    {
        poDS->oHeader.read_from( poDS->oGTAIO );
    }
    catch( gta::exception &e )
    {
        CPLError( CE_Failure, CPLE_OpenFailed, "GTA error: %s\n", e.what() );
        delete poDS;
        return nullptr;
    }
    poDS->DataOffset = poDS->oGTAIO.tell();
    poDS->eAccess = poOpenInfo->eAccess;

    if( poDS->oHeader.compression() != gta::none
            && poOpenInfo->eAccess == GA_Update )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "The GTA driver does not support update access to compressed "
                  "data sets.\nUncompress the data set first.\n" );
        delete poDS;
        return nullptr;
    }

    if( poDS->oHeader.dimensions() != 2 )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                "The GTA driver does not support GTAs with %s than 2 "
                "dimensions.\n",
                poDS->oHeader.dimensions() < 2 ? "less" : "more" );
        delete poDS;
        return nullptr;
    }

    // We know the dimensions are > 0 (guaranteed by libgta), but they may be
    // unrepresentable in GDAL.
    if( poDS->oHeader.dimension_size(0) > INT_MAX
            || poDS->oHeader.dimension_size(1) > INT_MAX )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                "The GTA driver does not support the size of this data set.\n" );
        delete poDS;
        return nullptr;
    }
    poDS->nRasterXSize = static_cast<int>(poDS->oHeader.dimension_size(0));
    poDS->nRasterYSize = static_cast<int>(poDS->oHeader.dimension_size(1));

    // Check the number of bands (called components in GTA)
    if( poDS->oHeader.components() > INT_MAX-1
            || poDS->oHeader.element_size() > ((size_t)-1) )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                "The GTA driver does not support the number or size of bands "
                "in this data set.\n" );
        delete poDS;
        return nullptr;
    }
    poDS->nBands = static_cast<int>(poDS->oHeader.components());

    // Check the data types (called component types in GTA)
    for( int iBand = 0; iBand < poDS->nBands; iBand++ )
    {
        if( poDS->oHeader.component_type(iBand) != gta::uint8
                && poDS->oHeader.component_type(iBand) != gta::int8
                && poDS->oHeader.component_type(iBand) != gta::uint16
                && poDS->oHeader.component_type(iBand) != gta::int16
                && poDS->oHeader.component_type(iBand) != gta::uint32
                && poDS->oHeader.component_type(iBand) != gta::int32
                && poDS->oHeader.component_type(iBand) != gta::float32
                && poDS->oHeader.component_type(iBand) != gta::float64
                && poDS->oHeader.component_type(iBand) != gta::cfloat32
                && poDS->oHeader.component_type(iBand) != gta::cfloat64 )
        {
            CPLError( CE_Failure, CPLE_NotSupported,
                    "The GTA driver does not support some of the data types "
                    "used in this data set.\n" );
            delete poDS;
            return nullptr;
        }
    }

/* -------------------------------------------------------------------- */
/*      Read and set meta information.                                  */
/* -------------------------------------------------------------------- */

    if( poDS->oHeader.global_taglist().get("GDAL/GEO_TRANSFORM") )
    {
        poDS->bHaveGeoTransform = true;
        ScanDoubles( poDS->oHeader.global_taglist().get( "GDAL/GEO_TRANSFORM" ),
                poDS->adfGeoTransform, 6 );
    }
    else
    {
        poDS->bHaveGeoTransform = false;
    }

    if( poDS->oHeader.global_taglist().get("GDAL/GCP_PROJECTION") )
    {
        poDS->pszGCPProjection = VSIStrdup( poDS->oHeader.global_taglist().get("GDAL/GCP_PROJECTION") );
    }
    if( poDS->oHeader.global_taglist().get("GDAL/GCP_COUNT") )
    {
        poDS->nGCPs = atoi( poDS->oHeader.global_taglist().get("GDAL/GCP_COUNT") );
        if( poDS->nGCPs < 1 )
        {
            poDS->nGCPs = 0;
        }
        else
        {
            poDS->pasGCPs = (GDAL_GCP *)VSI_MALLOC2_VERBOSE( poDS->nGCPs, sizeof(GDAL_GCP) );
            if( poDS->pasGCPs == nullptr )
            {
                delete poDS;
                return nullptr;
            }
            for( int i = 0; i < poDS->nGCPs; i++ )
            {
                poDS->pasGCPs[i].pszInfo = nullptr;
                poDS->pasGCPs[i].dfGCPPixel = 0.0;
                poDS->pasGCPs[i].dfGCPLine = 0.0;
                poDS->pasGCPs[i].dfGCPX = 0.0;
                poDS->pasGCPs[i].dfGCPY = 0.0;
                poDS->pasGCPs[i].dfGCPZ = 0.0;
                poDS->pasGCPs[i].pszId = VSIStrdup( CPLSPrintf( "%d", i ) );
                char pszGCPTagName[64];
                char pszGCPInfoTagName[64];
                snprintf( pszGCPTagName, sizeof(pszGCPTagName), "GDAL/GCP%d", i );
                snprintf( pszGCPInfoTagName, sizeof(pszGCPTagName), "GDAL/GCP%d_INFO", i );
                if( poDS->oHeader.global_taglist().get(pszGCPInfoTagName) )
                {
                    poDS->pasGCPs[i].pszInfo = VSIStrdup( poDS->oHeader.global_taglist().get(pszGCPInfoTagName) );
                }
                else
                {
                    poDS->pasGCPs[i].pszInfo = VSIStrdup( "" );
                }
                if( poDS->oHeader.global_taglist().get(pszGCPTagName) )
                {
                    double adfTempDoubles[5];
                    ScanDoubles( poDS->oHeader.global_taglist().get(pszGCPTagName), adfTempDoubles, 5 );
                    poDS->pasGCPs[i].dfGCPPixel = adfTempDoubles[0];
                    poDS->pasGCPs[i].dfGCPLine = adfTempDoubles[1];
                    poDS->pasGCPs[i].dfGCPX = adfTempDoubles[2];
                    poDS->pasGCPs[i].dfGCPY = adfTempDoubles[3];
                    poDS->pasGCPs[i].dfGCPZ = adfTempDoubles[4];
                }
            }
        }
    }

    if( poDS->oHeader.global_taglist().get("DESCRIPTION") )
    {
        poDS->SetDescription( poDS->oHeader.global_taglist().get("DESCRIPTION") );
    }
    for( uintmax_t i = 0; i < poDS->oHeader.global_taglist().tags(); i++)
    {
        const char *pszTagName = poDS->oHeader.global_taglist().name( i );
        if( STARTS_WITH(pszTagName, "GDAL/META/") )
        {
            const char *pDomainEnd = strchr( pszTagName + 10, '/' );
            if( pDomainEnd && pDomainEnd - (pszTagName + 10) > 0 )
            {
                char *pszDomain = (char *)VSI_MALLOC_VERBOSE( pDomainEnd - (pszTagName + 10) + 1 );
                if( !pszDomain )
                {
                    delete poDS;
                    return nullptr;
                }
                int j;
                for( j = 0; j < pDomainEnd - (pszTagName + 10); j++ )
                {
                    pszDomain[j] = pszTagName[10 + j];
                }
                pszDomain[j] = '\0';
                const char *pszName = pszTagName + 10 + j + 1;
                const char *pszValue = poDS->oHeader.global_taglist().value( i );
                poDS->SetMetadataItem( pszName, pszValue,
                        strcmp( pszDomain, "DEFAULT" ) == 0 ? nullptr : pszDomain );
                VSIFree( pszDomain );
            }
        }
    }

    if( poDS->nBands > 0 )
    {
        poDS->SetMetadataItem( "INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE" );
    }
    if( poDS->oHeader.compression() == gta::bzip2 )
        poDS->SetMetadataItem( "COMPRESSION", "BZIP2", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::xz )
        poDS->SetMetadataItem( "COMPRESSION", "XZ", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib1 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB1", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib2 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB2", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib3 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB3", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib4 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB4", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib5 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB5", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib6 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB6", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib7 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB7", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib8 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB8", "IMAGE_STRUCTURE" );
    else if( poDS->oHeader.compression() == gta::zlib9 )
        poDS->SetMetadataItem( "COMPRESSION", "ZLIB9", "IMAGE_STRUCTURE" );

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */
    for( int iBand = 0; iBand < poDS->nBands; iBand++ )
    {
        poDS->SetBand( iBand+1, new GTARasterBand( poDS, iBand+1 ) );
    }

/* -------------------------------------------------------------------- */
/*      Initialize any PAM information.                                 */
/* -------------------------------------------------------------------- */
    poDS->SetDescription( poOpenInfo->pszFilename );
    poDS->TryLoadXML();

/* -------------------------------------------------------------------- */
/*      Check for overviews.                                            */
/* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize( poDS, poOpenInfo->pszFilename );

    return poDS;
}

/************************************************************************/
/*                             CreateCopy()                             */
/************************************************************************/

static GDALDataset*
GTACreateCopy( const char * pszFilename, GDALDataset *poSrcDS,
                int bStrict, char ** papszOptions,
                GDALProgressFunc pfnProgress, void * pProgressData )

{
    if( !pfnProgress( 0.0, nullptr, pProgressData ) )
        return nullptr;

/* -------------------------------------------------------------------- */
/*      Create a GTA header                                             */
/* -------------------------------------------------------------------- */

    gta::compression eGTACompression = gta::none;
    const char *pszCompressionValue = CSLFetchNameValue( papszOptions,
            "COMPRESS" );
    if( pszCompressionValue != nullptr )
    {
        if( EQUAL( pszCompressionValue, "NONE" ) )
            eGTACompression = gta::none;
        else if( EQUAL( pszCompressionValue, "BZIP2" ) )
            eGTACompression = gta::bzip2;
        else if( EQUAL( pszCompressionValue, "XZ" ) )
            eGTACompression = gta::xz;
        else if( EQUAL( pszCompressionValue, "ZLIB" ))
            eGTACompression = gta::zlib;
        else if( EQUAL( pszCompressionValue, "ZLIB1" ))
            eGTACompression = gta::zlib1;
        else if( EQUAL( pszCompressionValue, "ZLIB2" ))
            eGTACompression = gta::zlib2;
        else if( EQUAL( pszCompressionValue, "ZLIB3" ))
            eGTACompression = gta::zlib3;
        else if( EQUAL( pszCompressionValue, "ZLIB4" ))
            eGTACompression = gta::zlib4;
        else if( EQUAL( pszCompressionValue, "ZLIB5" ))
            eGTACompression = gta::zlib5;
        else if( EQUAL( pszCompressionValue, "ZLIB6" ))
            eGTACompression = gta::zlib6;
        else if( EQUAL( pszCompressionValue, "ZLIB7" ))
            eGTACompression = gta::zlib7;
        else if( EQUAL( pszCompressionValue, "ZLIB8" ))
            eGTACompression = gta::zlib8;
        else if( EQUAL( pszCompressionValue, "ZLIB9" ))
            eGTACompression = gta::zlib9;
        else
            CPLError( CE_Warning, CPLE_IllegalArg,
                      "COMPRESS=%s value not recognised, ignoring.",
                      pszCompressionValue );
    }

    gta::type *peGTATypes = (gta::type *)VSI_MALLOC2_VERBOSE( poSrcDS->GetRasterCount(), sizeof(gta::type) );
    if( peGTATypes == nullptr )
    {
        return nullptr;
    }
    for( int i = 0; i < poSrcDS->GetRasterCount(); i++ )
    {
        GDALRasterBand *poSrcBand = poSrcDS->GetRasterBand( i+1 );
        if( poSrcBand->GetColorInterpretation() == GCI_PaletteIndex )
        {
            CPLError( CE_Failure, CPLE_NotSupported,
                    "The GTA driver does not support color palettes.\n" );
            VSIFree( peGTATypes );
            return nullptr;
        }
        GDALDataType eDT = poSrcBand->GetRasterDataType();
        switch( eDT )
        {
        case GDT_Byte:
        {
            const char *pszPixelType = poSrcBand->GetMetadataItem("PIXELTYPE", "IMAGE_STRUCTURE");
            if (pszPixelType && EQUAL(pszPixelType, "SIGNEDBYTE"))
                peGTATypes[i] = gta::int8;
            else
                peGTATypes[i] = gta::uint8;
            break;
        }
        case GDT_UInt16:
            peGTATypes[i] = gta::uint16;
            break;
        case GDT_Int16:
            peGTATypes[i] = gta::int16;
            break;
        case GDT_UInt32:
            peGTATypes[i] = gta::uint32;
            break;
        case GDT_Int32:
            peGTATypes[i] = gta::int32;
            break;
        case GDT_Float32:
            peGTATypes[i] = gta::float32;
            break;
        case GDT_Float64:
            peGTATypes[i] = gta::float64;
            break;
        case GDT_CInt16:
            if( bStrict )
            {
                CPLError( CE_Failure, CPLE_NotSupported,
                        "The GTA driver does not support the CInt16 data "
                        "type.\n"
                        "(If no strict copy is required, the driver can "
                        "use CFloat32 instead.)\n" );
                VSIFree( peGTATypes );
                return nullptr;
            }
            peGTATypes[i] = gta::cfloat32;
            break;
        case GDT_CInt32:
            if( bStrict )
            {
                CPLError( CE_Failure, CPLE_NotSupported,
                        "The GTA driver does not support the CInt32 data "
                        "type.\n"
                        "(If no strict copy is required, the driver can "
                        "use CFloat64 instead.)\n" );
                VSIFree( peGTATypes );
                return nullptr;
            }
            peGTATypes[i] = gta::cfloat64;
            break;
        case GDT_CFloat32:
            peGTATypes[i] = gta::cfloat32;
            break;
        case GDT_CFloat64:
            peGTATypes[i] = gta::cfloat64;
            break;
        default:
            CPLError( CE_Failure, CPLE_NotSupported,
                    "The GTA driver does not support source data sets using "
                    "unknown data types.\n");
            VSIFree( peGTATypes );
            return nullptr;
        }
    }

    gta::header oHeader;
    try
    {
        oHeader.set_compression( eGTACompression );
        oHeader.set_dimensions( poSrcDS->GetRasterXSize(), poSrcDS->GetRasterYSize() );
        oHeader.set_components( poSrcDS->GetRasterCount(), peGTATypes );
        const char *pszDescription = poSrcDS->GetDescription();
        // Metadata from GDALMajorObject
        if( pszDescription && pszDescription[0] != '\0' )
        {
            oHeader.global_taglist().set( "DESCRIPTION", pszDescription );
        }
        const char *papszMetadataDomains[] = { nullptr /* default */, "RPC" };
        size_t nMetadataDomains = sizeof( papszMetadataDomains ) / sizeof( papszMetadataDomains[0] );
        for( size_t iDomain = 0; iDomain < nMetadataDomains; iDomain++ )
        {
            char **papszMetadata = poSrcDS->GetMetadata( papszMetadataDomains[iDomain] );
            if( papszMetadata )
            {
                for( int i = 0; papszMetadata[i]; i++ )
                {
                    char *pEqualSign = strchr( papszMetadata[i], '=' );
                    if( pEqualSign && pEqualSign - papszMetadata[i] > 0 )
                    {
                        *pEqualSign = '\0';
                        oHeader.global_taglist().set(
                                CPLSPrintf( "GDAL/META/%s/%s",
                                    papszMetadataDomains[iDomain] ? papszMetadataDomains[iDomain] : "DEFAULT",
                                    papszMetadata[i] ),
                                pEqualSign + 1 );
                        *pEqualSign = '=';
                    }
                }
            }
        }
        // Projection and transformation
        const char *pszWKT = poSrcDS->GetProjectionRef();
        if( pszWKT && pszWKT[0] != '\0' )
        {
            oHeader.global_taglist().set( "GDAL/PROJECTION", pszWKT );
        }
        double adfTransform[6];
        if( poSrcDS->GetGeoTransform( adfTransform ) == CE_None )
        {
            oHeader.global_taglist().set( "GDAL/GEO_TRANSFORM",
                    PrintDoubles( adfTransform, 6 ).c_str() );
        }
        // GCPs
        if( poSrcDS->GetGCPCount() > 0 )
        {
            oHeader.global_taglist().set( "GDAL/GCP_COUNT", CPLSPrintf( "%d", poSrcDS->GetGCPCount() ) );
            oHeader.global_taglist().set( "GDAL/GCP_PROJECTION", poSrcDS->GetGCPProjection() );
            const GDAL_GCP *pasGCPs = poSrcDS->GetGCPs();
            for( int i = 0; i < poSrcDS->GetGCPCount(); i++ )
            {
                char pszGCPTagName[64];
                char pszGCPInfoTagName[64];
                snprintf( pszGCPTagName, sizeof(pszGCPTagName), "GDAL/GCP%d", i );
                snprintf( pszGCPInfoTagName, sizeof(pszGCPInfoTagName), "GDAL/GCP%d_INFO", i );
                if( pasGCPs[i].pszInfo && pasGCPs[i].pszInfo[0] != '\0' )
                {
                    oHeader.global_taglist().set( pszGCPInfoTagName, pasGCPs[i].pszInfo );
                }
                double adfTempDoubles[5];
                adfTempDoubles[0] = pasGCPs[i].dfGCPPixel;
                adfTempDoubles[1] = pasGCPs[i].dfGCPLine;
                adfTempDoubles[2] = pasGCPs[i].dfGCPX;
                adfTempDoubles[3] = pasGCPs[i].dfGCPY;
                adfTempDoubles[4] = pasGCPs[i].dfGCPZ;
                oHeader.global_taglist().set( pszGCPTagName, PrintDoubles( adfTempDoubles, 5 ).c_str() );
            }
        }
        // Now the bands
        for( int iBand = 0; iBand < poSrcDS->GetRasterCount(); iBand++ )
        {
            GDALRasterBand *poSrcBand = poSrcDS->GetRasterBand( iBand+1 );
            // Metadata from GDALMajorObject
            const char *pszBandDescription = poSrcBand->GetDescription();
            if( pszBandDescription && pszBandDescription[0] != '\0' )
            {
                oHeader.component_taglist( iBand ).set( "DESCRIPTION", pszBandDescription );
            }
            for( size_t iDomain = 0; iDomain < nMetadataDomains; iDomain++ )
            {
                char **papszBandMetadata = poSrcBand->GetMetadata( papszMetadataDomains[iDomain] );
                if( papszBandMetadata )
                {
                    for( int i = 0; papszBandMetadata[i]; i++ )
                    {
                        char *pEqualSign = strchr( papszBandMetadata[i], '=' );
                        if( pEqualSign && pEqualSign - papszBandMetadata[i] > 0 )
                        {
                            *pEqualSign = '\0';
                            oHeader.component_taglist( iBand ).set(
                                    CPLSPrintf( "GDAL/META/%s/%s",
                                        papszMetadataDomains[iDomain] ? papszMetadataDomains[iDomain] : "DEFAULT",
                                        papszBandMetadata[i] ),
                                    pEqualSign + 1 );
                            *pEqualSign = '=';
                        }
                    }
                }
            }
            // Category names
            char **papszCategoryNames = poSrcBand->GetCategoryNames( );
            if( papszCategoryNames )
            {
                int i;
                for( i = 0; papszCategoryNames[i]; i++ )
                {
                    oHeader.component_taglist( iBand ).set(
                            CPLSPrintf( "GDAL/CATEGORY%d", i ),
                            papszCategoryNames[i] );
                }
                oHeader.component_taglist( iBand ).set(
                        "GDAL/CATEGORY_COUNT", CPLSPrintf( "%d", i ) );
            }
            // No data value
            int bHaveNoDataValue;
            double dfNoDataValue;
            dfNoDataValue = poSrcBand->GetNoDataValue( &bHaveNoDataValue );
            if( bHaveNoDataValue )
                oHeader.component_taglist( iBand ).set( "NO_DATA_VALUE",
                        PrintDoubles( &dfNoDataValue, 1 ).c_str() );
            // Min/max values
            int bHaveMinValue;
            double dfMinValue;
            dfMinValue = poSrcBand->GetMinimum( &bHaveMinValue );
            if( bHaveMinValue )
                oHeader.component_taglist( iBand ).set( "MIN_VALUE",
                        PrintDoubles( &dfMinValue, 1 ).c_str() );
            int bHaveMaxValue;
            double dfMaxValue;
            dfMaxValue = poSrcBand->GetMaximum( &bHaveMaxValue );
            if( bHaveMaxValue )
                oHeader.component_taglist( iBand ).set( "MAX_VALUE",
                        PrintDoubles( &dfMaxValue, 1 ).c_str() );
            // Offset/scale values
            int bHaveOffsetValue;
            double dfOffsetValue;
            dfOffsetValue = poSrcBand->GetOffset( &bHaveOffsetValue );
            if( bHaveOffsetValue )
                oHeader.component_taglist( iBand ).set( "GDAL/OFFSET",
                        PrintDoubles( &dfOffsetValue, 1 ).c_str() );
            int bHaveScaleValue;
            double dfScaleValue;
            dfScaleValue = poSrcBand->GetScale( &bHaveScaleValue );
            if( bHaveScaleValue )
                oHeader.component_taglist( iBand ).set( "GDAL/SCALE",
                        PrintDoubles( &dfScaleValue, 1 ).c_str() );
            // Unit
            const char *pszUnit = poSrcBand->GetUnitType( );
            if( pszUnit != nullptr && pszUnit[0] != '\0' )
                oHeader.component_taglist( iBand ).set( "UNIT", pszUnit );
            // Color interpretation
            GDALColorInterp eCI = poSrcBand->GetColorInterpretation();
            if( eCI == GCI_GrayIndex )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "GRAY" );
            else if( eCI == GCI_RedBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "RED" );
            else if( eCI == GCI_GreenBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "GREEN" );
            else if( eCI == GCI_BlueBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "BLUE" );
            else if( eCI == GCI_AlphaBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "ALPHA" );
            else if( eCI == GCI_HueBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "HSL/H" );
            else if( eCI == GCI_SaturationBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "HSL/S" );
            else if( eCI == GCI_LightnessBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "HSL/L" );
            else if( eCI == GCI_CyanBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "CMYK/C" );
            else if( eCI == GCI_MagentaBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "CMYK/M" );
            else if( eCI == GCI_YellowBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "CMYK/Y" );
            else if( eCI == GCI_BlackBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "CMYK/K" );
            else if( eCI == GCI_YCbCr_YBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "YCBCR/Y" );
            else if( eCI == GCI_YCbCr_CbBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "YCBCR/CB" );
            else if( eCI == GCI_YCbCr_CrBand )
                oHeader.component_taglist( iBand ).set( "INTERPRETATION", "YCBCR/CR" );
        }
    }
    catch( gta::exception &e )
    {
        CPLError( CE_Failure, CPLE_NotSupported, "GTA error: %s\n", e.what() );
        VSIFree( peGTATypes );
        return nullptr;
    }
    VSIFree( peGTATypes );

/* -------------------------------------------------------------------- */
/*      Write header and data to the file                               */
/* -------------------------------------------------------------------- */

    GTAIO oGTAIO;
    if( oGTAIO.open( pszFilename, "w" ) != 0 )
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                "Cannot create GTA file %s.\n", pszFilename );
        return nullptr;
    }

    void *pLine = VSI_MALLOC2_VERBOSE( static_cast<size_t>(oHeader.element_size()), static_cast<size_t>(oHeader.dimension_size(0)) );
    if( pLine == nullptr )
    {
        VSIFree( pLine );
        return nullptr;
    }

    try
    {
        // Write header
        oHeader.write_to( oGTAIO );
        // Write data line by line
        gta::io_state oGTAIOState;
        for( int iLine = 0; iLine < poSrcDS->GetRasterYSize(); iLine++ )
        {
            size_t nComponentOffset = 0;
            for( int iBand = 0; iBand < poSrcDS->GetRasterCount(); iBand++ )
            {
                GDALRasterBand *poSrcBand = poSrcDS->GetRasterBand( iBand+1 );
                GDALDataType eDT = poSrcBand->GetRasterDataType();
                if( eDT == GDT_CInt16 )
                {
                    eDT = GDT_CFloat32;
                }
                else if( eDT == GDT_CInt32 )
                {
                    eDT = GDT_CFloat64;
                }
                char *pDst = (char *)pLine + nComponentOffset;
                CPLErr eErr = poSrcBand->RasterIO( GF_Read, 0, iLine,
                        poSrcDS->GetRasterXSize(), 1,
                        pDst, poSrcDS->GetRasterXSize(), 1, eDT,
                        oHeader.element_size(), 0, nullptr );
                if( eErr != CE_None )
                {
                    CPLError( CE_Failure, CPLE_FileIO, "Cannot read source data set.\n" );
                    VSIFree( pLine );
                    return nullptr;
                }
                nComponentOffset += oHeader.component_size( iBand );
            }
            oHeader.write_elements( oGTAIOState, oGTAIO, poSrcDS->GetRasterXSize(), pLine );
            if( !pfnProgress( (iLine+1) / (double) poSrcDS->GetRasterYSize(),
                        nullptr, pProgressData ) )
            {
                CPLError( CE_Failure, CPLE_UserInterrupt, "User terminated CreateCopy()" );
                VSIFree( pLine );
                return nullptr;
            }
        }
    }
    catch( gta::exception &e )
    {
        CPLError( CE_Failure, CPLE_FileIO, "GTA write error: %s\n", e.what() );
        VSIFree( pLine );
        return nullptr;
    }
    VSIFree( pLine );

    oGTAIO.close();

/* -------------------------------------------------------------------- */
/*      Re-open dataset, and copy any auxiliary pam information.         */
/* -------------------------------------------------------------------- */

    GTADataset *poDS = (GTADataset *) GDALOpen( pszFilename,
            eGTACompression == gta::none ? GA_Update : GA_ReadOnly );

    if( poDS )
        poDS->CloneInfo( poSrcDS, GCIF_PAM_DEFAULT );

    return poDS;
}

/************************************************************************/
/*                          GDALRegister_GTA()                          */
/************************************************************************/

void GDALRegister_GTA()

{
    if( GDALGetDriverByName( "GTA" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "GTA" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                               "Generic Tagged Arrays (.gta)" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "frmt_gta.html" );
    poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "gta" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES,
                               "Byte UInt16 Int16 UInt32 Int32 Float32 Float64 "
                               "CInt16 CInt32 CFloat32 CFloat64" );
    poDriver->SetMetadataItem(
        GDAL_DMD_CREATIONOPTIONLIST,
        "<CreationOptionList>"
        "  <Option name='COMPRESS' type='string-select'>"
        "    <Value>NONE</Value>"
        "    <Value>BZIP2</Value>"
        "    <Value>XZ</Value>"
        "    <Value>ZLIB</Value>"
        "    <Value>ZLIB1</Value>"
        "    <Value>ZLIB2</Value>"
        "    <Value>ZLIB3</Value>"
        "    <Value>ZLIB4</Value>"
        "    <Value>ZLIB5</Value>"
        "    <Value>ZLIB6</Value>"
        "    <Value>ZLIB7</Value>"
        "    <Value>ZLIB8</Value>"
        "    <Value>ZLIB9</Value>"
        "  </Option>"
        "</CreationOptionList>" );

    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->pfnOpen = GTADataset::Open;
    poDriver->pfnCreateCopy = GTACreateCopy;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
