// This implements the Fs cache

package rc

import (
	"sync"
	"time"

	"github.com/ncw/rclone/fs"
)

var (
	fsCacheMu           sync.Mutex
	fsCache             = map[string]*cacheEntry{}
	fsNewFs             = fs.NewFs // for tests
	expireRunning       = false
	cacheExpireDuration = 300 * time.Second // expire the cache entry when it is older than this
	cacheExpireInterval = 60 * time.Second  // interval to run the cache expire
)

type cacheEntry struct {
	f        fs.Fs
	fsString string
	lastUsed time.Time
}

// GetCachedFs gets a fs.Fs named fsString either from the cache or creates it afresh
func GetCachedFs(fsString string) (f fs.Fs, err error) {
	fsCacheMu.Lock()
	defer fsCacheMu.Unlock()
	entry, ok := fsCache[fsString]
	if !ok {
		f, err = fsNewFs(fsString)
		if err != nil {
			return nil, err
		}
		entry = &cacheEntry{
			f:        f,
			fsString: fsString,
		}
		fsCache[fsString] = entry
	}
	entry.lastUsed = time.Now()
	if !expireRunning {
		time.AfterFunc(cacheExpireInterval, cacheExpire)
		expireRunning = true
	}
	return entry.f, err
}

// PutCachedFs puts an fs.Fs named fsString into the cache
func PutCachedFs(fsString string, f fs.Fs) {
	fsCacheMu.Lock()
	defer fsCacheMu.Unlock()
	fsCache[fsString] = &cacheEntry{
		f:        f,
		fsString: fsString,
		lastUsed: time.Now(),
	}
	if !expireRunning {
		time.AfterFunc(cacheExpireInterval, cacheExpire)
		expireRunning = true
	}
}

// cacheExpire expires any entries that haven't been used recently
func cacheExpire() {
	fsCacheMu.Lock()
	defer fsCacheMu.Unlock()
	now := time.Now()
	for fsString, entry := range fsCache {
		if now.Sub(entry.lastUsed) > cacheExpireDuration {
			delete(fsCache, fsString)
		}
	}
	if len(fsCache) != 0 {
		time.AfterFunc(cacheExpireInterval, cacheExpire)
		expireRunning = true
	} else {
		expireRunning = false
	}
}

// GetFsNamed gets a fs.Fs named fsName either from the cache or creates it afresh
func GetFsNamed(in Params, fsName string) (f fs.Fs, err error) {
	fsString, err := in.GetString(fsName)
	if err != nil {
		return nil, err
	}

	return GetCachedFs(fsString)
}

// GetFs gets a fs.Fs named "fs" either from the cache or creates it afresh
func GetFs(in Params) (f fs.Fs, err error) {
	return GetFsNamed(in, "fs")
}

// GetFsAndRemoteNamed gets the fsName parameter from in, makes a
// remote or fetches it from the cache then gets the remoteName
// parameter from in too.
func GetFsAndRemoteNamed(in Params, fsName, remoteName string) (f fs.Fs, remote string, err error) {
	remote, err = in.GetString(remoteName)
	if err != nil {
		return
	}
	f, err = GetFsNamed(in, fsName)
	return

}

// GetFsAndRemote gets the `fs` parameter from in, makes a remote or
// fetches it from the cache then gets the `remote` parameter from in
// too.
func GetFsAndRemote(in Params) (f fs.Fs, remote string, err error) {
	return GetFsAndRemoteNamed(in, "fs", "remote")
}