// 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 (
	"fmt"
	"log"
	"strings"
	"time"

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

const _XATTRSEP = "@XATTR@"

type attrResponse struct {
	*fuse.Attr
	fuse.Status
}

type xattrResponse struct {
	data []byte
	fuse.Status
}

type dirResponse struct {
	entries []fuse.DirEntry
	fuse.Status
}

type linkResponse struct {
	linkContent string
	fuse.Status
}

// Caches filesystem metadata.
type cachingFileSystem struct {
	pathfs.FileSystem

	attributes *TimedCache
	dirs       *TimedCache
	links      *TimedCache
	xattr      *TimedCache
}

func readDir(fs pathfs.FileSystem, name string) *dirResponse {
	origStream, code := fs.OpenDir(name, nil)

	r := &dirResponse{nil, code}
	if !code.Ok() {
		return r
	}
	r.entries = origStream
	return r
}

func getAttr(fs pathfs.FileSystem, name string) *attrResponse {
	a, code := fs.GetAttr(name, nil)
	return &attrResponse{
		Attr:   a,
		Status: code,
	}
}

func getXAttr(fs pathfs.FileSystem, nameAttr string) *xattrResponse {
	ns := strings.SplitN(nameAttr, _XATTRSEP, 2)
	a, code := fs.GetXAttr(ns[0], ns[1], nil)
	return &xattrResponse{
		data:   a,
		Status: code,
	}
}

func readLink(fs pathfs.FileSystem, name string) *linkResponse {
	a, code := fs.Readlink(name, nil)
	return &linkResponse{
		linkContent: a,
		Status:      code,
	}
}

func NewCachingFileSystem(fs pathfs.FileSystem, ttl time.Duration) pathfs.FileSystem {
	c := new(cachingFileSystem)
	c.FileSystem = fs
	c.attributes = NewTimedCache(func(n string) (interface{}, bool) {
		a := getAttr(fs, n)
		return a, a.Ok()
	}, ttl)
	c.dirs = NewTimedCache(func(n string) (interface{}, bool) {
		d := readDir(fs, n)
		return d, d.Ok()
	}, ttl)
	c.links = NewTimedCache(func(n string) (interface{}, bool) {
		l := readLink(fs, n)
		return l, l.Ok()
	}, ttl)
	c.xattr = NewTimedCache(func(n string) (interface{}, bool) {
		l := getXAttr(fs, n)
		return l, l.Ok()
	}, ttl)
	return c
}

func (fs *cachingFileSystem) DropCache() {
	for _, c := range []*TimedCache{fs.attributes, fs.dirs, fs.links, fs.xattr} {
		c.DropAll(nil)
	}
}

func (fs *cachingFileSystem) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
	if name == _DROP_CACHE {
		return &fuse.Attr{
			Mode: fuse.S_IFREG | 0777,
		}, fuse.OK
	}

	r := fs.attributes.Get(name).(*attrResponse)
	return r.Attr, r.Status
}

func (fs *cachingFileSystem) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
	key := name + _XATTRSEP + attr
	r := fs.xattr.Get(key).(*xattrResponse)
	return r.data, r.Status
}

func (fs *cachingFileSystem) Readlink(name string, context *fuse.Context) (string, fuse.Status) {
	r := fs.links.Get(name).(*linkResponse)
	return r.linkContent, r.Status
}

func (fs *cachingFileSystem) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
	r := fs.dirs.Get(name).(*dirResponse)
	return r.entries, r.Status
}

func (fs *cachingFileSystem) String() string {
	return fmt.Sprintf("cachingFileSystem(%v)", fs.FileSystem)
}

func (fs *cachingFileSystem) Open(name string, flags uint32, context *fuse.Context) (f nodefs.File, status fuse.Status) {
	if flags&fuse.O_ANYWRITE != 0 && name == _DROP_CACHE {
		log.Println("Dropping cache for", fs)
		fs.DropCache()
	}
	return fs.FileSystem.Open(name, flags, context)
}
