/*
   Directory caching code for Netdrive plugins.
   Copyright (C) netlabs.org 2010-2016
*/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>

#include "dircache.h"

PLUGINHELPERTABLE2L *ph;

/*
 * An entry holds file name and a pointer to custom data
 */
typedef struct DirectoryCacheEntryData
{
    const char  fname[PATH_MAX];
    const void* customData;
} DirectoryCacheEntryData;

/*
 * An entry in the directory cache contains one directory listing.
 */
typedef struct DirectoryCacheEntry
{
    struct DirectoryCacheEntry *pNext;
    struct DirectoryCacheEntry *pPrev;

    DirectoryCacheEntryData *aInfos;
    int cInfos;
    int cInfosAllocated;

    char *pszPath;
    ULONG ulHash;
    ULONG ulLastUpdateTime;
    int fInvalid;
} DirectoryCacheEntry;

typedef struct DirectoryCache
{
    NDMUTEX mutex;

    DirectoryCacheEntry *pEntriesHead;
    DirectoryCacheEntry *pEntriesTail;
    int cEntries;
    int fEnabled;
    unsigned long ulExpirationTime;
    int cMaxEntries;
    // resource handle, used only for per-share logging
    void* resource;
    // callback called to release data structures
    PFNFREEDIRENTRY release;

} DirectoryCache;

enum {
    CacheFault = 0,
    CacheOk = 1
};

/*
 * Static helpers.
 */
static int lockCreate (NDMUTEX *pMutex)
{
    return ph->fsphCreateMutex (pMutex);
}
        
static void lockDelete (NDMUTEX mutex)
{
    ph->fsphCloseMutex (mutex);
}
        
static int lockRequest (NDMUTEX mutex)
{
    return ph->fsphRequestMutex (mutex, 10000);
}

void lockRelease (NDMUTEX mutex)
{
    ph->fsphReleaseMutex (mutex);
}

static ULONG Hash (const char *pData, ULONG ulLen) 
{
    ULONG hash = 0ul, g;
    static ULONG ulHashModule = 30031ul;
    
    const char *p;
    int i;
    for( p = pData, i = 0; i < ulLen; i++, p++ ) 
    {
        hash = ( hash << 4 ) + (*p);
        g = hash & 0xf0000000ul;
        if ( g ) 
        {
            hash ^= ( g >> 24 );
            hash ^= g;
        }
    }
    return ( hash % ulHashModule );
}

static unsigned long timesec (void) 
{
    ULONG ul = 0;
    DosQuerySysInfo (QSV_TIME_LOW, QSV_TIME_LOW, &ul, sizeof (ul));
    return ul;
}

static unsigned long computehash (const char *s)
{
    return s? Hash ((char *)s, strlen (s)): 0;
}

static int dceCreate (DirectoryCacheEntry **ppdce, DirectoryCache* pdc, const char *path, int cFiles)
{
    DirectoryCacheEntry *pdce = (DirectoryCacheEntry *)malloc(sizeof(DirectoryCacheEntry));

    if (!pdce)
    {
        return ERROR_NOT_ENOUGH_MEMORY;
    }

    pdce->aInfos = (DirectoryCacheEntryData *)
            malloc(cFiles * sizeof (DirectoryCacheEntryData));
    if (!pdce->aInfos)
    {
        free (pdce);
        return ERROR_NOT_ENOUGH_MEMORY;
    }
    pdce->cInfos = 0;
    pdce->cInfosAllocated = cFiles;
    pdce->ulHash = computehash (path);
    pdce->ulLastUpdateTime = 0;
    pdce->fInvalid = 0;

    int l = strlen (path);
    pdce->pszPath = (char *)malloc (l + 1);
    memcpy (pdce->pszPath, path, l + 1);
    
    pdce->pNext = NULL;
    pdce->pPrev = NULL;

    *ppdce = pdce;
    
    return NO_ERROR;
}

static void dceDelete (DirectoryCache *pdc, DirectoryCacheEntry *pdce)
{
    int i;

    debuglocal(pdc->resource, 9, "dceDelete: pdc %p, pdce %p\n", pdc, pdce);

    /* use plugin callback to deallocate memory stored into aInfos */
    if (pdc->release)
       for (i = 0; i < pdce->cInfos; i++)
       {
          debuglocal(pdc->resource, 9, "dceDelete: #%d->%s->%p\n",
                     i, pdce->aInfos[i].fname, pdce->aInfos[i].customData);
          pdc->release( (void*)pdce->aInfos[i].customData);
       }

    /* now free dce local data */
    free(pdce->aInfos);
    free(pdce->pszPath);
    free(pdce);
}
                
static void dceClear (DirectoryCacheEntry *pdce)
{
    pdce->cInfos = 0;
}

static void dceWriteEntry (DirectoryCache *pdc, DirectoryCacheEntry *pdce,
                           const char *fname, const void *finfo)
{
    if (pdce->cInfos >= pdce->cInfosAllocated)
    {
        /* 50% increase of the buffer, but at least 1 more. */
        int cNewAllocated = pdce->cInfosAllocated + pdce->cInfosAllocated / 2 + 1;

        DirectoryCacheEntryData *pNewInfos = (DirectoryCacheEntryData *)
                realloc(pdce->aInfos, cNewAllocated *
                        sizeof (DirectoryCacheEntryData));

        debuglocal(pdc->resource, 9, "dceWriteEntry: [%s] realloc %d -> %d\n", pdce->pszPath, pdce->cInfosAllocated, cNewAllocated);
        if (pNewInfos)
        {
            pdce->cInfosAllocated = cNewAllocated;
            pdce->aInfos = pNewInfos;
        }
        else
        {
            /* Mark the entry as invalid. The entry will be deleted in dircache_write_end */
            pdce->fInvalid = 1;
            return;
        }
    }

    // store entry name and custom data
    strcpy( (char*)pdce->aInfos[pdce->cInfos].fname, fname);
    pdce->aInfos[pdce->cInfos].customData = finfo;
    pdce->cInfos++;
}

static void dceAdjust (DirectoryCache *pdc, DirectoryCacheEntry *pdce)
{
    /* If the entry has too many preallocated info structures, adjust the memory allocation */
    int cFree = pdce->cInfosAllocated - pdce->cInfos;

    if (cFree > pdce->cInfos / 2)
    {
        /* More than 50% is free. Make the free space 2 times smaller. */
        int cNewAllocated = pdce->cInfos + cFree / 2;

        DirectoryCacheEntryData *pNewInfos = (DirectoryCacheEntryData *)
                realloc(pdce->aInfos,
                        cNewAllocated * sizeof (DirectoryCacheEntryData));

        debuglocal(pdc->resource, 9, "dceAdjust: [%s] realloc %d -> %d\n", pdce->pszPath, pdce->cInfosAllocated, cNewAllocated);
        if (pNewInfos)
        {
            pdce->cInfosAllocated = cNewAllocated;
            pdce->aInfos = pNewInfos;
        }
        else
        {
            /* Ignore. The old buffer remains. */
        }
    }
}

static int dcePathEqualsTo (DirectoryCache *pdc, DirectoryCacheEntry *pdce, const char *path)
{
    debuglocal(pdc->resource, 9, "dcePathEqualTo: [%s] [%s]\n", path, pdce->pszPath);
    return ph->fsphStrICmp (path, pdce->pszPath) == 0; 
}

static int dcePathPrefixedWith (DirectoryCacheEntry *pdce, const char *path)
{ 
    return ph->fsphStrNICmp (path, pdce->pszPath, strlen (path)) == 0; 
}

static int dceExpired (DirectoryCacheEntry *pdce, unsigned long ulExpirationTime)
{
    return (timesec () - pdce->ulLastUpdateTime >= ulExpirationTime);
}
                
static struct DirectoryCacheEntry *dcFindDirCache (DirectoryCache *pdc, const char *path, int fPrefix)
{
    unsigned long hash = computehash (path);

    DirectoryCacheEntry *iter = pdc->pEntriesHead;

    debuglocal(pdc->resource, 9, "dcFindDirCache: [%s]\n", path);
    debuglocal(pdc->resource, 9, "dcFindDirCache: iter [%p]\n", iter);

    while (iter != NULL)
    {
        debuglocal(pdc->resource, 9, "dcFindDirCache: entry [%s]\n", iter->pszPath);
        if (fPrefix)
        {
            if (dcePathPrefixedWith (iter, path))
            {
                break;
            }
        }
        else
        {
            if (    iter->ulHash == hash
                 && dcePathEqualsTo (pdc, iter, path)
               )
            {
                break;
            }
        }
        iter = iter->pNext;
    }

    debuglocal(pdc->resource, 9, "dcFindDirCache: %p\n", iter);

    return iter;
}

static int dcCreate (struct DirectoryCache **ppdc, void* pRes)
{
    int rc;

    DirectoryCache *pdc = (DirectoryCache *)malloc(sizeof (DirectoryCache));
    if (!pdc)
    {
        return ERROR_NOT_ENOUGH_MEMORY;
    }

    rc = lockCreate (&pdc->mutex);
    if (rc != NO_ERROR)
    {
        free(pdc);
        return rc;
    }

    pdc->pEntriesHead = NULL;
    pdc->pEntriesTail = NULL;
    pdc->cEntries = 0;

    pdc->fEnabled = 0;
    pdc->ulExpirationTime = -1;
    pdc->resource = pRes;
    
    *ppdc = pdc;
    
    return NO_ERROR;
}

static void dcDelete (struct DirectoryCache *pdc)
{
    DirectoryCacheEntry *iter = pdc->pEntriesHead;
    debuglocal(pdc->resource, 9, "dcDelete: pdc %p, iter %p\n", pdc, iter);

    while (iter != NULL)
    {
        DirectoryCacheEntry *next = iter->pNext;

        dceDelete (pdc, iter);
        
        iter = next;
    }

    lockDelete (pdc->mutex);
    free (pdc);
}

static void dcRemoveEntry (DirectoryCache *pdc, DirectoryCacheEntry *pdce)
{
    if (pdce->pNext)
    {
        pdce->pNext->pPrev = pdce->pPrev;
    }
    else
    {
        pdc->pEntriesTail = pdce->pPrev;
    }

    if (pdce->pPrev)
    {
        pdce->pPrev->pNext = pdce->pNext;
    }
    else
    {
        pdc->pEntriesHead = pdce->pNext;
    }
    
    pdce->pNext = NULL;
    pdce->pPrev = NULL;

    pdc->cEntries--;
}

static void dcInsertEntry (DirectoryCache *pdc, DirectoryCacheEntry *pdce)
{
    pdce->pNext = pdc->pEntriesHead;

    if (pdc->pEntriesHead)
    {
        pdc->pEntriesHead->pPrev = pdce;
    }
    else
    {
        pdc->pEntriesTail = pdce;
    }
    
    pdc->pEntriesHead = pdce;
    pdc->cEntries++;
}

        
static void dcEnable (DirectoryCache *pdc, int fEnable, unsigned long ulExpirationTime, int cMaxEntries)
{
    pdc->fEnabled = fEnable;
    pdc->ulExpirationTime = ulExpirationTime;
    pdc->cMaxEntries = cMaxEntries;
};

static int dcExistsInCache (DirectoryCache *pdc, const char *path)
{
    int rc = CacheFault;
        
    if (pdc->fEnabled)
    {
        if (dcFindDirCache (pdc, path, 0) != NULL)
        {
            rc = CacheOk;
        }
        
        debuglocal(pdc->resource, 9, "ExistsInCache: [%s], %d\n", path, rc);
    }
    
    return rc;
}
        
static int dcRead (DirectoryCache *pdc, const char *path,
                   PFNADDDIRENTRY fn,
                   void* plist,
                   int *ptotal_received, int fForceCache)
{
    int i;
    int rc = CacheFault;

    if (pdc->fEnabled)
    {
        DirectoryCacheEntry *pdce;

        pdce = dcFindDirCache (pdc, path, 0);
        
        if (pdce)
        {
            debuglocal(pdc->resource, 9, "dcRead: entry %p found for [%s]\n", pdce, path);
            
            if (fForceCache)
            {
                for (i = 0; i < pdce->cInfos; i++)
                {
                    fn (plist, (void*)pdce->aInfos[i].customData);
                }

                rc = CacheOk;
            }
            else
            {
                if (dceExpired (pdce, pdc->ulExpirationTime))
                {
                    debuglocal(pdc->resource, 9, "dcRead: expired\n");
                    dcRemoveEntry (pdc, pdce);
                    dceDelete (pdc, pdce);
                }
                else
                {
                    debuglocal(pdc->resource, 9, "dcRead: adding %d entries from cache\n", pdce->cInfos);
                    for (i = 0; i < pdce->cInfos; i++)
                    {
                        debuglocal(pdc->resource, 9, "dcRead: #%d->%s->%p\n",
                                   i, pdce->aInfos[i].fname, pdce->aInfos[i].customData);
                        fn (plist, (void*)pdce->aInfos[i].customData);
                    }

                    rc = CacheOk;
                }
            }
            
            if (rc == CacheOk)
            {
                *ptotal_received = (int)pdce->cInfos;
            }
        }
        
        debuglocal(pdc->resource, 9, "dcRead: [%s], %d\n", path, rc);
    }
    
    return rc;
}

static DirectoryCacheEntry *dcWriteBegin (DirectoryCache *pdc, const char *path, int cFiles)
{
    DirectoryCacheEntry *pdce = NULL;

    if (pdc->fEnabled)
    {
        pdce = dcFindDirCache (pdc, path, 0);
        
        if (!pdce)
        {
            /* Does not exist in the cache yet. */
            dceCreate (&pdce, pdc, path, cFiles);
        }
        else
        {
            /* Discard the listing. */
            dceClear (pdce);
            /* Remove the entry from list. It will be added again in dircache_write_end. */
            dcRemoveEntry (pdc, pdce);
        }

        if (pdce)
        {
            pdce->ulLastUpdateTime = timesec ();
        }
        
        debuglocal(pdc->resource, 9, "CacheWriteBegin: %s\n", path);
    }
    
    return pdce;
}


static void dcInvalidate (DirectoryCache *pdc, const char *path, int fPrefix)
{
    DirectoryCacheEntry *pdce;
    
    debuglocal(pdc->resource, 9, "CacheInvalidate: [%s]\n", path);

    pdce = dcFindDirCache (pdc, path, fPrefix);

    while (pdce)
    {
        dcRemoveEntry (pdc, pdce);
        dceDelete (pdc, pdce);
        
        pdce = dcFindDirCache (pdc, path, fPrefix);
    }
}

static int dcFindPath (DirectoryCache *pdc,
                       const char *path,
                       const char *parent,
                       const char *name,
                       void **finfo,
                       unsigned long *pulAge)
{
    DirectoryCacheEntry *pdce = pdc->pEntriesHead;

    unsigned long hash = computehash (parent);

    debuglocal(pdc->resource, 9, "dcFindPath: [%s][%s]\n", parent, name);

    while (pdce != NULL)
    {
        debuglocal(pdc->resource, 9, "dcFindPath: entry [%s]\n", pdce->pszPath);

        if (   pdce->ulHash == hash
            && dcePathEqualsTo (pdc, pdce, parent))
        {
            /* This entry should contain the path. */
            int i;
            for (i = 0; i < pdce->cInfos; i++)
            {
                if (ph->fsphStrICmp (pdce->aInfos[i].fname, name) == 0)
                {
                    *finfo = (void*) pdce->aInfos[i].customData;
                    *pulAge = timesec () - pdce->ulLastUpdateTime;
                    debuglocal(pdc->resource, 9, "dcFindPath: FindPath %s found, age %d\n", path, *pulAge);
                    return 1;
                }
            }
        }

        pdce = pdce->pNext;
    }

    debuglocal(pdc->resource, 9, "dcFindPath: FindPath %s not found\n", path);
    return 0;
}

static int dcCreateDirPath(char *path, int cbPath, char* dir_mask, char* fullpath)
{
    /* State contains the original path passed to the plugin (fullpath) and
     * the actual filename mask (dir_mask), which should be used to filter
     * appropriate filenames from the full listing.
     * So the directory name can be constructed by removing the mask from the fullpath.
     */
    int cbDir;
    int cbMask = strlen(dir_mask) + 1;
    if (cbMask > cbPath)
    {
        /* This actually should never happen, because mask is a part of path.
         * But still return a failure, better no dircache than a crash.
         */
        return 0;
    }
    cbDir = cbPath - cbMask;
    if (cbDir > 0)
    {
       cbDir--; /* Exclude the slash. */
       memcpy(path, fullpath, cbDir);
    }
    path[cbDir] = 0;
    return 1;
}


/*
 *  Public API.
 */

int dircache_create( DirectoryCache **ppdc, void* pRes,
                     int cachetimeout, int cachedepth, PFNFREEDIRENTRY _release,
                     PLUGINHELPERTABLE2L *_ph)
{
    unsigned long ulExpirationTime = cachetimeout;
    int cMaxEntries = cachedepth;
    int rc;

    debuglocal( pRes, 9, "dircache_create: %u seconds, %d entries\n", ulExpirationTime, cMaxEntries);

    // save pointer to helpers
    ph = _ph;

    rc = dcCreate(ppdc, pRes);
    if (rc == NO_ERROR)
    {
        (*ppdc)->release = _release;
        dcEnable (*ppdc, 1, ulExpirationTime, cMaxEntries);

    }

    debuglocal( (*ppdc)->resource, 9, "dircache_create: %p, rc = %d\n", *ppdc, rc);
    return rc;
}

void dircache_delete(DirectoryCache *pdc)
{
    debuglocal(pdc->resource, 9, "dircache_delete: %p\n", pdc);

    if (pdc)
    {
        dcDelete(pdc);
    }
}

int dircache_list_files(DirectoryCache *pdc, PFNADDDIRENTRY fn,
                        void* plist,
                        char* dir_mask, char* fullpath,
                        int *ptotal_received)
{
    int rc;

    int cbPath = strlen(fullpath) + 1;
    char *path = (char*) alloca(cbPath);
    debuglocal(pdc->resource, 9, "dircache_list_files pdc %p, dir_mask [%s], fullpath [%s]\n",
               pdc, dir_mask, fullpath);

    if (!dcCreateDirPath(path, cbPath, dir_mask, fullpath))
    {
        debuglocal(pdc->resource, 9, "dircache_list_files pdc %p, dcCreateDirPath failed\n",
                   pdc);
        return 0;
    }

    debuglocal(pdc->resource, 9, "dircache_list_files [%s]\n", path);
    if (!pdc)
    {
        return 0;
    }

    lockRequest (pdc->mutex);

    rc = dcRead (pdc, path, fn, plist, ptotal_received, 0);

    lockRelease (pdc->mutex);

    debuglocal(pdc->resource, 9, "dircache_list_files rc=%d\n", (rc == CacheOk));
    return (rc == CacheOk);
}

void *dircache_write_begin(DirectoryCache *pdc,
                           char* dir_mask, char* fullpath,
                           int cFiles)
{
    DirectoryCacheEntry *pdce = NULL;
    debuglocal(pdc->resource, 9, "dircache_write_begin pdc %p path [%s]\n", pdc, fullpath);

    int cbPath = strlen(fullpath) + 1;
    char *path = (char*) alloca(cbPath);
    if (!dcCreateDirPath(path, cbPath, dir_mask, fullpath))
    {
        debuglocal(pdc->resource, 9, "dircache_write_begin dcCreateDirPath failed\n");
        return NULL;
    }

    lockRequest (pdc->mutex);
    
    if (pdc->cEntries >= pdc->cMaxEntries)
    {
        /* Remove oldest entry. */
        pdce = pdc->pEntriesTail;
        dcRemoveEntry (pdc, pdce);
        dceDelete (pdc, pdce);
        pdce = NULL;
    }
    
    pdce = dcWriteBegin (pdc, path, cFiles);
    
    lockRelease (pdc->mutex);

    debuglocal(pdc->resource, 9, "dircache_write_begin returning ctx %p\n", pdce);
    return (void *)pdce;
}

void dircache_write_entry(DirectoryCache *pdc, void *dircachectx,
                          const char *fname, const void *finfo)
{
    DirectoryCacheEntry *pdce = (DirectoryCacheEntry *)dircachectx;

    debuglocal(pdc->resource, 9, "dircache_write_entry %p\n", pdce);
    
    if (!pdce)
    {
        return;
    }
    
    lockRequest (pdc->mutex);
    
    dceWriteEntry (pdc, pdce, fname, finfo);
    
    lockRelease (pdc->mutex);
}

void dircache_write_end(DirectoryCache *pdc, void *dircachectx)
{
    DirectoryCacheEntry *pdce = (DirectoryCacheEntry *)dircachectx;

    debuglocal(pdc->resource, 9, "dircache_write_end: pdce %p\n", pdce);

    if (!pdce)
    {
        return;
    }
    
    lockRequest (pdc->mutex);
    
    if (pdce->fInvalid)
    {
        /* Something happened during writing to the entry. Delete it. */
        dceDelete (pdc, pdce);
    }
    else
    {
        dceAdjust (pdc, pdce);
        dcInsertEntry (pdc, pdce);
    }
    
    lockRelease (pdc->mutex);
}

void dircache_invalidate(DirectoryCache *pdc,
                         const char *path,
                         int fParent)
{
    debuglocal(pdc->resource, 9, "dircache_invalidate [%s], parent %d\n", path, fParent);

    if (!pdc)
    {
        return;
    }

    lockRequest (pdc->mutex);
    
    if (fParent)
    {
        int cb = strlen (path) + 1;
        char *p = (char *)alloca(cb);
        memcpy(p, path, cb);
        char *lastSlash = ph->fsphStrRChr(p, '\\');
        if (!lastSlash)
            lastSlash = ph->fsphStrRChr(p, '/');
        if (lastSlash)
        {
            *lastSlash = 0;
            dcInvalidate (pdc, p, 0);
        }
        else
        {
            dcInvalidate (pdc, "", 0);
        }
    }
    else
    {
        dcInvalidate (pdc, path, 0);
    }
    
    lockRelease (pdc->mutex);

    return;
}

int dircache_find_path(DirectoryCache *pdc,
                       const char *path,
                       void **finfo,
                       unsigned long *pulAge)
{
    int cb;
    char *p;
    char *lastSlash;
    int fFound = 0;

    debuglocal(pdc->resource, 9, "dircache_find_path [%s]\n", path);

    if (!pdc)
    {
        return 0;
    }
    
    if (ph->fsphStrChr(path, '*') || ph->fsphStrChr(path, '?'))
    {
        /* Wildcards are not allowed in the input path. */
        return 0;
    }

    lockRequest (pdc->mutex);

    /* Prepare the parent path. */
    cb = strlen (path) + 1;
    p = (char *)alloca(cb);
    memcpy(p, path, cb);
    lastSlash = ph->fsphStrRChr(p, '\\');
    if (!lastSlash)
        lastSlash = ph->fsphStrRChr(p, '/');
    if (lastSlash)
    {
        *lastSlash = 0;
        fFound = dcFindPath (pdc, path, p, lastSlash + 1, finfo, pulAge);
    }
    else
    {
        /* Find in the root directory. p is the name. */
        fFound = dcFindPath (pdc, path, "", p, finfo, pulAge);
    }
    
    lockRelease (pdc->mutex);

    debuglocal(pdc->resource, 9, "dircache_find_path fFound %d\n", fFound);
    return fFound;
}
