/******************************************************************************
 *
 * Project:  WCS Client Driver
 * Purpose:  Implementation of Dataset class for WCS 1.1.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2006, Frank Warmerdam
 * Copyright (c) 2008-2013, Even Rouault <even dot rouault at spatialys.com>
 * Copyright (c) 2017, Ari Jolma
 * Copyright (c) 2017, Finnish Environment Institute
 *
 * 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 "cpl_string.h"
#include "cpl_minixml.h"
#include "cpl_http.h"
#include "gmlutils.h"
#include "gdal_frmts.h"
#include "gdal_pam.h"
#include "ogr_spatialref.h"
#include "gmlcoverage.h"

#include <algorithm>

#include "wcsdataset.h"
#include "wcsutils.h"

using namespace WCSUtils;

/************************************************************************/
/*                         GetExtent()                                  */
/*                                                                      */
/************************************************************************/

std::vector<double> WCSDataset110::GetExtent(int nXOff, int nYOff,
                                             int nXSize, int nYSize,
                                             CPL_UNUSED int nBufXSize, CPL_UNUSED int nBufYSize)
{
    std::vector<double> extent;

    // outer edges of outer pixels.
    extent.push_back(adfGeoTransform[0] +
                     (nXOff) * adfGeoTransform[1]);
    extent.push_back(adfGeoTransform[3] +
                     (nYOff + nYSize) * adfGeoTransform[5]);
    extent.push_back(adfGeoTransform[0] +
                     (nXOff + nXSize) * adfGeoTransform[1]);
    extent.push_back(adfGeoTransform[3] +
                     (nYOff) * adfGeoTransform[5]);

    bool no_shrink = CPLGetXMLBoolean(psService, "OuterExtents");

    // WCS 1.1 extents are centers of outer pixels.
    if (!no_shrink) {
        extent[2] -= adfGeoTransform[1] * 0.5;
        extent[0] += adfGeoTransform[1] * 0.5;
        extent[1] -= adfGeoTransform[5] * 0.5;
        extent[3] += adfGeoTransform[5] * 0.5;
    }

    double dfXStep, dfYStep;

    if (!no_shrink) {
        dfXStep = (nXSize/(double)nBufXSize) * adfGeoTransform[1];
        dfYStep = (nYSize/(double)nBufYSize) * adfGeoTransform[5];
        // Carefully adjust bounds for pixel centered values at new
        // sampling density.
        if( nBufXSize != nXSize || nBufYSize != nYSize )
        {
            dfXStep = (nXSize/(double)nBufXSize) * adfGeoTransform[1];
            dfYStep = (nYSize/(double)nBufYSize) * adfGeoTransform[5];

            extent[0]  = nXOff * adfGeoTransform[1] + adfGeoTransform[0]
                + dfXStep * 0.5;
            extent[2]  = extent[0] + (nBufXSize - 1) * dfXStep;

            extent[3]  = nYOff * adfGeoTransform[5] + adfGeoTransform[3]
                + dfYStep * 0.5;
            extent[1]  = extent[3] + (nBufYSize - 1) * dfYStep;
        }
    } else {
        double adjust = CPLAtof(CPLGetXMLValue(psService, "BufSizeAdjust", "0.0"));
        dfXStep = (nXSize/((double)nBufXSize + adjust)) * adfGeoTransform[1];
        dfYStep = (nYSize/((double)nBufYSize + adjust)) * adfGeoTransform[5];
    }

    extent.push_back(dfXStep);
    extent.push_back(dfYStep);

    return extent;
}

/************************************************************************/
/*                        GetCoverageRequest()                          */
/*                                                                      */
/************************************************************************/

CPLString WCSDataset110::GetCoverageRequest(bool scaled,
                                            CPL_UNUSED int nBufXSize, CPL_UNUSED int nBufYSize,
                                            const std::vector<double> &extent,
                                            CPLString osBandList)
{
    CPLString osRequest;

/* -------------------------------------------------------------------- */
/*      URL encode strings that could have questionable characters.     */
/* -------------------------------------------------------------------- */
    CPLString osCoverage = CPLGetXMLValue( psService, "CoverageName", "" );

    char *pszEncoded = CPLEscapeString( osCoverage, -1, CPLES_URL );
    osCoverage = pszEncoded;
    CPLFree( pszEncoded );

    CPLString osFormat = CPLGetXMLValue( psService, "PreferredFormat", "" );

    pszEncoded = CPLEscapeString( osFormat, -1, CPLES_URL );
    osFormat = pszEncoded;
    CPLFree( pszEncoded );

    CPLString osRangeSubset = CPLGetXMLValue(psService, "FieldName", "");

    // todo: MapServer seems to require interpolation

    CPLString interpolation = CPLGetXMLValue( psService, "Interpolation", "" );
    if (interpolation == "") {
        // old undocumented key for interpolation in service
        interpolation = CPLGetXMLValue( psService, "Resample", "" );
    }
    if (interpolation != "") {
        osRangeSubset += ":" + interpolation;
    }

    if( osBandList != "" )
    {
        if (osBandIdentifier != "") {
            osRangeSubset +=
                CPLString().Printf( "[%s[%s]]",
                                    osBandIdentifier.c_str(),
                                    osBandList.c_str() );
        }
    }

    osRangeSubset = "&RangeSubset=" + URLEncode(osRangeSubset);

    double
        bbox_0 = extent[0], // min X
        bbox_1 = extent[1], // min Y
        bbox_2 = extent[2], // max X
        bbox_3 = extent[3]; // max Y

    if (axis_order_swap) {
        bbox_0 = extent[1]; // min Y
        bbox_1 = extent[0]; // min X
        bbox_2 = extent[3]; // max Y
        bbox_3 = extent[2]; // max X
    }
    CPLString request = CPLGetXMLValue( psService, "ServiceURL", "" );
    request = CPLURLAddKVP(request, "SERVICE", "WCS");
    request += CPLString().Printf(
        "&VERSION=%s&REQUEST=GetCoverage&IDENTIFIER=%s"
        "&FORMAT=%s&BOUNDINGBOX=%.15g,%.15g,%.15g,%.15g,%s%s",
        CPLGetXMLValue( psService, "Version", "" ),
        osCoverage.c_str(),
        osFormat.c_str(),
        bbox_0, bbox_1, bbox_2, bbox_3,
        osCRS.c_str(),
        osRangeSubset.c_str() );
    double
        origin_1 = extent[0], // min X
        origin_2 = extent[3], // max Y
        offset_1 = extent[4], // dX
        offset_2 = extent[5]; // dY

    if (axis_order_swap) {
        origin_1 = extent[3]; // max Y
        origin_2 = extent[0]; // min X
        offset_1 = extent[5]; // dY
        offset_2 = extent[4]; // dX
    }
    CPLString offsets;
    if (CPLGetXMLBoolean(psService, "OffsetsPositive")) {
        offset_1 = fabs(offset_1);
        offset_2 = fabs(offset_2);
    }
    if (EQUAL(CPLGetXMLValue(psService, "NrOffsets", "4"), "2")) {
        offsets = CPLString().Printf("%.15g,%.15g", offset_1, offset_2);
    } else {
        if (axis_order_swap) {
            // Only tested with GeoServer but this is the correct offset(?)
            offsets = CPLString().Printf("0,%.15g,%.15g,0", offset_2, offset_1);
        } else {
            offsets = CPLString().Printf("%.15g,0,0,%.15g", offset_1, offset_2);
        }
    }
    bool do_not_include = CPLGetXMLBoolean(psService, "GridCRSOptional") && !scaled;
    if (!do_not_include) {
        request += CPLString().Printf(
            "&GridBaseCRS=%s"
            "&GridCS=urn:ogc:def:cs:OGC:0.0:Grid2dSquareCS"
            "&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs"
            "&GridOrigin=%.15g,%.15g"
            "&GridOffsets=%s",
            osCRS.c_str(),
            origin_1, origin_2,
            offsets.c_str()
            );
    }
    CPLString extra = CPLGetXMLValue(psService, "Parameters", "");
    if (extra != "") {
        std::vector<CPLString> pairs = Split(extra, "&");
        for (unsigned int i = 0; i < pairs.size(); ++i) {
            std::vector<CPLString> pair = Split(pairs[i], "=");
            request = CPLURLAddKVP(request, pair[0], pair[1]);
        }
    }
    extra = CPLGetXMLValue(psService, "GetCoverageExtra", "");
    if (extra != "") {
        std::vector<CPLString> pairs = Split(extra, "&");
        for (unsigned int i = 0; i < pairs.size(); ++i) {
            std::vector<CPLString> pair = Split(pairs[i], "=");
            request = CPLURLAddKVP(request, pair[0], pair[1]);
        }
    }
    CPLDebug("WCS", "Requesting %s", request.c_str());
    return request;
}

/************************************************************************/
/*                        DescribeCoverageRequest()                     */
/*                                                                      */
/************************************************************************/

CPLString WCSDataset110::DescribeCoverageRequest()
{
    CPLString request = CPLGetXMLValue( psService, "ServiceURL", "" );
    request = CPLURLAddKVP(request, "SERVICE", "WCS");
    request = CPLURLAddKVP(request, "REQUEST", "DescribeCoverage");
    request = CPLURLAddKVP(request, "VERSION", CPLGetXMLValue( psService, "Version", "1.1.0" ));
    request = CPLURLAddKVP(request, "IDENTIFIERS", CPLGetXMLValue( psService, "CoverageName", "" ));
    request = CPLURLAddKVP(request, "FORMAT", "text/xml");
    CPLString extra = CPLGetXMLValue(psService, "Parameters", "");
    if (extra != "") {
        std::vector<CPLString> pairs = Split(extra, "&");
        for (unsigned int i = 0; i < pairs.size(); ++i) {
            std::vector<CPLString> pair = Split(pairs[i], "=");
            request = CPLURLAddKVP(request, pair[0], pair[1]);
        }
    }
    extra = CPLGetXMLValue(psService, "DescribeCoverageExtra", "");
    if (extra != "") {
        std::vector<CPLString> pairs = Split(extra, "&");
        for (unsigned int i = 0; i < pairs.size(); ++i) {
            std::vector<CPLString> pair = Split(pairs[i], "=");
            request = CPLURLAddKVP(request, pair[0], pair[1]);
        }
    }
    return request;
}

/************************************************************************/
/*                         CoverageOffering()                           */
/*                                                                      */
/************************************************************************/

CPLXMLNode *WCSDataset110::CoverageOffering(CPLXMLNode *psDC)
{
    return CPLGetXMLNode( psDC,"=CoverageDescriptions.CoverageDescription");
}


/************************************************************************/
/*                          ExtractGridInfo()                           */
/*                                                                      */
/*      Collect info about grid from describe coverage for WCS 1.1.     */
/*                                                                      */
/************************************************************************/

bool WCSDataset110::ExtractGridInfo()

{
    CPLXMLNode * psCO = CPLGetXMLNode( psService, "CoverageDescription" );

    if( psCO == nullptr )
        return false;

/* -------------------------------------------------------------------- */
/*      We need to strip off name spaces so it is easier to             */
/*      searchfor plain gml names.                                      */
/* -------------------------------------------------------------------- */
    CPLStripXMLNamespace( psCO, nullptr, TRUE );

/* -------------------------------------------------------------------- */
/*      Verify we have a SpatialDomain and GridCRS.                     */
/* -------------------------------------------------------------------- */
    CPLXMLNode *psSD =
        CPLGetXMLNode( psCO, "Domain.SpatialDomain" );
    CPLXMLNode *psGCRS =
        CPLGetXMLNode( psSD, "GridCRS" );

    if( psSD == nullptr || psGCRS == nullptr )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Unable to find GridCRS in CoverageDescription,\n"
                  "unable to process WCS Coverage." );
        return false;
    }

/* -------------------------------------------------------------------- */
/*      Establish our coordinate system.                                */
/*   This is needed before geometry since we may have axis order swap.  */
/* -------------------------------------------------------------------- */
    CPLString crs = ParseCRS(psGCRS);

    if (crs.empty()) {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Unable to find GridCRS.GridBaseCRS" );
        return false;
    }

    // SetCRS should fail only if the CRS is really unknown to GDAL
    if (!SetCRS(crs, true)) {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Unable to interpret GridBaseCRS '%s'.",
                  crs.c_str() );
        return false;
    }

/* -------------------------------------------------------------------- */
/*      Collect size, origin, and offsets for SetGeometry()             */
/*                                                                      */
/*      Extract Geotransform from GridCRS.                              */
/*                                                                      */
/* -------------------------------------------------------------------- */
    const char *pszGridType = CPLGetXMLValue( psGCRS, "GridType",
                                              "urn:ogc:def:method:WCS::2dSimpleGrid" );
    bool swap = axis_order_swap && !CPLGetXMLBoolean(psService, "NoGridAxisSwap");
    std::vector<double> origin = Flist(Split(CPLGetXMLValue(psGCRS, "GridOrigin", ""), " ", swap));

    std::vector<CPLString> offset_1 = Split(CPLGetXMLValue(psGCRS, "GridOffsets", ""), " ");
    std::vector<CPLString> offset_2;
    size_t n = offset_1.size();
    if (n % 2 != 0) {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "GridOffsets has incorrect amount of coefficients.\n"
                  "Unable to process WCS coverage.");
        return false;
    }
    for (unsigned int i = 0; i < n / 2; ++i) {
        CPLString s = offset_1.back();
        offset_1.erase(offset_1.end() - 1);
        offset_2.insert(offset_2.begin(), s);
    }
    std::vector<std::vector<double> > offsets;
    if (swap) {
        offsets.push_back(Flist(offset_2));
        offsets.push_back(Flist(offset_1));
    } else {
        offsets.push_back(Flist(offset_1));
        offsets.push_back(Flist(offset_2));
    }

    if( strstr(pszGridType,":2dGridIn2dCrs")
        || strstr(pszGridType,":2dGridin2dCrs") )
    {
        if( !(offset_1.size() == 2
              && origin.size() == 2 ))
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "2dGridIn2dCrs does not have expected GridOrigin or\n"
                      "GridOffsets values - unable to process WCS coverage.");
            return false;
        }
    }

    else if( strstr(pszGridType,":2dGridIn3dCrs") )
    {
        if( !(offset_1.size() == 3
              && origin.size() == 3 ))
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "2dGridIn3dCrs does not have expected GridOrigin or\n"
                      "GridOffsets values - unable to process WCS coverage.");
            return false;
        }
    }

    else if( strstr(pszGridType,":2dSimpleGrid") )
    {
        if( !(offset_1.size() == 1
              && origin.size() == 2 ))
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "2dSimpleGrid does not have expected GridOrigin or\n"
                      "GridOffsets values - unable to process WCS coverage.");
            return false;
        }
    }

    else
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Unrecognized GridCRS.GridType value '%s',\n"
                  "unable to process WCS coverage.",
                  pszGridType );
        return false;
    }

/* -------------------------------------------------------------------- */
/*      Search for an ImageCRS for raster size.                         */
/* -------------------------------------------------------------------- */
    std::vector<int> size;
    CPLXMLNode *psNode;

    for( psNode = psSD->psChild;
         psNode != nullptr && size.size() == 0;
         psNode = psNode->psNext )
    {
        if( psNode->eType != CXT_Element
            || !EQUAL(psNode->pszValue,"BoundingBox") )
            continue;

        CPLString osBBCRS = ParseCRS(psNode);
        if (strstr(osBBCRS,":imageCRS")) {
            std::vector<CPLString> bbox = ParseBoundingBox(psNode);
            if (bbox.size() >= 2) {
                std::vector<int> low = Ilist(Split(bbox[0], " "), 0, 2);
                std::vector<int> high = Ilist(Split(bbox[1], " "), 0, 2);
                if (low[0] == 0 && low[1] == 0) {
                    size.push_back(high[0]);
                    size.push_back(high[1]);
                }
            }
        }
    }

/* -------------------------------------------------------------------- */
/*      Otherwise we search for a bounding box in our coordinate        */
/*      system and derive the size from that.                           */
/* -------------------------------------------------------------------- */
    for( psNode = psSD->psChild;
         psNode != nullptr && size.size() == 0;
         psNode = psNode->psNext )
    {
        if( psNode->eType != CXT_Element
            || !EQUAL(psNode->pszValue,"BoundingBox") )
            continue;

        CPLString osBBCRS = ParseCRS(psNode);
        if (osBBCRS == osCRS) {
            std::vector<CPLString> bbox = ParseBoundingBox(psNode);
            bool not_rot = (offsets[0].size() == 1 && offsets[1].size() == 1)
                || ((swap && offsets[0][0] == 0.0 && offsets[1][1] == 0.0)
                    || (!swap && offsets[0][1] == 0.0 && offsets[1][0] == 0.0));
            if (bbox.size() >= 2 && not_rot)
            {
                std::vector<double> low = Flist(Split(bbox[0], " ", axis_order_swap), 0, 2);
                std::vector<double> high = Flist(Split(bbox[1], " ", axis_order_swap), 0, 2);
                double c1 = offsets[0][0];
                double c2 = offsets[1].size() == 1 ? offsets[1][0] : offsets[1][1];
                size.push_back((int)((high[0] - low[0]) / c1 + 1.01));
                size.push_back((int)((high[1] - low[1]) / fabs(c2) + 1.01));
            }
        }
    }

    if( size.size() < 2 )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Could not determine the size of the grid." );
        return false;
    }

    SetGeometry(size, origin, offsets);

/* -------------------------------------------------------------------- */
/*      Do we have a coordinate system override?                        */
/* -------------------------------------------------------------------- */
    const char *pszProjOverride = CPLGetXMLValue( psService, "SRS", nullptr );

    if( pszProjOverride )
    {
        OGRSpatialReference oSRS;

        if( oSRS.SetFromUserInput( pszProjOverride, OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) != OGRERR_NONE )
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "<SRS> element contents not parsable:\n%s",
                      pszProjOverride );
            return false;
        }

        CPLFree( pszProjection );
        oSRS.exportToWkt( &pszProjection );
    }

/* -------------------------------------------------------------------- */
/*      Pick a format type if we don't already have one selected.       */
/*                                                                      */
/*      We will prefer anything that sounds like TIFF, otherwise        */
/*      falling back to the first supported format.  Should we          */
/*      consider preferring the nativeFormat if available?              */
/* -------------------------------------------------------------------- */
    if( CPLGetXMLValue( psService, "PreferredFormat", nullptr ) == nullptr )
    {
        CPLString osPreferredFormat;

        for( psNode = psCO->psChild; psNode != nullptr; psNode = psNode->psNext )
        {
            if( psNode->eType == CXT_Element
                && EQUAL(psNode->pszValue,"SupportedFormat")
                && psNode->psChild
                && psNode->psChild->eType == CXT_Text )
            {
                if( osPreferredFormat.empty() )
                    osPreferredFormat = psNode->psChild->pszValue;

                if( strstr(psNode->psChild->pszValue,"tiff") != nullptr
                    || strstr(psNode->psChild->pszValue,"TIFF") != nullptr
                    || strstr(psNode->psChild->pszValue,"Tiff") != nullptr )
                {
                    osPreferredFormat = psNode->psChild->pszValue;
                    break;
                }
            }
        }

        if( !osPreferredFormat.empty() )
        {
            bServiceDirty = true;
            CPLCreateXMLElementAndValue( psService, "PreferredFormat",
                                         osPreferredFormat );
        }
    }

/* -------------------------------------------------------------------- */
/*      Try to identify a nodata value.  For now we only support the    */
/*      singleValue mechanism.                                          */
/* -------------------------------------------------------------------- */
    if( CPLGetXMLValue( psService, "NoDataValue", nullptr ) == nullptr )
    {
        const char *pszSV =
            CPLGetXMLValue( psCO, "Range.Field.NullValue", nullptr );

        if( pszSV != nullptr && (CPLAtof(pszSV) != 0.0 || *pszSV == DIGIT_ZERO) )
        {
            bServiceDirty = true;
            CPLCreateXMLElementAndValue( psService, "NoDataValue",
                                         pszSV );
        }
    }

/* -------------------------------------------------------------------- */
/*      Grab the field name, if possible.                               */
/* -------------------------------------------------------------------- */
    if( CPLGetXMLValue( psService, "FieldName", nullptr ) == nullptr )
    {
        CPLString osFieldName =
            CPLGetXMLValue( psCO, "Range.Field.Identifier", "" );

        if( !osFieldName.empty() )
        {
            bServiceDirty = true;
            CPLCreateXMLElementAndValue( psService, "FieldName",
                                         osFieldName );
        }
        else
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Unable to find required Identifier name %s for Range Field.",
                      osCRS.c_str() );
            return false;
        }
    }

/* -------------------------------------------------------------------- */
/*      Do we have a "Band" axis?  If so try to grab the bandcount      */
/*      and data type from it.                                          */
/* -------------------------------------------------------------------- */
    osBandIdentifier = CPLGetXMLValue( psService, "BandIdentifier", "" );
    CPLXMLNode * psAxis = CPLGetXMLNode(
        psService, "CoverageDescription.Range.Field.Axis" );

    if( osBandIdentifier.empty()
        && (EQUAL(CPLGetXMLValue(psAxis,"Identifier",""),"Band")
            || EQUAL(CPLGetXMLValue(psAxis,"Identifier",""),"Bands"))
        && CPLGetXMLNode(psAxis,"AvailableKeys") != nullptr )
    {
        osBandIdentifier = CPLGetXMLValue(psAxis,"Identifier","");

        // verify keys are ascending starting at 1
        CPLXMLNode *psValues = CPLGetXMLNode(psAxis,"AvailableKeys");
        CPLXMLNode *psSV;
        int iBand;

        for( psSV = psValues->psChild, iBand = 1;
             psSV != nullptr;
             psSV = psSV->psNext, iBand++ )
        {
            if( psSV->eType != CXT_Element
                || !EQUAL(psSV->pszValue,"Key")
                || psSV->psChild == nullptr
                || psSV->psChild->eType != CXT_Text
                || atoi(psSV->psChild->pszValue) != iBand )
            {
                osBandIdentifier = "";
                break;
            }
        }

        if( !osBandIdentifier.empty() )
        {
            if( CPLGetXMLValue(psService,"BandIdentifier",nullptr) == nullptr ) {
                bServiceDirty = true;
                CPLSetXMLValue( psService, "BandIdentifier",
                                osBandIdentifier );
            }

            if( CPLGetXMLValue(psService,"BandCount",nullptr) == nullptr ) {
                bServiceDirty = true;
                CPLSetXMLValue( psService, "BandCount",
                                CPLString().Printf("%d",iBand-1));
            }
        }

        // Is this an ESRI server returning a GDAL recognised data type?
        CPLString osDataType = CPLGetXMLValue( psAxis, "DataType", "" );
        if( GDALGetDataTypeByName(osDataType) != GDT_Unknown
            && CPLGetXMLValue(psService,"BandType",nullptr) == nullptr )
        {
            bServiceDirty = true;
            CPLCreateXMLElementAndValue( psService, "BandType", osDataType );
        }
    }

    return true;
}

/************************************************************************/
/*                      ParseCapabilities()                             */
/************************************************************************/

CPLErr WCSDataset110::ParseCapabilities( CPLXMLNode * Capabilities, CPLString url )
{
    CPLStripXMLNamespace(Capabilities, nullptr, TRUE);

    // make sure this is a capabilities document
    if( strcmp(Capabilities->pszValue, "Capabilities") != 0 )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "Error in capabilities document.\n" );
        return CE_Failure;
    }

    char **metadata = nullptr;
    CPLString path = "WCS_GLOBAL#";

    CPLString key = path + "version";
    metadata = CSLSetNameValue(metadata, key, Version());

    for( CPLXMLNode *node = Capabilities->psChild; node != nullptr; node = node->psNext)
    {
        const char *attr = node->pszValue;
        if( node->eType == CXT_Attribute && EQUAL(attr, "updateSequence") )
        {
            key = path + "updateSequence";
            CPLString value = CPLGetXMLValue(node, nullptr, "");
            metadata = CSLSetNameValue(metadata, key, value);
        }
    }

    // identification metadata
    CPLString path2 = path;
    std::vector<CPLString> keys2;
    keys2.push_back("Title");
    keys2.push_back("Abstract");
    keys2.push_back("Fees");
    keys2.push_back("AccessConstraints");
    CPLXMLNode *service = AddSimpleMetaData(&metadata, Capabilities, path2, "ServiceIdentification", keys2);
    CPLString kw = GetKeywords(service, "Keywords", "Keyword");
    if (kw != "") {
        CPLString name = path + "Keywords";
        metadata = CSLSetNameValue(metadata, name, kw);
    }
    CPLString profiles = GetKeywords(service, "", "Profile");
    if (profiles != "") {
        CPLString name = path + "Profiles";
        metadata = CSLSetNameValue(metadata, name, profiles);
    }

    // provider metadata
    path2 = path;
    keys2.clear();
    keys2.push_back("ProviderName");
    CPLXMLNode *provider = AddSimpleMetaData(&metadata, Capabilities, path2, "ServiceProvider", keys2);
    if (provider) {
        CPLXMLNode *site = CPLGetXMLNode(provider, "ProviderSite");
        if( site )
        {
            CPLString path3 = path2 + "ProviderSite";
            CPLString value = CPLGetXMLValue(CPLGetXMLNode(site, "href"), nullptr, "");
            metadata = CSLSetNameValue( metadata, path3, value );
        }
        CPLString path3 = path2;
        std::vector<CPLString> keys3;
        keys3.push_back("IndividualName");
        keys3.push_back("PositionName");
        keys3.push_back("Role");
        CPLXMLNode *contact = AddSimpleMetaData(&metadata, provider, path3, "ServiceContact", keys3);
        if (contact) {
            CPLString path4 = path3;
            std::vector<CPLString> keys4;
            keys4.push_back("HoursOfService");
            keys4.push_back("ContactInstructions");
            CPLXMLNode *info = AddSimpleMetaData(&metadata, contact, path4, "ContactInfo", keys4);
            if (info) {
                CPLString path5 = path4;
                std::vector<CPLString> keys5;
                keys5.push_back("DeliveryPoint");
                keys5.push_back("City");
                keys5.push_back("AdministrativeArea");
                keys5.push_back("PostalCode");
                keys5.push_back("Country");
                keys5.push_back("ElectronicMailAddress");
                CPLString path6 = path4;
                std::vector<CPLString> keys6;
                keys6.push_back("Voice");
                keys6.push_back("Facsimile");
                AddSimpleMetaData(&metadata, info, path5, "Address", keys5);
                AddSimpleMetaData(&metadata, info, path6, "Phone", keys6);
            }
        }
    }

    // operations metadata
    CPLString DescribeCoverageURL = "";
    CPLXMLNode *service2 = CPLGetXMLNode(Capabilities, "OperationsMetadata");
    if( service2 )
    {
        for( CPLXMLNode *operation = service2->psChild; operation != nullptr; operation = operation->psNext)
        {
            if( operation->eType != CXT_Element
                || !EQUAL(operation->pszValue, "Operation") )
            {
                continue;
            }
            if( EQUAL(CPLGetXMLValue(CPLGetXMLNode(operation, "name"), nullptr, ""), "DescribeCoverage") )
            {
                DescribeCoverageURL = CPLGetXMLValue(
                    CPLGetXMLNode(CPLSearchXMLNode(operation, "Get"),
                                  "href"), nullptr, "");
            }
        }
    }
    // if DescribeCoverageURL looks wrong, we change it
    if (DescribeCoverageURL.find("localhost") != std::string::npos) {
        DescribeCoverageURL = URLRemoveKey(url, "request");
    }

    // service metadata (in 2.0)
    CPLString ext = "ServiceMetadata";
    CPLString formats = GetKeywords(Capabilities, ext, "formatSupported");
    if (formats != "") {
        CPLString name = path + "formatSupported";
        metadata = CSLSetNameValue(metadata, name, formats);
    }
    // wcs:Extensions: interpolation, CRS, others?
    ext += ".Extension";
    CPLString interpolation = GetKeywords(Capabilities, ext, "interpolationSupported");
    if (interpolation == "") {
        interpolation = GetKeywords(Capabilities, ext + ".InterpolationMetadata", "InterpolationSupported");
    }
    if (interpolation != "") {
        CPLString name = path + "InterpolationSupported";
        metadata = CSLSetNameValue(metadata, name, interpolation);
    }
    CPLString crs = GetKeywords(Capabilities, ext, "crsSupported");
    if (crs == "") {
        crs = GetKeywords(Capabilities, ext + ".CrsMetadata", "crsSupported");
    }
    if (crs != "") {
        CPLString name = path + "crsSupported";
        metadata = CSLSetNameValue(metadata, name, crs);
    }

    this->SetMetadata( metadata, "" );
    CSLDestroy( metadata );
    metadata = nullptr;

    // contents metadata
    CPLXMLNode *contents = CPLGetXMLNode(Capabilities, "Contents");
    if( contents )
    {
        int index = 1;
        for( CPLXMLNode *summary = contents->psChild; summary != nullptr; summary = summary->psNext)
        {
            if( summary->eType != CXT_Element
                || !EQUAL(summary->pszValue, "CoverageSummary") )
            {
                continue;
            }
            CPLString path3;
            path3.Printf( "SUBDATASET_%d_", index);
            index += 1;

            // the name and description of the subdataset:
            // GDAL Data Model:
            // The value of the _NAME is a string that can be passed to GDALOpen() to access the file.

            CPLString key2 = path3 + "NAME";

            CPLString name = DescribeCoverageURL;
            name = CPLURLAddKVP(name, "version", this->Version());

            CPLXMLNode *node = CPLGetXMLNode(summary, "CoverageId");
            CPLString id;
            if (node) {
                id = CPLGetXMLValue(node, nullptr, "");
            } else {
                node = CPLGetXMLNode(summary, "Identifier");
                if (node) {
                    id = CPLGetXMLValue(node, nullptr, "");
                } else {
                    // todo: maybe not an error since CoverageSummary may be within CoverageSummary (07-067r5 Fig4)
                    CSLDestroy( metadata );
                    CPLError( CE_Failure, CPLE_AppDefined,
                              "Error in capabilities document.\n" );
                    return CE_Failure;
                }
            }
            name = CPLURLAddKVP(name, "coverage", id);
            name = "WCS:" + name;
            metadata = CSLSetNameValue(metadata, key2, name);

            key2 = path3 + "DESC";
            CPLString desc;

            node = CPLGetXMLNode(summary, "Title");
            if (node) {
                desc = CPLGetXMLValue(node, nullptr, "");
            } else {
                desc = id;
            }

            metadata = CSLSetNameValue(metadata, key2, desc);

            // todo: compose global bounding box from WGS84BoundingBox and BoundingBox

            // further subdataset (coverage) parameters are parsed in ParseCoverageCapabilities

        }
    }
    this->SetMetadata( metadata, "SUBDATASETS" );
    CSLDestroy( metadata );
    return CE_None;
}

void WCSDataset110::ParseCoverageCapabilities(CPLXMLNode *capabilities, const CPLString &coverage, CPLXMLNode *metadata)
{
    CPLStripXMLNamespace(capabilities, nullptr, TRUE);
    CPLXMLNode *contents = CPLGetXMLNode(capabilities, "Contents");
    if( contents )
    {
        for( CPLXMLNode *summary = contents->psChild; summary != nullptr; summary = summary->psNext)
        {
            if( summary->eType != CXT_Element
                || !EQUAL(summary->pszValue, "CoverageSummary") )
            {
                continue;
            }
            CPLXMLNode *node = CPLGetXMLNode(summary, "CoverageId");
            CPLString id;
            if (node) {
                id = CPLGetXMLValue(node, nullptr, "");
            } else {
                node = CPLGetXMLNode(summary, "Identifier");
                if (node) {
                    id = CPLGetXMLValue(node, nullptr, "");
                } else {
                    id = "";
                }
            }
            if (id != coverage) {
                continue;
            }

            // Description
            // todo: there could be Title and Abstract for each supported language
            XMLCopyMetadata(summary, metadata, "Title");
            XMLCopyMetadata(summary, metadata, "Abstract");

            // 2.0.1 stuff
            XMLCopyMetadata(summary, metadata, "CoverageSubtype");

            // Keywords
            CPLString kw = GetKeywords(summary, "Keywords", "Keyword");
            CPLAddXMLAttributeAndValue(
                CPLCreateXMLElementAndValue(metadata, "MDI", kw), "key", "Keywords");

            // WCSContents
            const char *tags[] = {
                "SupportedCRS",
                "SupportedFormat",
                "OtherSource"
            };
            for (unsigned int i = 0; i < CPL_ARRAYSIZE(tags); i++) {
                kw = GetKeywords(summary, "", tags[i]);
                CPLAddXMLAttributeAndValue(
                    CPLCreateXMLElementAndValue(metadata, "MDI", kw), "key", tags[i]);
            }

            // skipping WGS84BoundingBox, BoundingBox, Metadata, Extension
            // since those we'll get from coverage description

        }
    }
}
