/******************************************************************************
 * Project:  OGR
 * Purpose:  OGRGMLASDriver implementation
 * Author:   Even Rouault, <even dot rouault at spatialys dot com>
 *
 * Initial development funded by the European Earth observation programme
 * Copernicus
 *
 ******************************************************************************
 * Copyright (c) 2016, Even Rouault, <even dot rouault at spatialys dot com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "ogr_gmlas.h"

#include "cpl_sha256.h"


/************************************************************************/
/*                         GMLASResourceCache()                         */
/************************************************************************/

GMLASResourceCache::GMLASResourceCache()
    : m_bHasCheckedCacheDirectory(false)
    , m_bRefresh(false)
    , m_bAllowDownload(true)
{
}

/************************************************************************/
/*                        ~GMLASResourceCache()                         */
/************************************************************************/

GMLASResourceCache::~GMLASResourceCache()
{
}

/************************************************************************/
/*                         SetCacheDirectory()                          */
/************************************************************************/

void GMLASResourceCache::SetCacheDirectory(const CPLString& osCacheDirectory)
{
    m_osCacheDirectory = osCacheDirectory;
}

/************************************************************************/
/*                     RecursivelyCreateDirectoryIfNeeded()             */
/************************************************************************/

bool GMLASResourceCache::RecursivelyCreateDirectoryIfNeeded(
                                                const CPLString& osDirname)
{
    VSIStatBufL sStat;
    if( VSIStatL(osDirname, &sStat) == 0 )
    {
        return true;
    }

    CPLString osParent = CPLGetDirname(osDirname);
    if( !osParent.empty() && osParent != "." )
    {
        if( !RecursivelyCreateDirectoryIfNeeded(osParent) )
            return false;
    }
    return VSIMkdir( osDirname, 0755 ) == 0;
}

bool GMLASResourceCache::RecursivelyCreateDirectoryIfNeeded()
{
    if( !m_bHasCheckedCacheDirectory )
    {
        m_bHasCheckedCacheDirectory = true;
        if( !RecursivelyCreateDirectoryIfNeeded(m_osCacheDirectory) )
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "Cannot create %s", m_osCacheDirectory.c_str());
            m_osCacheDirectory.clear();
            return false;
        }
    }
    return true;
}

/************************************************************************/
/*                        GetCachedFilename()                           */
/************************************************************************/

CPLString GMLASResourceCache::GetCachedFilename(const CPLString& osResource)
{
    CPLString osLaunderedName(osResource);
    if( osLaunderedName.find("/vsicurl_streaming/") == 0 )
        osLaunderedName = osLaunderedName.substr(
                                strlen("/vsicurl_streaming/") );
    if( osLaunderedName.find("http://") == 0 )
        osLaunderedName = osLaunderedName.substr( strlen("http://") );
    else if( osLaunderedName.find("https://") == 0 )
        osLaunderedName = osLaunderedName.substr( strlen("https://") );
    for(size_t i=0; i<osLaunderedName.size(); i++)
    {
        if( !isalnum(osLaunderedName[i]) && osLaunderedName[i] != '.' )
            osLaunderedName[i] = '_';
    }

    // If filename is too long, then truncate it and put a hash at the end
    // We try to make sure that the whole filename (including the cache path)
    // fits into 255 characters, for windows compat

    const size_t nWindowsMaxFilenameSize = 255;
    // 60 is arbitrary but should be sufficient for most people. We could
    // always take into account m_osCacheDirectory.size(), but if we want to
    // to be able to share caches between computers, then this would be impractical.
    const size_t nTypicalMaxSizeForDirName = 60;
    const size_t nSizeForDirName =
    (m_osCacheDirectory.size() > nTypicalMaxSizeForDirName &&
     m_osCacheDirectory.size() <
        nWindowsMaxFilenameSize - strlen(".tmp") -  2 * CPL_SHA256_HASH_SIZE) ?
                m_osCacheDirectory.size() : nTypicalMaxSizeForDirName;
    CPLAssert( nWindowsMaxFilenameSize >= nSizeForDirName );
    const size_t nMaxFilenameSize = nWindowsMaxFilenameSize - nSizeForDirName;

    CPLAssert( nMaxFilenameSize >= strlen(".tmp") );
    if( osLaunderedName.size() >= nMaxFilenameSize - strlen(".tmp") )
    {
        GByte abyHash[CPL_SHA256_HASH_SIZE];
        CPL_SHA256(osResource, osResource.size(), abyHash);
        char* pszHash = CPLBinaryToHex(CPL_SHA256_HASH_SIZE, abyHash);
        osLaunderedName.resize(nMaxFilenameSize - strlen(".tmp") -  2 * CPL_SHA256_HASH_SIZE);
        osLaunderedName += pszHash;
        CPLFree(pszHash);
        CPLDebug("GMLAS", "Cached filename truncated to %s",
                    osLaunderedName.c_str());
    }

    return CPLFormFilename( m_osCacheDirectory, osLaunderedName, nullptr );
}

/************************************************************************/
/*                          GMLASXSDCache()                             */
/************************************************************************/

GMLASXSDCache::GMLASXSDCache()
{
}

/************************************************************************/
/*                         ~GMLASXSDCache()                             */
/************************************************************************/

GMLASXSDCache::~GMLASXSDCache()
{
}
/************************************************************************/
/*                               Open()                                 */
/************************************************************************/

VSILFILE* GMLASXSDCache::Open( const CPLString& osResource,
                               const CPLString& osBasePath,
                               CPLString& osOutFilename )
{
    osOutFilename = osResource;
    if( osResource.find("http://") == 0 ||
        osResource.find("https://") == 0 )
    {
        osOutFilename = "/vsicurl_streaming/" + osResource;
    }
    else if( CPLIsFilenameRelative( osResource ) && !osResource.empty() )
    {
        /* Transform a/b + ../c --> a/c */
        CPLString osResourceModified(osResource);
        CPLString osBasePathModified(osBasePath);
        while( (osResourceModified.find("../") == 0 ||
                osResourceModified.find("..\\") == 0) &&
               !osBasePathModified.empty() )
        {
            osBasePathModified = CPLGetDirname(osBasePathModified);
            osResourceModified = osResourceModified.substr(3);
        }

        osOutFilename = CPLFormFilename(osBasePathModified,
                                        osResourceModified, nullptr);
    }

    CPLDebug("GMLAS", "Resolving %s (%s) to %s",
                osResource.c_str(),
                osBasePath.c_str(),
                osOutFilename.c_str());

    VSILFILE* fp = nullptr;
    if( !m_osCacheDirectory.empty() &&
        osOutFilename.find("/vsicurl_streaming/") == 0 &&
        RecursivelyCreateDirectoryIfNeeded() )
    {
        CPLString osCachedFileName(GetCachedFilename(osOutFilename));
        if( !m_bRefresh ||
            m_aoSetRefreshedFiles.find(osCachedFileName) !=
                                            m_aoSetRefreshedFiles.end() )
        {
            fp = VSIFOpenL( osCachedFileName, "rb");
        }
        if( fp != nullptr )
        {
            CPLDebug("GMLAS", "Use cached %s", osCachedFileName.c_str());
        }
        else if( m_bAllowDownload )
        {
            if( m_bRefresh )
                m_aoSetRefreshedFiles.insert(osCachedFileName);

            CPLString osTmpfilename( osCachedFileName + ".tmp" );
            if( CPLCopyFile( osTmpfilename, osOutFilename) == 0 )
            {
                // Due to the caching done by /vsicurl_streaming/, if the
                // web server is no longer available but was before in the
                // same process, then file opening will succeed. Hence we
                // check that the downloaded file is not 0. This will only
                // happen in practice with the unit tests.
                VSIStatBufL sStat;
                if( VSIStatL(osTmpfilename, &sStat) == 0 &&
                    sStat.st_size != 0 )
                {
                    VSIRename( osTmpfilename, osCachedFileName );
                    fp = VSIFOpenL(osCachedFileName, "rb");
                }
                else
                {
                    VSIUnlink(osTmpfilename);
                }
            }
        }
    }
    else
    {
        if( m_bAllowDownload ||
            osOutFilename.find("/vsicurl_streaming/") != 0 )
        {
            fp = VSIFOpenL(osOutFilename, "rb");
        }
    }

    if( fp == nullptr )
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Cannot resolve %s", osResource.c_str());
    }

    return fp;
}
