// Copyright 2016 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package unionfs

import (
	"sync"
	"time"

	"github.com/hanwen/go-fuse/fuse"
	"github.com/hanwen/go-fuse/fuse/pathfs"
)

// newDirnameMap reads the contents of the given directory. On error,
// returns a nil map. This forces reloads in the dirCache until we
// succeed.
func newDirnameMap(fs pathfs.FileSystem, dir string) map[string]struct{} {
	stream, code := fs.OpenDir(dir, nil)
	if code == fuse.ENOENT {
		// The directory not existing is not an error.
		return map[string]struct{}{}
	}

	if !code.Ok() {
		return nil
	}

	result := make(map[string]struct{})
	for _, e := range stream {
		if e.Mode&fuse.S_IFREG != 0 {
			result[e.Name] = struct{}{}
		}
	}
	return result
}

// dirCache caches names in a directory for some time.
//
// If called when the cache is expired, the filenames are read afresh in
// the background.
type dirCache struct {
	dir string
	ttl time.Duration
	fs  pathfs.FileSystem
	// Protects data below.
	lock sync.RWMutex

	// If nil, you may call refresh() to schedule a new one.
	names         map[string]struct{}
	updateRunning bool
}

func (c *dirCache) setMap(newMap map[string]struct{}) {
	c.lock.Lock()
	defer c.lock.Unlock()

	c.names = newMap
	c.updateRunning = false
	_ = time.AfterFunc(c.ttl,
		func() { c.DropCache() })
}

func (c *dirCache) DropCache() {
	c.lock.Lock()
	defer c.lock.Unlock()
	c.names = nil
}

// Try to refresh: if another update is already running, do nothing,
// otherwise, read the directory and set it.
func (c *dirCache) maybeRefresh() {
	c.lock.Lock()
	defer c.lock.Unlock()
	if c.updateRunning {
		return
	}
	c.updateRunning = true
	go func() {
		newmap := newDirnameMap(c.fs, c.dir)
		c.setMap(newmap)
	}()
}

func (c *dirCache) RemoveEntry(name string) {
	c.lock.Lock()
	defer c.lock.Unlock()
	if c.names == nil {
		go c.maybeRefresh()
		return
	}

	delete(c.names, name)
}

func (c *dirCache) AddEntry(name string) {
	c.lock.Lock()
	defer c.lock.Unlock()
	if c.names == nil {
		go c.maybeRefresh()
		return
	}

	c.names[name] = struct{}{}
}

func newDirCache(fs pathfs.FileSystem, dir string, ttl time.Duration) *dirCache {
	dc := new(dirCache)
	dc.dir = dir
	dc.fs = fs
	dc.ttl = ttl
	return dc
}

func (c *dirCache) HasEntry(name string) (mapPresent bool, found bool) {
	c.lock.RLock()
	defer c.lock.RUnlock()

	if c.names == nil {
		go c.maybeRefresh()
		return false, false
	}

	_, ok := c.names[name]
	return true, ok
}
