//go:build cmount && ((linux && cgo) || (darwin && cgo) || (freebsd && cgo) || (openbsd && cgo) || windows)

package cmount

import (
	"io"
	"os"
	"path"
	"runtime"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/rclone/rclone/cmd/mountlib"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/fserrors"
	"github.com/rclone/rclone/fs/log"
	"github.com/rclone/rclone/vfs"
	"github.com/winfsp/cgofuse/fuse"
)

const fhUnset = ^uint64(0)

// FS represents the top level filing system
type FS struct {
	VFS       *vfs.VFS
	f         fs.Fs
	opt       *mountlib.Options
	ready     chan (struct{})
	mu        sync.Mutex // to protect the below
	handles   []vfs.Handle
	destroyed atomic.Int32
}

// NewFS makes a new FS
func NewFS(VFS *vfs.VFS, opt *mountlib.Options) *FS {
	fsys := &FS{
		VFS:   VFS,
		f:     VFS.Fs(),
		opt:   opt,
		ready: make(chan (struct{})),
	}
	return fsys
}

// Open a handle returning an integer file handle
func (fsys *FS) openHandle(handle vfs.Handle) (fh uint64) {
	fsys.mu.Lock()
	defer fsys.mu.Unlock()
	var i int
	var oldHandle vfs.Handle
	for i, oldHandle = range fsys.handles {
		if oldHandle == nil {
			fsys.handles[i] = handle
			goto found
		}
	}
	fsys.handles = append(fsys.handles, handle)
	i = len(fsys.handles) - 1
found:
	return uint64(i)
}

// get the handle for fh, call with the lock held
func (fsys *FS) _getHandle(fh uint64) (i int, handle vfs.Handle, errc int) {
	if fh > uint64(len(fsys.handles)) {
		fs.Debugf(nil, "Bad file handle: too big: 0x%X", fh)
		return i, nil, -fuse.EBADF
	}
	i = int(fh)
	handle = fsys.handles[i]
	if handle == nil {
		fs.Debugf(nil, "Bad file handle: nil handle: 0x%X", fh)
		return i, nil, -fuse.EBADF
	}
	return i, handle, 0
}

// Get the handle for the file handle
func (fsys *FS) getHandle(fh uint64) (handle vfs.Handle, errc int) {
	fsys.mu.Lock()
	_, handle, errc = fsys._getHandle(fh)
	fsys.mu.Unlock()
	return
}

// Close the handle
func (fsys *FS) closeHandle(fh uint64) (errc int) {
	fsys.mu.Lock()
	i, _, errc := fsys._getHandle(fh)
	if errc == 0 {
		fsys.handles[i] = nil
	}
	fsys.mu.Unlock()
	return
}

// lookup a Node given a path
func (fsys *FS) lookupNode(path string) (node vfs.Node, errc int) {
	node, err := fsys.VFS.Stat(path)
	return node, translateError(err)
}

// lookup a Dir given a path
func (fsys *FS) lookupDir(path string) (dir *vfs.Dir, errc int) {
	node, errc := fsys.lookupNode(path)
	if errc != 0 {
		return nil, errc
	}
	dir, ok := node.(*vfs.Dir)
	if !ok {
		return nil, -fuse.ENOTDIR
	}
	return dir, 0
}

// lookup a parent Dir given a path returning the dir and the leaf
func (fsys *FS) lookupParentDir(filePath string) (leaf string, dir *vfs.Dir, errc int) {
	parentDir, leaf := path.Split(filePath)
	dir, errc = fsys.lookupDir(parentDir)
	return leaf, dir, errc
}

// get a node and handle from the path or from the fh if not fhUnset
//
// handle may be nil
func (fsys *FS) getNode(path string, fh uint64) (node vfs.Node, handle vfs.Handle, errc int) {
	if fh == fhUnset {
		node, errc = fsys.lookupNode(path)
	} else {
		handle, errc = fsys.getHandle(fh)
		if errc == 0 {
			node = handle.Node()
		}
	}
	return
}

// stat fills up the stat block for Node
func (fsys *FS) stat(node vfs.Node, stat *fuse.Stat_t) (errc int) {
	Size := uint64(node.Size())
	Blocks := (Size + 511) / 512
	modTime := node.ModTime()
	//stat.Dev = 1
	stat.Ino = node.Inode() // FIXME do we need to set the inode number?
	stat.Mode = getMode(node)
	stat.Nlink = 1
	stat.Uid = fsys.VFS.Opt.UID
	stat.Gid = fsys.VFS.Opt.GID
	//stat.Rdev
	stat.Size = int64(Size)
	t := fuse.NewTimespec(modTime)
	stat.Atim = t
	stat.Mtim = t
	stat.Ctim = t
	stat.Blksize = 512
	stat.Blocks = int64(Blocks)
	stat.Birthtim = t
	// fs.Debugf(nil, "stat = %+v", *stat)
	return 0
}

// Init is called after the filesystem is ready
func (fsys *FS) Init() {
	defer log.Trace(fsys.f, "")("")
	close(fsys.ready)
}

// Destroy is called when it is unmounted (note that depending on how
// the file system is terminated the file system may not receive the
// Destroy call).
func (fsys *FS) Destroy() {
	defer log.Trace(fsys.f, "")("")
	fsys.destroyed.Store(1)
}

// Getattr reads the attributes for path
func (fsys *FS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
	defer log.Trace(path, "fh=0x%X", fh)("errc=%v", &errc)
	node, _, errc := fsys.getNode(path, fh)
	if errc == 0 {
		errc = fsys.stat(node, stat)
	}
	return
}

// Opendir opens path as a directory
func (fsys *FS) Opendir(path string) (errc int, fh uint64) {
	defer log.Trace(path, "")("errc=%d, fh=0x%X", &errc, &fh)
	handle, err := fsys.VFS.OpenFile(path, os.O_RDONLY, 0777)
	if err != nil {
		return translateError(err), fhUnset
	}
	return 0, fsys.openHandle(handle)
}

// Readdir reads the directory at dirPath
func (fsys *FS) Readdir(dirPath string,
	fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
	ofst int64,
	fh uint64) (errc int) {
	itemsRead := -1
	defer log.Trace(dirPath, "ofst=%d, fh=0x%X", ofst, fh)("items=%d, errc=%d", &itemsRead, &errc)

	dir, errc := fsys.lookupDir(dirPath)
	if errc != 0 {
		return errc
	}

	// We can't seek in directories and FUSE should know that so
	// return an error if ofst is ever set.
	if ofst > 0 {
		// However openbsd doesn't seem to know this - perhaps a bug in its
		// FUSE implementation or a bug in cgofuse?
		// See: https://github.com/billziss-gh/cgofuse/issues/49
		if runtime.GOOS == "openbsd" {
			return 0
		}
		return -fuse.ESPIPE
	}

	nodes, err := dir.ReadDirAll()
	if err != nil {
		return translateError(err)
	}

	// Optionally, create a struct stat that describes the file as
	// for getattr (but FUSE only looks at st_ino and the
	// file-type bits of st_mode).
	//
	// We have called host.SetCapReaddirPlus() so WinFsp will
	// use the full stat information - a Useful optimization on
	// Windows.
	//
	// NB we are using the first mode for readdir: The readdir
	// implementation ignores the offset parameter, and passes
	// zero to the filler function's offset. The filler function
	// will not return '1' (unless an error happens), so the whole
	// directory is read in a single readdir operation.
	fill(".", nil, 0)
	fill("..", nil, 0)
	for _, node := range nodes {
		name := node.Name()
		if len(name) > mountlib.MaxLeafSize {
			fs.Errorf(dirPath, "Name too long (%d bytes) for FUSE, skipping: %s", len(name), name)
			continue
		}
		// We have called host.SetCapReaddirPlus() so supply the stat information
		// It is very cheap at this point so supply it regardless of OS capabilities
		var stat fuse.Stat_t
		_ = fsys.stat(node, &stat) // not capable of returning an error
		fill(name, &stat, 0)
	}
	itemsRead = len(nodes)
	return 0
}

// Releasedir finished reading the directory
func (fsys *FS) Releasedir(path string, fh uint64) (errc int) {
	defer log.Trace(path, "fh=0x%X", fh)("errc=%d", &errc)
	return fsys.closeHandle(fh)
}

// Statfs reads overall stats on the filesystem
func (fsys *FS) Statfs(path string, stat *fuse.Statfs_t) (errc int) {
	defer log.Trace(path, "")("stat=%+v, errc=%d", stat, &errc)
	const blockSize = 4096
	total, _, free := fsys.VFS.Statfs()
	stat.Blocks = uint64(total) / blockSize // Total data blocks in file system.
	stat.Bfree = uint64(free) / blockSize   // Free blocks in file system.
	stat.Bavail = stat.Bfree                // Free blocks in file system if you're not root.
	stat.Files = 1e9                        // Total files in file system.
	stat.Ffree = 1e9                        // Free files in file system.
	stat.Bsize = blockSize                  // Block size
	stat.Namemax = 255                      // Maximum file name length?
	stat.Frsize = blockSize                 // Fragment size, smallest addressable data size in the file system.
	mountlib.ClipBlocks(&stat.Blocks)
	mountlib.ClipBlocks(&stat.Bfree)
	mountlib.ClipBlocks(&stat.Bavail)
	return 0
}

// OpenEx opens a file
func (fsys *FS) OpenEx(path string, fi *fuse.FileInfo_t) (errc int) {
	defer log.Trace(path, "flags=0x%X", fi.Flags)("errc=%d, fh=0x%X", &errc, &fi.Fh)
	fi.Fh = fhUnset

	// translate the fuse flags to os flags
	flags := translateOpenFlags(fi.Flags)
	handle, err := fsys.VFS.OpenFile(path, flags, 0777)
	if err != nil {
		return translateError(err)
	}

	// If size unknown then use direct io to read
	if entry := handle.Node().DirEntry(); entry != nil && entry.Size() < 0 {
		fi.DirectIo = true
	}
	if fsys.opt.DirectIO {
		fi.DirectIo = true
	}

	fi.Fh = fsys.openHandle(handle)
	return 0
}

// Open opens a file
func (fsys *FS) Open(path string, flags int) (errc int, fh uint64) {
	var fi = fuse.FileInfo_t{
		Flags: flags,
	}
	errc = fsys.OpenEx(path, &fi)
	return errc, fi.Fh
}

// CreateEx creates and opens a file.
func (fsys *FS) CreateEx(filePath string, mode uint32, fi *fuse.FileInfo_t) (errc int) {
	defer log.Trace(filePath, "flags=0x%X, mode=0%o", fi.Flags, mode)("errc=%d, fh=0x%X", &errc, &fi.Fh)
	fi.Fh = fhUnset
	leaf, parentDir, errc := fsys.lookupParentDir(filePath)
	if errc != 0 {
		return errc
	}
	file, err := parentDir.Create(leaf, fi.Flags)
	if err != nil {
		return translateError(err)
	}
	// translate the fuse flags to os flags
	flags := translateOpenFlags(fi.Flags) | os.O_CREATE
	handle, err := file.Open(flags)
	if err != nil {
		return translateError(err)
	}
	fi.Fh = fsys.openHandle(handle)
	return 0
}

// Create creates and opens a file.
func (fsys *FS) Create(filePath string, flags int, mode uint32) (errc int, fh uint64) {
	var fi = fuse.FileInfo_t{
		Flags: flags,
	}
	errc = fsys.CreateEx(filePath, mode, &fi)
	return errc, fi.Fh
}

// Truncate truncates a file to size
func (fsys *FS) Truncate(path string, size int64, fh uint64) (errc int) {
	defer log.Trace(path, "size=%d, fh=0x%X", size, fh)("errc=%d", &errc)
	node, handle, errc := fsys.getNode(path, fh)
	if errc != 0 {
		return errc
	}
	var err error
	if handle != nil {
		err = handle.Truncate(size)
	} else {
		err = node.Truncate(size)
	}
	if err != nil {
		return translateError(err)
	}
	return 0
}

// Read data from file handle
func (fsys *FS) Read(path string, buff []byte, ofst int64, fh uint64) (n int) {
	defer log.Trace(path, "ofst=%d, fh=0x%X", ofst, fh)("n=%d", &n)
	handle, errc := fsys.getHandle(fh)
	if errc != 0 {
		return errc
	}
	n, err := handle.ReadAt(buff, ofst)
	if err == io.EOF {
	} else if err != nil {
		return translateError(err)
	}
	return n
}

// Write data to file handle
func (fsys *FS) Write(path string, buff []byte, ofst int64, fh uint64) (n int) {
	defer log.Trace(path, "ofst=%d, fh=0x%X", ofst, fh)("n=%d", &n)
	handle, errc := fsys.getHandle(fh)
	if errc != 0 {
		return errc
	}
	n, err := handle.WriteAt(buff, ofst)
	if err != nil {
		return translateError(err)
	}
	return n
}

// Flush flushes an open file descriptor or path
func (fsys *FS) Flush(path string, fh uint64) (errc int) {
	defer log.Trace(path, "fh=0x%X", fh)("errc=%d", &errc)
	handle, errc := fsys.getHandle(fh)
	if errc != 0 {
		return errc
	}
	return translateError(handle.Flush())
}

// Release closes the file if still open
func (fsys *FS) Release(path string, fh uint64) (errc int) {
	defer log.Trace(path, "fh=0x%X", fh)("errc=%d", &errc)
	handle, errc := fsys.getHandle(fh)
	if errc != 0 {
		return errc
	}
	_ = fsys.closeHandle(fh)
	return translateError(handle.Release())
}

// Unlink removes a file.
func (fsys *FS) Unlink(filePath string) (errc int) {
	defer log.Trace(filePath, "")("errc=%d", &errc)
	leaf, parentDir, errc := fsys.lookupParentDir(filePath)
	if errc != 0 {
		return errc
	}
	return translateError(parentDir.RemoveName(leaf))
}

// Mkdir creates a directory.
func (fsys *FS) Mkdir(dirPath string, mode uint32) (errc int) {
	defer log.Trace(dirPath, "mode=0%o", mode)("errc=%d", &errc)
	leaf, parentDir, errc := fsys.lookupParentDir(dirPath)
	if errc != 0 {
		return errc
	}
	_, err := parentDir.Mkdir(leaf)
	return translateError(err)
}

// Rmdir removes a directory
func (fsys *FS) Rmdir(dirPath string) (errc int) {
	defer log.Trace(dirPath, "")("errc=%d", &errc)
	leaf, parentDir, errc := fsys.lookupParentDir(dirPath)
	if errc != 0 {
		return errc
	}
	return translateError(parentDir.RemoveName(leaf))
}

// Rename renames a file.
func (fsys *FS) Rename(oldPath string, newPath string) (errc int) {
	defer log.Trace(oldPath, "newPath=%q", newPath)("errc=%d", &errc)
	return translateError(fsys.VFS.Rename(oldPath, newPath))
}

// Windows sometimes seems to send times that are the epoch which is
// 1601-01-01 +/- timezone so filter out times that are earlier than
// this.
var invalidDateCutoff = time.Date(1601, 1, 2, 0, 0, 0, 0, time.UTC)

// Utimens changes the access and modification times of a file.
func (fsys *FS) Utimens(path string, tmsp []fuse.Timespec) (errc int) {
	defer log.Trace(path, "tmsp=%+v", tmsp)("errc=%d", &errc)
	node, errc := fsys.lookupNode(path)
	if errc != 0 {
		return errc
	}
	if tmsp == nil || len(tmsp) < 2 {
		fs.Debugf(path, "Utimens: Not setting time as timespec isn't complete: %v", tmsp)
		return 0
	}
	t := tmsp[1].Time()
	if t.Before(invalidDateCutoff) {
		fs.Debugf(path, "Utimens: Not setting out of range time: %v", t)
		return 0
	}
	fs.Debugf(path, "Utimens: SetModTime: %v", t)
	return translateError(node.SetModTime(t))
}

// Mknod creates a file node.
func (fsys *FS) Mknod(path string, mode uint32, dev uint64) (errc int) {
	defer log.Trace(path, "mode=0x%X, dev=0x%X", mode, dev)("errc=%d", &errc)
	return -fuse.ENOSYS
}

// Fsync synchronizes file contents.
func (fsys *FS) Fsync(path string, datasync bool, fh uint64) (errc int) {
	defer log.Trace(path, "datasync=%v, fh=0x%X", datasync, fh)("errc=%d", &errc)
	// This is a no-op for rclone
	return 0
}

// Link creates a hard link to a file.
func (fsys *FS) Link(oldpath string, newpath string) (errc int) {
	defer log.Trace(oldpath, "newpath=%q", newpath)("errc=%d", &errc)
	return -fuse.ENOSYS
}

// Symlink creates a symbolic link.
func (fsys *FS) Symlink(target string, newpath string) (errc int) {
	defer log.Trace(target, "newpath=%q, target=%q", newpath, target)("errc=%d", &errc)
	return translateError(fsys.VFS.Symlink(target, newpath))
}

// Readlink reads the target of a symbolic link.
func (fsys *FS) Readlink(path string) (errc int, linkPath string) {
	defer log.Trace(path, "")("errc=%v, linkPath=%q", &errc, linkPath)
	linkPath, err := fsys.VFS.Readlink(path)
	return translateError(err), linkPath
}

// Chmod changes the permission bits of a file.
func (fsys *FS) Chmod(path string, mode uint32) (errc int) {
	defer log.Trace(path, "mode=0%o", mode)("errc=%d", &errc)
	// This is a no-op for rclone
	return 0
}

// Chown changes the owner and group of a file.
func (fsys *FS) Chown(path string, uid uint32, gid uint32) (errc int) {
	defer log.Trace(path, "uid=%d, gid=%d", uid, gid)("errc=%d", &errc)
	// This is a no-op for rclone
	return 0
}

// Access checks file access permissions.
func (fsys *FS) Access(path string, mask uint32) (errc int) {
	defer log.Trace(path, "mask=0%o", mask)("errc=%d", &errc)
	// This is a no-op for rclone
	return 0
}

// Fsyncdir synchronizes directory contents.
func (fsys *FS) Fsyncdir(path string, datasync bool, fh uint64) (errc int) {
	defer log.Trace(path, "datasync=%v, fh=0x%X", datasync, fh)("errc=%d", &errc)
	// This is a no-op for rclone
	return 0
}

// Setxattr sets extended attributes.
func (fsys *FS) Setxattr(path string, name string, value []byte, flags int) (errc int) {
	defer log.Trace(path, "name=%q, value=%q, flags=%d", name, value, flags)("errc=%d", &errc)
	return -fuse.ENOSYS
}

// Getxattr gets extended attributes.
func (fsys *FS) Getxattr(path string, name string) (errc int, value []byte) {
	defer log.Trace(path, "name=%q", name)("errc=%d, value=%q", &errc, &value)
	return -fuse.ENOSYS, nil
}

// Removexattr removes extended attributes.
func (fsys *FS) Removexattr(path string, name string) (errc int) {
	defer log.Trace(path, "name=%q", name)("errc=%d", &errc)
	return -fuse.ENOSYS
}

// Listxattr lists extended attributes.
func (fsys *FS) Listxattr(path string, fill func(name string) bool) (errc int) {
	defer log.Trace(path, "fill=%p", fill)("errc=%d", &errc)
	return -fuse.ENOSYS
}

// Getpath allows a case-insensitive file system to report the correct case of
// a file path.
func (fsys *FS) Getpath(path string, fh uint64) (errc int, normalisedPath string) {
	defer log.Trace(path, "Getpath fh=%d", fh)("errc=%d, normalisedPath=%q", &errc, &normalisedPath)
	node, _, errc := fsys.getNode(path, fh)
	if errc != 0 {
		return errc, ""
	}
	normalisedPath = node.Path()
	if !strings.HasPrefix(normalisedPath, "/") {
		normalisedPath = "/" + normalisedPath
	}
	return 0, normalisedPath
}

// Translate errors from mountlib
func translateError(err error) (errc int) {
	if err == nil {
		return 0
	}
	_, uErr := fserrors.Cause(err)
	switch uErr {
	case vfs.OK:
		return 0
	case vfs.ENOENT, fs.ErrorDirNotFound, fs.ErrorObjectNotFound:
		return -fuse.ENOENT
	case vfs.EEXIST, fs.ErrorDirExists:
		return -fuse.EEXIST
	case vfs.EPERM, fs.ErrorPermissionDenied:
		return -fuse.EPERM
	case vfs.ECLOSED:
		return -fuse.EBADF
	case vfs.ENOTEMPTY:
		return -fuse.ENOTEMPTY
	case vfs.ESPIPE:
		return -fuse.ESPIPE
	case vfs.EBADF:
		return -fuse.EBADF
	case vfs.EROFS:
		return -fuse.EROFS
	case vfs.ENOSYS, fs.ErrorNotImplemented:
		return -fuse.ENOSYS
	case vfs.EINVAL:
		return -fuse.EINVAL
	case vfs.ELOOP:
		return -fuse.ELOOP
	}
	fs.Errorf(nil, "IO error: %v", err)
	return -fuse.EIO
}

// Translate Open Flags from FUSE to os (as used in the vfs layer)
func translateOpenFlags(inFlags int) (outFlags int) {
	switch inFlags & fuse.O_ACCMODE {
	case fuse.O_RDONLY:
		outFlags = os.O_RDONLY
	case fuse.O_WRONLY:
		outFlags = os.O_WRONLY
	case fuse.O_RDWR:
		outFlags = os.O_RDWR
	}
	if inFlags&fuse.O_APPEND != 0 {
		outFlags |= os.O_APPEND
	}
	if inFlags&fuse.O_CREAT != 0 {
		outFlags |= os.O_CREATE
	}
	if inFlags&fuse.O_EXCL != 0 {
		outFlags |= os.O_EXCL
	}
	if inFlags&fuse.O_TRUNC != 0 {
		outFlags |= os.O_TRUNC
	}
	// NB O_SYNC isn't defined by fuse
	return outFlags
}

// get the Mode from a vfs Node
func getMode(node os.FileInfo) uint32 {
	vfsMode := node.Mode()
	Mode := vfsMode.Perm()
	if vfsMode&os.ModeDir != 0 {
		Mode |= fuse.S_IFDIR
	} else if vfsMode&os.ModeSymlink != 0 {
		Mode |= fuse.S_IFLNK
	} else if vfsMode&os.ModeNamedPipe != 0 {
		Mode |= fuse.S_IFIFO
	} else {
		Mode |= fuse.S_IFREG
	}
	return uint32(Mode)
}

// Make sure interfaces are satisfied
var (
	_ fuse.FileSystemInterface = (*FS)(nil)
	_ fuse.FileSystemOpenEx    = (*FS)(nil)
	_ fuse.FileSystemGetpath   = (*FS)(nil)
	//_ fuse.FileSystemChflags    = (*FS)(nil)
	//_ fuse.FileSystemSetcrtime  = (*FS)(nil)
	//_ fuse.FileSystemSetchgtime = (*FS)(nil)
)
