/******************************************************************************
 *
 * Project:  GXF Reader
 * Purpose:  GDAL binding for GXF reader.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1998, Frank Warmerdam
 * Copyright (c) 2008-2012, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "gdal_frmts.h"
#include "gdal_pam.h"
#include "gdal_driver.h"
#include "gdal_drivermanager.h"
#include "gdal_openinfo.h"
#include "gdal_cpp_functions.h"
#include "gxfopen.h"

/************************************************************************/
/* ==================================================================== */
/*                              GXFDataset                              */
/* ==================================================================== */
/************************************************************************/

class GXFRasterBand;

class GXFDataset final : public GDALPamDataset
{
    friend class GXFRasterBand;

    GXFHandle hGXF;

    OGRSpatialReference m_oSRS{};
    double dfNoDataValue;
    GDALDataType eDataType;

  public:
    GXFDataset();
    ~GXFDataset() override;

    static GDALDataset *Open(GDALOpenInfo *);

    CPLErr GetGeoTransform(GDALGeoTransform &gt) const override;
    const OGRSpatialReference *GetSpatialRef() const override;
};

/************************************************************************/
/* ==================================================================== */
/*                            GXFRasterBand                             */
/* ==================================================================== */
/************************************************************************/

class GXFRasterBand final : public GDALPamRasterBand
{
    friend class GXFDataset;

  public:
    GXFRasterBand(GXFDataset *, int);
    double GetNoDataValue(int *bGotNoDataValue) override;

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

/************************************************************************/
/*                           GXFRasterBand()                            */
/************************************************************************/

GXFRasterBand::GXFRasterBand(GXFDataset *poDSIn, int nBandIn)

{
    poDS = poDSIn;
    nBand = nBandIn;

    eDataType = poDSIn->eDataType;

    nBlockXSize = poDS->GetRasterXSize();
    nBlockYSize = 1;
}

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

double GXFRasterBand::GetNoDataValue(int *bGotNoDataValue)

{
    GXFDataset *poGXF_DS = cpl::down_cast<GXFDataset *>(poDS);
    if (bGotNoDataValue)
        *bGotNoDataValue = (fabs(poGXF_DS->dfNoDataValue - -1e12) > .1);
    if (eDataType == GDT_Float32)
        return (double)(float)poGXF_DS->dfNoDataValue;

    return poGXF_DS->dfNoDataValue;
}

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

CPLErr GXFRasterBand::IReadBlock(CPL_UNUSED int nBlockXOff, int nBlockYOff,
                                 void *pImage)
{
    GXFDataset *const poGXF_DS = cpl::down_cast<GXFDataset *>(poDS);

    if (eDataType == GDT_Float32)
    {
        double *padfBuffer = (double *)VSIMalloc2(sizeof(double), nBlockXSize);
        if (padfBuffer == nullptr)
            return CE_Failure;
        const CPLErr eErr =
            GXFGetScanline(poGXF_DS->hGXF, nBlockYOff, padfBuffer);

        float *pafBuffer = (float *)pImage;
        for (int i = 0; i < nBlockXSize; i++)
            pafBuffer[i] = (float)padfBuffer[i];

        CPLFree(padfBuffer);

        return eErr;
    }

    const CPLErr eErr =
        eDataType == GDT_Float64
            ? GXFGetScanline(poGXF_DS->hGXF, nBlockYOff, (double *)pImage)
            : CE_Failure;

    return eErr;
}

/************************************************************************/
/* ==================================================================== */
/*                              GXFDataset                              */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                             GXFDataset()                             */
/************************************************************************/

GXFDataset::GXFDataset()
    : hGXF(nullptr), dfNoDataValue(0), eDataType(GDT_Float32)
{
    m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
}

/************************************************************************/
/*                            ~GXFDataset()                             */
/************************************************************************/

GXFDataset::~GXFDataset()

{
    FlushCache(true);
    if (hGXF != nullptr)
        GXFClose(hGXF);
}

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

CPLErr GXFDataset::GetGeoTransform(GDALGeoTransform &gt) const

{
    double dfXOrigin = 0.0;
    double dfYOrigin = 0.0;
    double dfXSize = 0.0;
    double dfYSize = 0.0;
    double dfRotation = 0.0;

    const CPLErr eErr = GXFGetPosition(hGXF, &dfXOrigin, &dfYOrigin, &dfXSize,
                                       &dfYSize, &dfRotation);

    if (eErr != CE_None)
        return eErr;

    // Transform to radians.
    dfRotation = (dfRotation / 360.0) * 2.0 * M_PI;

    gt.xscale = dfXSize * cos(dfRotation);
    gt.xrot = dfYSize * sin(dfRotation);
    gt.yrot = dfXSize * sin(dfRotation);
    gt.yscale = -1 * dfYSize * cos(dfRotation);

    // take into account that GXF is point or center of pixel oriented.
    gt.xorig = dfXOrigin - 0.5 * gt.xscale - 0.5 * gt.xrot;
    gt.yorig = dfYOrigin - 0.5 * gt.yrot - 0.5 * gt.yscale;

    return CE_None;
}

/************************************************************************/
/*                           GetSpatialRef()                            */
/************************************************************************/

const OGRSpatialReference *GXFDataset::GetSpatialRef() const
{
    return m_oSRS.IsEmpty() ? nullptr : &m_oSRS;
}

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

GDALDataset *GXFDataset::Open(GDALOpenInfo *poOpenInfo)

{
    /* -------------------------------------------------------------------- */
    /*      Before trying GXFOpen() we first verify that there is at        */
    /*      least one "\n#keyword" type signature in the first chunk of     */
    /*      the file.                                                       */
    /* -------------------------------------------------------------------- */
    if (poOpenInfo->nHeaderBytes < 50 || poOpenInfo->fpL == nullptr)
        return nullptr;

    bool bFoundKeyword = false;
    bool bFoundIllegal = false;
    for (int i = 0; i < poOpenInfo->nHeaderBytes - 1; i++)
    {
        if ((poOpenInfo->pabyHeader[i] == 10 ||
             poOpenInfo->pabyHeader[i] == 13) &&
            poOpenInfo->pabyHeader[i + 1] == '#')
        {
            if (STARTS_WITH((const char *)poOpenInfo->pabyHeader + i + 2,
                            "include"))
                return nullptr;
            if (STARTS_WITH((const char *)poOpenInfo->pabyHeader + i + 2,
                            "define"))
                return nullptr;
            if (STARTS_WITH((const char *)poOpenInfo->pabyHeader + i + 2,
                            "ifdef"))
                return nullptr;
            bFoundKeyword = true;
        }
        if (poOpenInfo->pabyHeader[i] == 0)
        {
            bFoundIllegal = true;
            break;
        }
    }

    if (!bFoundKeyword || bFoundIllegal)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      At this point it is plausible that this is a GXF file, but      */
    /*      we also now verify that there is a #GRID keyword before         */
    /*      passing it off to GXFOpen().  We check in the first 50K.        */
    /* -------------------------------------------------------------------- */
    CPL_IGNORE_RET_VAL(poOpenInfo->TryToIngest(50000));
    bool bGotGrid = false;

    const char *pszBigBuf = (const char *)poOpenInfo->pabyHeader;
    for (int i = 0; i < poOpenInfo->nHeaderBytes - 5 && !bGotGrid; i++)
    {
        if (pszBigBuf[i] == '#' && STARTS_WITH_CI(pszBigBuf + i + 1, "GRID"))
            bGotGrid = true;
    }

    if (!bGotGrid)
        return nullptr;

    VSIFCloseL(poOpenInfo->fpL);
    poOpenInfo->fpL = nullptr;

    /* -------------------------------------------------------------------- */
    /*      Try opening the dataset.                                        */
    /* -------------------------------------------------------------------- */

    GXFHandle l_hGXF = GXFOpen(poOpenInfo->pszFilename);

    if (l_hGXF == nullptr)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Confirm the requested access is supported.                      */
    /* -------------------------------------------------------------------- */
    if (poOpenInfo->eAccess == GA_Update)
    {
        GXFClose(l_hGXF);
        ReportUpdateNotSupportedByDriver("GXF");
        return nullptr;
    }

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

    const char *pszGXFDataType = CPLGetConfigOption("GXF_DATATYPE", "Float32");
    GDALDataType eDT = GDALGetDataTypeByName(pszGXFDataType);
    if (!(eDT == GDT_Float32 || eDT == GDT_Float64))
    {
        CPLError(CE_Warning, CPLE_NotSupported,
                 "Unsupported value for GXF_DATATYPE : %s", pszGXFDataType);
        eDT = GDT_Float32;
    }

    poDS->hGXF = l_hGXF;
    poDS->eDataType = eDT;

    /* -------------------------------------------------------------------- */
    /*      Establish the projection.                                       */
    /* -------------------------------------------------------------------- */
    char *pszProjection = GXFGetMapProjectionAsOGCWKT(l_hGXF);
    if (pszProjection && pszProjection[0] != '\0')
        poDS->m_oSRS.importFromWkt(pszProjection);
    CPLFree(pszProjection);

    /* -------------------------------------------------------------------- */
    /*      Capture some information from the file that is of interest.     */
    /* -------------------------------------------------------------------- */
    GXFGetRawInfo(l_hGXF, &(poDS->nRasterXSize), &(poDS->nRasterYSize), nullptr,
                  nullptr, nullptr, &(poDS->dfNoDataValue));

    if (poDS->nRasterXSize <= 0 || poDS->nRasterYSize <= 0)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid dimensions : %d x %d",
                 poDS->nRasterXSize, poDS->nRasterYSize);
        delete poDS;
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Create band information objects.                                */
    /* -------------------------------------------------------------------- */
    poDS->nBands = 1;
    poDS->SetBand(1, new GXFRasterBand(poDS, 1));

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

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

    return poDS;
}

/************************************************************************/
/*                          GDALRegister_GXF()                          */
/************************************************************************/

void GDALRegister_GXF()

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

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription("GXF");
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
                              "GeoSoft Grid Exchange Format");
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/gxf.html");
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "gxf");
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");

    poDriver->pfnOpen = GXFDataset::Open;

    GetGDALDriverManager()->RegisterDriver(poDriver);
}
