package unionfs

import (
	"bytes"
	"fmt"
	"github.com/hanwen/go-fuse/fuse"
	"os"
	"strings"
)

var _ = fmt.Println

const _XATTRSEP = "@XATTR@"

type attrResponse struct {
	*os.FileInfo
	fuse.Status
}

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

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

type linkResponse struct {
	linkContent string
	fuse.Status
}

type openResponse struct {
	fuse.File
	fuse.Status
}

// Caches filesystem metadata.
type CachingFileSystem struct {
	fuse.FileSystem

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

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

	r := &dirResponse{nil, code}
	if code != fuse.OK {
		return r
	}

	for {
		d, ok := <-origStream
		if !ok {
			break
		}
		r.entries = append(r.entries, d)
	}
	return r
}

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

func openFile(fs fuse.FileSystem, name string) (result *openResponse) {
	result = &openResponse{}
	flags := uint32(os.O_RDONLY)

	f, code := fs.Open(name, flags)
	if !code.Ok() {
		result.Status = code
		return
	}
	defer f.Release()
	defer f.Flush()

	buf := bytes.NewBuffer(nil)
	input := fuse.ReadIn{
		Offset: 0,
		Size:   128 * (1 << 10),
		Flags:  flags,
	}

	bp := fuse.NewGcBufferPool()
	for {
		data, status := f.Read(&input, bp)
		buf.Write(data)
		if !status.Ok() {
			result.Status = status
			return
		}
		if len(data) < int(input.Size) {
			break
		}
		input.Offset += uint64(len(data))
	}

	result.File = fuse.NewReadOnlyFile(buf.Bytes())
	return
}

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

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

func NewCachingFileSystem(fs fuse.FileSystem, ttlNs int64) *CachingFileSystem {
	c := new(CachingFileSystem)
	c.FileSystem = fs
	c.attributes = NewTimedCache(func(n string) interface{} { return getAttr(fs, n) }, ttlNs)
	c.dirs = NewTimedCache(func(n string) interface{} { return readDir(fs, n) }, ttlNs)
	c.links = NewTimedCache(func(n string) interface{} { return readLink(fs, n) }, ttlNs)
	c.xattr = NewTimedCache(func(n string) interface{} {
		return getXAttr(fs, n)
	}, ttlNs)
	c.files = NewTimedCache(func(n string) interface{} {
		return openFile(fs, n)
	}, ttlNs)
	return c
}

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

func (me *CachingFileSystem) GetAttr(name string) (*os.FileInfo, fuse.Status) {
	r := me.attributes.Get(name).(*attrResponse)
	return r.FileInfo, r.Status
}

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

func (me *CachingFileSystem) Readlink(name string) (string, fuse.Status) {
	r := me.links.Get(name).(*linkResponse)
	return r.linkContent, r.Status
}

func (me *CachingFileSystem) OpenDir(name string) (stream chan fuse.DirEntry, status fuse.Status) {
	r := me.dirs.Get(name).(*dirResponse)
	if r.Status.Ok() {
		stream = make(chan fuse.DirEntry, len(r.entries))
		for _, d := range r.entries {
			stream <- d
		}
		close(stream)
		return stream, r.Status
	}

	return nil, r.Status
}

// Caching file contents easily overflows available memory.
func (me *CachingFileSystem) DisabledOpen(name string, flags uint32) (f fuse.File, status fuse.Status) {
	if flags&fuse.O_ANYWRITE != 0 {
		return nil, fuse.EPERM
	}

	r := me.files.Get(name).(*openResponse)
	return r.File, r.Status
}