package fusefrontend_reverse

import (
	"context"
	"log"
	"syscall"

	"github.com/hanwen/go-fuse/v2/fs"
	"github.com/hanwen/go-fuse/v2/fuse"

	"github.com/rfjakob/gocryptfs/v2/internal/configfile"
	"github.com/rfjakob/gocryptfs/v2/internal/inomap"
	"github.com/rfjakob/gocryptfs/v2/internal/nametransform"
)

const (
	// virtualFileMode is the mode to use for virtual files (gocryptfs.diriv and
	// *.name). They are always readable, as stated in func Access
	virtualFileMode = syscall.S_IFREG | 0444
	// We use inomap's `Tag` feature to generate unique inode numbers for
	// virtual files. These are the tags we use.
	inoTagDirIV    = 1
	inoTagNameFile = 2
)

type fileType int

// Values returned by lookupFileType
const (
	// A real file/directory/symlink in the backing plaintext directory
	typeReal fileType = iota
	// A DirIV (gocryptfs.diriv) file
	typeDiriv
	// A gocryptfs.longname.*.name file for a file with a long name
	typeName
	// The config file gocryptfs.conf
	typeConfig
)

// lookupFileType returns the type of child file name
// (one of the fileType constants above). Called from Lookup().
func (n *Node) lookupFileType(cName string) fileType {
	rn := n.rootNode()
	// In -plaintextname mode, neither diriv nor longname files exist.
	if !rn.args.PlaintextNames {
		if !rn.args.DeterministicNames {
			// Is it a gocryptfs.diriv file?
			if cName == nametransform.DirIVFilename {
				return typeDiriv
			}
		}
		// Is it a gocryptfs.longname.*.name file?
		if t := nametransform.NameType(cName); t == nametransform.LongNameFilename {
			return typeName
		}
	}
	// gocryptfs.conf in the root directory. This is passed through to
	// .gocryptfs.reverse.conf in the backing plaintext directory.
	if n.isRoot() && !rn.args.ConfigCustom && cName == configfile.ConfDefaultName {
		return typeConfig
	}
	return typeReal
}

// VirtualMemNode is an in-memory node that does not have a representation
// on disk.
type VirtualMemNode struct {
	fs.Inode

	// file content
	content []byte
	// attributes for Getattr()
	attr fuse.Attr
}

// newVirtualMemNode creates a new in-memory file that does not have a representation
// on disk. "content" is the file content. Timestamps and file owner are copied
// from "parentFile" (file descriptor).
// For a "gocryptfs.diriv" file, you would use the parent directory as
// "parentFile".
func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, inoTag uint8) (vf *VirtualMemNode, errno syscall.Errno) {
	if inoTag == 0 {
		log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!")
	}

	// Adjust inode number and size
	rn := n.rootNode()
	st := parentStat
	if inoTag == inoTagNameFile {
		// No stable mapping for gocryptfs.longname.*.name files, instead use an
		// incrementing counter. We don't want two of those files to ever have the
		// same inode number, even for hard-linked files.
		st.Ino = rn.inoMap.NextSpillIno()
	} else {
		q := inomap.NewQIno(uint64(st.Dev), inoTag, uint64(st.Ino))
		st.Ino = rn.inoMap.Translate(q)
	}
	st.Size = int64(len(content))
	st.Mode = virtualFileMode
	st.Nlink = 1
	var a fuse.Attr
	a.FromStat(st)
	// With inode number reuse and hard links, we could have returned
	// wrong data for gocryptfs.diriv and gocryptfs.xyz.longname files, respectively
	// (https://github.com/rfjakob/gocryptfs/issues/802).
	//
	// Now that this is fixed, ensure that rsync and similar tools pick up the new
	// correct files by advancing mtime and ctime by 10 seconds, which should be more
	// than any filesytems' timestamp granularity (FAT32 has 2 seconds).
	a.Mtime += 10
	a.Ctime += 10
	if rn.args.ForceOwner != nil {
		a.Owner = *rn.args.ForceOwner
	}
	vf = &VirtualMemNode{content: content, attr: a}
	return
}

// Open - FUSE call
func (f *VirtualMemNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
	return nil, fuse.FOPEN_KEEP_CACHE, 0
}

// GetAttr - FUSE call
func (f *VirtualMemNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
	out.Attr = f.attr
	return 0
}

// Read - FUSE call
func (f *VirtualMemNode) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
	end := int(off) + len(dest)
	if end > len(f.content) {
		end = len(f.content)
	}
	return fuse.ReadResultData(f.content[off:end]), 0
}
