/******************************************************************************
 *
 * Project:  jpip read driver
 * Purpose:  GDAL bindings for JPIP.
 * Author:   Norman Barker, ITT VIS, norman.barker@gmail.com
 *
 ******************************************************************************
 * ITT Visual Information Systems grants you use of this code, under the
 * following license:
 *
 * Copyright (c) 2000-2007, ITT Visual Information Solutions
 *
 * 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 "jpipkakdataset.h"

CPL_CVSID("$Id$")

/*
** The following are for testing premature stream termination support.
** This is a mechanism to test handling of failed or incomplete reads
** from the server, and is not normally active.  For this reason we
** don't worry about the non-threadsafe nature of the debug support
** variables below.
*/

#ifdef DEBUG
#  define PST_DEBUG 1
#endif

#ifdef PST_DEBUG
static int nPSTTargetInstance = -1;
static int nPSTThisInstance = -1;
static int nPSTTargetOffset = -1;
#endif

static bool kakadu_initialized = false;

/************************************************************************/
/* ==================================================================== */
/*                     Set up messaging services                        */
/* ==================================================================== */
/************************************************************************/

class jpipkak_kdu_cpl_error_message : public kdu_message
{
public: // Member classes
    using kdu_message::put_text;

    explicit jpipkak_kdu_cpl_error_message( CPLErr eErrClass ) :
        m_eErrClass ( eErrClass ),
        m_pszError ( nullptr )
    {}

    void put_text(const char *string) override
    {
        if( m_pszError == nullptr )
            m_pszError = CPLStrdup( string );
        else
        {
            m_pszError = (char *)
                CPLRealloc(m_pszError, strlen(m_pszError) + strlen(string)+1 );
            strcat( m_pszError, string );
        }
    }

    class JP2KAKException
    {
    };

    void flush(bool end_of_message=false) override
    {
        if( m_pszError == nullptr )
            return;
        if( m_pszError[strlen(m_pszError)-1] == '\n' )
            m_pszError[strlen(m_pszError)-1] = '\0';

        CPLError( m_eErrClass, CPLE_AppDefined, "%s", m_pszError );
        CPLFree( m_pszError );
        m_pszError = nullptr;

        if( end_of_message && m_eErrClass == CE_Failure )
        {
            throw new JP2KAKException();
        }
    }

private:
    CPLErr m_eErrClass;
    char *m_pszError;
};

/************************************************************************/
/* ==================================================================== */
/*                            JPIPKAKRasterBand                         */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                         JPIPKAKRasterBand()                          */
/************************************************************************/

JPIPKAKRasterBand::JPIPKAKRasterBand( int nBandIn, int nDiscardLevelsIn,
                                      kdu_codestream *oCodeStreamIn,
                                      int nResCount,
                                      JPIPKAKDataset *poBaseDSIn )

{
    this->nBand = nBandIn;
    poBaseDS = poBaseDSIn;

    eDataType = poBaseDSIn->eDT;

    this->nDiscardLevels = nDiscardLevelsIn;
    this->oCodeStream = oCodeStreamIn;

    oCodeStream->apply_input_restrictions( 0, 0, nDiscardLevels, 0, nullptr );
    oCodeStream->get_dims( 0, band_dims );

    nRasterXSize = band_dims.size.x;
    nRasterYSize = band_dims.size.y;

/* -------------------------------------------------------------------- */
/*      Use a 2048x128 "virtual" block size unless the file is small.    */
/* -------------------------------------------------------------------- */
    if( nRasterXSize >= 2048 )
        nBlockXSize = 2048;
    else
        nBlockXSize = nRasterXSize;

    if( nRasterYSize >= 256 )
        nBlockYSize = 128;
    else
        nBlockYSize = nRasterYSize;

/* -------------------------------------------------------------------- */
/*      Figure out the color interpretation for this band.              */
/* -------------------------------------------------------------------- */

    eInterp = GCI_Undefined;

/* -------------------------------------------------------------------- */
/*      Do we have any overviews?  Only check if we are the full res    */
/*      image.                                                          */
/* -------------------------------------------------------------------- */
    nOverviewCount = 0;
    papoOverviewBand = nullptr;

    if( nDiscardLevels == 0 )
    {
        int nXSize = nRasterXSize;
        int nYSize = nRasterYSize;

        for( int nDiscard = 1; nDiscard < nResCount; nDiscard++ )
        {

            nXSize = (nXSize + 1) / 2;
            nYSize = (nYSize + 1) / 2;

            if( (nXSize+nYSize) < 128 || nXSize < 4 || nYSize < 4 )
                continue; /* skip super reduced resolution layers */

            oCodeStream->apply_input_restrictions( 0, 0, nDiscard, 0, nullptr );
            kdu_dims dims;
            oCodeStream->get_dims( 0, dims );

            if( (dims.size.x == nXSize || dims.size.x == nXSize-1)
                && (dims.size.y == nYSize || dims.size.y == nYSize-1) )
            {
                nOverviewCount++;
                papoOverviewBand = (JPIPKAKRasterBand **)
                    CPLRealloc( papoOverviewBand,
                                sizeof(void*) * nOverviewCount );
                papoOverviewBand[nOverviewCount-1] =
                    new JPIPKAKRasterBand( nBand, nDiscard, oCodeStream, 0,
                                           poBaseDS );
            }
            else
            {
                CPLDebug( "GDAL", "Discard %dx%d JPEG2000 overview layer,\n"
                          "expected %dx%d.",
                          dims.size.x, dims.size.y, nXSize, nYSize );
            }
        }
    }
}

/************************************************************************/
/*                         ~JPIPKAKRasterBand()                          */
/************************************************************************/

JPIPKAKRasterBand::~JPIPKAKRasterBand()

{
    for( int i = 0; i < nOverviewCount; i++ )
        delete papoOverviewBand[i];

    CPLFree( papoOverviewBand );
}

/************************************************************************/
/*                          GetOverviewCount()                          */
/************************************************************************/

int JPIPKAKRasterBand::GetOverviewCount()

{
    return nOverviewCount;
}

/************************************************************************/
/*                            GetOverview()                             */
/************************************************************************/

GDALRasterBand *JPIPKAKRasterBand::GetOverview( int iOverviewIndex )

{
    if( iOverviewIndex < 0 || iOverviewIndex >= nOverviewCount )
        return nullptr;
    else
        return papoOverviewBand[iOverviewIndex];
}

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

CPLErr JPIPKAKRasterBand::IReadBlock( int nBlockXOff, int nBlockYOff,
                                      void * pImage )
{
    CPLDebug( "JPIPKAK", "IReadBlock(%d,%d) on band %d.",
              nBlockXOff, nBlockYOff, nBand );

/* -------------------------------------------------------------------- */
/*      Fix the buffer layer.                                           */
/* -------------------------------------------------------------------- */
    int nPixelSpace = GDALGetDataTypeSize(eDataType) / 8;
    int nLineSpace = nPixelSpace * nBlockXSize;
    int nBandSpace = nLineSpace * nBlockYSize;

/* -------------------------------------------------------------------- */
/*      Zoom up file window based on overview level so we are           */
/*      referring to the full res image.                                */
/* -------------------------------------------------------------------- */
    int nZoom = 1 << nDiscardLevels;

    int xOff = nBlockXOff * nBlockXSize * nZoom;
    int yOff = nBlockYOff * nBlockYSize * nZoom;
    int xSize = nBlockXSize * nZoom;
    int ySize = nBlockYSize * nZoom;

    int nBufXSize = nBlockXSize;
    int nBufYSize = nBlockYSize;

/* -------------------------------------------------------------------- */
/*      Make adjustments for partial blocks on right and bottom.        */
/* -------------------------------------------------------------------- */
    if( xOff + xSize > poBaseDS->GetRasterXSize() )
    {
        xSize = poBaseDS->GetRasterXSize() - xOff;
        nBufXSize= MAX(xSize/nZoom,1);
    }

    if( yOff + ySize > poBaseDS->GetRasterYSize() )
    {
        ySize = poBaseDS->GetRasterYSize() - yOff;
        nBufYSize = MAX(ySize/nZoom,1);
    }

/* -------------------------------------------------------------------- */
/*      Start the reader and run till complete.                         */
/* -------------------------------------------------------------------- */
    GDALAsyncReader* ario = poBaseDS->
        BeginAsyncReader(xOff, yOff, xSize, ySize,
                         pImage, nBufXSize,nBufYSize,
                         eDataType, 1, &nBand,
                         nPixelSpace, nLineSpace, nBandSpace, nullptr);

    if( ario == nullptr )
        return CE_Failure;

    int nXBufOff; // absolute x image offset
    int nYBufOff; // absolute y image offset
    int nXBufSize;
    int nYBufSize;

    GDALAsyncStatusType status;

    do
    {
        status = ario->GetNextUpdatedRegion(-1.0,
                                            &nXBufOff, &nYBufOff,
                                            &nXBufSize, &nYBufSize );
    } while (status != GARIO_ERROR && status != GARIO_COMPLETE );

    poBaseDS->EndAsyncReader(ario);

    if (status == GARIO_ERROR)
        return CE_Failure;
    else
        return CE_None;
}

/************************************************************************/
/*                             IRasterIO()                              */
/************************************************************************/

CPLErr
JPIPKAKRasterBand::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)

{
/* -------------------------------------------------------------------- */
/*      We need various criteria to skip out to block based methods.    */
/* -------------------------------------------------------------------- */
    int anBand[1] = { nBand };
    if( poBaseDS->TestUseBlockIO( nXOff, nYOff, nXSize, nYSize,
                                  nBufXSize, nBufYSize,
                                  eBufType, 1, &anBand[0] ) )
        return GDALPamRasterBand::IRasterIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize,
            pData, nBufXSize, nBufYSize, eBufType,
            nPixelSpace, nLineSpace, psExtraArg );

/* -------------------------------------------------------------------- */
/*      Otherwise do this as a single uncached async rasterio.          */
/* -------------------------------------------------------------------- */
    GDALAsyncReader* ario =
        poBaseDS->BeginAsyncReader(nXOff, nYOff, nXSize, nYSize,
                                   pData, nBufXSize, nBufYSize, eBufType,
                                   1, &anBand[0],
                                   static_cast<int>(nPixelSpace),
                                   static_cast<int>(nLineSpace), 0, nullptr);

    if( ario == nullptr )
        return CE_Failure;

    GDALAsyncStatusType status;

    do
    {
        int nXBufOff,nYBufOff,nXBufSize,nYBufSize;

        status = ario->GetNextUpdatedRegion(-1.0,
                                            &nXBufOff, &nYBufOff,
                                            &nXBufSize, &nYBufSize );
    } while (status != GARIO_ERROR && status != GARIO_COMPLETE );

    poBaseDS->EndAsyncReader(ario);

    if (status == GARIO_ERROR)
        return CE_Failure;
    else
        return CE_None;
}

/*****************************************/
/*         JPIPKAKDataset()              */
/*****************************************/
JPIPKAKDataset::JPIPKAKDataset()
{
    adfGeoTransform[0] = 0.0;
    adfGeoTransform[1] = 1.0;
    adfGeoTransform[2] = 0.0;
    adfGeoTransform[3] = 0.0;
    adfGeoTransform[4] = 0.0;
    adfGeoTransform[5] = 1.0;

    pGlobalMutex = CPLCreateMutex();
    CPLReleaseMutex(pGlobalMutex);
}

/*****************************************/
/*         ~JPIPKAKDataset()             */
/*****************************************/
JPIPKAKDataset::~JPIPKAKDataset()
{
    char** papszOptions = nullptr;
    papszOptions = CSLSetNameValue(papszOptions,
                        "CLOSE_PERSISTENT", CPLSPrintf("JPIPKAK:%p", this));
    CPLHTTPDestroyResult(CPLHTTPFetch("", papszOptions));
    CSLDestroy(papszOptions);

    Deinitialize();

    CPLFree(pszPath);

    if (nGCPCount > 0 )
    {
        GDALDeinitGCPs( nGCPCount, pasGCPList );
        CPLFree( pasGCPList );
    }
}

/************************************************************************/
/*                            Deinitialize()                            */
/*                                                                      */
/*      Cleanup stuff that we will rebuild during a                     */
/*      reinitialization.                                               */
/************************************************************************/

void JPIPKAKDataset::Deinitialize()

{
    CPLFree(pszCid);
    pszCid = nullptr;

    // frees decompressor as well
    if (poCodestream)
    {
        poCodestream->destroy();
        delete poCodestream;
        poCodestream = nullptr;
    }

    delete poDecompressor;
    poDecompressor = nullptr;

    delete poCache;
    poCache = nullptr;

    bNeedReinitialize = TRUE;
}

/************************************************************************/
/*                          KakaduInitialize()                          */
/************************************************************************/

void JPIPKAKDataset::KakaduInitialize()

{
/* -------------------------------------------------------------------- */
/*      Initialize Kakadu warning/error reporting subsystem.            */
/* -------------------------------------------------------------------- */
    if( !kakadu_initialized )
    {
        kakadu_initialized = true;

        jpipkak_kdu_cpl_error_message oErrHandler( CE_Failure );
        jpipkak_kdu_cpl_error_message oWarningHandler( CE_Warning );
        CPL_IGNORE_RET_VAL(oErrHandler);
        CPL_IGNORE_RET_VAL(oWarningHandler);

        kdu_customize_warnings(new jpipkak_kdu_cpl_error_message( CE_Warning ) );
        kdu_customize_errors(new jpipkak_kdu_cpl_error_message( CE_Failure ) );
    }
}

/*****************************************/
/*         Initialize()                  */
/*****************************************/
int JPIPKAKDataset::Initialize(const char* pszDatasetName, int bReinitializing )
{
    KakaduInitialize();

    // create necessary http headers
    CPLString osHeaders = "HEADERS=Accept: jpp-stream";
    CPLString osPersistent;

    osPersistent.Printf( "PERSISTENT=JPIPKAK:%p", this );

    char *apszOptions[] = {
        (char *) osHeaders.c_str(),
        (char *) osPersistent.c_str(),
        nullptr
    };

    // Setup url to have http in place of jpip protocol indicator.
    CPLString osURL = "http";
    osURL += (pszDatasetName + 4);

    CPLAssert( STARTS_WITH(pszDatasetName, "jpip") );

    // make initial request to the server for a session, we are going to
    // assume that the jpip communication is stateful, rather than one-shot
    // stateless requests append pszUrl with jpip request parameters for a
    // stateful session (multi-shot communications)
    // "cnew=http&type=jpp-stream&stream=0&tid=0&len="
    CPLString osRequest;
    osRequest.Printf("%s?%s%i", osURL.c_str(),
                     "cnew=http&type=jpp-stream&stream=0&tid=0&len=", 2000);

    CPLHTTPResult *psResult = CPLHTTPFetch(osRequest, apszOptions);

    if ( psResult == nullptr)
        return FALSE;

    if( psResult->nDataLen == 0 || psResult->pabyData == nullptr  )
    {

        CPLError(CE_Failure, CPLE_AppDefined,
                 "No data was returned from the given URL" );
        CPLHTTPDestroyResult( psResult );
        return FALSE;
    }

    if (psResult->nStatus != 0)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Curl reports error: %d: %s", psResult->nStatus, psResult->pszErrBuf );
        CPLHTTPDestroyResult( psResult );
        return FALSE;
    }

    // parse the response headers, and the initial data until we get to the
    // codestream definition
    char** pszHdrs = psResult->papszHeaders;
    const char* pszCnew = CSLFetchNameValue(pszHdrs, "JPIP-cnew");

    if( pszCnew == nullptr )
    {
        if( psResult->pszContentType != nullptr
            && STARTS_WITH_CI(psResult->pszContentType, "text/html") )
            CPLDebug( "JPIPKAK", "%s",
                      psResult->pabyData );

        CPLHTTPDestroyResult( psResult );
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to parse required cnew and tid response headers" );

        return FALSE;
    }

    // parse cnew response
    // JPIP-cnew:
    // cid=DC69DF980A641A4BBDEB50E484A66578,path=MyPath,transport=http
    char **papszTokens = CSLTokenizeString2( pszCnew, ",", CSLT_HONOURSTRINGS);
    for (int i = 0; i < CSLCount(papszTokens); i++)
    {
        // looking for cid, path
        if (STARTS_WITH_CI(papszTokens[i], "cid"))
        {
            char *pszKey = nullptr;
            const char *pszValue = CPLParseNameValue(papszTokens[i], &pszKey );
            pszCid = CPLStrdup(pszValue);
            CPLFree( pszKey );
        }

        if (STARTS_WITH_CI(papszTokens[i], "path"))
        {
            char *pszKey = nullptr;
            const char *pszValue = CPLParseNameValue(papszTokens[i], &pszKey );
            pszPath = CPLStrdup(pszValue);
            CPLFree( pszKey );
        }
    }

    CSLDestroy(papszTokens);

    if( pszPath == nullptr || pszCid == nullptr )
    {
        CPLHTTPDestroyResult(psResult);
        CPLError(CE_Failure, CPLE_AppDefined, "Error parsing path and cid from cnew - %s", pszCnew);
        return FALSE;
    }

    // Okay, good to go with JPIP, get to the codestream before returning
    // successful initialization of the driver
    try
    {
        poCache = new kdu_cache();
        poCodestream = new kdu_codestream();
        poDecompressor = new kdu_region_decompressor();

        int bFinished = FALSE;
        int bError = FALSE;
        bFinished = ReadFromInput(psResult->pabyData, psResult->nDataLen,
                                  bError );
        CPLHTTPDestroyResult(psResult);

        // continue making requests in the main thread to get all the available
        // metadata for data bin 0, and reach the codestream

        // format the new request
        // and set as pszRequestUrl;
        // get the protocol from the original request
        size_t found = osRequest.find_first_of("/");
        CPLString osProtocol = osRequest.substr(0, found + 2);
        osRequest.erase(0, found + 2);
        // find context path
        found = osRequest.find_first_of("/");
        osRequest.erase(found);

        osRequestUrl.Printf("%s%s/%s?cid=%s&stream=0&len=%i", osProtocol.c_str(), osRequest.c_str(), pszPath, pszCid, 2000);

        while (!bFinished && !bError )
        {
            psResult = CPLHTTPFetch(osRequestUrl, apszOptions);
            bFinished = ReadFromInput(psResult->pabyData, psResult->nDataLen,
                                      bError );
            CPLHTTPDestroyResult(psResult);
        }

        if( bError )
            return FALSE;

        // clean up osRequest, remove variable len= parameter
        size_t pos = osRequestUrl.find_last_of("&");
        osRequestUrl.erase(pos);

        // create codestream
        poCache->set_read_scope(KDU_MAIN_HEADER_DATABIN, 0, 0);
        poCodestream->create(poCache);
        poCodestream->set_persistent();

/* -------------------------------------------------------------------- */
/*      If this is a reinitialization then we can hop out at this       */
/*      point.  The rest of the stuff was already done, and             */
/*      hopefully the configuration is unchanged.                       */
/* -------------------------------------------------------------------- */
        if( bReinitializing )
        {
            bNeedReinitialize = FALSE;
            return TRUE;
        }

/* -------------------------------------------------------------------- */
/*      Collect GDAL raster configuration information.                  */
/* -------------------------------------------------------------------- */
        kdu_channel_mapping oChannels;
        oChannels.configure(*poCodestream);
        kdu_coords* ref_expansion = new kdu_coords(1, 1);

        // get available resolutions, image width / height etc.
        kdu_dims view_dims = poDecompressor->
            get_rendered_image_dims(*poCodestream, &oChannels, -1, 0,
                                    *ref_expansion, *ref_expansion,
                                    KDU_WANT_OUTPUT_COMPONENTS);

        nRasterXSize = view_dims.size.x;
        nRasterYSize = view_dims.size.y;

        // Establish the datatype - we will use the same datatype for
        // all bands based on the first.  This really doesn't do something
        // great for >16 bit images.
        if( poCodestream->get_bit_depth(0) > 8
            && poCodestream->get_signed(0) )
        {
            eDT = GDT_Int16;
        }
        else if( poCodestream->get_bit_depth(0) > 8
                 && !poCodestream->get_signed(0) )
        {
            eDT = GDT_UInt16;
        }
        else
            eDT = GDT_Byte;

        if( (poCodestream->get_bit_depth(0) % 8) != 0
            && poCodestream->get_bit_depth(0) < 16 )
            SetMetadataItem(
                "NBITS",
                CPLString().Printf("%d",poCodestream->get_bit_depth(0)),
                "IMAGE_STRUCTURE" );

        // TODO add color interpretation

        // calculate overviews
        siz_params* siz_in = poCodestream->access_siz();
        kdu_params* cod_in = siz_in->access_cluster("COD");

        delete ref_expansion;

        siz_in->get("Scomponents", 0, 0, nComps);
        siz_in->get("Sprecision", 0, 0, nBitDepth);

        cod_in->get("Clayers", 0, 0, nQualityLayers);
        cod_in->get("Clevels", 0, 0, nResLevels);

        bYCC=TRUE;
        cod_in->get("Cycc", 0, 0, bYCC);
    }
    catch(...)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Trapped Kakadu exception attempting to initialize JPIP access." );
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      YCC images are always processed as 3 bands.                     */
/* -------------------------------------------------------------------- */
    if( bYCC )
        nBands = 3;
    else
        nBands = nComps;

/* -------------------------------------------------------------------- */
/*      Setup band objects.                                             */
/* -------------------------------------------------------------------- */
    int iBand;

    for( iBand = 1; iBand <= nBands; iBand++ )
    {
        JPIPKAKRasterBand *poBand =
            new JPIPKAKRasterBand(iBand,0,poCodestream,nResLevels,
                                  this );

        SetBand( iBand, poBand );
    }

    // set specific metadata items
    CPLString osNQualityLayers;
    osNQualityLayers.Printf("%i", nQualityLayers);
    CPLString osNResolutionLevels;
    osNResolutionLevels.Printf("%i", nResLevels);
    CPLString osNComps;
    osNComps.Printf("%i", nComps);
    CPLString osBitDepth;
    osBitDepth.Printf("%i", nBitDepth);

    SetMetadataItem("JPIP_NQUALITYLAYERS", osNQualityLayers.c_str(), "JPIP");
    SetMetadataItem("JPIP_NRESOLUTIONLEVELS", osNResolutionLevels.c_str(), "JPIP");
    SetMetadataItem("JPIP_NCOMPS", osNComps.c_str(), "JPIP");
    SetMetadataItem("JPIP_SPRECISION", osBitDepth.c_str(), "JPIP");

    if( bYCC )
        SetMetadataItem("JPIP_YCC", "YES", "JPIP");
    else
        SetMetadataItem("JPIP_YCC", "NO", "JPIP");

/* ==================================================================== */
/*      Parse geojp2, or gmljp2, we will assume that the core           */
/*      metadata  of gml or a geojp2 uuid have been sent in the         */
/*      initial metadata response.                                      */
/*      If the server has used placeholder boxes for this               */
/*      information then the image will be interpreted as x,y           */
/* ==================================================================== */
    GDALJP2Metadata oJP2Geo;
    int nLen = poCache->get_databin_length(KDU_META_DATABIN, nCodestream, 0);

    if( nLen == 0 )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to open stream to parse metadata boxes" );
        return FALSE;
    }

    // create in memory file using vsimem
    CPLString osFileBoxName;
    osFileBoxName.Printf("/vsimem/jpip/%s.dat", pszCid);
    VSILFILE *fpLL = VSIFOpenL(osFileBoxName.c_str(), "w+");
    poCache->set_read_scope(KDU_META_DATABIN, nCodestream, 0);
    kdu_byte* pabyBuffer = (kdu_byte *)CPLMalloc(nLen);
    poCache->read(pabyBuffer, nLen);
    VSIFWriteL(pabyBuffer, nLen, 1, fpLL);
    CPLFree( pabyBuffer );

    VSIFFlushL(fpLL);
    VSIFSeekL(fpLL, 0, SEEK_SET);

    nPamFlags |= GPF_NOSAVE;

    try
    {
        oJP2Geo.ReadBoxes(fpLL);
        // parse gml first, followed by geojp2 as a fallback
        if (oJP2Geo.ParseGMLCoverageDesc() || oJP2Geo.ParseJP2GeoTIFF())
        {
            m_oSRS = oJP2Geo.m_oSRS;
            bGeoTransformValid = TRUE;

            memcpy(adfGeoTransform, oJP2Geo.adfGeoTransform,
                   sizeof(double) * 6 );
            nGCPCount = oJP2Geo.nGCPCount;
            pasGCPList = oJP2Geo.pasGCPList;

            oJP2Geo.pasGCPList = nullptr;
            oJP2Geo.nGCPCount = 0;

            int iBox;

            for( iBox = 0;
                 oJP2Geo.papszGMLMetadata
                     && oJP2Geo.papszGMLMetadata[iBox] != nullptr;
                 iBox++ )
            {
                char *pszName = nullptr;
                const char *pszXML =
                    CPLParseNameValue( oJP2Geo.papszGMLMetadata[iBox],
                                       &pszName );
                CPLString osDomain;
                char *apszMDList[2];

                osDomain.Printf( "xml:%s", pszName );
                apszMDList[0] = (char *) pszXML;
                apszMDList[1] = nullptr;

                GDALPamDataset::SetMetadata( apszMDList, osDomain );
                CPLFree( pszName );
            }
        }
        else
        {
            // treat as Cartesian, no geo metadata
            CPLError(CE_Warning, CPLE_AppDefined,
                     "Parsed metadata boxes from jpip stream, geographic metadata not found - is the server using placeholders for this data?" );
        }
    }
    catch(...)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unable to parse geographic metadata boxes from jpip stream" );
    }

    VSIFCloseL(fpLL);
    VSIUnlink( osFileBoxName.c_str());

    bNeedReinitialize = FALSE;

    return TRUE;
}

/******************************************/
/*           ReadVBAS()                   */
/******************************************/
long JPIPKAKDataset::ReadVBAS(GByte* pabyData, int nLen )
{
    int c = -1;
    long val = 0;
    nVBASLen = 0;

    while ((c & 0x80) != 0)
    {
        if (nVBASLen >= 9)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "VBAS Length not supported");
            return -1;
        }

        if (nPos > nLen)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "EOF reached before completing VBAS");
            return -1;
        }

#ifdef PST_DEBUG
        if( nPSTThisInstance == nPSTTargetInstance
            && nPos >= nPSTTargetOffset )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Artificial PST EOF reached before completing VBAS");
            return -1;
        }
#endif

        c = pabyData[nPos];
        nPos++;

        val = (val << 7) | (long)(c & 0x7F);

        if (nVBASLen == 0)
            nVBASFirstByte = c;

        nVBASLen++;
    }

    return val;
}

/******************************************/
/*            ReadSegment()               */
/******************************************/
JPIPDataSegment* JPIPKAKDataset::ReadSegment(GByte* pabyData, int nLen,
                                             int& bError )
{
    long nId = ReadVBAS(pabyData, nLen);
    bError = FALSE;

    if (nId < 0)
    {
        bError = TRUE;
        return nullptr;
    }
    else
    {
        JPIPDataSegment* segment = new JPIPDataSegment();
        segment->SetId(nId);

        if (nVBASFirstByte == 0)
        {
            segment->SetEOR(TRUE);
            segment->SetId(pabyData[nPos]);
        }
        else
        {
            segment->SetEOR(FALSE);
            nId &= ~(0x70 << ((nVBASLen -1) * 7));
            segment->SetId(nId);
            segment->SetFinal((nVBASFirstByte & 0x10) != 0);

            int m = (nVBASFirstByte & 0x7F) >> 5;

            if (m == 0)
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Invalid Bin-ID value format");
                bError = TRUE;
                delete segment;
                return nullptr;
            }
            else if (m >= 2) {
                nClassId = static_cast<int>(ReadVBAS(pabyData, nLen));
                if (m > 2)
                {
                    nCodestream = static_cast<int>(ReadVBAS(pabyData, nLen));
                    if( nCodestream < 0 )
                    {
                        bError = TRUE;
                        delete segment;
                        return nullptr;
                    }
                }
            }

            long nNextVal;

            segment->SetClassId(nClassId);
            segment->SetCodestreamIdx(nCodestream);

            nNextVal = ReadVBAS(pabyData, nLen);
            if( nNextVal == -1 )
            {
                bError = TRUE;
                delete segment;
                return nullptr;
            }
            else
                segment->SetOffset( nNextVal );

            nNextVal = ReadVBAS(pabyData, nLen);
            if( nNextVal == -1 )
            {
                bError = TRUE;
                delete segment;
                return nullptr;
            }
            else
                segment->SetLen(nNextVal);
        }

        if ((segment->GetLen() > 0) && (!segment->IsEOR()))
        {
            GByte* pabyDataSegment = (GByte *) CPLCalloc(1,segment->GetLen());

            // copy data from input array pabyData to the data segment
            memcpy(pabyDataSegment,
                   pabyData + nPos,
                   segment->GetLen());

            segment->SetData(pabyDataSegment);
        }

        nPos += segment->GetLen();

        if (!segment->IsEOR())
            nDatabins++;

        if ((segment->GetId() == JPIPKAKDataset::JPIP_EOR_WINDOW_DONE) && (segment->IsEOR()))
            bWindowDone = TRUE;

        return segment;
    }
}

/******************************************/
/*           KakaduClassId()              */
/******************************************/
int JPIPKAKDataset::KakaduClassId(int nClassIdIn)
{
    if (nClassIdIn == 0)
        return KDU_PRECINCT_DATABIN;
    else if (nClassIdIn == 2)
        return KDU_TILE_HEADER_DATABIN;
    else if (nClassIdIn == 6)
        return KDU_MAIN_HEADER_DATABIN;
    else if (nClassIdIn == 8)
        return KDU_META_DATABIN;
    else if (nClassIdIn == 4)
        return KDU_TILE_DATABIN;
    else
        return -1;
}

/******************************************/
/*            ReadFromInput()             */
/******************************************/
int JPIPKAKDataset::ReadFromInput(GByte* pabyData, int nLen, int &bError )
{
    int res = FALSE;
    bError = FALSE;

    if (nLen <= 0 )
        return FALSE;

#ifdef PST_DEBUG
    nPSTThisInstance++;
    if( CPLGetConfigOption( "PST_OFFSET", nullptr ) != nullptr )
    {
        nPSTTargetOffset = atoi(CPLGetConfigOption("PST_OFFSET","0"));
        nPSTTargetInstance = 0;
    }
    if( CPLGetConfigOption( "PST_INSTANCE", nullptr ) != nullptr )
    {
        nPSTTargetInstance = atoi(CPLGetConfigOption("PST_INSTANCE","0"));
    }

    if( nPSTTargetOffset != -1 && nPSTThisInstance == 0 )
    {
        CPLDebug( "JPIPKAK", "Premature Stream Termination Activated, PST_OFFSET=%d, PST_INSTANCE=%d",
                  nPSTTargetOffset, nPSTTargetInstance );
    }
    if( nPSTTargetOffset != -1
        && nPSTThisInstance == nPSTTargetInstance )
    {
        CPLDebug( "JPIPKAK", "Premature Stream Termination in force for this input instance, PST_OFFSET=%d, data length=%d",
                  nPSTTargetOffset, nLen );
    }
#endif // def PST_DEBUG

    // parse the data stream, reading the vbas and adding to the kakadu cache
    // we could parse all the boxes by hand, and just add data to the kakadu cache
    // we will do it the easy way and retrieve the metadata through the kakadu query api

    nPos = 0;
    JPIPDataSegment* pSegment = nullptr;

    while ((pSegment = ReadSegment(pabyData, nLen, bError)) != nullptr)
    {
        if (pSegment->IsEOR())
        {
            if ((pSegment->GetId() == JPIPKAKDataset::JPIP_EOR_IMAGE_DONE) ||
                (pSegment->GetId() == JPIPKAKDataset::JPIP_EOR_WINDOW_DONE))
                res = TRUE;

            delete pSegment;
            break;
        }
        else
        {
            // add data to kakadu
            //CPLDebug("JPIPKAK", "Parsed JPIP Segment class=%i stream=%i id=%i offset=%i len=%i isFinal=%i isEOR=%i", pSegment->GetClassId(), pSegment->GetCodestreamIdx(), pSegment->GetId(), pSegment->GetOffset(), pSegment->GetLen(), pSegment->IsFinal(), pSegment->IsEOR());
            poCache->add_to_databin(KakaduClassId(static_cast<int>(pSegment->GetClassId())), pSegment->GetCodestreamIdx(),
                                    pSegment->GetId(), pSegment->GetData(),
                                    static_cast<int>(pSegment->GetOffset()),
                                    static_cast<int>(pSegment->GetLen()), pSegment->IsFinal());

            delete pSegment;
        }
    }

    return res;
}

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

const OGRSpatialReference *JPIPKAKDataset::GetSpatialRef() const

{
    if( !m_oSRS.IsEmpty() )
        return &m_oSRS;
    else
        return GDALPamDataset::GetSpatialRef();
}

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

CPLErr JPIPKAKDataset::GetGeoTransform( double * padfTransform )

{
    if( bGeoTransformValid )
    {
        memcpy( padfTransform, adfGeoTransform, sizeof(double)*6 );

        return CE_None;
    }
    else
        return GDALPamDataset::GetGeoTransform( padfTransform );
}

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

int JPIPKAKDataset::GetGCPCount()

{
    return nGCPCount;
}

/************************************************************************/
/*                           GetGCPSpatialRef()                         */
/************************************************************************/

const OGRSpatialReference *JPIPKAKDataset::GetGCPSpatialRef() const

{
    if( nGCPCount > 0 && !m_oSRS.IsEmpty() )
        return &m_oSRS;
    else
        return nullptr;
}

/************************************************************************/
/*                               GetGCP()                               */
/************************************************************************/

const GDAL_GCP *JPIPKAKDataset::GetGCPs()

{
    return pasGCPList;
}

/************************************************************************/
/*                             IRasterIO()                              */
/************************************************************************/

CPLErr JPIPKAKDataset::IRasterIO( GDALRWFlag eRWFlag,
                                  int nXOff, int nYOff, int nXSize, int nYSize,
                                  void * pData, int nBufXSize, int nBufYSize,
                                  GDALDataType eBufType,
                                  int nBandCount, int *panBandMap,
                                  GSpacing nPixelSpace, GSpacing nLineSpace,
                                  GSpacing nBandSpace,
                                  GDALRasterIOExtraArg* psExtraArg)

{
/* -------------------------------------------------------------------- */
/*      We need various criteria to skip out to block based methods.    */
/* -------------------------------------------------------------------- */
    if( TestUseBlockIO( nXOff, nYOff, nXSize, nYSize, nBufXSize, nBufYSize,
                        eBufType, nBandCount, panBandMap ) )
        return GDALPamDataset::IRasterIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize,
            pData, nBufXSize, nBufYSize, eBufType,
            nBandCount, panBandMap, nPixelSpace, nLineSpace, nBandSpace, psExtraArg );

/* -------------------------------------------------------------------- */
/*      Otherwise do this as a single uncached async rasterio.          */
/* -------------------------------------------------------------------- */
    GDALAsyncReader* ario =
        BeginAsyncReader(nXOff, nYOff, nXSize, nYSize,
                         pData, nBufXSize, nBufYSize, eBufType,
                         nBandCount, panBandMap,
                         static_cast<int>(nPixelSpace),
                         static_cast<int>(nLineSpace),
                         static_cast<int>(nBandSpace), nullptr);

    if( ario == nullptr )
        return CE_Failure;

    GDALAsyncStatusType status;

    do
    {
        int nXBufOff,nYBufOff,nXBufSize,nYBufSize;

        status = ario->GetNextUpdatedRegion(-1.0,
                                            &nXBufOff, &nYBufOff,
                                            &nXBufSize, &nYBufSize );
    } while (status != GARIO_ERROR && status != GARIO_COMPLETE );

    EndAsyncReader(ario);

    if (status == GARIO_ERROR)
        return CE_Failure;
    else
        return CE_None;
}

/************************************************************************/
/*                           TestUseBlockIO()                           */
/************************************************************************/

int
JPIPKAKDataset::TestUseBlockIO( CPL_UNUSED int nXOff, CPL_UNUSED int nYOff,
                                int nXSize, int nYSize,
                                int nBufXSize, int nBufYSize,
                                CPL_UNUSED GDALDataType eDataType,
                                int nBandCount, const int *panBandList ) const

{
/* -------------------------------------------------------------------- */
/*      Due to limitations in DirectRasterIO() we can only handle       */
/*      it no duplicates in the band list.                              */
/* -------------------------------------------------------------------- */
    int i, j;

    for( i = 0; i < nBandCount; i++ )
    {
        for( j = i+1; j < nBandCount; j++ )
            if( panBandList[j] == panBandList[i] )
                return TRUE;
    }

/* -------------------------------------------------------------------- */
/*      The rest of the rules are io strategy stuff, and use            */
/*      configuration checks.                                           */
/* -------------------------------------------------------------------- */
    int bUseBlockedIO = bForceCachedIO;

    if( nYSize == 1 || nXSize * ((double) nYSize) < 100.0 )
        bUseBlockedIO = TRUE;

    if( nBufYSize == 1 || nBufXSize * ((double) nBufYSize) < 100.0 )
        bUseBlockedIO = TRUE;

    if( bUseBlockedIO
        && CPLTestBool( CPLGetConfigOption( "GDAL_ONE_BIG_READ", "NO") ) )
        bUseBlockedIO = FALSE;

    return bUseBlockedIO;
}

/*************************************************************************/
/*                     BeginAsyncReader()                              */
/*************************************************************************/

GDALAsyncReader*
JPIPKAKDataset::BeginAsyncReader(int xOff, int yOff,
                                   int xSize, int ySize,
                                   void *pBuf,
                                   int bufXSize, int bufYSize,
                                   GDALDataType bufType,
                                   int nBandCount, int* pBandMap,
                                   int nPixelSpace, int nLineSpace,
                                   int nBandSpace,
                                   char **papszOptions)
{
    CPLDebug( "JPIP", "BeginAsyncReadeR(%d,%d,%d,%d -> %dx%d)",
              xOff, yOff, xSize, ySize, bufXSize, bufYSize );

/* -------------------------------------------------------------------- */
/*      Recreate the code stream access if needed.                      */
/* -------------------------------------------------------------------- */
    if( bNeedReinitialize )
    {
        CPLDebug( "JPIPKAK", "\n\nReinitializing after error! ******\n" );

        Deinitialize();
        if( !Initialize(GetDescription(),TRUE) )
            return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Provide default packing if needed.                              */
/* -------------------------------------------------------------------- */
    if( nPixelSpace == 0 )
        nPixelSpace = GDALGetDataTypeSize(bufType) / 8;
    if( nLineSpace == 0 )
        nLineSpace = nPixelSpace * bufXSize;
    if( nBandSpace == 0 )
        nBandSpace = nLineSpace * bufYSize;

/* -------------------------------------------------------------------- */
/*      check we have sensible values for windowing.                    */
/* -------------------------------------------------------------------- */
    if (xOff > GetRasterXSize()
        || yOff > GetRasterYSize()
        || (xOff + xSize) > GetRasterXSize()
        || (yOff + ySize) > GetRasterYSize() )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Requested window (%d,%d %dx%d) off dataset.",
                  xOff, yOff, xSize, ySize );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Record request information.                                     */
/* -------------------------------------------------------------------- */
    JPIPKAKAsyncReader* ario = new JPIPKAKAsyncReader();
    ario->poDS = this;
    ario->nBufXSize = bufXSize;
    ario->nBufYSize = bufYSize;
    ario->eBufType = bufType;
    ario->nBandCount = nBandCount;
    ario->nXOff = xOff;
    ario->nYOff = yOff;
    ario->nXSize = xSize;
    ario->nYSize = ySize;

    ario->panBandMap = new int[nBandCount];
    if (pBandMap)
    {
        for (int i = 0; i < nBandCount; i++)
            ario->panBandMap[i] = pBandMap[i];
    }
    else
    {
        for (int i = 0; i < nBandCount; i++)
            ario->panBandMap[i] = i+1;
    }

/* -------------------------------------------------------------------- */
/*      If the buffer type is of other than images type, we need to     */
/*      allocate a private buffer the same type as the image which      */
/*      will be converted later.                                        */
/* -------------------------------------------------------------------- */
    if( bufType != eDT )
    {
        ario->nPixelSpace = GDALGetDataTypeSize(eDT) / 8;
        ario->nLineSpace = ario->nPixelSpace * bufXSize;
        ario->nBandSpace = ario->nLineSpace * bufYSize;

        ario->nAppPixelSpace = nPixelSpace;
        ario->nAppLineSpace = nLineSpace;
        ario->nAppBandSpace = nBandSpace;

        ario->pBuf = VSI_MALLOC3_VERBOSE(bufXSize,bufYSize,ario->nPixelSpace*nBandCount);
        if( ario->pBuf == nullptr )
        {
            delete ario;
            return nullptr;
        }

        ario->pAppBuf = pBuf;
    }
    else
    {
        ario->pBuf = pBuf;
        ario->pAppBuf = pBuf;

        ario->nAppPixelSpace = nPixelSpace;
        ario->nPixelSpace = nPixelSpace;

        ario->nAppLineSpace = nLineSpace;
        ario->nLineSpace = nLineSpace;

        ario->nAppBandSpace = nBandSpace;
        ario->nBandSpace = nBandSpace;
    }

    // parse options
    const char* pszLevel = CSLFetchNameValue(papszOptions, "LEVEL");
    const char* pszLayers = CSLFetchNameValue(papszOptions, "LAYERS");
    const char* pszPriority = CSLFetchNameValue(papszOptions, "PRIORITY");

    if (pszLayers)
        ario->nQualityLayers = atoi(pszLayers);
    else
        ario->nQualityLayers = nQualityLayers;

    if (pszPriority)
    {
        if (EQUAL(pszPriority, "0"))
            ario->bHighPriority = 0;
        else
            ario->bHighPriority = 1;
    }
    else
        ario->bHighPriority = 1;

/* -------------------------------------------------------------------- */
/*      Select an appropriate level based on the ratio of buffer        */
/*      size to full resolution image.  We aim for the next             */
/*      resolution *lower* than we might expect for the target          */
/*      buffer unless it falls on a power of two.  This is because      */
/*      the region decompressor only seems to support upsampling        */
/*      via the numerator/denominator magic.                            */
/* -------------------------------------------------------------------- */
    if (pszLevel)
        ario->nLevel = atoi(pszLevel);
    else
    {
        int nRXSize = xSize;
        int nRYSize = ySize;
        ario->nLevel = 0;

        while( ario->nLevel < nResLevels
               && (nRXSize > bufXSize || nRYSize > bufYSize) )
        {
            nRXSize = (nRXSize+1) / 2;
            nRYSize = (nRYSize+1) / 2;
            ario->nLevel++;
        }
    }

    ario->Start();

    return ario;
}

/************************************************************************/
/*                  EndAsyncReader()                                  */
/************************************************************************/

void JPIPKAKDataset::EndAsyncReader(GDALAsyncReader *poARIO)
{
    delete poARIO;
}

/*****************************************/
/*             Open()                    */
/*****************************************/
GDALDataset *JPIPKAKDataset::Open(GDALOpenInfo * poOpenInfo)
{
    // test jpip and jpips, assuming jpip is using http as the transport layer
    // jpip = http, jpips = https (note SSL is allowed, but jpips is not in the ISO spec)
    if (STARTS_WITH_CI(poOpenInfo->pszFilename, "jpip://")
        || STARTS_WITH_CI(poOpenInfo->pszFilename, "jpips://"))
    {
        // perform the initial connection
        // using cpl_http for the connection
        if  (CPLHTTPEnabled() == TRUE)
        {
            JPIPKAKDataset *poDS = new JPIPKAKDataset();
            if (poDS->Initialize(poOpenInfo->pszFilename,FALSE))
            {
                poDS->SetDescription( poOpenInfo->pszFilename );
                return poDS;
            }
            else
            {
                delete poDS;
                return nullptr;
            }
        }
        else
        {
            CPLError( CE_Failure, CPLE_OpenFailed,
                      "Failed to open %s within JPIPKAK driver CPL HTTP not enabled.\n",
                      poOpenInfo->pszFilename );
            return nullptr;
        }
    }
    else
        return nullptr;
}

/************************************************************************/
/*                        GDALRegister_JPIPKAK()                        */
/************************************************************************/

void GDALRegister_JPIPKAK()
{
    if( !GDAL_CHECK_VERSION( "JPIPKAK driver" ) )
        return;

    if( GDALGetDriverByName( "JPIPKAK" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "JPIPKAK" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "JPIP (based on Kakadu)" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/raster/jpipkak.html" );
    poDriver->SetMetadataItem( GDAL_DMD_MIMETYPE, "image/jpp-stream" );

    poDriver->pfnOpen = JPIPKAKDataset::Open;
    GetGDALDriverManager()->RegisterDriver( poDriver );
}

/************************************************************************/
/*                         JPIPKAKAsyncReader                         */
/************************************************************************/
JPIPKAKAsyncReader::JPIPKAKAsyncReader()
{
    panBandMap = nullptr;
    pAppBuf = nullptr;
    pBuf = nullptr;
    nDataRead = 0;
    nAppPixelSpace = 0;
    nAppLineSpace = 0;
    nAppBandSpace = 0;
    nLevel = 0;
    nQualityLayers = 0;
    bHighPriority = FALSE;
    bComplete = FALSE;
}

/************************************************************************/
/*                        ~JPIPKAKAsyncReader                         */
/************************************************************************/
JPIPKAKAsyncReader::~JPIPKAKAsyncReader()
{
    Stop();

    // don't own the buffer
    delete [] panBandMap;

    if( pAppBuf != pBuf )
        CPLFree( pBuf );
}

/************************************************************************/
/*                               Start()                                */
/************************************************************************/

void JPIPKAKAsyncReader::Start()
{
    JPIPKAKDataset *poJDS = (JPIPKAKDataset*)poDS;

    // stop the currently running thread
    // start making requests to the server to the server
    nDataRead = 0;
    bComplete = 0;

    // check a thread is not already running
    if (((bHighPriority) && (poJDS->bHighThreadRunning)) ||
        ((!bHighPriority) && (poJDS->bLowThreadRunning)))
        CPLError(CE_Failure, CPLE_AppDefined, "JPIPKAKAsyncReader supports at most two concurrent server communication threads");
    else
    {
        // Ensure we are working against full res
        ((JPIPKAKDataset*)poDS)->poCodestream->
            apply_input_restrictions( 0, 0, 0, 0, nullptr );

        // calculate the url the worker function is going to retrieve
        // calculate the kakadu adjust image size
        channels.configure(*(((JPIPKAKDataset*)poDS)->poCodestream));

        // find current canvas width and height in the cache and check we don't
        // exceed this in our process request
        kdu_dims view_dims;
        kdu_coords ref_expansion;
        ref_expansion.x = 1;
        ref_expansion.y = 1;

        view_dims = ((JPIPKAKDataset*)poDS)->poDecompressor->
            get_rendered_image_dims(*((JPIPKAKDataset*)poDS)->poCodestream, &channels,
                                    -1, nLevel,
                                    ref_expansion );

        kdu_coords* view_siz = view_dims.access_size();

        // Establish the decimation implied by our resolution level.
        int nRes = 1;
        if (nLevel > 0)
            nRes = 2 << (nLevel - 1);

        // setup expansion to account for the difference between
        // the selected level and the buffer resolution.
        exp_numerator.x = nBufXSize;
        exp_numerator.y = nBufYSize;

        exp_denominator.x = (int) ceil(nXSize / (double) nRes);
        exp_denominator.y = (int) ceil(nYSize / (double) nRes);

        // formulate jpip parameters and adjust offsets for current level
        int fx = view_siz->x / nRes;
        int fy = view_siz->y / nRes;

        rr_win.pos.x = (int) ceil(nXOff / (1.0 * nRes)); // roffx
        rr_win.pos.y = (int) ceil(nYOff / (1.0 * nRes)); // roffy
        rr_win.size.x = (int) ceil(nXSize / (1.0 * nRes)); // rsizx
        rr_win.size.y = (int) ceil(nYSize / (1.0 * nRes)); // rsizy

        if ( rr_win.pos.x + rr_win.size.x > fx)
            rr_win.size.x = fx - rr_win.pos.x;
        if ( rr_win.pos.y + rr_win.size.y > fy)
            rr_win.size.y = fy - rr_win.pos.y;

        CPLString jpipUrl;
        CPLString comps;

        if( poJDS->bYCC )
        {
            comps = "0,1,2";
        }
        else
        {
            for (int i = 0; i < nBandCount; i++)
                comps.Printf("%s%i,", comps.c_str(), panBandMap[i]-1);

            comps.erase(comps.length() -1);
        }

        jpipUrl.Printf("%s&type=jpp-stream&roff=%i,%i&rsiz=%i,%i&fsiz=%i,%i,closest&quality=%i&comps=%s",
                       ((JPIPKAKDataset*)poDS)->osRequestUrl.c_str(),
                       rr_win.pos.x, rr_win.pos.y,
                       rr_win.size.x, rr_win.size.y,
                       fx, fy,
                       nQualityLayers, comps.c_str());

        JPIPRequest* pRequest = new JPIPRequest();
        pRequest->bPriority = bHighPriority;
        pRequest->osRequest = jpipUrl;
        pRequest->poARIO = this;

        if( bHighPriority )
            poJDS->bHighThreadFinished = 0;
        else
            poJDS->bLowThreadFinished = 0;

        //CPLDebug("JPIPKAKAsyncReader", "THREADING TURNED OFF");
        if (CPLCreateThread(JPIPWorkerFunc, pRequest) == -1)
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Unable to create worker jpip  thread" );
        // run in main thread as a test
        //JPIPWorkerFunc(pRequest);
    }
}

/************************************************************************/
/*                                Stop()                                */
/************************************************************************/
void JPIPKAKAsyncReader::Stop()
{
    JPIPKAKDataset *poJDS = (JPIPKAKDataset*)poDS;

    bComplete = 1;
    if (poJDS->pGlobalMutex)
    {
        if (((bHighPriority) && (!poJDS->bHighThreadFinished)) ||
            ((!bHighPriority) && (!poJDS->bLowThreadFinished)))
        {
            CPLDebug( "JPIPKAK", "JPIPKAKAsyncReader::Stop() requested." );

            // stop the thread
            if (bHighPriority)
            {
                CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);
                poJDS->bHighThreadRunning = 0;
                CPLReleaseMutex(poJDS->pGlobalMutex);

                while (!poJDS->bHighThreadFinished)
                    CPLSleep(0.1);
            }
            else
            {
                CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);
                poJDS->bLowThreadRunning = 0;
                CPLReleaseMutex(poJDS->pGlobalMutex);

                while (!poJDS->bLowThreadFinished)
                {
                    CPLSleep(0.1);
                }
            }
            CPLDebug( "JPIPKAK", "JPIPKAKAsyncReader::Stop() confirmed." );
        }
    }
}

/************************************************************************/
/*                        GetNextUpdatedRegion()                        */
/************************************************************************/

GDALAsyncStatusType
JPIPKAKAsyncReader::GetNextUpdatedRegion(double dfTimeout,
                                         int* pnxbufoff,
                                         int* pnybufoff,
                                         int* pnxbufsize,
                                         int* pnybufsize)
{
    GDALAsyncStatusType result = GARIO_ERROR;
    JPIPKAKDataset *poJDS = (JPIPKAKDataset*)poDS;

    long nSize = 0;
    // take a snapshot of the volatile variables
    if (bHighPriority)
    {
        const long s = poJDS->nHighThreadByteCount - nDataRead;
        nSize = s;
    }
    else
    {
        const long s = poJDS->nLowThreadByteCount - nDataRead;
        nSize = s;
    }

/* -------------------------------------------------------------------- */
/*      Wait for new data to return if required.                        */
/* -------------------------------------------------------------------- */
    if ((nSize == 0) && dfTimeout != 0 )
    {
        // poll for change in cache size
        clock_t end_wait = 0;

        end_wait = clock() + (int) (dfTimeout * CLOCKS_PER_SEC);
        while ((nSize == 0) && ((bHighPriority && poJDS->bHighThreadRunning) ||
                                (!bHighPriority && poJDS->bLowThreadRunning)))
        {
            if (end_wait)
                if (clock() > end_wait && dfTimeout >= 0 )
                    break;

            CPLSleep(0.1);

            if (bHighPriority)
            {
                const long s = poJDS->nHighThreadByteCount - nDataRead;
                nSize = s;
            }
            else
            {
                const long s = poJDS->nLowThreadByteCount - nDataRead;
                nSize = s;
            }
        }
    }

    // if there is no pending data and we don't want to wait.
    if( nSize == 0 )
    {
        *pnxbufoff = 0;
        *pnybufoff = 0;
        *pnxbufsize = 0;
        *pnybufsize = 0;

        // Indicate an error if the thread finished prematurely
        if( (bHighPriority
             && !poJDS->bHighThreadRunning
             && poJDS->bHighThreadFinished)
            || (!bHighPriority
                && !poJDS->bLowThreadRunning
                && poJDS->bLowThreadFinished) )
        {
            if( osErrorMsg != "" )
                CPLError( CE_Failure, CPLE_AppDefined,
                          "%s", osErrorMsg.c_str() );
            else
                CPLError( CE_Failure, CPLE_AppDefined,
                          "Working thread failed without complete data. (%d,%d,%d)",
                          bHighPriority,
                          poJDS->bHighThreadRunning,
                          poJDS->bHighThreadFinished );
            poJDS->bNeedReinitialize = TRUE;
            return GARIO_ERROR;
        }

        // Otherwise there is still pending data to wait for.
        return GARIO_PENDING;
    }

/* -------------------------------------------------------------------- */
/*      Establish the canvas region with the expansion factor           */
/*      applied, and compute region from the original window cut        */
/*      down to the target canvas.                                      */
/* -------------------------------------------------------------------- */
    kdu_dims view_dims, region;
    int nBytesPerPixel = GDALGetDataTypeSize(poJDS->eDT) / 8;
    int nPrecision = 0;

    try
    {
        // Ensure we are working against full res
        poJDS->poCodestream->apply_input_restrictions( 0, 0, 0, 0, nullptr );

        view_dims = poJDS->poDecompressor->get_rendered_image_dims(
            *poJDS->poCodestream, &channels,
            -1, nLevel, exp_numerator, exp_denominator );

        double x_ratio, y_ratio;
        x_ratio = view_dims.size.x / (double) poDS->GetRasterXSize();
        y_ratio = view_dims.size.y / (double) poDS->GetRasterYSize();

        region = rr_win;

        region.pos.x = (int) ceil(region.pos.x * x_ratio);
        region.pos.y = (int) ceil(region.pos.y * y_ratio);
        region.size.x = (int) ceil(region.size.x * x_ratio);
        region.size.y = (int) ceil(region.size.y * y_ratio);

        region.size.x = MIN(region.size.x,nBufXSize);
        region.size.y = MIN(region.size.y,nBufYSize);

        if( region.pos.x + region.size.x > view_dims.size.x )
            region.size.x = view_dims.size.x - region.pos.x;
        if( region.pos.y + region.size.y > view_dims.size.y )
            region.size.y = view_dims.size.y - region.pos.y;

        region.pos += view_dims.pos;

        CPLAssert( nBytesPerPixel == 1 || nBytesPerPixel == 2  );

        if( poJDS->poCodestream->get_bit_depth(0) > 16 )
            nPrecision = 16;
    }
    catch(...)
    {
        // The error handler should already have posted an error message.
        return GARIO_ERROR;
    }

/* ==================================================================== */
/*      Now we process the available cached jpeg2000 data into          */
/*      imagery.  The kdu_region_decompressor only seems to support     */
/*      reading back one or three components at a time, we may need     */
/*      to do several processing passes to get the bands we             */
/*      want. We try to do groups of three were possible, and handle    */
/*      the rest one band at a time.                                    */
/* ==================================================================== */
    int nBandsCompleted = 0;

    while( nBandsCompleted < nBandCount )
    {
/* -------------------------------------------------------------------- */
/*      Set up channel list requested.                                  */
/* -------------------------------------------------------------------- */
        std::vector<int> component_indices;
        unsigned int i;

        if( nBandCount - nBandsCompleted >= 3 )
        {
            CPLDebug( "JPIPKAK", "process bands %d,%d,%d",
                      panBandMap[nBandsCompleted],
                      panBandMap[nBandsCompleted+1],
                      panBandMap[nBandsCompleted+2] );

            component_indices.push_back( panBandMap[nBandsCompleted++]-1 );
            component_indices.push_back( panBandMap[nBandsCompleted++]-1 );
            component_indices.push_back( panBandMap[nBandsCompleted++]-1 );
        }
        else
        {
            CPLDebug( "JPIPKAK", "process band %d",
                      panBandMap[nBandsCompleted] );
            component_indices.push_back( panBandMap[nBandsCompleted++]-1 );
        }

/* -------------------------------------------------------------------- */
/*      Apply region, channel and overview level restrictions.          */
/* -------------------------------------------------------------------- */
        kdu_dims region_pass = region;

        poJDS->poCodestream->apply_input_restrictions(
            static_cast<int>(component_indices.size()), &(component_indices[0]),
            nLevel, nQualityLayers, &region_pass,
            KDU_WANT_CODESTREAM_COMPONENTS);

        channels.configure(*(poJDS->poCodestream));

        for( i=0; i < component_indices.size(); i++ )
            channels.source_components[i] = component_indices[i];

        kdu_dims incomplete_region = region_pass;
        kdu_coords origin = region_pass.pos;

        int bIsDecompressing = FALSE;

        CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);

        try
        {

            bIsDecompressing = poJDS->poDecompressor->start(
                *(poJDS->poCodestream),
                &channels, -1, nLevel, nQualityLayers,
                region_pass, exp_numerator, exp_denominator, TRUE);

            *pnxbufoff = 0;
            *pnybufoff = 0;
            *pnxbufsize = region_pass.access_size()->x;
            *pnybufsize = region_pass.access_size()->y;

            // Setup channel buffers
            std::vector<kdu_byte*> channel_bufs;

            for( i=0; i < component_indices.size(); i++ )
                channel_bufs.push_back(
                    ((kdu_byte *) pBuf)
                    + (i+nBandsCompleted-component_indices.size()) * nBandSpace );

            int pixel_gap = nPixelSpace / nBytesPerPixel;
            int row_gap = nLineSpace / nBytesPerPixel;

            while ((bIsDecompressing == 1) || (incomplete_region.area() != 0))
            {
                if( nBytesPerPixel == 1 )
                {
                    bIsDecompressing = poJDS->poDecompressor->
                        process(&(channel_bufs[0]), false,
                                pixel_gap, origin, row_gap, 1000000, 0,
                                incomplete_region, region_pass,
                                0, false );
                }
                else if( nBytesPerPixel == 2 )
                {
                    bIsDecompressing = poJDS->poDecompressor->
                        process((kdu_uint16**) &(channel_bufs[0]), false,
                                pixel_gap, origin, row_gap, 1000000, 0,
                                incomplete_region, region_pass,
                                nPrecision, false );
                }

                CPLDebug( "JPIPKAK",
                          "processed=%d,%d %dx%d   - incomplete=%d,%d %dx%d",
                          region_pass.pos.x, region_pass.pos.y,
                          region_pass.size.x, region_pass.size.y,
                          incomplete_region.pos.x, incomplete_region.pos.y,
                          incomplete_region.size.x, incomplete_region.size.y );
            }

            poJDS->poDecompressor->finish();
            CPLReleaseMutex(poJDS->pGlobalMutex);
        }
        catch(...)
        {
            poJDS->poDecompressor->finish();
            CPLReleaseMutex(poJDS->pGlobalMutex);
            // The error handler should already have posted an error message.
            return GARIO_ERROR;
        }
    } // nBandsCompleted < nBandCount

/* -------------------------------------------------------------------- */
/*      If the application buffer is of a different type than our       */
/*      band we need to copy into the application buffer at this        */
/*      point.                                                          */
/*                                                                      */
/*      We could optimize to only update affected area, but that is     */
/*      always the whole area for the JPIP driver it seems.             */
/* -------------------------------------------------------------------- */
    if( pAppBuf != pBuf )
    {
        int iY, iBand;
        GByte *pabySrc = (GByte *) pBuf;
        GByte *pabyDst = (GByte *) pAppBuf;

        for( iBand = 0; iBand < nBandCount; iBand++ )
        {
            for( iY = 0; iY < nBufYSize; iY++ )
            {
                GDALCopyWords( pabySrc + nLineSpace * iY + nBandSpace * iBand,
                               poJDS->eDT, nPixelSpace,
                               pabyDst + nAppLineSpace*iY + nAppBandSpace*iBand,
                               eBufType, nAppPixelSpace,
                               nBufXSize );
            }
        }
    }

/* -------------------------------------------------------------------- */
/*      has there been any more data read while were have been processing?*/
/* -------------------------------------------------------------------- */
    long size = 0;
    if (bHighPriority)
    {
        const long x = poJDS->nHighThreadByteCount - nDataRead;
        size = x;
    }
    else
    {
        const long x = poJDS->nLowThreadByteCount - nDataRead;
        size = x;
    }

    if ((bComplete) && (nSize == size))
        result = GARIO_COMPLETE;
    else
        result = GARIO_UPDATE;

    nDataRead += nSize;

    if( result == GARIO_ERROR )
        poJDS->bNeedReinitialize = TRUE;

    return result;
}

/************************************************************************/
/*                          JPIPDataSegment()                           */
/************************************************************************/
JPIPDataSegment::JPIPDataSegment()
{
    nId = 0;
    nAux = 0;
    nClassId = 0;
    nCodestream = 0;
    nOffset = 0;
    nLen = 0;
    pabyData = nullptr;
    bIsFinal = FALSE;
    bIsEOR = FALSE;
}

/************************************************************************/
/*                          ~JPIPDataSegment()                          */
/************************************************************************/
JPIPDataSegment::~JPIPDataSegment()
{
    CPLFree(pabyData);
}

/************************************************************************/
/*                           JPIPWorkerFunc()                           */
/************************************************************************/
static void JPIPWorkerFunc(void *req)
{
    int nCurrentTransmissionLength = 2000;
    int nMinimumTransmissionLength = 2000;

    JPIPRequest *pRequest = (JPIPRequest *)req;
    JPIPKAKDataset *poJDS =
        (JPIPKAKDataset*)(pRequest->poARIO->GetGDALDataset());

    int bPriority = pRequest->bPriority;

    CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);

    CPLDebug( "JPIPKAK", "working thread starting." );
    // set the running status
    if (bPriority)
    {
        poJDS->bHighThreadRunning = 1;
        poJDS->bHighThreadFinished = 0;
    }
    else
    {
        poJDS->bLowThreadRunning = 1;
        poJDS->bLowThreadFinished = 0;
    }

    CPLReleaseMutex(poJDS->pGlobalMutex);

    CPLString osHeaders = "HEADERS=";
    osHeaders += "Accept: jpp-stream";
    CPLString osPersistent;

    osPersistent.Printf( "PERSISTENT=JPIPKAK:%p", poJDS );

    char *apszOptions[] = {
        (char *) osHeaders.c_str(),
        (char *) osPersistent.c_str(),
        nullptr
    };

    while( true )
    {
        // modulate the len= parameter to use the currently available bandwidth
        long nStart = clock();

        if (((bPriority) && (!poJDS->bHighThreadRunning)) || ((!bPriority) && (!poJDS->bLowThreadRunning)))
            break;

        // make jpip requests
        // adjust transmission length
        CPLString osCurrentRequest;
        osCurrentRequest.Printf("%s&len=%i", pRequest->osRequest.c_str(), nCurrentTransmissionLength);
        CPLHTTPResult *psResult = CPLHTTPFetch(osCurrentRequest, apszOptions);
        if (psResult->nDataLen == 0)
        {
            CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);
            if( psResult->pszErrBuf )
                pRequest->poARIO->
                    osErrorMsg.Printf( "zero data returned from server, timeout?\n%s", psResult->pszErrBuf );
            else
                pRequest->poARIO->
                    osErrorMsg =  "zero data returned from server, timeout?";

            // status is not being set, always zero in cpl_http
            CPLDebug("JPIPWorkerFunc", "zero data returned from server");
            CPLReleaseMutex(poJDS->pGlobalMutex);
            CPLHTTPDestroyResult(psResult);
            break;
        }

        if( psResult->pszContentType != nullptr )
            CPLDebug( "JPIPKAK", "Content-type: %s", psResult->pszContentType );

        if( psResult->pszContentType != nullptr
            && strstr(psResult->pszContentType,"html") != nullptr )
        {
            CPLDebug( "JPIPKAK", "%s", psResult->pabyData );
        }

        int bytes = psResult->nDataLen;
        long nEnd = clock();

        if ((nEnd - nStart) > 0)
            nCurrentTransmissionLength = (int) MAX(bytes / ((1.0 * (nEnd - nStart)) / CLOCKS_PER_SEC), nMinimumTransmissionLength);

        CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);

        int bError;
        int bComplete = ((JPIPKAKDataset*)pRequest->poARIO->GetGDALDataset())->ReadFromInput(psResult->pabyData, psResult->nDataLen, bError);
        if (bPriority)
            poJDS->nHighThreadByteCount += psResult->nDataLen;
        else
            poJDS->nLowThreadByteCount += psResult->nDataLen;

        pRequest->poARIO->SetComplete(bComplete);

        CPLReleaseMutex(poJDS->pGlobalMutex);
        CPLHTTPDestroyResult(psResult);

        if (bComplete || bError )
            break;
    }

    CPLAcquireMutex(poJDS->pGlobalMutex, 100.0);

    CPLDebug( "JPIPKAK", "Worker shutting down." );

    if (bPriority)
    {
        poJDS->bHighThreadRunning = 0;
        poJDS->bHighThreadFinished = 1;
    }
    else
    {
        poJDS->bLowThreadRunning = 0;
        poJDS->bLowThreadFinished = 1;
    }

    CPLReleaseMutex(poJDS->pGlobalMutex);

    // end of thread
    delete pRequest;
}
