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

#include "gdal_frmts.h"
#include "ogr_spatialref.h"
#include "rawdataset.h"


static const char * const apszISCE2GDALDatatypes[] = {
    "BYTE:Byte",
    "CHAR:Byte",
    "SHORT:Int16",
    "INT:Int32",
    "LONG:Int64",
    "FLOAT:Float32",
    "DOUBLE:Float64",
    "CBYTE:Unknown",
    "CCHAR:Unknown",
    "CSHORT:CInt16",
    "CINT:CInt32",
    "CLONG:CInt64",
    "CFLOAT:CFloat32",
    "CDOUBLE:CFloat64",
    nullptr };

static const char * const apszGDAL2ISCEDatatypes[] = {
    "Byte:BYTE",
    "Int16:SHORT",
    "Int32:INT",
    "Int64:LONG",
    "Float32:FLOAT",
    "Float64:DOUBLE",
    "CInt16:CSHORT",
    "CInt32:CINT",
    "CInt64:CLONG",
    "CFloat32:CFLOAT",
    "CFloat64:CDOUBLE",
    nullptr };

enum Scheme { BIL = 0, BIP = 1, BSQ = 2 };
static const char * const apszSchemeNames[] = { "BIL", "BIP", "BSQ", nullptr };

/************************************************************************/
/* ==================================================================== */
/*                              ISCEDataset                             */
/* ==================================================================== */
/************************************************************************/

class ISCERasterBand;

class ISCEDataset final: public RawDataset
{
    friend class ISCERasterBand;

    VSILFILE    *fpImage;

    char        *pszXMLFilename;

    enum Scheme eScheme;

    CPL_DISALLOW_COPY_ASSIGN(ISCEDataset)

  public:
    ISCEDataset();
    ~ISCEDataset() override;

    void FlushCache(bool bAtClosing) override;
    char **GetFileList() override;

    static int          Identify( GDALOpenInfo *poOpenInfo );
    static GDALDataset *Open( GDALOpenInfo *poOpenInfo );
    static GDALDataset *Open( GDALOpenInfo *poOpenInfo, bool bFileSizeCheck );
    static GDALDataset *Create( const char *pszFilename,
                                int nXSize, int nYSize, int nBandsIn,
                                GDALDataType eType, char **papszOptions );
};

/************************************************************************/
/* ==================================================================== */
/*                            ISCERasterBand                            */
/* ==================================================================== */
/************************************************************************/

class ISCERasterBand final: public RawRasterBand
{
        CPL_DISALLOW_COPY_ASSIGN(ISCERasterBand)

    public:
                ISCERasterBand( GDALDataset *poDS, int nBand, VSILFILE *fpRaw,
                                  vsi_l_offset nImgOffset, int nPixelOffset,
                                  int nLineOffset,
                                  GDALDataType eDataType, int bNativeOrder );
};

/************************************************************************/
/*                           getXMLFilename()                           */
/************************************************************************/

static CPLString getXMLFilename( GDALOpenInfo *poOpenInfo )
{
    CPLString osXMLFilename;

    if( poOpenInfo->fpL == nullptr )
        return CPLString();

    char **papszSiblingFiles = poOpenInfo->GetSiblingFiles();
    if ( papszSiblingFiles == nullptr )
    {
        osXMLFilename = CPLFormFilename( nullptr, poOpenInfo->pszFilename,
                                         "xml" );
        VSIStatBufL psXMLStatBuf;
        if ( VSIStatL( osXMLFilename, &psXMLStatBuf ) != 0 )
        {
            osXMLFilename = "";
        }
    }
    else
    {
        /* ------------------------------------------------------------ */
        /*      We need to tear apart the filename to form a .xml       */
        /*      filename.                                               */
        /* ------------------------------------------------------------ */
        const CPLString osPath = CPLGetPath( poOpenInfo->pszFilename );
        const CPLString osName = CPLGetFilename( poOpenInfo->pszFilename );

        const int iFile =
            CSLFindString( papszSiblingFiles,
                           CPLFormFilename( nullptr, osName, "xml" ) );
        if( iFile >= 0 )
        {
            osXMLFilename = CPLFormFilename( osPath,
                                             papszSiblingFiles[iFile],
                                             nullptr );
        }
    }

    return osXMLFilename;
}

/************************************************************************/
/*                             ISCEDataset()                            */
/************************************************************************/

ISCEDataset::ISCEDataset() :
    fpImage(nullptr),
    pszXMLFilename(nullptr),
    eScheme(BIL)
{}

/************************************************************************/
/*                            ~ISCEDataset()                          */
/************************************************************************/

ISCEDataset::~ISCEDataset( void )
{
    ISCEDataset::FlushCache(true);
    if ( fpImage != nullptr )
    {
        if( VSIFCloseL( fpImage ) != 0 )
        {
            CPLError(CE_Failure, CPLE_FileIO, "I/O error");
        }
    }
    CPLFree( pszXMLFilename );
}

/************************************************************************/
/*                            FlushCache()                              */
/************************************************************************/

void ISCEDataset::FlushCache(bool bAtClosing)
{
    RawDataset::FlushCache(bAtClosing);

    GDALRasterBand *band = (GetRasterCount() > 0) ? GetRasterBand(1) : nullptr;

    if ( eAccess == GA_ReadOnly || band == nullptr )
        return;

/* -------------------------------------------------------------------- */
/*      Recreate a XML doc with the dataset information.                */
/* -------------------------------------------------------------------- */
    char sBuf[64] = { '\0' };
    CPLXMLNode *psDocNode = CPLCreateXMLNode( nullptr, CXT_Element, "imageFile" );

    CPLXMLNode *psTmpNode
        = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "WIDTH" );
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nRasterXSize);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "LENGTH" );
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nRasterYSize);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "NUMBER_BANDS" );
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nBands);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    const char *sType = GDALGetDataTypeName( band->GetRasterDataType() );
    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "DATA_TYPE" );
    CPLCreateXMLElementAndValue(
        psTmpNode, "value",
        CSLFetchNameValue(
            const_cast<char **>(apszGDAL2ISCEDatatypes),
            sType ) );

    const char *pszScheme = apszSchemeNames[eScheme];
    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "SCHEME" );
    CPLCreateXMLElementAndValue( psTmpNode, "value", pszScheme );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "BYTE_ORDER" );
#ifdef CPL_LSB
    CPLCreateXMLElementAndValue( psTmpNode, "value", "l" );
#else
    CPLCreateXMLElementAndValue( psTmpNode, "value", "b" );
#endif

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "ACCESS_MODE" );
    CPLCreateXMLElementAndValue( psTmpNode, "value", "read" );

    const char *pszFilename = CPLGetBasename( pszXMLFilename );
    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "FILE_NAME" );
    CPLCreateXMLElementAndValue( psTmpNode, "value", pszFilename );

/* -------------------------------------------------------------------- */
/*      Then, add the ISCE domain metadata.                             */
/* -------------------------------------------------------------------- */
    char **papszISCEMetadata = GetMetadata( "ISCE" );
    for (int i = 0; i < CSLCount( papszISCEMetadata ); i++)
    {
        /* Get the tokens from the metadata item */
        char **papszTokens = CSLTokenizeString2( papszISCEMetadata[i],
                                                 "=",
                                                 CSLT_STRIPLEADSPACES
                                                 | CSLT_STRIPENDSPACES);
        if ( CSLCount( papszTokens ) != 2 )
        {
            CPLDebug( "ISCE",
                      "Line of header file could not be split at = into two"
                      " elements: %s",
                      papszISCEMetadata[i] );
            CSLDestroy( papszTokens );
            continue;
        }

        /* Don't write it out if it is one of the bits of metadata that is
         * written out elsewhere in this routine */
        if ( EQUAL( papszTokens[0], "WIDTH" )
              || EQUAL( papszTokens[0], "LENGTH" )
              || EQUAL( papszTokens[0], "NUMBER_BANDS" )
              || EQUAL( papszTokens[0], "DATA_TYPE" )
              || EQUAL( papszTokens[0], "SCHEME" )
              || EQUAL( papszTokens[0], "BYTE_ORDER" ) )
        {
            CSLDestroy( papszTokens );
            continue;
        }

        psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
        CPLAddXMLAttributeAndValue( psTmpNode, "name", papszTokens[0] );
        CPLCreateXMLElementAndValue( psTmpNode, "value", papszTokens[1] );

        CSLDestroy( papszTokens );
    }

/* -------------------------------------------------------------------- */
/*      Create the "Coordinate" component elements, possibly with       */
/*      georeferencing.                                                 */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psCoordinate1Node, *psCoordinate2Node;
    double adfGeoTransform[6];

    /* Coordinate 1 */
    psCoordinate1Node = CPLCreateXMLNode( psDocNode,
                                          CXT_Element,
                                          "component" );
    CPLAddXMLAttributeAndValue( psCoordinate1Node, "name", "Coordinate1" );
    CPLCreateXMLElementAndValue( psCoordinate1Node,
                                 "factorymodule",
                                 "isceobj.Image" );
    CPLCreateXMLElementAndValue( psCoordinate1Node,
                                 "factoryname",
                                 "createCoordinate" );
    CPLCreateXMLElementAndValue( psCoordinate1Node,
                                 "doc",
                                 "First coordinate of a 2D image (width)." );
    /* Property name */
    psTmpNode = CPLCreateXMLNode( psCoordinate1Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "name" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 "ImageCoordinate_name" );
    /* Property family */
    psTmpNode = CPLCreateXMLNode( psCoordinate1Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "family" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 "ImageCoordinate" );
    /* Property size */
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nRasterXSize);
    psTmpNode = CPLCreateXMLNode( psCoordinate1Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "size" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 sBuf );

    /* Coordinate 2 */
    psCoordinate2Node = CPLCreateXMLNode( psDocNode,
                                          CXT_Element,
                                          "component" );
    CPLAddXMLAttributeAndValue( psCoordinate2Node, "name", "Coordinate2" );
    CPLCreateXMLElementAndValue( psCoordinate2Node,
                                 "factorymodule",
                                 "isceobj.Image" );
    CPLCreateXMLElementAndValue( psCoordinate2Node,
                                 "factoryname",
                                 "createCoordinate" );
    /* Property name */
    psTmpNode = CPLCreateXMLNode( psCoordinate2Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "name" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 "ImageCoordinate_name" );
    /* Property family */
    psTmpNode = CPLCreateXMLNode( psCoordinate2Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "family" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 "ImageCoordinate" );
    /* Property size */
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nRasterYSize);
    psTmpNode = CPLCreateXMLNode( psCoordinate2Node,
                                  CXT_Element,
                                  "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "size" );
    CPLCreateXMLElementAndValue( psTmpNode,
                                 "value",
                                 sBuf );

    if ( GetGeoTransform( adfGeoTransform ) == CE_None )
    {
        if ( adfGeoTransform[2] != 0 || adfGeoTransform[4] != 0 )
        {
            CPLError( CE_Warning, CPLE_AppDefined,
                      "ISCE format do not support geotransform with "
                          "rotation, discarding info.");
        }
        else {
            CPLsnprintf( sBuf, sizeof(sBuf), "%g", adfGeoTransform[0] );
            psTmpNode = CPLCreateXMLNode( psCoordinate1Node,
                                          CXT_Element,
                                          "property" );
            CPLAddXMLAttributeAndValue( psTmpNode, "name", "startingValue" );
            CPLCreateXMLElementAndValue( psTmpNode,
                                         "value",
                                         sBuf );

            CPLsnprintf( sBuf, sizeof(sBuf), "%g", adfGeoTransform[1] );
            psTmpNode = CPLCreateXMLNode( psCoordinate1Node,
                                          CXT_Element,
                                          "property" );
            CPLAddXMLAttributeAndValue( psTmpNode, "name", "delta" );
            CPLCreateXMLElementAndValue( psTmpNode,
                                         "value",
                                         sBuf );

            CPLsnprintf( sBuf, sizeof(sBuf), "%g", adfGeoTransform[3] );
            psTmpNode = CPLCreateXMLNode( psCoordinate2Node,
                                          CXT_Element,
                                          "property" );
            CPLAddXMLAttributeAndValue( psTmpNode, "name", "startingValue" );
            CPLCreateXMLElementAndValue( psTmpNode,
                                         "value",
                                         sBuf );

            CPLsnprintf( sBuf, sizeof(sBuf), "%g", adfGeoTransform[5] );
            psTmpNode = CPLCreateXMLNode( psCoordinate2Node,
                                          CXT_Element,
                                          "property" );
            CPLAddXMLAttributeAndValue( psTmpNode, "name", "delta" );
            CPLCreateXMLElementAndValue( psTmpNode,
                                         "value",
                                         sBuf );
        }
    }

/* -------------------------------------------------------------------- */
/*      Write the XML file.                                             */
/* -------------------------------------------------------------------- */
    CPLSerializeXMLTreeToFile( psDocNode, pszXMLFilename );

/* -------------------------------------------------------------------- */
/*      Free the XML Doc.                                               */
/* -------------------------------------------------------------------- */
    CPLDestroyXMLNode( psDocNode );
}

/************************************************************************/
/*                            GetFileList()                             */
/************************************************************************/

char **ISCEDataset::GetFileList()
{
    /* Main data file, etc. */
    char **papszFileList = RawDataset::GetFileList();

    /* XML file. */
    papszFileList = CSLAddString( papszFileList, pszXMLFilename );

    return papszFileList;
}

/************************************************************************/
/*                             Identify()                               */
/************************************************************************/

int ISCEDataset::Identify( GDALOpenInfo *poOpenInfo )
{
/* -------------------------------------------------------------------- */
/*      TODO: This function is unusable now:                            */
/*          * we can't just check for the presence of a XML file        */
/*          * we cannot parse it to check basic tree (Identify() is     */
/*            supposed to be faster than this                           */
/*          * we could read only a few bytes and strstr() for           */
/*            "imageData", but what if a file is padded with comments   */
/*            and/or whitespaces? it would still be legit, but the      */
/*            driver would fail...                                      */
/* -------------------------------------------------------------------- */
/* -------------------------------------------------------------------- */
/*      Check if there is a .xml file                                   */
/* -------------------------------------------------------------------- */
    CPLString osXMLFilename = getXMLFilename( poOpenInfo );
    if ( osXMLFilename.empty() )
    {
        return false;
    }

    return true;
}

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

GDALDataset *ISCEDataset::Open( GDALOpenInfo *poOpenInfo )
{
    return Open(poOpenInfo, true);
}

GDALDataset *ISCEDataset::Open( GDALOpenInfo *poOpenInfo, bool bFileSizeCheck )
{
/* -------------------------------------------------------------------- */
/*      Confirm that the header is compatible with a ISCE dataset.    */
/* -------------------------------------------------------------------- */
    if ( !Identify(poOpenInfo) || poOpenInfo->fpL == nullptr )
    {
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Open and parse the .xml file                                    */
/* -------------------------------------------------------------------- */
    const CPLString osXMLFilename = getXMLFilename( poOpenInfo );
    CPLXMLNode *psNode = CPLParseXMLFile( osXMLFilename );
    if ( psNode == nullptr || CPLGetXMLNode( psNode, "=imageFile" ) == nullptr )
    {
        CPLDestroyXMLNode( psNode );
        return nullptr;
    }
    CPLXMLNode *psCur  = CPLGetXMLNode( psNode, "=imageFile" )->psChild;
    char **papszXmlProps = nullptr;
    while ( psCur != nullptr )
    {
        if ( EQUAL( psCur->pszValue, "property" ) )
        {
            /* Top-level property */
            const char *pszName = CPLGetXMLValue( psCur, "name", nullptr );
            const char *pszValue = CPLGetXMLValue( psCur, "value", nullptr );
            if ( pszName != nullptr && pszValue != nullptr)
            {
                papszXmlProps = CSLSetNameValue( papszXmlProps,
                                                 pszName, pszValue );
            }
        }
        else if ( EQUAL( psCur->pszValue, "component" ) )
        {
            /* "components" elements in ISCE store set of properties.   */
            /* For now, they are avoided as I am not sure the full      */
            /* scope of these. An exception is made for the ones named  */
            /* Coordinate1 and Coordinate2, because they may have the   */
            /* georeferencing information.                              */
            const char *pszCurName = CPLGetXMLValue( psCur, "name", nullptr );
            if ( pszCurName != nullptr
                && ( EQUAL( pszCurName, "Coordinate1" )
                    || EQUAL( pszCurName, "Coordinate2" ) ) )
            {
                /* We need two subproperties: startingValue and delta.  */
                /* To simplify parsing code, we will store them in      */
                /* papszXmlProps with the coordinate name prefixed to   */
                /* the property name.                                   */
                CPLXMLNode *psCur2 = psCur->psChild;
                while ( psCur2 != nullptr )
                {
                    if ( ! EQUAL( psCur2->pszValue, "property" ) )
                    {
                        psCur2 = psCur2->psNext;
                        continue; /* Skip non property elements */
                    }

                    const char
                       *pszCur2Name = CPLGetXMLValue( psCur2, "name", nullptr ),
                       *pszCur2Value = CPLGetXMLValue( psCur2, "value", nullptr );

                    if ( pszCur2Name == nullptr || pszCur2Value == nullptr )
                    {
                        psCur2 = psCur2->psNext;
                        continue; /* Skip malformatted elements */
                    }

                    if ( EQUAL( pszCur2Name, "startingValue" )
                        || EQUAL( pszCur2Name, "delta" ) )
                    {
                        char szPropName[32];
                        snprintf(szPropName, sizeof(szPropName), "%s%s",
                                 pszCurName, pszCur2Name);

                        papszXmlProps =
                            CSLSetNameValue( papszXmlProps,
                                             szPropName,
                                             pszCur2Value );
                    }
                    psCur2 = psCur2->psNext;
                }
            }
        }
        psCur = psCur->psNext;
    }

    CPLDestroyXMLNode( psNode );

/* -------------------------------------------------------------------- */
/*      Fetch required fields.                                          */
/* -------------------------------------------------------------------- */
    if ( CSLFetchNameValue( papszXmlProps, "WIDTH" ) == nullptr
        || CSLFetchNameValue( papszXmlProps, "LENGTH" ) == nullptr
        || CSLFetchNameValue( papszXmlProps, "NUMBER_BANDS" ) == nullptr
        || CSLFetchNameValue( papszXmlProps, "DATA_TYPE" ) == nullptr
        || CSLFetchNameValue( papszXmlProps, "SCHEME" ) == nullptr )
    {
        CSLDestroy( papszXmlProps );
        return nullptr;
    }
    const int nWidth = atoi( CSLFetchNameValue( papszXmlProps, "WIDTH" ) );
    const int nHeight = atoi( CSLFetchNameValue( papszXmlProps, "LENGTH" ) );
    const int nBands = atoi( CSLFetchNameValue( papszXmlProps, "NUMBER_BANDS" ) );

    if (!GDALCheckDatasetDimensions(nWidth, nHeight) ||
        !GDALCheckBandCount(nBands, FALSE))
    {
        CSLDestroy( papszXmlProps );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Update byte order info if image specify something.              */
/* -------------------------------------------------------------------- */
    bool bNativeOrder = true;

    const char *pszByteOrder = CSLFetchNameValue( papszXmlProps,
                                                    "BYTE_ORDER" );
    if ( pszByteOrder != nullptr )
    {
#ifdef CPL_LSB
        if ( EQUAL( pszByteOrder, "b" ) )
#else
        if ( EQUAL( pszByteOrder, "l" ) )
#endif
            bNativeOrder = false;
    }

/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    ISCEDataset *poDS = new ISCEDataset();
    poDS->nRasterXSize = nWidth;
    poDS->nRasterYSize = nHeight;
    poDS->eAccess = poOpenInfo->eAccess;
    poDS->pszXMLFilename = CPLStrdup( osXMLFilename.c_str() );
    poDS->fpImage = poOpenInfo->fpL;
    poOpenInfo->fpL = nullptr;

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */
    const char *pszDataType =
        CSLFetchNameValue( const_cast<char **>(apszISCE2GDALDatatypes),
                           CSLFetchNameValue( papszXmlProps, "DATA_TYPE" ) );
    if( pszDataType == nullptr )
    {
        delete poDS;
        CSLDestroy( papszXmlProps );
        return nullptr;
    }
    const GDALDataType eDataType = GDALGetDataTypeByName( pszDataType );
    const int nDTSize = GDALGetDataTypeSizeBytes(eDataType);
    if( nDTSize == 0 )
    {
        delete poDS;
        CSLDestroy( papszXmlProps );
        return nullptr;
    }
    const char *pszScheme = CSLFetchNameValue( papszXmlProps, "SCHEME" );
    int nPixelOffset = 0;
    int nLineOffset = 0;
    vsi_l_offset nBandOffset = 0;
    bool bIntOverflow = false;
    if( EQUAL( pszScheme, "BIL" ) )
    {
        poDS->eScheme = BIL;
        nPixelOffset = nDTSize;
        if( nWidth > INT_MAX / (nPixelOffset * nBands) )
            bIntOverflow = true;
        else
        {
            nLineOffset = nPixelOffset * nWidth * nBands;
            nBandOffset = nDTSize * static_cast<vsi_l_offset>(nWidth);
        }
    }
    else if( EQUAL( pszScheme, "BIP" ) )
    {
        poDS->eScheme = BIP;
        nPixelOffset = nDTSize * nBands;
        if( nWidth > INT_MAX / nPixelOffset )
            bIntOverflow = true;
        else
        {
            nLineOffset = nPixelOffset * nWidth;
            if( nBands > 1 && nLineOffset < INT_MAX / nBands )
            {
                // GDAL 2.1.0 had a value of nLineOffset that was equal to the theoretical
                // nLineOffset multiplied by nBands...
                VSIFSeekL( poDS->fpImage, 0, SEEK_END );
                const GUIntBig nWrongFileSize = nDTSize *
                    nWidth * (static_cast<GUIntBig>(nHeight - 1) *
                    nBands * nBands + nBands);
                if( VSIFTellL( poDS->fpImage ) == nWrongFileSize )
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                            "This file has been incorrectly generated by an older "
                            "GDAL version whose line offset computation was erroneous. "
                            "Taking that into account, but the file should be re-encoded ideally");
                    nLineOffset = nLineOffset * nBands;
                }
            }
            nBandOffset = nDTSize;
        }
    }
    else if ( EQUAL( pszScheme, "BSQ" ) )
    {
        poDS->eScheme = BSQ;
        nPixelOffset = nDTSize;
        if( nWidth > INT_MAX / nPixelOffset )
            bIntOverflow = true;
        else
        {
            nLineOffset = nPixelOffset * nWidth;
            nBandOffset = nLineOffset * static_cast<vsi_l_offset>(nHeight);
        }
    }
    else
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                  "Unknown scheme \"%s\" within ISCE raster.",
                  pszScheme );
        CSLDestroy( papszXmlProps );
        delete poDS;
        return nullptr;
    }

    if (bIntOverflow)
    {
        delete poDS;
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Int overflow occurred." );
        CSLDestroy( papszXmlProps );
        return nullptr;
    }

    if( bFileSizeCheck &&
        !RAWDatasetCheckMemoryUsage(
                        poDS->nRasterXSize, poDS->nRasterYSize, nBands,
                        nDTSize,
                        nPixelOffset, nLineOffset, 0, nBandOffset,
                        poDS->fpImage) )
    {
        delete poDS;
        CSLDestroy( papszXmlProps );
        return nullptr;
    }

    poDS->nBands = nBands;
    for (int b = 0; b < nBands; b++)
    {
        poDS->SetBand( b + 1,
                       new ISCERasterBand( poDS, b + 1, poDS->fpImage,
                                           nBandOffset * b,
                                           nPixelOffset, nLineOffset,
                                           eDataType, bNativeOrder ) );
    }

/* -------------------------------------------------------------------- */
/*      Interpret georeferencing, if present.                           */
/* -------------------------------------------------------------------- */
    if ( CSLFetchNameValue( papszXmlProps, "Coordinate1startingValue" ) != nullptr
         && CSLFetchNameValue( papszXmlProps, "Coordinate1delta" ) != nullptr
         && CSLFetchNameValue( papszXmlProps, "Coordinate2startingValue" ) != nullptr
         && CSLFetchNameValue( papszXmlProps, "Coordinate2delta" ) != nullptr )
    {
        double adfGeoTransform[6];
        adfGeoTransform[0] = CPLAtof( CSLFetchNameValue( papszXmlProps,
                                                         "Coordinate1startingValue" ) );
        adfGeoTransform[1] = CPLAtof( CSLFetchNameValue( papszXmlProps,
                                                         "Coordinate1delta" ) );
        adfGeoTransform[2] = 0.0;
        adfGeoTransform[3] = CPLAtof( CSLFetchNameValue( papszXmlProps,
                                                         "Coordinate2startingValue" ) );
        adfGeoTransform[4] = 0.0;
        adfGeoTransform[5] = CPLAtof( CSLFetchNameValue( papszXmlProps,
                                                               "Coordinate2delta" ) );
        poDS->SetGeoTransform( adfGeoTransform );

        /* ISCE format seems not to have a projection field, but uses   */
        /* WGS84.                                                       */
        poDS->SetProjection( SRS_WKT_WGS84_LAT_LONG );
    }

/* -------------------------------------------------------------------- */
/*      Set all the other header metadata into the ISCE domain          */
/* -------------------------------------------------------------------- */
    for( int i = 0; papszXmlProps != nullptr && papszXmlProps[i] != nullptr; i++ )
    {
        char **papszTokens =
            CSLTokenizeString2( papszXmlProps[i],
                                "=",
                                CSLT_STRIPLEADSPACES
                                | CSLT_STRIPENDSPACES);
        if ( CSLCount(papszTokens) < 2
              || EQUAL( papszTokens[0], "WIDTH" )
              || EQUAL( papszTokens[0], "LENGTH" )
              || EQUAL( papszTokens[0], "NUMBER_BANDS" )
              || EQUAL( papszTokens[0], "DATA_TYPE" )
              || EQUAL( papszTokens[0], "SCHEME" )
              || EQUAL( papszTokens[0], "BYTE_ORDER" )
              || EQUAL( papszTokens[0], "Coordinate1startingValue" )
              || EQUAL( papszTokens[0], "Coordinate1delta" )
              || EQUAL( papszTokens[0], "Coordinate2startingValue" )
              || EQUAL( papszTokens[0], "Coordinate2delta" ) )
        {
            CSLDestroy( papszTokens );
            continue;
        }
        poDS->SetMetadataItem(papszTokens[0], papszTokens[1], "ISCE");
        CSLDestroy( papszTokens );
    }

/* -------------------------------------------------------------------- */
/*      Free papszXmlProps                                              */
/* -------------------------------------------------------------------- */
    CSLDestroy( papszXmlProps );

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

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

    return poDS;
}

/************************************************************************/
/*                              Create()                                */
/************************************************************************/

GDALDataset *ISCEDataset::Create( const char *pszFilename,
                                  int nXSize, int nYSize, int nBandsIn,
                                  GDALDataType eType,
                                  char **papszOptions )
{
    const char *sType = GDALGetDataTypeName( eType );
    const char *pszScheme = CSLFetchNameValueDef( papszOptions,
                                                "SCHEME",
                                                "BIP" );

/* -------------------------------------------------------------------- */
/*      Try to create the file.                                         */
/* -------------------------------------------------------------------- */
    VSILFILE *fp = VSIFOpenL( pszFilename, "wb" );
    if( fp == nullptr )
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                  "Attempt to create file `%s' failed.",
                  pszFilename );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Just write out a couple of bytes to establish the binary        */
/*      file, and then close it.                                        */
/* -------------------------------------------------------------------- */
    CPL_IGNORE_RET_VAL(VSIFWriteL( "\0\0", 2, 1, fp ));
    CPL_IGNORE_RET_VAL(VSIFCloseL( fp ));

/* -------------------------------------------------------------------- */
/*      Create a minimal XML document.                                  */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psDocNode = CPLCreateXMLNode( nullptr, CXT_Element, "imageFile" );

    CPLXMLNode *psTmpNode
        = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "WIDTH" );
    char sBuf[64] = { '\0' };
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nXSize);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "LENGTH" );
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nYSize);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "NUMBER_BANDS" );
    CPLsnprintf(sBuf, sizeof(sBuf), "%d", nBandsIn);
    CPLCreateXMLElementAndValue( psTmpNode, "value", sBuf );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "DATA_TYPE" );
    CPLCreateXMLElementAndValue(
        psTmpNode, "value",
        CSLFetchNameValue(
            const_cast<char **>(apszGDAL2ISCEDatatypes),
            sType ));

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "SCHEME" );
    CPLCreateXMLElementAndValue( psTmpNode, "value", pszScheme );

    psTmpNode = CPLCreateXMLNode( psDocNode, CXT_Element, "property" );
    CPLAddXMLAttributeAndValue( psTmpNode, "name", "BYTE_ORDER" );
#ifdef CPL_LSB
    CPLCreateXMLElementAndValue( psTmpNode, "value", "l" );
#else
    CPLCreateXMLElementAndValue( psTmpNode, "value", "b" );
#endif

/* -------------------------------------------------------------------- */
/*      Write the XML file.                                             */
/* -------------------------------------------------------------------- */
    const char  *pszXMLFilename = CPLFormFilename( nullptr, pszFilename, "xml" );
    CPLSerializeXMLTreeToFile( psDocNode, pszXMLFilename );

/* -------------------------------------------------------------------- */
/*      Free the XML Doc.                                               */
/* -------------------------------------------------------------------- */
    CPLDestroyXMLNode( psDocNode );

    GDALOpenInfo oOpenInfo( pszFilename, GA_Update );
    return Open(&oOpenInfo, false);
}

/************************************************************************/
/*                          ISCERasterBand()                            */
/************************************************************************/

ISCERasterBand::ISCERasterBand( GDALDataset *poDSIn, int nBandIn, VSILFILE *fpRawIn,
                                vsi_l_offset nImgOffsetIn, int nPixelOffsetIn,
                                int nLineOffsetIn,
                                GDALDataType eDataTypeIn, int bNativeOrderIn ) :
    RawRasterBand( poDSIn, nBandIn, fpRawIn, nImgOffsetIn, nPixelOffsetIn,
                   nLineOffsetIn, eDataTypeIn, bNativeOrderIn,
                   RawRasterBand::OwnFP::NO )
{}

/************************************************************************/
/*                         GDALRegister_ISCE()                          */
/************************************************************************/

void GDALRegister_ISCE()
{
    if( GDALGetDriverByName( "ISCE" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "ISCE" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "ISCE raster" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/raster/isce.html" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES,
                               "Byte Int16 Int32 Int64 Float32"
                               " Float64 CInt16 CInt64 CFloat32 "
                               " CFloat64" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
                               "<CreationOptionList>"
                               "   <Option name='SCHEME' type='string-select'>"
                               "       <Value>BIP</Value>"
                               "       <Value>BIL</Value>"
                               "       <Value>BSQ</Value>"
                               "   </Option>"
                               "</CreationOptionList>" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->pfnOpen = ISCEDataset::Open;
    poDriver->pfnCreate = ISCEDataset::Create;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
