/******************************************************************************
 *
 * Project:  WMS Client Driver
 * Purpose:  Implementation of Dataset and RasterBand classes for WMS
 *           and other similar services.
 * Author:   Adam Nowacki, nowak@xpam.de
 *
 ******************************************************************************
 * Copyright (c) 2007, Adam Nowacki
 * Copyright (c) 2017, Dmitry Baryshnikov, <polimax@mail.ru>
 * Copyright (c) 2017, NextGIS, <info@nextgis.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 "cpl_md5.h"
#include "wmsdriver.h"

static void CleanCacheThread(void *pData)
{
    GDALWMSCache *pCache = static_cast<GDALWMSCache *>(pData);
    pCache->Clean();
}

//------------------------------------------------------------------------------
// GDALWMSFileCache
//------------------------------------------------------------------------------
class GDALWMSFileCache : public GDALWMSCacheImpl
{
  public:
    GDALWMSFileCache(const CPLString &soPath, CPLXMLNode *pConfig)
        : GDALWMSCacheImpl(soPath, pConfig), m_osPostfix(""), m_nDepth(2),
          m_nExpires(604800),            // 7 days
          m_nMaxSize(67108864),          // 64 Mb
          m_nCleanThreadRunTimeout(120)  // 3 min
    {
        const char *pszCacheDepth = CPLGetXMLValue(pConfig, "Depth", "2");
        if (pszCacheDepth != nullptr)
            m_nDepth = atoi(pszCacheDepth);

        const char *pszCacheExtension =
            CPLGetXMLValue(pConfig, "Extension", nullptr);
        if (pszCacheExtension != nullptr)
            m_osPostfix = pszCacheExtension;

        const char *pszCacheExpires =
            CPLGetXMLValue(pConfig, "Expires", nullptr);
        if (pszCacheExpires != nullptr)
        {
            m_nExpires = atoi(pszCacheExpires);
            CPLDebug("WMS", "Cache expires in %d sec", m_nExpires);
        }

        const char *pszCacheMaxSize =
            CPLGetXMLValue(pConfig, "MaxSize", nullptr);
        if (pszCacheMaxSize != nullptr)
            m_nMaxSize = atol(pszCacheMaxSize);

        const char *pszCleanThreadRunTimeout =
            CPLGetXMLValue(pConfig, "CleanTimeout", nullptr);
        if (pszCleanThreadRunTimeout != nullptr)
        {
            m_nCleanThreadRunTimeout = atoi(pszCleanThreadRunTimeout);
            CPLDebug("WMS", "Clean Thread Run Timeout is %d sec",
                     m_nCleanThreadRunTimeout);
        }
    }

    virtual int GetCleanThreadRunTimeout() override
    {
        return m_nCleanThreadRunTimeout;
    }

    virtual CPLErr Insert(const char *pszKey,
                          const CPLString &osFileName) override
    {
        // Warns if it fails to write, but returns success
        CPLString soFilePath = GetFilePath(pszKey);
        MakeDirs(CPLGetDirname(soFilePath));
        if (CPLCopyFile(soFilePath, osFileName) == CE_None)
            return CE_None;
        // Warn if it fails after folder creation
        CPLError(CE_Warning, CPLE_FileIO, "Error writing to WMS cache %s",
                 m_soPath.c_str());
        return CE_None;
    }

    virtual enum GDALWMSCacheItemStatus
    GetItemStatus(const char *pszKey) const override
    {
        VSIStatBufL sStatBuf;
        if (VSIStatL(GetFilePath(pszKey), &sStatBuf) == 0)
        {
            long seconds = static_cast<long>(time(nullptr) - sStatBuf.st_mtime);
            return seconds < m_nExpires ? CACHE_ITEM_OK : CACHE_ITEM_EXPIRED;
        }
        return CACHE_ITEM_NOT_FOUND;
    }

    virtual GDALDataset *GetDataset(const char *pszKey,
                                    char **papszOpenOptions) const override
    {
        return GDALDataset::FromHandle(GDALOpenEx(
            GetFilePath(pszKey),
            GDAL_OF_RASTER | GDAL_OF_READONLY | GDAL_OF_VERBOSE_ERROR, nullptr,
            papszOpenOptions, nullptr));
    }

    virtual void Clean() override
    {
        char **papszList = VSIReadDirRecursive(m_soPath);
        if (papszList == nullptr)
        {
            return;
        }

        int counter = 0;
        std::vector<int> toDelete;
        long nSize = 0;
        time_t nTime = time(nullptr);
        while (papszList[counter] != nullptr)
        {
            const char *pszPath =
                CPLFormFilename(m_soPath, papszList[counter], nullptr);
            VSIStatBufL sStatBuf;
            if (VSIStatL(pszPath, &sStatBuf) == 0)
            {
                if (!VSI_ISDIR(sStatBuf.st_mode))
                {
                    long seconds = static_cast<long>(nTime - sStatBuf.st_mtime);
                    if (seconds > m_nExpires)
                    {
                        toDelete.push_back(counter);
                    }

                    nSize += static_cast<long>(sStatBuf.st_size);
                }
            }
            counter++;
        }

        if (nSize > m_nMaxSize)
        {
            CPLDebug("WMS", "Delete %u items from cache",
                     static_cast<unsigned int>(toDelete.size()));
            for (size_t i = 0; i < toDelete.size(); ++i)
            {
                const char *pszPath =
                    CPLFormFilename(m_soPath, papszList[toDelete[i]], nullptr);
                VSIUnlink(pszPath);
            }
        }

        CSLDestroy(papszList);
    }

  private:
    CPLString GetFilePath(const char *pszKey) const
    {
        CPLString soHash(CPLMD5String(pszKey));
        CPLString soCacheFile(m_soPath);

        if (!soCacheFile.empty() && soCacheFile.back() != '/')
        {
            soCacheFile.append(1, '/');
        }

        for (int i = 0; i < m_nDepth; ++i)
        {
            soCacheFile.append(1, soHash[i]);
            soCacheFile.append(1, '/');
        }
        soCacheFile.append(soHash);
        soCacheFile.append(m_osPostfix);
        return soCacheFile;
    }

    static void MakeDirs(const char *pszPath)
    {
        if (IsPathExists(pszPath))
        {
            return;
        }
        // Recursive makedirs, ignoring errors
        const char *pszDirPath = CPLGetDirname(pszPath);
        MakeDirs(pszDirPath);

        VSIMkdir(pszPath, 0744);
    }

    static bool IsPathExists(const char *pszPath)
    {
        VSIStatBufL sbuf;
        return VSIStatL(pszPath, &sbuf) == 0;
    }

  private:
    CPLString m_osPostfix;
    int m_nDepth;
    int m_nExpires;
    long m_nMaxSize;
    int m_nCleanThreadRunTimeout;
};

//------------------------------------------------------------------------------
// GDALWMSCache
//------------------------------------------------------------------------------

GDALWMSCache::GDALWMSCache()
    : m_osCachePath("./gdalwmscache"), m_bIsCleanThreadRunning(false),
      m_nCleanThreadLastRunTime(0), m_poCache(nullptr), m_hThread(nullptr)
{
}

GDALWMSCache::~GDALWMSCache()
{
    if (m_hThread)
        CPLJoinThread(m_hThread);
    delete m_poCache;
}

CPLErr GDALWMSCache::Initialize(const char *pszUrl, CPLXMLNode *pConfig)
{
    const char *pszXmlCachePath = CPLGetXMLValue(pConfig, "Path", nullptr);
    const char *pszUserCachePath =
        CPLGetConfigOption("GDAL_DEFAULT_WMS_CACHE_PATH", nullptr);
    if (pszXmlCachePath != nullptr)
    {
        m_osCachePath = pszXmlCachePath;
    }
    else if (pszUserCachePath != nullptr)
    {
        m_osCachePath = pszUserCachePath;
    }

    // Separate folder for each unique dataset url
    if (CPLTestBool(CPLGetXMLValue(pConfig, "Unique", "True")))
    {
        m_osCachePath =
            CPLFormFilename(m_osCachePath, CPLMD5String(pszUrl), nullptr);
    }

    // TODO: Add sqlite db cache type
    const char *pszType = CPLGetXMLValue(pConfig, "Type", "file");
    if (EQUAL(pszType, "file"))
    {
        m_poCache = new GDALWMSFileCache(m_osCachePath, pConfig);
    }

    return CE_None;
}

CPLErr GDALWMSCache::Insert(const char *pszKey, const CPLString &soFileName)
{
    if (m_poCache != nullptr && pszKey != nullptr)
    {
        // Add file to cache
        CPLErr result = m_poCache->Insert(pszKey, soFileName);
        if (result == CE_None)
        {
            // Start clean thread
            int cleanThreadRunTimeout = m_poCache->GetCleanThreadRunTimeout();
            if (cleanThreadRunTimeout > 0 && !m_bIsCleanThreadRunning &&
                time(nullptr) - m_nCleanThreadLastRunTime >
                    cleanThreadRunTimeout)
            {
                if (m_hThread)
                    CPLJoinThread(m_hThread);
                m_bIsCleanThreadRunning = true;
                m_hThread = CPLCreateJoinableThread(CleanCacheThread, this);
            }
        }
        return result;
    }

    return CE_Failure;
}

enum GDALWMSCacheItemStatus
GDALWMSCache::GetItemStatus(const char *pszKey) const
{
    if (m_poCache != nullptr)
    {
        return m_poCache->GetItemStatus(pszKey);
    }
    return CACHE_ITEM_NOT_FOUND;
}

GDALDataset *GDALWMSCache::GetDataset(const char *pszKey,
                                      char **papszOpenOptions) const
{
    if (m_poCache != nullptr)
    {
        return m_poCache->GetDataset(pszKey, papszOpenOptions);
    }
    return nullptr;
}

void GDALWMSCache::Clean()
{
    if (m_poCache != nullptr)
    {
        CPLDebug("WMS", "Clean cache");
        m_poCache->Clean();
    }

    m_nCleanThreadLastRunTime = time(nullptr);
    m_bIsCleanThreadRunning = false;
}
