//go:build linux || (darwin && amd64)

package fuse

import (
	"context"
	"os"
	"syscall"

	fusefs "github.com/hanwen/go-fuse/v2/fs"
	"github.com/hanwen/go-fuse/v2/fuse"
	"gitlink.org.cn/cloudream/common/pkgs/logger"
)

type DirNode struct {
	NodeBase
	dir FsDir
}

func newDirNode(fs *Fuse, dir FsDir) *DirNode {
	return &DirNode{NodeBase: NodeBase{fs: fs}, dir: dir}
}

func (n *DirNode) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse.AttrOut) syscall.Errno {
	logger.Tracef("DirNode.Getattr: %v", n.dir.Name())

	n.fs.fillAttrOut(n.dir, out)
	return 0
}

// Setattr sets attributes for an Inode.
func (n *DirNode) Setattr(ctx context.Context, f fusefs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) (errno syscall.Errno) {
	logger.Tracef("DirNode.Setattr: %v", n.dir.Name())

	n.fs.fillAttrOut(n.dir, out)

	_, ok := in.GetSize()
	if ok {
		return syscall.ENOSYS
	}

	modTime, ok := in.GetMTime()
	if ok {
		err := n.dir.SetModTime(modTime)
		if err != nil {
			return translateError(err)
		}
		out.Mtime = uint64(modTime.Unix())
		out.Mtimensec = uint32(modTime.Nanosecond())
	}

	return 0
}

var _ = (fusefs.NodeSetattrer)((*DirNode)(nil))

// accessModeMask masks off the read modes from the flags
const accessModeMask = (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)

// Open opens an Inode (of regular file type) for reading. It
// is optional but recommended to return a FileHandle.
func (n *DirNode) Open(ctx context.Context, flags uint32) (fh fusefs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Open")
	log.Tracef("args: %v, %#o", n.dir.Name(), flags)

	rdwrMode := int(flags) & accessModeMask
	if rdwrMode != os.O_RDONLY {
		return nil, 0, syscall.EPERM
	}

	reader, err := n.dir.ReadChildren()
	if err != nil {
		log.Warnf("read children: %v", err)
		return nil, 0, translateError(err)
	}

	return newDirHandle(n.dir, reader), 0, 0
}

var _ = (fusefs.NodeOpener)((*DirNode)(nil))

func (n *DirNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (inode *fusefs.Inode, errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Lookup")
	log.Tracef("args: %v, %v", n.dir.Name(), name)

	child, err := n.dir.Child(ctx, name)
	if err != nil {
		e2 := translateError(err)
		if e2 != syscall.ENOENT {
			log.Warnf("child: %v", err)
		}
		return nil, e2
	}

	switch child := child.(type) {
	case FsDir:
		node := newDirNode(n.fs, child)
		n.fs.fillEntryOut(child, out)

		return n.NewInode(ctx, node, fusefs.StableAttr{
			Mode: out.Attr.Mode,
		}), 0

	case FsFile:
		node := newFileNode(n.fs, child)
		n.fs.fillEntryOut(child, out)

		return n.NewInode(ctx, node, fusefs.StableAttr{
			Mode: out.Attr.Mode,
		}), 0

	default:
		return nil, syscall.EINVAL
	}
}

var _ = (fusefs.NodeLookuper)((*DirNode)(nil))

func (n *DirNode) Opendir(ctx context.Context) syscall.Errno {
	return 0
}

var _ = (fusefs.NodeOpendirer)((*DirNode)(nil))

type dirStream struct {
	reader DirReader
	fs     *Fuse
}

func (s *dirStream) HasNext() bool {
	return s.reader.HasNext()
}

func (s *dirStream) Next() (fuse.DirEntry, syscall.Errno) {
	log := logger.WithField("F", "dirStream.Next")

	entries, err := s.reader.Next(1)
	if err != nil {
		log.Warnf("next: %v", err)
		return fuse.DirEntry{}, translateError(err)
	}
	entry := entries[0]

	return fuse.DirEntry{
		Name: entry.Name(),
		Mode: s.fs.getMode(entry),
	}, 0
}

func (s *dirStream) Close() {
	s.reader.Close()
}

func (n *DirNode) Readdir(ctx context.Context) (ds fusefs.DirStream, errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Readdir")

	reader, err := n.dir.ReadChildren()
	if err != nil {
		log.Warnf("read children: %v", err)
		return nil, translateError(err)
	}

	return &dirStream{reader: reader, fs: n.fs}, 0
}

var _ = (fusefs.NodeReaddirer)((*DirNode)(nil))

func (n *DirNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (inode *fusefs.Inode, errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Mkdir")
	log.Tracef("args: %v, %v, %#o", n.dir.Name(), name, mode)

	newDir, err := n.dir.NewDir(ctx, name)
	if err != nil {
		log.Warnf("new dir: %v", err)
		return nil, translateError(err)
	}

	node := newDirNode(n.fs, newDir)
	n.fs.fillEntryOut(newDir, out)

	return n.NewInode(ctx, node, fusefs.StableAttr{
		Mode: out.Attr.Mode,
	}), 0
}

var _ = (fusefs.NodeMkdirer)((*DirNode)(nil))

func (n *DirNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fusefs.Inode, fh fusefs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Create")
	logger.Tracef("args: %v, %v, %#x, %#o", n.dir.Name(), name, flags, mode)

	hd, flags, err := n.dir.NewFile(ctx, name, flags)
	if err != nil {
		log.Warnf("new file: %v", err)
		return nil, nil, 0, translateError(err)
	}

	n.fs.fillEntryOut(hd.Entry(), out)

	fileNode := newFileNode(n.fs, hd.Entry())
	return n.NewInode(ctx, fileNode, fusefs.StableAttr{
		Mode: out.Attr.Mode,
	}), newFileHandle(hd), 0, 0
}

var _ = (fusefs.NodeCreater)((*DirNode)(nil))

// Unlink should remove a child from this directory.  If the
// return status is OK, the Inode is removed as child in the
// FS tree automatically. Default is to return EROFS.
func (n *DirNode) Unlink(ctx context.Context, name string) (errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Unlink")
	log.Tracef("args: %v, %v", n.dir.Name(), name)

	err := n.dir.RemoveChild(ctx, name)
	if err != nil {
		log.Warnf("remove child: %v", err)
		return translateError(err)
	}

	return 0
}

var _ = (fusefs.NodeUnlinker)((*DirNode)(nil))

// Rmdir is like Unlink but for directories.
// Default is to return EROFS.
func (n *DirNode) Rmdir(ctx context.Context, name string) (errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Rmdir")
	log.Tracef("args: %v, %v", n.dir.Name(), name)

	err := n.dir.RemoveChild(ctx, name)
	if err != nil {
		log.Warnf("remove child: %v", err)
		return translateError(err)
	}

	return 0
}

var _ = (fusefs.NodeRmdirer)((*DirNode)(nil))

func (n *DirNode) Rename(ctx context.Context, oldName string, newParent fusefs.InodeEmbedder, newName string, flags uint32) (errno syscall.Errno) {
	log := logger.WithField("F", "DirNode.Rename")
	log.Tracef("args: %v/%v->%v, %#o", n.dir.Name(), oldName, newName, flags)

	newParentNode, ok := newParent.(*DirNode)
	if !ok {
		return syscall.ENOTDIR
	}

	err := n.dir.MoveChild(ctx, oldName, newName, newParentNode.dir)
	if err != nil {
		log.Warnf("move child: %v", err)
		return translateError(err)
	}

	return 0
}

var _ = (fusefs.NodeRenamer)((*DirNode)(nil))
