/******************************************************************************
 *
 * Project:  BLX Driver
 * Purpose:  GDAL BLX support.
 * Author:   Henrik Johansson, henrik@johome.net
 *
 ******************************************************************************
 * Copyright (c) 2006, Henrik Johansson <henrik@johome.net>
 * Copyright (c) 2008-2011, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ******************************************************************************
 *
 */

#include "cpl_string.h"
#include "gdal_frmts.h"
#include "gdal_pam.h"

CPL_C_START
#include "blx.h"
CPL_C_END

class BLXDataset final : public GDALPamDataset
{
    friend class BLXRasterBand;

    CPLErr GetGeoTransform(double *padfTransform) override;
    const OGRSpatialReference *GetSpatialRef() const override;

    OGRSpatialReference m_oSRS{};
    blxcontext_t *blxcontext = nullptr;

    bool bIsOverview = false;
    std::vector<std::unique_ptr<BLXDataset>> apoOverviewDS{};

  public:
    BLXDataset();
    ~BLXDataset();

    static GDALDataset *Open(GDALOpenInfo *);
};

class BLXRasterBand final : public GDALPamRasterBand
{
    int overviewLevel;

  public:
    BLXRasterBand(BLXDataset *, int, int overviewLevel = 0);

    double GetNoDataValue(int *pbSuccess = nullptr) override;
    GDALColorInterp GetColorInterpretation(void) override;
    int GetOverviewCount() override;
    GDALRasterBand *GetOverview(int) override;

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

GDALDataset *BLXDataset::Open(GDALOpenInfo *poOpenInfo)

{
    // --------------------------------------------------------------------
    //      First that the header looks like a BLX header
    // --------------------------------------------------------------------
    if (poOpenInfo->fpL == nullptr || poOpenInfo->nHeaderBytes < 102)
        return nullptr;

    if (!blx_checkheader((const char *)poOpenInfo->pabyHeader))
        return nullptr;

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

    // --------------------------------------------------------------------
    //      Open BLX file
    // --------------------------------------------------------------------
    poDS->blxcontext = blx_create_context();
    if (poDS->blxcontext == nullptr)
    {
        delete poDS;
        return nullptr;
    }
    if (blxopen(poDS->blxcontext, poOpenInfo->pszFilename, "rb") != 0)
    {
        delete poDS;
        return nullptr;
    }

    if ((poDS->blxcontext->cell_xsize % (1 << (1 + BLX_OVERVIEWLEVELS))) != 0 ||
        (poDS->blxcontext->cell_ysize % (1 << (1 + BLX_OVERVIEWLEVELS))) != 0)
    {
        delete poDS;
        return nullptr;
    }

    // Update dataset header from BLX context
    poDS->nRasterXSize = poDS->blxcontext->xsize;
    poDS->nRasterYSize = poDS->blxcontext->ysize;

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

    // Create overview bands
    for (int i = 0; i < BLX_OVERVIEWLEVELS; i++)
    {
        poDS->apoOverviewDS.emplace_back(std::make_unique<BLXDataset>());
        poDS->apoOverviewDS[i]->blxcontext = poDS->blxcontext;
        poDS->apoOverviewDS[i]->bIsOverview = true;
        poDS->apoOverviewDS[i]->nRasterXSize = poDS->nRasterXSize >> (i + 1);
        poDS->apoOverviewDS[i]->nRasterYSize = poDS->nRasterYSize >> (i + 1);
        poDS->nBands = 1;
        poDS->apoOverviewDS[i]->SetBand(
            1, new BLXRasterBand(poDS->apoOverviewDS[i].get(), 1, i + 1));
    }

    /* -------------------------------------------------------------------- */
    /*      Confirm the requested access is supported.                      */
    /* -------------------------------------------------------------------- */
    if (poOpenInfo->eAccess == GA_Update)
    {
        delete poDS;
        CPLError(CE_Failure, CPLE_NotSupported,
                 "The BLX driver does not support update access to existing"
                 " datasets.\n");
        return nullptr;
    }

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

    return poDS;
}

BLXDataset::BLXDataset()
{
    m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
    m_oSRS.importFromWkt(SRS_WKT_WGS84_LAT_LONG);
}

BLXDataset::~BLXDataset()
{
    if (!bIsOverview)
    {
        if (blxcontext)
        {
            blxclose(blxcontext);
            blx_free_context(blxcontext);
        }
    }
}

CPLErr BLXDataset::GetGeoTransform(double *padfTransform)

{
    padfTransform[0] = blxcontext->lon;
    padfTransform[1] = blxcontext->pixelsize_lon;
    padfTransform[2] = 0.0;
    padfTransform[3] = blxcontext->lat;
    padfTransform[4] = 0.0;
    padfTransform[5] = blxcontext->pixelsize_lat;

    return CE_None;
}

const OGRSpatialReference *BLXDataset::GetSpatialRef() const
{
    return &m_oSRS;
}

BLXRasterBand::BLXRasterBand(BLXDataset *poDSIn, int nBandIn,
                             int overviewLevelIn)
    : overviewLevel(overviewLevelIn)
{
    BLXDataset *poGDS = poDSIn;

    poDS = poDSIn;
    nBand = nBandIn;

    eDataType = GDT_Int16;

    nBlockXSize = poGDS->blxcontext->cell_xsize >> overviewLevel;
    nBlockYSize = poGDS->blxcontext->cell_ysize >> overviewLevel;
}

int BLXRasterBand::GetOverviewCount()
{
    BLXDataset *poGDS = cpl::down_cast<BLXDataset *>(poDS);
    return static_cast<int>(poGDS->apoOverviewDS.size());
}

GDALRasterBand *BLXRasterBand::GetOverview(int i)
{
    BLXDataset *poGDS = cpl::down_cast<BLXDataset *>(poDS);

    if (i < 0 || static_cast<size_t>(i) >= poGDS->apoOverviewDS.size())
        return nullptr;

    return poGDS->apoOverviewDS[i]->GetRasterBand(nBand);
}

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

{
    BLXDataset *poGDS = reinterpret_cast<BLXDataset *>(poDS);

    if (blx_readcell(poGDS->blxcontext, nBlockYOff, nBlockXOff, (short *)pImage,
                     nBlockXSize * nBlockYSize * 2, overviewLevel) == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Failed to read BLX cell");
        return CE_Failure;
    }

    return CE_None;
}

double BLXRasterBand::GetNoDataValue(int *pbSuccess)
{
    if (pbSuccess)
        *pbSuccess = TRUE;
    return BLX_UNDEF;
}

GDALColorInterp BLXRasterBand::GetColorInterpretation(void)
{
    return GCI_GrayIndex;
}

/* TODO: check if georeference is the same as for BLX files, WGS84
 */
static GDALDataset *BLXCreateCopy(const char *pszFilename, GDALDataset *poSrcDS,
                                  int bStrict, char **papszOptions,
                                  GDALProgressFunc pfnProgress,
                                  void *pProgressData)

{
    // --------------------------------------------------------------------
    //      Some rudimentary checks
    // --------------------------------------------------------------------
    const int nBands = poSrcDS->GetRasterCount();
    if (nBands != 1)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "BLX driver doesn't support %d bands.  Must be 1 (grey) ",
                 nBands);
        return nullptr;
    }

    if (poSrcDS->GetRasterBand(1)->GetRasterDataType() != GDT_Int16 && bStrict)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "BLX driver doesn't support data type %s. "
                 "Only 16 bit byte bands supported.\n",
                 GDALGetDataTypeName(
                     poSrcDS->GetRasterBand(1)->GetRasterDataType()));

        return nullptr;
    }

    const int nXSize = poSrcDS->GetRasterXSize();
    const int nYSize = poSrcDS->GetRasterYSize();
    if ((nXSize % 128 != 0) || (nYSize % 128 != 0))
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "BLX driver doesn't support dimensions that are not a "
                 "multiple of 128.\n");

        return nullptr;
    }

    // --------------------------------------------------------------------
    //      What options has the user selected?
    // --------------------------------------------------------------------
    int zscale = 1;
    if (CSLFetchNameValue(papszOptions, "ZSCALE") != nullptr)
    {
        zscale = atoi(CSLFetchNameValue(papszOptions, "ZSCALE"));
        if (zscale < 1)
        {
            CPLError(CE_Failure, CPLE_IllegalArg,
                     "ZSCALE=%s is not a legal value in the range >= 1.",
                     CSLFetchNameValue(papszOptions, "ZSCALE"));
            return nullptr;
        }
    }

    int fillundef = 1;
    if (CSLFetchNameValue(papszOptions, "FILLUNDEF") != nullptr &&
        EQUAL(CSLFetchNameValue(papszOptions, "FILLUNDEF"), "NO"))
        fillundef = 0;

    int fillundefval = 0;
    if (CSLFetchNameValue(papszOptions, "FILLUNDEFVAL") != nullptr)
    {
        fillundefval = atoi(CSLFetchNameValue(papszOptions, "FILLUNDEFVAL"));
        if ((fillundefval < -32768) || (fillundefval > 32767))
        {
            CPLError(CE_Failure, CPLE_IllegalArg,
                     "FILLUNDEFVAL=%s is not a legal value in the range "
                     "-32768, 32767.",
                     CSLFetchNameValue(papszOptions, "FILLUNDEFVAL"));
            return nullptr;
        }
    }

    int endian = LITTLEENDIAN;
    if (CSLFetchNameValue(papszOptions, "BIGENDIAN") != nullptr &&
        !EQUAL(CSLFetchNameValue(papszOptions, "BIGENDIAN"), "NO"))
        endian = BIGENDIAN;

    // --------------------------------------------------------------------
    //      Create the dataset.
    // --------------------------------------------------------------------

    // Create a BLX context
    blxcontext_t *ctx = blx_create_context();

    // Setup BLX parameters
    ctx->cell_rows = nYSize / ctx->cell_ysize;
    ctx->cell_cols = nXSize / ctx->cell_xsize;
    ctx->zscale = zscale;
    ctx->fillundef = fillundef;
    ctx->fillundefval = fillundefval;
    ctx->endian = endian;

    if (blxopen(ctx, pszFilename, "wb"))
    {
        CPLError(CE_Failure, CPLE_OpenFailed, "Unable to create blx file %s.\n",
                 pszFilename);
        blx_free_context(ctx);
        return nullptr;
    }

    // --------------------------------------------------------------------
    //      Loop over image, copying image data.
    // --------------------------------------------------------------------

    GInt16 *pabyTile = (GInt16 *)VSI_MALLOC_VERBOSE(
        sizeof(GInt16) * ctx->cell_xsize * ctx->cell_ysize);
    if (pabyTile == nullptr)
    {
        blxclose(ctx);
        blx_free_context(ctx);
        return nullptr;
    }

    CPLErr eErr = CE_None;
    if (!pfnProgress(0.0, nullptr, pProgressData))
        eErr = CE_Failure;

    for (int i = 0; (i < ctx->cell_rows) && (eErr == CE_None); i++)
        for (int j = 0; j < ctx->cell_cols; j++)
        {
            GDALRasterBand *poBand = poSrcDS->GetRasterBand(1);
            eErr = poBand->RasterIO(GF_Read, j * ctx->cell_xsize,
                                    i * ctx->cell_ysize, ctx->cell_xsize,
                                    ctx->cell_ysize, pabyTile, ctx->cell_xsize,
                                    ctx->cell_ysize, GDT_Int16, 0, 0, nullptr);
            if (eErr >= CE_Failure)
                break;
            blxdata *celldata = pabyTile;
            if (blx_writecell(ctx, celldata, i, j) != 0)
            {
                eErr = CE_Failure;
                break;
            }

            if (!pfnProgress(1.0 * (i * ctx->cell_cols + j) /
                                 (ctx->cell_rows * ctx->cell_cols),
                             nullptr, pProgressData))
            {
                eErr = CE_Failure;
                break;
            }
        }

    pfnProgress(1.0, nullptr, pProgressData);

    CPLFree(pabyTile);

    double adfGeoTransform[6];
    if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None)
    {
        ctx->lon = adfGeoTransform[0];
        ctx->lat = adfGeoTransform[3];
        ctx->pixelsize_lon = adfGeoTransform[1];
        ctx->pixelsize_lat = adfGeoTransform[5];
    }

    blxclose(ctx);
    blx_free_context(ctx);

    if (eErr == CE_None)
        return GDALDataset::FromHandle(GDALOpen(pszFilename, GA_ReadOnly));

    return nullptr;
}

void GDALRegister_BLX()

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

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription("BLX");
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Magellan topo (.blx)");
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/blx.html");
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "blx");

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

    poDriver->pfnOpen = BLXDataset::Open;
    poDriver->pfnCreateCopy = BLXCreateCopy;

    GetGDALDriverManager()->RegisterDriver(poDriver);
}
