/*******************************************************************************
 *  Project: NextGIS Web Driver
 *  Purpose: Implements NextGIS Web Driver
 *  Author: Dmitry Baryshnikov, dmitry.baryshnikov@nextgis.com
 *  Language: C++
 *******************************************************************************
 *  The MIT License (MIT)
 *
 *  Copyright (c) 2018, NextGIS
 *
 *  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 "ogr_ngw.h"

#include "cpl_http.h"

namespace NGWAPI {

std::string GetPermisions(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/" + osResourceId + "/permission";
}

std::string GetResource(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/" + osResourceId;
}

std::string GetChildren(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/?parent=" + osResourceId;
}

std::string GetFeature(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/" + osResourceId + "/feature/";
}

std::string GetTMS(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/component/render/tile?z=${z}&amp;x=${x}&amp;y=${y}&amp;resource=" +
        osResourceId;
}

std::string GetFeaturePage(CPL_UNUSED const std::string &osUrl, CPL_UNUSED const std::string &osResourceId,
    CPL_UNUSED GIntBig nStart, CPL_UNUSED int nCount)
{
    return ""; // TODO: Add paging support.
}

std::string GetRoute(const std::string &osUrl)
{
    return osUrl + "/api/component/pyramid/route";
}

Uri ParseUri(const std::string &osUrl)
{
    Uri stOut;
    std::size_t nFound = osUrl.find(":");
    if( nFound == std::string::npos )
    {
        return stOut;
    }

    stOut.osPrefix = osUrl.substr(0, nFound);
    std::string osUrlInt = CPLString(osUrl.substr(nFound + 1)).tolower();

    nFound = osUrlInt.find("/resource/");
    if( nFound == std::string::npos )
    {
        return stOut;
    }

    stOut.osAddress = osUrlInt.substr(0, nFound);

    std::string osResourceId = CPLString(osUrlInt.substr(nFound + strlen("/resource/"))).Trim();

    nFound = osResourceId.find('/');
    if( nFound != std::string::npos )
    {
        stOut.osResourceId = osResourceId.substr(0, nFound);
        stOut.osNewResourceName = osResourceId.substr(nFound + 1);
    }
    else
    {
        stOut.osResourceId = osResourceId;
    }

    return stOut;
}

static void ReportError(const GByte *pabyData, int nDataLen)
{
    CPLJSONDocument oResult;
    if( oResult.LoadMemory(pabyData, nDataLen) )
    {
        CPLJSONObject oRoot = oResult.GetRoot();
        if( oRoot.IsValid() )
        {
            std::string osErrorMessage = oRoot.GetString("message");
            if( !osErrorMessage.empty() )
            {
                CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMessage.c_str());
            }
        }
    }
}

std::string CreateResource(const std::string &osUrl, const std::string &osPayload,
    char **papszHTTPOptions)
{
    CPLErrorReset();
    std::string osPayloadInt = "POSTFIELDS=" + osPayload;

    papszHTTPOptions = CSLAddString( papszHTTPOptions, "CUSTOMREQUEST=POST" );
    papszHTTPOptions = CSLAddString( papszHTTPOptions, osPayloadInt.c_str() );
    papszHTTPOptions = CSLAddString( papszHTTPOptions,
        "HEADERS=Content-Type: application/json\r\nAccept: */*" );

    CPLDebug("NGW", "UpdateResource request payload: %s", osPayload.c_str());

    CPLJSONDocument oCreateReq;
    bool bResult = oCreateReq.LoadUrl( GetResource( osUrl, "" ),
        papszHTTPOptions );
    CSLDestroy( papszHTTPOptions );
    std::string osResourceId("-1");
    CPLJSONObject oRoot = oCreateReq.GetRoot();
    if( oRoot.IsValid() )
    {
        if( bResult )
        {
            osResourceId = oRoot.GetString("id", "-1");
        }
        else
        {
            std::string osErrorMessage = oRoot.GetString("message");
            if( !osErrorMessage.empty() )
            {
                CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMessage.c_str());
            }
        }
    }
    return osResourceId;
}

bool UpdateResource(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osPayload, char **papszHTTPOptions)
{
    CPLErrorReset();
    std::string osPayloadInt = "POSTFIELDS=" + osPayload;

    papszHTTPOptions = CSLAddString( papszHTTPOptions, "CUSTOMREQUEST=PUT" );
    papszHTTPOptions = CSLAddString( papszHTTPOptions, osPayloadInt.c_str() );
    papszHTTPOptions = CSLAddString( papszHTTPOptions,
        "HEADERS=Content-Type: application/json\r\nAccept: */*" );

    CPLDebug("NGW", "UpdateResource request payload: %s", osPayload.c_str());

    CPLHTTPResult *psResult = CPLHTTPFetch( GetResource(osUrl, osResourceId).c_str(),
        papszHTTPOptions );
    CSLDestroy( papszHTTPOptions );
    bool bResult = false;
    if( psResult )
    {
        bResult = psResult->nStatus == 0 && psResult->pszErrBuf == nullptr;

        // Get error message.
        if( !bResult )
        {
            ReportError(psResult->pabyData, psResult->nDataLen);
        }
        CPLHTTPDestroyResult(psResult);
    }
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Update resource #%s failed",
            osResourceId.c_str());
    }
    return bResult;
}

bool DeleteResource(const std::string &osUrl, const std::string &osResourceId,
    char **papszHTTPOptions)
{
    CPLErrorReset();
    papszHTTPOptions = CSLAddString(papszHTTPOptions, "CUSTOMREQUEST=DELETE");
    CPLHTTPResult *psResult = CPLHTTPFetch( GetResource(osUrl, osResourceId).c_str(),
        papszHTTPOptions);
    bool bResult = false;
    if( psResult )
    {
        bResult = psResult->nStatus == 0 && psResult->pszErrBuf == nullptr;
        // Get error message.
        if( !bResult )
        {
            ReportError(psResult->pabyData, psResult->nDataLen);
        }
        CPLHTTPDestroyResult(psResult);
    }
    CSLDestroy( papszHTTPOptions );
    return bResult;
}

bool RenameResource(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osNewName, char **papszHTTPOptions)
{
    CPLJSONObject oPayload;
    CPLJSONObject oResource("resource", oPayload);
    oPayload.Add("display_name", osNewName);
    std::string osPayload = oPayload.Format(CPLJSONObject::Plain);

    return UpdateResource( osUrl, osResourceId, osPayload, papszHTTPOptions);
}

OGRwkbGeometryType NGWGeomTypeToOGRGeomType(const std::string &osGeomType)
{
    // http://docs.nextgis.com/docs_ngweb_dev/doc/developer/vector_data_types.html#nextgisweb.feature_layer.interface.GEOM_TYPE
    if( osGeomType == "POINT")
        return wkbPoint;
    else if ( osGeomType == "LINESTRING")
        return wkbLineString;
    else if ( osGeomType == "POLYGON")
        return wkbPolygon;
    else if ( osGeomType == "MULTIPOINT")
        return wkbMultiPoint;
    else if ( osGeomType == "MULTILINESTRING")
        return wkbMultiLineString;
    else if ( osGeomType == "MULTIPOLYGON")
        return wkbMultiPolygon;
    else
        return wkbUnknown;
}

std::string OGRGeomTypeToNGWGeomType(OGRwkbGeometryType eType)
{
    switch(eType)
    { // Don't flatten
        case wkbPoint:
            return "POINT";
        case wkbLineString:
            return "LINESTRING";
        case wkbPolygon:
            return "POLYGON";
        case wkbMultiPoint:
            return "MULTIPOINT";
        case wkbMultiLineString:
            return "MULTILINESTRING";
        case wkbMultiPolygon:
            return "MULTIPOLYGON";
        default:
            return "";
    }
}

OGRFieldType NGWFieldTypeToOGRFieldType(const std::string &osFieldType)
{
    // http://docs.nextgis.com/docs_ngweb_dev/doc/developer/vector_data_types.html#nextgisweb.feature_layer.interface.FIELD_TYPE
    if( osFieldType == "INTEGER")
        return OFTInteger;
    else if ( osFieldType == "BIGINT")
        return OFTInteger64;
    else if ( osFieldType == "REAL")
        return OFTReal;
    else if ( osFieldType == "STRING")
        return OFTString;
    else if ( osFieldType == "DATE")
        return OFTDate;
    else if ( osFieldType == "TIME")
        return OFTTime;
    else if ( osFieldType == "DATETIME")
        return OFTDateTime;
    else
        return OFTString;
}

std::string OGRFieldTypeToNGWFieldType(OGRFieldType eType)
{
    switch(eType) {
        case OFTInteger:
            return "INTEGER";
        case OFTInteger64:
            return "BIGINT";
        case OFTReal:
            return "REAL";
        case OFTString:
            return "STRING";
        case OFTDate:
            return "DATE";
        case OFTTime:
            return "TIME";
        case OFTDateTime:
            return "DATETIME";
        default:
            return "STRING";
    }
}

Permissions CheckPermissions(const std::string &osUrl,
    const std::string &osResourceId, char **papszHTTPOptions, bool bReadWrite)
{
    Permissions stOut;
    CPLErrorReset();
    CPLJSONDocument oPermissionReq;
    bool bResult = oPermissionReq.LoadUrl( GetPermisions( osUrl, osResourceId ),
        papszHTTPOptions );

    CPLJSONObject oRoot = oPermissionReq.GetRoot();
    if( oRoot.IsValid() )
    {
        if( bResult )
        {
            stOut.bResourceCanRead = oRoot.GetBool( "resource/read", true );
            stOut.bResourceCanCreate = oRoot.GetBool( "resource/create", bReadWrite );
            stOut.bResourceCanUpdate = oRoot.GetBool( "resource/update", bReadWrite );
            stOut.bResourceCanDelete = oRoot.GetBool( "resource/delete", bReadWrite );

            stOut.bDatastructCanRead = oRoot.GetBool( "datastruct/read", true );
            stOut.bDatastructCanWrite = oRoot.GetBool( "datastruct/write", bReadWrite );

            stOut.bDataCanRead = oRoot.GetBool( "data/read", true );
            stOut.bDataCanWrite = oRoot.GetBool( "data/write", bReadWrite );

            stOut.bMetadataCanRead = oRoot.GetBool( "metadata/read", true );
            stOut.bMetadataCanWrite = oRoot.GetBool( "metadata/write", bReadWrite );
        }
        else
        {
            std::string osErrorMessage = oRoot.GetString("message");
            if( osErrorMessage.empty() )
            {
                osErrorMessage = "Get permissions failed";
            }
            CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMessage.c_str());
        }
    }
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Get permissions failed");
    }

    return stOut;
}

std::string GetFeatureCount(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/" + osResourceId + "/feature_count";
}

std::string GetLayerExtent(const std::string &osUrl, const std::string &osResourceId)
{
    return osUrl + "/api/resource/" + osResourceId + "/extent";
}

std::string GetResmetaSuffix(CPLJSONObject::Type eType)
{
    switch( eType ) {
        case CPLJSONObject::Integer:
        case CPLJSONObject::Long:
            return ".d";
        case CPLJSONObject::Double:
            return ".f";
        default:
            return "";
    }
}

void FillResmeta(CPLJSONObject &oRoot, char **papszMetadata)
{
    CPLJSONObject oResMeta("resmeta", oRoot);
    CPLJSONObject oResMetaItems("items", oResMeta);
    CPLStringList oaMetadata(papszMetadata, FALSE);
    for( int i = 0; i < oaMetadata.size(); ++i )
    {
        std::string osItem = oaMetadata[i];
        size_t nPos = osItem.find("=");
        if( nPos != std::string::npos )
        {
            std::string osItemName = osItem.substr( 0, nPos );
            CPLString osItemValue = osItem.substr( nPos + 1 );

            if( osItemName.size() > 2 )
            {
                size_t nSuffixPos = osItemName.size() - 2;
                std::string osSuffix = osItemName.substr(nSuffixPos);
                if( osSuffix == ".d")
                {
                    GInt64 nVal = CPLAtoGIntBig( osItemValue.c_str() );
                    oResMetaItems.Add( osItemName.substr(0, nSuffixPos), nVal );
                    continue;
                }

                if( osSuffix == ".f")
                {
                    oResMetaItems.Add( osItemName.substr(0, nSuffixPos),
                        CPLAtofM(osItemValue.c_str()) );
                    continue;
                }
            }

            oResMetaItems.Add(osItemName, osItemValue);
        }
    }
}

bool FlushMetadata(const std::string &osUrl, const std::string &osResourceId,
    char **papszMetadata, char **papszHTTPOptions )
{
    if( nullptr == papszMetadata )
    {
        return true;
    }
    CPLJSONObject oMetadataJson;
    FillResmeta(oMetadataJson, papszMetadata);

    return UpdateResource( osUrl, osResourceId,
        oMetadataJson.Format(CPLJSONObject::Plain), papszHTTPOptions);
}

bool DeleteFeature(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osFeatureId, char **papszHTTPOptions)
{
    CPLErrorReset();
    papszHTTPOptions = CSLAddString(papszHTTPOptions, "CUSTOMREQUEST=DELETE");
    std::string osUrlInt = GetFeature(osUrl, osResourceId) + osFeatureId;
    CPLHTTPResult *psResult = CPLHTTPFetch( osUrlInt.c_str(), papszHTTPOptions);
    CSLDestroy( papszHTTPOptions );
    bool bResult = false;
    if( psResult )
    {
        bResult = psResult->nStatus == 0 && psResult->pszErrBuf == nullptr;
        // Get error message.
        if( !bResult )
        {
            ReportError(psResult->pabyData, psResult->nDataLen);
        }
        CPLHTTPDestroyResult(psResult);
    }
    return bResult;
}

GIntBig CreateFeature(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osFeatureJson, char **papszHTTPOptions)
{
    CPLErrorReset();
    std::string osPayloadInt = "POSTFIELDS=" + osFeatureJson;

    papszHTTPOptions = CSLAddString( papszHTTPOptions, "CUSTOMREQUEST=POST" );
    papszHTTPOptions = CSLAddString( papszHTTPOptions, osPayloadInt.c_str() );
    papszHTTPOptions = CSLAddString( papszHTTPOptions,
        "HEADERS=Content-Type: application/json\r\nAccept: */*" );

    CPLDebug("NGW", "CreateFeature request payload: %s", osFeatureJson.c_str());

    std::string osUrlInt = GetFeature( osUrl, osResourceId );

    CPLJSONDocument oCreateFeatureReq;
    bool bResult = oCreateFeatureReq.LoadUrl( osUrlInt, papszHTTPOptions );
    CSLDestroy( papszHTTPOptions );

    CPLJSONObject oRoot = oCreateFeatureReq.GetRoot();
    GIntBig nOutFID = OGRNullFID;
    if( oRoot.IsValid() )
    {
        if( bResult )
        {
            nOutFID = oRoot.GetLong( "id", OGRNullFID );
        }
        else
        {
            std::string osErrorMessage = oRoot.GetString("message");
            if( osErrorMessage.empty() )
            {
                osErrorMessage = "Create new feature failed";
            }
            CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMessage.c_str());
        }
    }
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Create new feature failed");
    }

    CPLDebug("NGW", "CreateFeature new FID: " CPL_FRMT_GIB, nOutFID);
    return nOutFID;
}

bool UpdateFeature(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osFeatureId, const std::string &osFeatureJson,
    char **papszHTTPOptions)
{
    CPLErrorReset();
    std::string osPayloadInt = "POSTFIELDS=" + osFeatureJson;

    papszHTTPOptions = CSLAddString( papszHTTPOptions, "CUSTOMREQUEST=PUT" );
    papszHTTPOptions = CSLAddString( papszHTTPOptions, osPayloadInt.c_str() );
    papszHTTPOptions = CSLAddString( papszHTTPOptions,
        "HEADERS=Content-Type: application/json\r\nAccept: */*" );

    CPLDebug("NGW", "UpdateFeature request payload: %s", osFeatureJson.c_str());

    std::string osUrlInt = GetFeature(osUrl, osResourceId) + osFeatureId;
    CPLHTTPResult *psResult = CPLHTTPFetch( osUrlInt.c_str(), papszHTTPOptions );
    CSLDestroy( papszHTTPOptions );
    bool bResult = false;
    if( psResult )
    {
        bResult = psResult->nStatus == 0 && psResult->pszErrBuf == nullptr;

        // Get error message.
        if( !bResult )
        {
            ReportError(psResult->pabyData, psResult->nDataLen);
        }
        CPLHTTPDestroyResult(psResult);
    }
    return bResult;
}

bool PatchFeatures(const std::string &osUrl, const std::string &osResourceId,
    const std::string &osFeaturesJson, char **papszHTTPOptions)
{
    CPLErrorReset();
    std::string osPayloadInt = "POSTFIELDS=" + osFeaturesJson;

    papszHTTPOptions = CSLAddString( papszHTTPOptions, "CUSTOMREQUEST=PATCH" );
    papszHTTPOptions = CSLAddString( papszHTTPOptions, osPayloadInt.c_str() );
    papszHTTPOptions = CSLAddString( papszHTTPOptions,
        "HEADERS=Content-Type: application/json\r\nAccept: */*" );

    CPLDebug("NGW", "UpdateFeature request payload: %s", osFeaturesJson.c_str());

    std::string osUrlInt = GetFeature(osUrl, osResourceId);
    CPLHTTPResult *psResult = CPLHTTPFetch( osUrlInt.c_str(), papszHTTPOptions );
    CSLDestroy( papszHTTPOptions );
    bool bResult = false;
    if( psResult )
    {
        bResult = psResult->nStatus == 0 && psResult->pszErrBuf == nullptr;

        // Get error message.
        if( !bResult )
        {
            ReportError(psResult->pabyData, psResult->nDataLen);
        }
        CPLHTTPDestroyResult(psResult);
    }
    return bResult;
}

bool GetExtent(const std::string &osUrl, const std::string &osResourceId,
    char **papszHTTPOptions, int nEPSG, OGREnvelope &stExtent)
{
    CPLErrorReset();
    CPLJSONDocument oExtentReq;
    bool bResult = oExtentReq.LoadUrl( GetLayerExtent( osUrl, osResourceId ),
        papszHTTPOptions);

    CPLJSONObject oRoot = oExtentReq.GetRoot();
    if( !bResult)
    {
        std::string osErrorMessage = oRoot.GetString("message");
        if( osErrorMessage.empty() )
        {
            osErrorMessage = "Create new feature failed";
        }
        CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMessage.c_str());
        return false;
    }
    // Response extent spatial reference is EPSG:4326.

    double dfMinX = oRoot.GetDouble("extent/minLon");
    double dfMinY = oRoot.GetDouble("extent/minLat");
    double dfMaxX = oRoot.GetDouble("extent/maxLon");
    double dfMaxY = oRoot.GetDouble("extent/maxLat");

    double adfCoordinatesX[4];
    double adfCoordinatesY[4];
    adfCoordinatesX[0] = dfMinX;
    adfCoordinatesY[0] = dfMinY;
    adfCoordinatesX[1] = dfMinX;
    adfCoordinatesY[1] = dfMaxY;
    adfCoordinatesX[2] = dfMaxX;
    adfCoordinatesY[2] = dfMaxY;
    adfCoordinatesX[3] = dfMaxX;
    adfCoordinatesY[3] = dfMinY;

    OGRSpatialReference *po4326SRS = OGRSpatialReference::GetWGS84SRS();
    OGRSpatialReference o3857SRS;
    if( o3857SRS.importFromEPSG(nEPSG) != OGRERR_NONE )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Project extent SRS to EPSG:3857 failed");
        return false;
    }

    OGRCoordinateTransformation *poTransform =
        OGRCreateCoordinateTransformation( po4326SRS, &o3857SRS );
    if( poTransform )
    {
        poTransform->Transform( 4, adfCoordinatesX, adfCoordinatesY );
        delete poTransform;

        stExtent.MinX = std::numeric_limits<double>::max();
        stExtent.MaxX = std::numeric_limits<double>::min();
        stExtent.MinY = std::numeric_limits<double>::max();
        stExtent.MaxY = std::numeric_limits<double>::min();

        for(int i = 1; i < 4; ++i)
        {
            if( stExtent.MinX > adfCoordinatesX[i] )
            {
                stExtent.MinX = adfCoordinatesX[i];
            }
            if( stExtent.MaxX < adfCoordinatesX[i] )
            {
                stExtent.MaxX = adfCoordinatesX[i];
            }
            if( stExtent.MinY > adfCoordinatesY[i] )
            {
                stExtent.MinY = adfCoordinatesY[i];
            }
            if( stExtent.MaxY < adfCoordinatesY[i] )
            {
                stExtent.MaxY = adfCoordinatesY[i];
            }
        }
    }
    return true;
}

} // namespace NGWAPI
