/***********************************************************************
 * File :    postgisrasterrasterband.cpp
 * Project:  PostGIS Raster driver
 * Purpose:  GDAL RasterBand implementation for PostGIS Raster driver
 * Author:   Jorge Arevalo, jorge.arevalo@deimos-space.com
 *                          jorgearevalo@libregis.org
 *
 * Author:       David Zwarg, dzwarg@azavea.com
 *
 * Last changes: $Id$
 *
 ***********************************************************************
 * Copyright (c) 2009 - 2013, Jorge Arevalo, David Zwarg
 * Copyright (c) 2013-2018, Even Rouault <even.rouault at spatialys.com>
 *
 * 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 "postgisraster.h"

/**
 * \brief Constructor.
 *
 * nBand it is just necessary for overview band creation
 */
PostGISRasterRasterBand::PostGISRasterRasterBand(PostGISRasterDataset *poDSIn,
                                                 int nBandIn,
                                                 GDALDataType eDataTypeIn,
                                                 GBool bNoDataValueSetIn,
                                                 double dfNodata)
    : VRTSourcedRasterBand(poDSIn, nBandIn), pszSchema(poDSIn->pszSchema),
      pszTable(poDSIn->pszTable), pszColumn(poDSIn->pszColumn)
{
    /* Basic properties */
    poDS = poDSIn;
    nBand = nBandIn;

    eDataType = eDataTypeIn;
    m_bNoDataValueSet = bNoDataValueSetIn;
    m_dfNoDataValue = dfNodata;

    nRasterXSize = poDS->GetRasterXSize();
    nRasterYSize = poDS->GetRasterYSize();

    /*******************************************************************
     * Finally, set the block size. We apply the same logic than in VRT
     * driver.
     *
     * We limit the size of a block with MAX_BLOCK_SIZE here to prevent
     * arrangements of just one big tile.
     *
     * This value is just used in case we only have 1 tile in the
     * table. Otherwise, the reading operations are performed by the
     * sources, not the PostGISRasterBand object itself.
     ******************************************************************/
    nBlockXSize = atoi(CPLGetConfigOption(
        "PR_BLOCKXSIZE",
        CPLSPrintf("%d", MIN(MAX_BLOCK_SIZE, this->nRasterXSize))));
    nBlockYSize = atoi(CPLGetConfigOption(
        "PR_BLOCKYSIZE",
        CPLSPrintf("%d", MIN(MAX_BLOCK_SIZE, this->nRasterYSize))));

#ifdef DEBUG_VERBOSE
    CPLDebug("PostGIS_Raster",
             "PostGISRasterRasterBand constructor: Band size: (%d X %d)",
             nRasterXSize, nRasterYSize);

    CPLDebug("PostGIS_Raster",
             "PostGISRasterRasterBand::Constructor: "
             "Block size (%dx%d)",
             this->nBlockXSize, this->nBlockYSize);
#endif
}

/***********************************************
 * \brief: Band destructor
 ***********************************************/
PostGISRasterRasterBand::~PostGISRasterRasterBand()
{
}

/********************************************************
 * \brief Set nodata value to a buffer
 ********************************************************/
void PostGISRasterRasterBand::NullBuffer(void *pData, int nBufXSize,
                                         int nBufYSize, GDALDataType eBufType,
                                         int nPixelSpace, int nLineSpace)
{
    int j;
    for (j = 0; j < nBufYSize; j++)
    {
        double dfVal = 0.0;
        if (m_bNoDataValueSet)
            dfVal = m_dfNoDataValue;
        GDALCopyWords(&dfVal, GDT_Float64, 0,
                      static_cast<GByte *>(pData) + j * nLineSpace, eBufType,
                      nPixelSpace, nBufXSize);
    }
}

/********************************************************
 * \brief SortTilesByPKID
 ********************************************************/
static int SortTilesByPKID(const void *a, const void *b)
{
    const PostGISRasterTileDataset *pa =
        *static_cast<const PostGISRasterTileDataset *const *>(a);
    const PostGISRasterTileDataset *pb =
        *static_cast<const PostGISRasterTileDataset *const *>(b);
    return strcmp(pa->GetPKID(), pb->GetPKID());
}

/**
 * Read/write a region of image data for this band.
 *
 * This method allows reading a region of a PostGISRasterBand into a buffer.
 * The write support is still under development
 *
 * The function fetches all the raster data that intersects with the region
 * provided, and store the data in the GDAL cache.
 *
 * It automatically takes care of data type translation if the data type
 * (eBufType) of the buffer is different than that of the
 * PostGISRasterRasterBand.
 *
 * The nPixelSpace and nLineSpace parameters allow reading into FROM various
 * organization of buffers.
 *
 * @param eRWFlag Either GF_Read to read a region of data (GF_Write, to write
 * a region of data, yet not supported)
 *
 * @param nXOff The pixel offset to the top left corner of the region of the
 * band to be accessed. This would be zero to start FROM the left side.
 *
 * @param nYOff The line offset to the top left corner of the region of the band
 * to be accessed. This would be zero to start FROM the top.
 *
 * @param nXSize The width of the region of the band to be accessed in pixels.
 *
 * @param nYSize The height of the region of the band to be accessed in lines.
 *
 * @param pData The buffer into which the data should be read, or FROM which it
 * should be written. This buffer must contain at least
 * nBufXSize * nBufYSize * nBandCount words of type eBufType. It is organized in
 * left to right,top to bottom pixel order. Spacing is controlled by the
 * nPixelSpace, and nLineSpace parameters.
 *
 * @param nBufXSize the width of the buffer image into which the desired region
 * is to be read, or FROM which it is to be written.
 *
 * @param nBufYSize the height of the buffer image into which the desired region
 * is to be read, or FROM which it is to be written.
 *
 * @param eBufType the type of the pixel values in the pData data buffer. The
 * pixel values will automatically be translated to/FROM the
 * PostGISRasterRasterBand data type as needed.
 *
 * @param nPixelSpace The byte offset FROM the start of one pixel value in pData
 * to the start of the next pixel value within a scanline. If defaulted (0) the
 * size of the datatype eBufType is used.
 *
 * @param nLineSpace The byte offset FROM the start of one scanline in pData to
 * the start of the next. If defaulted (0) the size of the datatype
 * eBufType * nBufXSize is used.
 *
 * @return CE_Failure if the access fails, otherwise CE_None.
 */

CPLErr PostGISRasterRasterBand::IRasterIO(
    GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize, int nYSize,
    void *pData, int nBufXSize, int nBufYSize, GDALDataType eBufType,
    GSpacing nPixelSpace, GSpacing nLineSpace, GDALRasterIOExtraArg *psExtraArg)
{
    /**
     * TODO: Write support not implemented yet
     **/
    if (eRWFlag == GF_Write)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Writing through PostGIS Raster band not supported yet");

        return CE_Failure;
    }

    /*******************************************************************
     * Do we have overviews that would be appropriate to satisfy this
     * request?
     ******************************************************************/
    if ((nBufXSize < nXSize || nBufYSize < nYSize) && GetOverviewCount() > 0)
    {
        if (OverviewRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData,
                             nBufXSize, nBufYSize, eBufType, nPixelSpace,
                             nLineSpace, psExtraArg) == CE_None)

            return CE_None;
    }

    PostGISRasterDataset *poRDS = cpl::down_cast<PostGISRasterDataset *>(poDS);

    int bSameWindowAsOtherBand =
        (nXOff == poRDS->nXOffPrev && nYOff == poRDS->nYOffPrev &&
         nXSize == poRDS->nXSizePrev && nYSize == poRDS->nYSizePrev);
    poRDS->nXOffPrev = nXOff;
    poRDS->nYOffPrev = nYOff;
    poRDS->nXSizePrev = nXSize;
    poRDS->nYSizePrev = nYSize;

    /* Logic to determine if bands are read in order 1, 2, ... N */
    /* If so, then use multi-band caching, otherwise do just single band caching
     */
    if (poRDS->bAssumeMultiBandReadPattern)
    {
        if (nBand != poRDS->nNextExpectedBand)
        {
            CPLDebug("PostGIS_Raster", "Disabling multi-band caching since "
                                       "band access pattern does not match");
            poRDS->bAssumeMultiBandReadPattern = false;
            poRDS->nNextExpectedBand = 1;
        }
        else
        {
            poRDS->nNextExpectedBand++;
            if (poRDS->nNextExpectedBand > poRDS->GetRasterCount())
                poRDS->nNextExpectedBand = 1;
        }
    }
    else
    {
        if (nBand == poRDS->nNextExpectedBand)
        {
            poRDS->nNextExpectedBand++;
            if (poRDS->nNextExpectedBand > poRDS->GetRasterCount())
            {
                CPLDebug("PostGIS_Raster", "Re-enabling multi-band caching");
                poRDS->bAssumeMultiBandReadPattern = true;
                poRDS->nNextExpectedBand = 1;
            }
        }
    }

#ifdef DEBUG_VERBOSE
    CPLDebug("PostGIS_Raster",
             "PostGISRasterRasterBand::IRasterIO: "
             "nBand = %d, nXOff = %d, nYOff = %d, nXSize = %d, nYSize = %d, "
             "nBufXSize = %d, nBufYSize = %d",
             nBand, nXOff, nYOff, nXSize, nYSize, nBufXSize, nBufYSize);
#endif

    /*******************************************************************
     * Several tiles: we first look in all our sources caches. Missing
     * blocks are queried
     ******************************************************************/
    double adfProjWin[8];
    int nFeatureCount = 0;
    CPLRectObj sAoi;

    poRDS->PolygonFromCoords(nXOff, nYOff, nXOff + nXSize, nYOff + nYSize,
                             adfProjWin);
    // (p[6], p[7]) is the minimum (x, y), and (p[2], p[3]) the max
    sAoi.minx = adfProjWin[6];
    sAoi.maxx = adfProjWin[2];
    if (adfProjWin[7] < adfProjWin[3])
    {
        sAoi.miny = adfProjWin[7];
        sAoi.maxy = adfProjWin[3];
    }
    else
    {
        sAoi.maxy = adfProjWin[7];
        sAoi.miny = adfProjWin[3];
    }

#ifdef DEBUG_VERBOSE
    CPLDebug("PostGIS_Raster",
             "PostGISRasterRasterBand::IRasterIO: "
             "Intersection box: (%f, %f) - (%f, %f)",
             sAoi.minx, sAoi.miny, sAoi.maxx, sAoi.maxy);
#endif

    if (poRDS->hQuadTree == nullptr)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "Could not read metadata index.");
        return CE_Failure;
    }

    NullBuffer(pData, nBufXSize, nBufYSize, eBufType,
               static_cast<int>(nPixelSpace), static_cast<int>(nLineSpace));

    if (poRDS->bBuildQuadTreeDynamically && !bSameWindowAsOtherBand)
    {
        if (!(poRDS->LoadSources(nXOff, nYOff, nXSize, nYSize, nBand)))
            return CE_Failure;
    }

    // Matching sources, to avoid a dumb for loop over the sources
    PostGISRasterTileDataset **papsMatchingTiles =
        reinterpret_cast<PostGISRasterTileDataset **>(
            CPLQuadTreeSearch(poRDS->hQuadTree, &sAoi, &nFeatureCount));

    // No blocks found. This is not an error (the raster may have holes)
    if (nFeatureCount == 0)
    {
        CPLFree(papsMatchingTiles);

        return CE_None;
    }

    int i;

    /**
     * We need to store the max, min coords for the missing tiles in
     * any place. This is as good as any other
     **/
    sAoi.minx = 0.0;
    sAoi.miny = 0.0;
    sAoi.maxx = 0.0;
    sAoi.maxy = 0.0;

    GIntBig nMemoryRequiredForTiles = 0;
    CPLString osIDsToFetch;
    int nTilesToFetch = 0;
    int nBandDataTypeSize = GDALGetDataTypeSize(eDataType) / 8;

    // Loop just over the intersecting sources
    for (i = 0; i < nFeatureCount; i++)
    {
        PostGISRasterTileDataset *poTile = papsMatchingTiles[i];
        PostGISRasterTileRasterBand *poTileBand =
            cpl::down_cast<PostGISRasterTileRasterBand *>(
                poTile->GetRasterBand(nBand));

        nMemoryRequiredForTiles +=
            static_cast<GIntBig>(poTileBand->GetXSize()) *
            poTileBand->GetYSize() * nBandDataTypeSize;

        // Missing tile: we'll need to query for it
        if (!poTileBand->IsCached())
        {

            // If we have a PKID, add the tile PKID to the list
            if (poTile->pszPKID != nullptr)
            {
                if (!osIDsToFetch.empty())
                    osIDsToFetch += ",";
                osIDsToFetch += "'";
                osIDsToFetch += poTile->pszPKID;
                osIDsToFetch += "'";
            }

            double dfTileMinX, dfTileMinY, dfTileMaxX, dfTileMaxY;
            poTile->GetExtent(&dfTileMinX, &dfTileMinY, &dfTileMaxX,
                              &dfTileMaxY);

            /**
             * We keep the general max and min values of all the missing
             * tiles, to raise a query that intersect just that area.
             *
             * TODO: In case of just a few tiles and very separated,
             * this strategy is clearly suboptimal. We'll get our
             * missing tiles, but with a lot of other not needed tiles.
             *
             * A possible optimization will be to simply rely on the
             * I/O method of the source (must be implemented), in case
             * we have minus than a reasonable amount of tiles missing.
             * Another criteria to decide would be how separated the
             * tiles are. Two queries for just two adjacent tiles is
             * also a dumb strategy.
             **/
            if (nTilesToFetch == 0)
            {
                sAoi.minx = dfTileMinX;
                sAoi.miny = dfTileMinY;
                sAoi.maxx = dfTileMaxX;
                sAoi.maxy = dfTileMaxY;
            }
            else
            {
                if (dfTileMinX < sAoi.minx)
                    sAoi.minx = dfTileMinX;

                if (dfTileMinY < sAoi.miny)
                    sAoi.miny = dfTileMinY;

                if (dfTileMaxX > sAoi.maxx)
                    sAoi.maxx = dfTileMaxX;

                if (dfTileMaxY > sAoi.maxy)
                    sAoi.maxy = dfTileMaxY;
            }

            nTilesToFetch++;
        }
    }

    /* Determine caching strategy */
    bool bAllBandCaching = false;
    if (nTilesToFetch > 0)
    {
        GIntBig nCacheMax = GDALGetCacheMax64();
        if (nMemoryRequiredForTiles > nCacheMax)
        {
            CPLDebug("PostGIS_Raster",
                     "For best performance, the block cache should be able to "
                     "store " CPL_FRMT_GIB
                     " bytes for the tiles of the requested window, "
                     "but it is only " CPL_FRMT_GIB " byte large",
                     nMemoryRequiredForTiles, nCacheMax);
            nTilesToFetch = 0;
        }

        if (poRDS->GetRasterCount() > 1 && poRDS->bAssumeMultiBandReadPattern)
        {
            GIntBig nMemoryRequiredForTilesAllBands =
                nMemoryRequiredForTiles * poRDS->GetRasterCount();
            if (nMemoryRequiredForTilesAllBands <= nCacheMax)
            {
                bAllBandCaching = true;
            }
            else
            {
                CPLDebug("PostGIS_Raster",
                         "Caching only this band, but not all bands. "
                         "Cache should be " CPL_FRMT_GIB " byte large for that",
                         nMemoryRequiredForTilesAllBands);
            }
        }
    }

    // Raise a query for missing tiles and cache them
    if (nTilesToFetch > 0)
    {

        /**
         * There are several options here, to raise the query.
         * - Get all the tiles which PKID is in a list of missing
         *   PKIDs.
         * - Get all the tiles that intersect a polygon constructed
         *   based on the (min - max) values calculated before.
         * - Get all the tiles with upper left pixel included in the
         *   range (min - max) calculated before.
         *
         * The first option is the most efficient one when a PKID exists.
         * After that, the second one is the most efficient one when a
         * spatial index exists.
         * The third one is the only one available when neither a PKID or
         *spatial index exist.
         **/

        CPLString osSchemaI(CPLQuotedSQLIdentifier(pszSchema));
        CPLString osTableI(CPLQuotedSQLIdentifier(pszTable));
        CPLString osColumnI(CPLQuotedSQLIdentifier(pszColumn));

        CPLString osWHERE;
        if (!osIDsToFetch.empty() &&
            (poRDS->bIsFastPK || !(poRDS->HasSpatialIndex())))
        {
            if (nTilesToFetch < poRDS->m_nTiles ||
                poRDS->bBuildQuadTreeDynamically)
            {
                osWHERE += poRDS->pszPrimaryKeyName;
                osWHERE += " IN (";
                osWHERE += osIDsToFetch;
                osWHERE += ")";
            }
        }
        else
        {
            if (poRDS->HasSpatialIndex())
            {
                osWHERE += CPLSPrintf(
                    "%s && "
                    "ST_GeomFromText('POLYGON((%.18f %.18f,%.18f %.18f,%.18f "
                    "%.18f,%.18f %.18f,%.18f %.18f))')",
                    osColumnI.c_str(), adfProjWin[0], adfProjWin[1],
                    adfProjWin[2], adfProjWin[3], adfProjWin[4], adfProjWin[5],
                    adfProjWin[6], adfProjWin[7], adfProjWin[0], adfProjWin[1]);
            }
            else
            {
#define EPS 1e-5
                osWHERE += CPLSPrintf(
                    "ST_UpperLeftX(%s)"
                    " BETWEEN %f AND %f AND ST_UpperLeftY(%s) BETWEEN "
                    "%f AND %f",
                    osColumnI.c_str(), sAoi.minx - EPS, sAoi.maxx + EPS,
                    osColumnI.c_str(), sAoi.miny - EPS, sAoi.maxy + EPS);
            }
        }

        if (poRDS->pszWhere != nullptr)
        {
            if (!osWHERE.empty())
                osWHERE += " AND ";
            osWHERE += "(";
            osWHERE += poRDS->pszWhere;
            osWHERE += ")";
        }

        bool bCanUseClientSide = true;
        if (poRDS->eOutDBResolution == OutDBResolution::CLIENT_SIDE_IF_POSSIBLE)
        {
            bCanUseClientSide =
                poRDS->CanUseClientSideOutDB(bAllBandCaching, nBand, osWHERE);
        }

        CPLString osRasterToFetch;
        if (bAllBandCaching)
            osRasterToFetch = osColumnI;
        else
            osRasterToFetch.Printf("ST_Band(%s, %d)", osColumnI.c_str(), nBand);
        if (poRDS->eOutDBResolution == OutDBResolution::SERVER_SIDE ||
            !bCanUseClientSide)
        {
            osRasterToFetch =
                "encode(ST_AsBinary(" + osRasterToFetch + ",TRUE),'hex')";
        }

        CPLString osCommand;
        osCommand.Printf("SELECT %s, ST_Metadata(%s), %s FROM %s.%s",
                         (poRDS->GetPrimaryKeyRef()) ? poRDS->GetPrimaryKeyRef()
                                                     : "NULL",
                         osColumnI.c_str(), osRasterToFetch.c_str(),
                         osSchemaI.c_str(), osTableI.c_str());
        if (!osWHERE.empty())
        {
            osCommand += " WHERE " + osWHERE;
        }

        PGresult *poResult = PQexec(poRDS->poConn, osCommand.c_str());

#ifdef DEBUG_QUERY
        CPLDebug("PostGIS_Raster",
                 "PostGISRasterRasterBand::IRasterIO(): Query = \"%s\" --> "
                 "number of rows = %d",
                 osCommand.c_str(), poResult ? PQntuples(poResult) : 0);
#endif

        if (poResult == nullptr ||
            PQresultStatus(poResult) != PGRES_TUPLES_OK ||
            PQntuples(poResult) < 0)
        {

            if (poResult)
                PQclear(poResult);

            CPLError(CE_Failure, CPLE_AppDefined,
                     "PostGISRasterRasterBand::IRasterIO(): %s",
                     PQerrorMessage(poRDS->poConn));

            // Free the object that holds pointers to matching tiles
            CPLFree(papsMatchingTiles);
            return CE_Failure;
        }

        /**
         * No data. Return the buffer filled with nodata values
         **/
        else if (PQntuples(poResult) == 0)
        {
            PQclear(poResult);

            // Free the object that holds pointers to matching tiles
            CPLFree(papsMatchingTiles);
            return CE_None;
        }

        /**
         * Ok, we loop over the results
         **/
        int nTuples = PQntuples(poResult);
        for (i = 0; i < nTuples; i++)
        {
            const char *pszPKID = PQgetvalue(poResult, i, 0);
            const char *pszMetadata = PQgetvalue(poResult, i, 1);
            const char *pszRaster = PQgetvalue(poResult, i, 2);
            poRDS->CacheTile(pszMetadata, pszRaster, pszPKID, nBand,
                             bAllBandCaching);
        }  // All tiles have been added to cache

        PQclear(poResult);
    }  // End missing tiles

    /* -------------------------------------------------------------------- */
    /*      Overlay each source in turn over top this.                      */
    /* -------------------------------------------------------------------- */

    CPLErr eErr = CE_None;
    /* Sort tiles by ascending PKID, so that the draw order is deterministic. */
    if (poRDS->GetPrimaryKeyRef() != nullptr)
    {
        qsort(papsMatchingTiles, nFeatureCount,
              sizeof(PostGISRasterTileDataset *), SortTilesByPKID);
    }

    VRTSource::WorkingState oWorkingState;
    for (i = 0; i < nFeatureCount && eErr == CE_None; i++)
    {
        PostGISRasterTileDataset *poTile = papsMatchingTiles[i];
        PostGISRasterTileRasterBand *poTileBand =
            cpl::down_cast<PostGISRasterTileRasterBand *>(
                poTile->GetRasterBand(nBand));
        eErr = poTileBand->poSource->RasterIO(
            eDataType, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize,
            nBufYSize, eBufType, nPixelSpace, nLineSpace, nullptr,
            oWorkingState);
    }

    // Free the object that holds pointers to matching tiles
    CPLFree(papsMatchingTiles);

    return eErr;
}

/**
 * \brief Set the no data value for this band.
 * Parameters:
 *  - double: The nodata value
 * Returns:
 *  - CE_None.
 */
CPLErr PostGISRasterRasterBand::SetNoDataValue(double dfNewValue)
{
    m_dfNoDataValue = dfNewValue;

    return CE_None;
}

/**
 * \brief Fetch the no data value for this band.
 * Parameters:
 *  - int *: pointer to a boolean to use to indicate if a value is actually
 *          associated with this layer. May be NULL (default).
 *  Returns:
 *  - double: the nodata value for this band.
 */
double PostGISRasterRasterBand::GetNoDataValue(int *pbSuccess)
{
    if (pbSuccess != nullptr)
        *pbSuccess = m_bNoDataValueSet;

    return m_dfNoDataValue;
}

/***************************************************
 * \brief Return the number of overview layers available
 ***************************************************/
int PostGISRasterRasterBand::GetOverviewCount()
{
    PostGISRasterDataset *poRDS = cpl::down_cast<PostGISRasterDataset *>(poDS);
    return poRDS->GetOverviewCount();
}

/**********************************************************
 * \brief Fetch overview raster band object
 **********************************************************/
GDALRasterBand *PostGISRasterRasterBand::GetOverview(int i)
{
    if (i < 0 || i >= GetOverviewCount())
        return nullptr;

    PostGISRasterDataset *poRDS = cpl::down_cast<PostGISRasterDataset *>(poDS);
    PostGISRasterDataset *poOverviewDS = poRDS->GetOverviewDS(i);
    if (!poOverviewDS)
    {
        CPLAssert(false);
        return nullptr;
    }
    if (poOverviewDS->nBands == 0)
    {
        if (!poOverviewDS->SetRasterProperties(nullptr) ||
            poOverviewDS->GetRasterCount() != poRDS->GetRasterCount())
        {
            CPLDebug("PostGIS_Raster",
                     "Request for overview %d of band %d failed", i, nBand);
            return nullptr;
        }
    }

    return poOverviewDS->GetRasterBand(nBand);
}

/**
 * \brief How should this band be interpreted as color?
 * GCI_Undefined is returned when the format doesn't know anything about the
 * color interpretation.
 **/
GDALColorInterp PostGISRasterRasterBand::GetColorInterpretation()
{
    if (poDS->GetRasterCount() == 1)
    {
        m_eColorInterp = GCI_GrayIndex;
    }

    else if (poDS->GetRasterCount() == 3)
    {
        if (nBand == 1)
            m_eColorInterp = GCI_RedBand;
        else if (nBand == 2)
            m_eColorInterp = GCI_GreenBand;
        else if (nBand == 3)
            m_eColorInterp = GCI_BlueBand;
        else
            m_eColorInterp = GCI_Undefined;
    }

    else
    {
        m_eColorInterp = GCI_Undefined;
    }

    return m_eColorInterp;
}

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

double PostGISRasterRasterBand::GetMinimum(int *pbSuccess)
{
    PostGISRasterDataset *poRDS = cpl::down_cast<PostGISRasterDataset *>(poDS);
    if (poRDS->bBuildQuadTreeDynamically && poRDS->m_nTiles == 0)
    {
        if (pbSuccess)
            *pbSuccess = FALSE;
        return 0.0;
    }
    return VRTSourcedRasterBand::GetMaximum(pbSuccess);
}

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

double PostGISRasterRasterBand::GetMaximum(int *pbSuccess)
{
    PostGISRasterDataset *poRDS = cpl::down_cast<PostGISRasterDataset *>(poDS);
    if (poRDS->bBuildQuadTreeDynamically && poRDS->m_nTiles == 0)
    {
        if (pbSuccess)
            *pbSuccess = FALSE;
        return 0.0;
    }
    return VRTSourcedRasterBand::GetMaximum(pbSuccess);
}

/************************************************************************/
/*                       ComputeRasterMinMax()                          */
/************************************************************************/

CPLErr PostGISRasterRasterBand::ComputeRasterMinMax(int bApproxOK,
                                                    double *adfMinMax)
{
    if (nRasterXSize < 1024 && nRasterYSize < 1024)
        return VRTSourcedRasterBand::ComputeRasterMinMax(bApproxOK, adfMinMax);

    int nOverviewCount = GetOverviewCount();
    for (int i = 0; i < nOverviewCount; i++)
    {
        auto poOverview = GetOverview(i);
        if (poOverview->GetXSize() < 1024 && poOverview->GetYSize() < 1024)
            return poOverview->ComputeRasterMinMax(bApproxOK, adfMinMax);
    }

    return CE_Failure;
}
