package winfsp

import (
	"strings"
	"sync"
	"syscall"
	"time"

	cgofuse "github.com/winfsp/cgofuse/fuse"

	"github.com/seaweedfs/go-fuse/v2/fuse"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/mount"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

const (
	rootInode = 1

	// WinFsp passes this when an operation carries no open handle.
	noHandle = ^uint64(0)

	// utimeOmit is the nanosecond marker asking for a timestamp to be left as
	// it is; cgofuse hands it through rather than resolving it.
	utimeOmit = (1 << 30) - 2

	// windowsEpochCutoff is 1601-01-02 in unix seconds. Anything at or below
	// it is Windows' own epoch leaking through rather than a real time.
	windowsEpochCutoff = -11644387200

	// How many entries to pull from one readdir round before handing them to
	// WinFsp. Bounded so a directory with millions of children does not
	// materialise in one slice.
	readdirBatch = 4096
)

// never is a nil channel: receiving blocks forever, which is what the raw
// operations expect from a caller that cannot cancel.
var never chan struct{}

// WinFS presents a mount.WFS through the path-based interface WinFsp speaks.
type WinFS struct {
	cgofuse.FileSystemBase

	wfs       *mount.WFS
	mountRoot util.FullPath
	uid       uint32
	gid       uint32
	readOnly  bool

	// paths holds the lookup references for resolved paths, standing in for
	// the kernel caches of the unix mounts.
	paths *pathCache

	// An open handle holds a lookup reference for its inode until Release,
	// the way the kernel keeps one for an open file. WinFsp hands back only
	// the handle, and the raw filesystem reuses one handle for repeated opens
	// of the same inode, so the references are counted.
	mu         sync.Mutex
	fileInodes map[uint64]*handleRef
	dirInodes  map[uint64]*handleRef
}

func NewWinFS(wfs *mount.WFS, uid, gid uint32, readOnly bool, cacheTimeout time.Duration) *WinFS {
	w := &WinFS{
		wfs:        wfs,
		mountRoot:  wfs.MountRoot(),
		uid:        uid,
		gid:        gid,
		readOnly:   readOnly,
		fileInodes: make(map[uint64]*handleRef),
		dirInodes:  make(map[uint64]*handleRef),
	}
	w.paths = newPathCache(cacheTimeout, w.forget)
	return w
}

// stillNames reports whether the mount still tracks inode at the mount-relative
// key, which a delete-on-close or a rename while the handle was open has
// changed.
func (w *WinFS) stillNames(key string, inode uint64) bool {
	current, ok := w.wfs.PathForInode(inode)
	if !ok {
		return false
	}
	rel, ok := relativeToMount(w.mountRoot, current)
	return ok && cacheKey(rel) == key
}

// invalidatePath is what the notifier calls when a metadata event arrives:
// whatever is cached for that path is out of date. Directories take their
// subtree with them, since the children's cached paths point through them.
func (w *WinFS) invalidatePath(path string, isDirectory bool) {
	w.paths.purge(cacheKey(path), isDirectory)
}

// handleRef is the lookup references an open handle is holding, one per open
// that has not yet been released.
type handleRef struct {
	inode uint64
	count int
}

// retain parks one reference under a handle.
func (w *WinFS) retain(table map[uint64]*handleRef, handle, inode uint64) {
	if handle == noHandle {
		return
	}
	var stranded uint64
	w.mu.Lock()
	switch existing, found := table[handle]; {
	case found && existing.inode == inode:
		existing.count++
	case found:
		// The handle was reissued for a different inode; its old reference
		// would otherwise never be returned.
		stranded = existing.inode
		table[handle] = &handleRef{inode: inode, count: 1}
	default:
		table[handle] = &handleRef{inode: inode, count: 1}
	}
	w.mu.Unlock()
	w.forget(stranded)
}

// releaseRetained hands back one reference, reporting the inode only once the
// last open of that handle is gone, so a repeated release cannot forget twice.
func (w *WinFS) releaseRetained(table map[uint64]*handleRef, handle uint64) uint64 {
	w.mu.Lock()
	defer w.mu.Unlock()
	existing, found := table[handle]
	if !found {
		return 0
	}
	existing.count--
	if existing.count > 0 {
		return 0
	}
	delete(table, handle)
	return existing.inode
}

// caller identifies who the raw filesystem should record as the owner of
// anything it creates. Windows has no uid to pass through, so entries carry
// the identity the mount was started with rather than root.
// denied reports whether a modification should be refused outright, which is
// how a read-only mount is enforced: WinFsp discards its own "ro" option.
func (w *WinFS) denied() bool { return w.readOnly }

// inodeForHandle reports the inode an open handle is holding, or 0. WinFsp
// keeps the path it opened with and never updates it across a rename, so a
// handle is the more reliable of the two.
func (w *WinFS) inodeForHandle(table map[uint64]*handleRef, handle uint64) uint64 {
	if handle == noHandle {
		return 0
	}
	w.mu.Lock()
	defer w.mu.Unlock()
	if existing, found := table[handle]; found {
		return existing.inode
	}
	return 0
}

// ptr is for the operations that take the header by pointer.
func ptr(h fuse.InHeader) *fuse.InHeader { return &h }

func (w *WinFS) caller(inode uint64) fuse.InHeader {
	return fuse.InHeader{
		NodeId: inode,
		Caller: fuse.Caller{Owner: fuse.Owner{Uid: w.uid, Gid: w.gid}},
	}
}

// forget returns one reference. The root is never looked up, so it never holds
// one to give back.
func (w *WinFS) forget(inode uint64) {
	if inode == rootInode || inode == 0 {
		return
	}
	w.wfs.Forget(inode, 1)
}

// walk resolves a path one component at a time, filling the path cache as it
// goes. Every reference a lookup grants is owned by the cache, which keeps the
// returned inode alive for at least one cache sweep; an operation that needs
// to hold the inode longer steals the reference from the cache.
func (w *WinFS) walk(parts []string) (uint64, fuse.Status) {
	inode := uint64(rootInode)
	// Taken before the lookups: a purge racing this walk - a rename or unlink
	// landing between a Lookup and its insert - makes what was just resolved
	// the very thing the purge removed.
	gen := w.paths.snapshot()
	for i, name := range parts {
		key := strings.Join(parts[:i+1], "/")
		if cached, _, ok := w.paths.lookup(key); ok {
			inode = cached
			continue
		}
		var out fuse.EntryOut
		if status := w.wfs.Lookup(never, ptr(w.caller(inode)), name, &out); status != fuse.OK {
			return 0, status
		}
		w.paths.insert(key, out.NodeId, out.Attr, gen)
		inode = out.NodeId
	}
	return inode, fuse.OK
}

// resolve walks a WinFsp path down to an inode, valid at least until the next
// cache sweep.
func (w *WinFS) resolve(path string) (uint64, fuse.Status) {
	return w.walk(splitPath(path))
}

// resolveParent resolves everything but the last component, which the create
// and delete operations need separately.
func (w *WinFS) resolveParent(path string) (uint64, string, fuse.Status) {
	parentParts, name, ok := splitParent(path)
	if !ok {
		return 0, "", fuse.EINVAL
	}
	parent, status := w.walk(parentParts)
	if status != fuse.OK {
		return 0, "", status
	}
	return parent, name, fuse.OK
}

// resolveAndSteal resolves path to an inode reference the caller owns, for
// the open paths that hold it for the life of a handle: a cached entry is
// stolen, anything else is looked up directly so the reference never passes
// through the cache - an open must not depend on an insert surviving the
// purges racing it. The root is handed out without a reference; it does not
// need one.
func (w *WinFS) resolveAndSteal(path string) (uint64, fuse.Status) {
	key := cacheKey(path)
	if key == "" {
		return rootInode, fuse.OK
	}
	if stolen, ok := w.paths.steal(key); ok {
		return stolen, fuse.OK
	}
	parent, name, status := w.resolveParent(path)
	if status != fuse.OK {
		return 0, status
	}
	var out fuse.EntryOut
	if status := w.wfs.Lookup(never, ptr(w.caller(parent)), name, &out); status != fuse.OK {
		return 0, status
	}
	return out.NodeId, fuse.OK
}

func (w *WinFS) attrToStat(attr *fuse.Attr, stat *cgofuse.Stat_t) {
	stat.Ino = attr.Ino
	stat.Mode = attr.Mode
	stat.Nlink = attr.Nlink
	stat.Size = int64(attr.Size)
	stat.Blocks = int64(attr.Blocks)
	stat.Blksize = int64(attr.Blksize)
	stat.Uid = w.uid
	stat.Gid = w.gid
	stat.Atim = cgofuse.Timespec{Sec: int64(attr.Atime), Nsec: int64(attr.Atimensec)}
	stat.Mtim = cgofuse.Timespec{Sec: int64(attr.Mtime), Nsec: int64(attr.Mtimensec)}
	stat.Ctim = cgofuse.Timespec{Sec: int64(attr.Ctime), Nsec: int64(attr.Ctimensec)}
	// The filer records a creation time, but the raw protocol's Attr carries no
	// field for it outside darwin, so ctime stands in until there is a way to
	// read it through.
	stat.Birthtim = stat.Ctim
	// Windows expects a regular file to be marked archived; with no flags at
	// all it synthesises NORMAL, which is a different thing and what
	// create_fileattr_test checks.
	if attr.Mode&cgofuse.S_IFMT == cgofuse.S_IFREG {
		stat.Flags |= cgofuse.UF_ARCHIVE
	}
}

// translateOpenFlags converts cgofuse's open flags, which follow MSVC's
// numbering, into the values the raw filesystem tests against. Only the access
// mode and O_TRUNC happen to agree; O_EXCL would otherwise read as O_APPEND.
func translateOpenFlags(flags int) uint32 {
	out := uint32(flags & cgofuse.O_ACCMODE)
	for _, pair := range []struct {
		from int
		to   int
	}{
		{cgofuse.O_APPEND, syscall.O_APPEND},
		{cgofuse.O_CREAT, syscall.O_CREAT},
		{cgofuse.O_TRUNC, syscall.O_TRUNC},
		{cgofuse.O_EXCL, syscall.O_EXCL},
	} {
		if flags&pair.from != 0 {
			out |= uint32(pair.to)
		}
	}
	return out
}

// logResolveFailure keeps a missing file quiet. Windows probes for entries
// that do not exist as a matter of course, so ENOENT is an answer rather than
// a fault; anything else is worth a line.
func logResolveFailure(op, path string, status fuse.Status) {
	if status == fuse.ENOENT {
		glog.V(4).Infof("%s %s: no such entry", op, path)
		return
	}
	glog.Errorf("%s %s: resolving: %v", op, path, status)
}

func (w *WinFS) Statfs(path string, stat *cgofuse.Statfs_t) int {
	var out fuse.StatfsOut
	if status := w.wfs.StatFs(never, ptr(w.caller(rootInode)), &out); status != fuse.OK {
		return toErrno(status)
	}
	stat.Bsize = uint64(out.Bsize)
	stat.Frsize = uint64(out.Frsize)
	stat.Blocks = out.Blocks
	stat.Bfree = out.Bfree
	stat.Bavail = out.Bavail
	stat.Files = out.Files
	stat.Ffree = out.Ffree
	stat.Favail = out.Ffree
	stat.Namemax = uint64(out.NameLen)
	return 0
}

// getattrFromCache serves attributes the way the kernel would from its
// attribute cache. A file with an open handle is excluded: its live size is
// on the handle, not in the cache.
func (w *WinFS) getattrFromCache(key string, stat *cgofuse.Stat_t) bool {
	if key == "" {
		return false
	}
	inode, attr, ok := w.paths.lookup(key)
	if !ok || w.wfs.IsFileOpen(inode) {
		return false
	}
	w.attrToStat(&attr, stat)
	return true
}

func (w *WinFS) Getattr(path string, stat *cgofuse.Stat_t, fh uint64) int {
	inode := w.inodeForHandle(w.fileInodes, fh)
	if inode == 0 {
		key := cacheKey(path)
		if fh == noHandle && w.getattrFromCache(key, stat) {
			return 0
		}
		var status fuse.Status
		inode, status = w.resolve(path)
		if status != fuse.OK {
			logResolveFailure("getattr", path, status)
			return toErrno(status)
		}
		// The walk just refreshed the cache, so this hits unless the file is
		// open or is the root, and saves the second load of the same entry.
		if fh == noHandle && w.getattrFromCache(key, stat) {
			return 0
		}
	}
	in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
	if fh != noHandle {
		in.Fh_ = fh
		in.Flags_ = fuse.FUSE_GETATTR_FH
	}
	var out fuse.AttrOut
	if status := w.wfs.GetAttr(never, in, &out); status != fuse.OK {
		return toErrno(status)
	}
	w.attrToStat(&out.Attr, stat)
	return 0
}

// purgeWithParent drops a mutated path from the cache along with its parent,
// whose cached mtime the mutation just outdated.
func (w *WinFS) purgeWithParent(path string, prefix bool) {
	key := cacheKey(path)
	w.paths.purge(key, prefix)
	if i := strings.LastIndexByte(key, '/'); i > 0 {
		w.paths.purge(key[:i], false)
	}
}

func (w *WinFS) Mkdir(path string, mode uint32) int {
	if w.denied() {
		return -eROFS
	}
	parent, name, status := w.resolveParent(path)
	if status != fuse.OK {
		return toErrno(status)
	}
	in := &fuse.MkdirIn{InHeader: w.caller(parent), Mode: mode}
	var out fuse.EntryOut
	status = w.wfs.Mkdir(never, in, name, &out)
	if status == fuse.OK {
		w.purgeWithParent(path, false)
		// The new directory is about to be filled; cache it, reference and all.
		w.paths.insert(cacheKey(path), out.NodeId, out.Attr, w.paths.snapshot())
	}
	return toErrno(status)
}

func (w *WinFS) Rmdir(path string) int {
	if w.denied() {
		return -eROFS
	}
	parent, name, status := w.resolveParent(path)
	if status != fuse.OK {
		return toErrno(status)
	}
	status = w.wfs.Rmdir(never, ptr(w.caller(parent)), name)
	if status == fuse.OK {
		w.purgeWithParent(path, true)
	}
	return toErrno(status)
}

func (w *WinFS) Unlink(path string) int {
	if w.denied() {
		return -eROFS
	}
	parent, name, status := w.resolveParent(path)
	if status != fuse.OK {
		return toErrno(status)
	}
	status = w.wfs.Unlink(never, ptr(w.caller(parent)), name)
	if status == fuse.OK {
		w.purgeWithParent(path, false)
	}
	return toErrno(status)
}

func (w *WinFS) Rename(oldpath string, newpath string) int {
	if w.denied() {
		return -eROFS
	}
	oldParent, oldName, status := w.resolveParent(oldpath)
	if status != fuse.OK {
		return toErrno(status)
	}
	newParent, newName, status := w.resolveParent(newpath)
	if status != fuse.OK {
		return toErrno(status)
	}
	in := &fuse.RenameIn{InHeader: w.caller(oldParent), Newdir: newParent}
	status = w.wfs.Rename(never, in, oldName, newName)
	if status == fuse.OK {
		// Directories carry their subtree's cached paths with them, and the
		// rename may have replaced an entry at the destination.
		w.purgeWithParent(oldpath, true)
		w.purgeWithParent(newpath, true)
	}
	return toErrno(status)
}

func (w *WinFS) Create(path string, flags int, mode uint32) (int, uint64) {
	if w.denied() {
		return -eROFS, noHandle
	}
	parent, name, status := w.resolveParent(path)
	if status != fuse.OK {
		glog.Errorf("create %s: resolving the parent directory: %v", path, status)
		return toErrno(status), noHandle
	}
	in := &fuse.CreateIn{InHeader: w.caller(parent), Flags: translateOpenFlags(flags), Mode: mode}
	var out fuse.CreateOut
	if status := w.wfs.Create(never, in, name, &out); status != fuse.OK {
		glog.Errorf("create %s in inode %d: %v", name, parent, status)
		return toErrno(status), noHandle
	}
	// Whatever the cache held for this path was replaced by the create.
	w.purgeWithParent(path, false)
	// Create grants a reference on the new inode; hold it for as long as the
	// handle lives and give it back in Release.
	w.retain(w.fileInodes, out.Fh, out.NodeId)
	return 0, out.Fh
}

func (w *WinFS) Open(path string, flags int) (int, uint64) {
	inode, status := w.resolveAndSteal(path)
	if status != fuse.OK {
		logResolveFailure("open", path, status)
		return toErrno(status), noHandle
	}
	in := &fuse.OpenIn{InHeader: w.caller(inode), Flags: translateOpenFlags(flags)}
	var out fuse.OpenOut
	if status := w.wfs.Open(never, in, &out); status != fuse.OK {
		glog.Errorf("open %s inode %d: %v", path, inode, status)
		w.forget(inode)
		return toErrno(status), noHandle
	}
	w.retain(w.fileInodes, out.Fh, inode)
	return 0, out.Fh
}

func (w *WinFS) Read(path string, buff []byte, ofst int64, fh uint64) int {
	if fh == noHandle {
		return -eBADF
	}
	in := &fuse.ReadIn{
		Fh:     fh,
		Offset: uint64(ofst),
		Size:   uint32(len(buff)),
	}
	result, status := w.wfs.Read(never, in, buff)
	if status != fuse.OK {
		return toErrno(status)
	}
	data, status := result.Bytes(buff)
	if status != fuse.OK {
		return toErrno(status)
	}
	if len(data) > 0 && &data[0] != &buff[0] {
		copy(buff, data)
	}
	return len(data)
}

func (w *WinFS) Write(path string, buff []byte, ofst int64, fh uint64) int {
	if w.denied() {
		return -eROFS
	}
	if fh == noHandle {
		return -eBADF
	}
	in := &fuse.WriteIn{
		Fh:     fh,
		Offset: uint64(ofst),
		Size:   uint32(len(buff)),
	}
	written, status := w.wfs.Write(never, in, buff)
	if status != fuse.OK {
		if status == fuse.ENOENT {
			glog.Errorf("write %s: handle %d is not open", path, fh)
		}
		return toErrno(status)
	}
	return int(written)
}

func (w *WinFS) Truncate(path string, size int64, fh uint64) int {
	if w.denied() {
		return -eROFS
	}
	inode := w.inodeForHandle(w.fileInodes, fh)
	if inode == 0 {
		var status fuse.Status
		inode, status = w.resolve(path)
		if status != fuse.OK {
			return toErrno(status)
		}
	}
	in := &fuse.SetAttrIn{}
	in.NodeId = inode
	in.Valid = fuse.FATTR_SIZE
	in.Size = uint64(size)
	if fh != noHandle {
		in.Valid |= fuse.FATTR_FH
		in.Fh = fh
	}
	var out fuse.AttrOut
	status := w.wfs.SetAttr(never, in, &out)
	if status == fuse.OK {
		w.paths.purge(cacheKey(path), false)
	}
	return toErrno(status)
}

func (w *WinFS) Chmod(path string, mode uint32) int {
	if w.denied() {
		return -eROFS
	}
	inode, status := w.resolve(path)
	if status != fuse.OK {
		return toErrno(status)
	}
	in := &fuse.SetAttrIn{}
	in.NodeId = inode
	in.Valid = fuse.FATTR_MODE
	in.Mode = mode
	var out fuse.AttrOut
	status = w.wfs.SetAttr(never, in, &out)
	if status == fuse.OK {
		w.paths.purge(cacheKey(path), false)
	}
	return toErrno(status)
}

func (w *WinFS) Utimens(path string, tmsp []cgofuse.Timespec) int {
	if w.denied() {
		return -eROFS
	}
	inode, status := w.resolve(path)
	if status != fuse.OK {
		return toErrno(status)
	}
	in := &fuse.SetAttrIn{}
	in.NodeId = inode
	if len(tmsp) < 2 {
		in.Valid = fuse.FATTR_ATIME_NOW | fuse.FATTR_MTIME_NOW
	} else {
		// Windows sends times around its own 1601 epoch, which arrive here as
		// a large negative second count and would be stored as a year-1601
		// timestamp that every other client then reads. Leave those alone.
		if applyTimespec(tmsp[0]) {
			in.Valid |= fuse.FATTR_ATIME
			in.Atime, in.Atimensec = uint64(tmsp[0].Sec), uint32(tmsp[0].Nsec)
		}
		if applyTimespec(tmsp[1]) {
			in.Valid |= fuse.FATTR_MTIME
			in.Mtime, in.Mtimensec = uint64(tmsp[1].Sec), uint32(tmsp[1].Nsec)
		}
		if in.Valid == 0 {
			return 0
		}
	}
	var out fuse.AttrOut
	status = w.wfs.SetAttr(never, in, &out)
	if status == fuse.OK {
		w.paths.purge(cacheKey(path), false)
	}
	return toErrno(status)
}

// applyTimespec reports whether a timestamp should be written. UTIME_OMIT asks
// for the existing value to be kept, and a time at or below Windows' own 1601
// epoch arrives as a large negative second count. Exactly zero is what Windows
// sends for a field it is not setting; storing it put 1970 in the atime
// overlay, which then read back as the file's access time. A date genuinely
// before 1970 is still allowed through — only the epoch itself is the
// sentinel.
func applyTimespec(ts cgofuse.Timespec) bool {
	if ts.Nsec == utimeOmit {
		return false
	}
	if ts.Sec == 0 && ts.Nsec == 0 {
		return false
	}
	return ts.Sec > windowsEpochCutoff
}

func (w *WinFS) Flush(path string, fh uint64) int {
	if fh == noHandle {
		return 0
	}
	return toErrno(w.wfs.Flush(never, &fuse.FlushIn{InHeader: w.caller(0), Fh: fh}))
}

func (w *WinFS) Fsync(path string, datasync bool, fh uint64) int {
	if fh == noHandle {
		return 0
	}
	return toErrno(w.wfs.Fsync(never, &fuse.FsyncIn{Fh: fh}))
}

func (w *WinFS) Release(path string, fh uint64) int {
	if fh == noHandle {
		return 0
	}
	// The handle's view of the file is the freshest there is, and the close
	// has flushed by now, so read it before the handle goes away: the next
	// operation on the path — a stat right after a copy, most often — is then
	// served from the cache instead of walking to the filer again.
	var attr *fuse.Attr
	if inode := w.inodeForHandle(w.fileInodes, fh); inode != 0 {
		in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
		in.Fh_ = fh
		in.Flags_ = fuse.FUSE_GETATTR_FH
		var out fuse.AttrOut
		if w.wfs.GetAttr(never, in, &out) == fuse.OK {
			attr = &out.Attr
		}
	}
	w.wfs.Release(never, &fuse.ReleaseIn{Fh: fh})
	if inode := w.releaseRetained(w.fileInodes, fh); inode != 0 {
		// The reference moves from the handle to the cache — but only if the
		// path still names this inode: WinFsp reports the path the handle
		// opened with, and after a delete-on-close or a rename caching it
		// would resurrect an entry that is gone.
		if key, gen := cacheKey(path), w.paths.snapshot(); attr != nil && w.stillNames(key, inode) {
			w.paths.insert(key, inode, *attr, gen)
		} else {
			w.forget(inode)
		}
	}
	return 0
}

func (w *WinFS) Opendir(path string) (int, uint64) {
	inode, status := w.resolveAndSteal(path)
	if status != fuse.OK {
		return toErrno(status), noHandle
	}
	var out fuse.OpenOut
	if status := w.wfs.OpenDir(never, &fuse.OpenIn{InHeader: w.caller(inode)}, &out); status != fuse.OK {
		w.forget(inode)
		return toErrno(status), noHandle
	}
	w.retain(w.dirInodes, out.Fh, inode)
	return 0, out.Fh
}

func (w *WinFS) Releasedir(path string, fh uint64) int {
	if fh == noHandle {
		return 0
	}
	w.wfs.ReleaseDir(&fuse.ReleaseIn{Fh: fh})
	w.forget(w.releaseRetained(w.dirInodes, fh))
	return 0
}

// readdirSink collects one readdir round. The raw operation fills the returned
// EntryOut after AddEntryPlus returns, so nothing can be converted until the
// round is over.
type readdirSink struct {
	names   []string
	offsets []uint64
	inodes  []uint64
	modes   []uint32
	attrs   []*fuse.EntryOut
	limit   int

	// lastOffset advances over dropped entries too, so a batch that is all
	// dot entries still moves the enumeration along.
	lastOffset uint64
	seen       int
}

// WinFsp strips "." and ".." for the root directory itself and expects every
// other directory to report them, the way a real NTFS enumeration does.
func (s *readdirSink) AddEntry(entry fuse.DirEntry) bool {
	if len(s.names) >= s.limit {
		return false
	}
	s.seen++
	s.lastOffset = entry.Off
	s.names = append(s.names, entry.Name)
	s.offsets = append(s.offsets, entry.Off)
	s.inodes = append(s.inodes, entry.Ino)
	s.modes = append(s.modes, entry.Mode)
	s.attrs = append(s.attrs, nil)
	return true
}

func (s *readdirSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
	if len(s.names) >= s.limit {
		return nil
	}
	s.seen++
	s.lastOffset = entry.Off
	out := &fuse.EntryOut{}
	s.names = append(s.names, entry.Name)
	s.offsets = append(s.offsets, entry.Off)
	s.inodes = append(s.inodes, entry.Ino)
	s.modes = append(s.modes, entry.Mode)
	s.attrs = append(s.attrs, out)
	return out
}

// WinFsp has no FORGET, and every EntryOut is converted before the round ends.
func (s *readdirSink) TakesLookupRef() bool { return false }

func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t, ofst int64) bool, ofst int64, fh uint64) int {
	inode := w.inodeForHandle(w.dirInodes, fh)
	if inode == 0 {
		var status fuse.Status
		inode, status = w.resolve(path)
		if status != fuse.OK {
			return toErrno(status)
		}
	}
	offset := uint64(ofst)
	for {
		sink := &readdirSink{limit: readdirBatch}
		in := &fuse.ReadIn{
			InHeader: w.caller(inode),
			Fh:       fh,
			Offset:   offset,
			Size:     1 << 20,
		}
		if status := w.wfs.ReadDirectoryInto(in, sink, true); status != fuse.OK {
			return toErrno(status)
		}
		if sink.seen == 0 {
			return 0
		}
		filled := true
		for i, name := range sink.names {
			var stat cgofuse.Stat_t
			var statp *cgofuse.Stat_t
			if attr := sink.attrs[i]; attr != nil && attr.Attr.Mode != 0 {
				w.attrToStat(&attr.Attr, &stat)
				statp = &stat
			} else if sink.modes[i] != 0 {
				// "." and ".." are reported without attributes: the readdir
				// only fills a block for real children. Windows still needs
				// their type, and enumerating a directory whose first entry is
				// not marked as one fails outright.
				stat.Mode = sink.modes[i]
				stat.Ino = sink.inodes[i]
				stat.Nlink = 1
				stat.Uid, stat.Gid = w.uid, w.gid
				statp = &stat
			}
			if filled && !fill(name, statp, int64(sink.offsets[i])) {
				filled = false
			}
		}
		if !filled {
			return 0
		}
		if sink.lastOffset <= offset {
			return 0
		}
		offset = sink.lastOffset
	}
}

func (w *WinFS) Readlink(path string) (int, string) {
	// WinFsp probes the root to decide whether the volume has symlinks, and
	// turns them on unless this fails. Leaving them on costs a getattr per
	// path component on every open, for a feature Symlink already refuses.
	if len(splitPath(path)) == 0 {
		return -eNOSYS, ""
	}
	inode, status := w.resolve(path)
	if status != fuse.OK {
		return toErrno(status), ""
	}
	target, status := w.wfs.Readlink(never, ptr(w.caller(inode)))
	if status != fuse.OK {
		return toErrno(status), ""
	}
	return 0, string(target)
}

// Symlink is refused for now. The entry is easy to create, but WinFsp only
// follows it once the reparse point is wired up, so it would otherwise read
// back as an empty file.
func (w *WinFS) Symlink(target string, newpath string) int {
	return -eNOSYS
}

// Chown accepts and discards. Windows has no uid to record, but WinFsp passes
// a chown failure straight out of SetSecurity, so refusing it breaks Explorer's
// Security tab and icacls for changes that are not about ownership at all.
func (w *WinFS) Chown(path string, uid uint32, gid uint32) int {
	return 0
}

// Link is not implemented: WinFsp has no hard links.
func (w *WinFS) Link(oldpath string, newpath string) int {
	return -eNOSYS
}
