// FileSystemConnector's implementation of RawFileSystem

package fuse

import (
	"bytes"
	"log"
	"time"

	"github.com/hanwen/go-fuse/raw"
)

var _ = log.Println

func (c *FileSystemConnector) String() string {
	if c.rootNode == nil || c.rootNode.mount == nil {
		return "go-fuse:unmounted"
	}
	return c.rootNode.mount.fs.String()
}

func (c *FileSystemConnector) Init(fsInit *RawFsInit) {
	c.fsInit = *fsInit
}

func (c *FileSystemConnector) lookupMountUpdate(out *Attr, mount *fileSystemMount) (node *Inode, code Status) {
	code = mount.fs.Root().GetAttr(out, nil, nil)
	if !code.Ok() {
		log.Println("Root getattr should not return error", code)
		out.Mode = S_IFDIR | 0755
		return mount.mountInode, OK
	}

	return mount.mountInode, OK
}

func (c *FileSystemConnector) internalLookup(out *Attr, parent *Inode, name string, context *Context) (node *Inode, code Status) {
	if subMount := c.findMount(parent, name); subMount != nil {
		return c.lookupMountUpdate(out, subMount)
	}

	child := parent.GetChild(name)
	if child != nil {
		parent = nil
	}
	var fsNode FsNode
	if child != nil {
		code = child.fsInode.GetAttr(out, nil, context)
		fsNode = child.FsNode()
	} else {
		fsNode, code = parent.fsInode.Lookup(out, name, context)
	}

	if child == nil && fsNode != nil {
		child = fsNode.Inode()
		if child == nil {
			log.Panicf("Lookup %q returned child without Inode: %v", name, fsNode)
		}
	}

	return child, code
}

func (c *FileSystemConnector) Lookup(out *raw.EntryOut, header *raw.InHeader, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	if !parent.IsDir() {
		log.Printf("Lookup %q called on non-Directory node %d", name, header.NodeId)
		return ENOTDIR
	}
	context := (*Context)(&header.Context)
	outAttr := (*Attr)(&out.Attr)
	child, code := c.internalLookup(outAttr, parent, name, context)
	if code == ENOENT && parent.mount.negativeEntry(out) {
		return OK
	}
	if !code.Ok() {
		return code
	}
	if child == nil {
		log.Println("Lookup returned OK with nil child", name)
	}

	child.mount.fillEntry(out)
	out.NodeId = c.lookupUpdate(child)
	out.Generation = child.generation
	out.Ino = out.NodeId

	return OK
}

func (c *FileSystemConnector) Forget(nodeID, nlookup uint64) {
	node := c.toInode(nodeID)
	c.forgetUpdate(node, int(nlookup))
}

func (c *FileSystemConnector) GetAttr(out *raw.AttrOut, header *raw.InHeader, input *raw.GetAttrIn) (code Status) {
	node := c.toInode(header.NodeId)

	var f File
	if input.Flags&raw.FUSE_GETATTR_FH != 0 {
		if opened := node.mount.getOpenedFile(input.Fh); opened != nil {
			f = opened.WithFlags.File
		}
	}

	dest := (*Attr)(&out.Attr)
	code = node.fsInode.GetAttr(dest, f, (*Context)(&header.Context))
	if !code.Ok() {
		return code
	}

	node.mount.fillAttr(out, header.NodeId)
	return OK
}

func (c *FileSystemConnector) OpenDir(out *raw.OpenOut, header *raw.InHeader, input *raw.OpenIn) (code Status) {
	node := c.toInode(header.NodeId)
	stream, err := node.fsInode.OpenDir((*Context)(&header.Context))
	if err != OK {
		return err
	}
	stream = append(stream, node.getMountDirEntries()...)
	de := &connectorDir{
		node:   node.FsNode(),
		stream: append(stream, DirEntry{S_IFDIR, "."}, DirEntry{S_IFDIR, ".."}),
	}
	h, opened := node.mount.registerFileHandle(node, de, nil, input.Flags)
	out.OpenFlags = opened.FuseFlags
	out.Fh = h
	return OK
}

func (c *FileSystemConnector) ReadDir(l *DirEntryList, header *raw.InHeader, input *raw.ReadIn) Status {
	node := c.toInode(header.NodeId)
	opened := node.mount.getOpenedFile(input.Fh)
	return opened.dir.ReadDir(l, input)
}

func (c *FileSystemConnector) Open(out *raw.OpenOut, header *raw.InHeader, input *raw.OpenIn) (status Status) {
	node := c.toInode(header.NodeId)
	f, code := node.fsInode.Open(input.Flags, (*Context)(&header.Context))
	if !code.Ok() {
		return code
	}
	h, opened := node.mount.registerFileHandle(node, nil, f, input.Flags)
	out.OpenFlags = opened.FuseFlags
	out.Fh = h
	return OK
}

func (c *FileSystemConnector) SetAttr(out *raw.AttrOut, header *raw.InHeader, input *raw.SetAttrIn) (code Status) {
	node := c.toInode(header.NodeId)
	var f File
	if input.Valid&raw.FATTR_FH != 0 {
		opened := node.mount.getOpenedFile(input.Fh)
		f = opened.WithFlags.File
	}

	if code.Ok() && input.Valid&raw.FATTR_MODE != 0 {
		permissions := uint32(07777) & input.Mode
		code = node.fsInode.Chmod(f, permissions, (*Context)(&header.Context))
	}
	if code.Ok() && (input.Valid&(raw.FATTR_UID|raw.FATTR_GID) != 0) {
		code = node.fsInode.Chown(f, uint32(input.Uid), uint32(input.Gid), (*Context)(&header.Context))
	}
	if code.Ok() && input.Valid&raw.FATTR_SIZE != 0 {
		code = node.fsInode.Truncate(f, input.Size, (*Context)(&header.Context))
	}
	if code.Ok() && (input.Valid&(raw.FATTR_ATIME|raw.FATTR_MTIME|raw.FATTR_ATIME_NOW|raw.FATTR_MTIME_NOW) != 0) {
		now := int64(0)
		if input.Valid&raw.FATTR_ATIME_NOW != 0 || input.Valid&raw.FATTR_MTIME_NOW != 0 {
			now = time.Now().UnixNano()
		}

		atime := int64(input.Atime*1e9) + int64(input.Atimensec)
		if input.Valid&raw.FATTR_ATIME_NOW != 0 {
			atime = now
		}

		mtime := int64(input.Mtime*1e9) + int64(input.Mtimensec)
		if input.Valid&raw.FATTR_MTIME_NOW != 0 {
			mtime = now
		}

		code = node.fsInode.Utimens(f, atime, mtime, (*Context)(&header.Context))
	}

	if !code.Ok() {
		return code
	}

	// Must call GetAttr(); the filesystem may override some of
	// the changes we effect here.
	attr := (*Attr)(&out.Attr)
	code = node.fsInode.GetAttr(attr, nil, (*Context)(&header.Context))
	if code.Ok() {
		node.mount.fillAttr(out, header.NodeId)
	}
	return code
}

func (c *FileSystemConnector) Readlink(header *raw.InHeader) (out []byte, code Status) {
	n := c.toInode(header.NodeId)
	return n.fsInode.Readlink((*Context)(&header.Context))
}

func (c *FileSystemConnector) Mknod(out *raw.EntryOut, header *raw.InHeader, input *raw.MknodIn, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	ctx := (*Context)(&header.Context)
	fsNode, code := parent.fsInode.Mknod(name, input.Mode, uint32(input.Rdev), ctx)
	if code.Ok() {
		c.childLookup(out, fsNode)
		code = fsNode.GetAttr((*Attr)(&out.Attr), nil, ctx)
	}
	return code
}

func (c *FileSystemConnector) Mkdir(out *raw.EntryOut, header *raw.InHeader, input *raw.MkdirIn, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	ctx := (*Context)(&header.Context)
	fsNode, code := parent.fsInode.Mkdir(name, input.Mode, ctx)
	if code.Ok() {
		c.childLookup(out, fsNode)
		code = fsNode.GetAttr((*Attr)(&out.Attr), nil, ctx)
	}
	return code
}

func (c *FileSystemConnector) Unlink(header *raw.InHeader, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	return parent.fsInode.Unlink(name, (*Context)(&header.Context))
}

func (c *FileSystemConnector) Rmdir(header *raw.InHeader, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	return parent.fsInode.Rmdir(name, (*Context)(&header.Context))
}

func (c *FileSystemConnector) Symlink(out *raw.EntryOut, header *raw.InHeader, pointedTo string, linkName string) (code Status) {
	parent := c.toInode(header.NodeId)
	ctx := (*Context)(&header.Context)
	fsNode, code := parent.fsInode.Symlink(linkName, pointedTo, ctx)
	if code.Ok() {
		c.childLookup(out, fsNode)
		code = fsNode.GetAttr((*Attr)(&out.Attr), nil, ctx)
	}
	return code
}

func (c *FileSystemConnector) Rename(header *raw.InHeader, input *raw.RenameIn, oldName string, newName string) (code Status) {
	oldParent := c.toInode(header.NodeId)
	isMountPoint := c.findMount(oldParent, oldName) != nil
	if isMountPoint {
		return EBUSY
	}

	newParent := c.toInode(input.Newdir)
	if oldParent.mount != newParent.mount {
		return EXDEV
	}

	return oldParent.fsInode.Rename(oldName, newParent.fsInode, newName, (*Context)(&header.Context))
}

func (c *FileSystemConnector) Link(out *raw.EntryOut, header *raw.InHeader, input *raw.LinkIn, name string) (code Status) {
	existing := c.toInode(input.Oldnodeid)
	parent := c.toInode(header.NodeId)

	if existing.mount != parent.mount {
		return EXDEV
	}
	ctx := (*Context)(&header.Context)
	fsNode, code := parent.fsInode.Link(name, existing.fsInode, ctx)
	if code.Ok() {
		c.childLookup(out, fsNode)
		code = fsNode.GetAttr((*Attr)(&out.Attr), nil, ctx)
	}

	return code
}

func (c *FileSystemConnector) Access(header *raw.InHeader, input *raw.AccessIn) (code Status) {
	n := c.toInode(header.NodeId)
	return n.fsInode.Access(input.Mask, (*Context)(&header.Context))
}

func (c *FileSystemConnector) Create(out *raw.CreateOut, header *raw.InHeader, input *raw.CreateIn, name string) (code Status) {
	parent := c.toInode(header.NodeId)
	f, fsNode, code := parent.fsInode.Create(name, uint32(input.Flags), input.Mode, (*Context)(&header.Context))
	if !code.Ok() {
		return code
	}

	c.childLookup(&out.EntryOut, fsNode)
	handle, opened := parent.mount.registerFileHandle(fsNode.Inode(), nil, f, input.Flags)

	out.OpenOut.OpenFlags = opened.FuseFlags
	out.OpenOut.Fh = handle
	return code
}

func (c *FileSystemConnector) Release(header *raw.InHeader, input *raw.ReleaseIn) {
	node := c.toInode(header.NodeId)
	opened := node.mount.unregisterFileHandle(input.Fh, node)
	opened.WithFlags.File.Release()
}

func (c *FileSystemConnector) ReleaseDir(header *raw.InHeader, input *raw.ReleaseIn) {
	node := c.toInode(header.NodeId)
	opened := node.mount.unregisterFileHandle(input.Fh, node)
	opened.dir.Release()
}

func (c *FileSystemConnector) GetXAttrSize(header *raw.InHeader, attribute string) (sz int, code Status) {
	node := c.toInode(header.NodeId)
	data, errno := node.fsInode.GetXAttr(attribute, (*Context)(&header.Context))
	return len(data), errno
}

func (c *FileSystemConnector) GetXAttrData(header *raw.InHeader, attribute string) (data []byte, code Status) {
	node := c.toInode(header.NodeId)
	return node.fsInode.GetXAttr(attribute, (*Context)(&header.Context))
}

func (c *FileSystemConnector) RemoveXAttr(header *raw.InHeader, attr string) Status {
	node := c.toInode(header.NodeId)
	return node.fsInode.RemoveXAttr(attr, (*Context)(&header.Context))
}

func (c *FileSystemConnector) SetXAttr(header *raw.InHeader, input *raw.SetXAttrIn, attr string, data []byte) Status {
	node := c.toInode(header.NodeId)
	return node.fsInode.SetXAttr(attr, data, int(input.Flags), (*Context)(&header.Context))
}

func (c *FileSystemConnector) ListXAttr(header *raw.InHeader) (data []byte, code Status) {
	node := c.toInode(header.NodeId)
	attrs, code := node.fsInode.ListXAttr((*Context)(&header.Context))
	if code != OK {
		return nil, code
	}

	b := bytes.NewBuffer([]byte{})
	for _, v := range attrs {
		b.Write([]byte(v))
		b.WriteByte(0)
	}

	return b.Bytes(), code
}

////////////////
// files.

func (c *FileSystemConnector) Write(header *raw.InHeader, input *raw.WriteIn, data []byte) (written uint32, code Status) {
	node := c.toInode(header.NodeId)
	opened := node.mount.getOpenedFile(input.Fh)
	return opened.WithFlags.File.Write(data, int64(input.Offset))
}

func (c *FileSystemConnector) Read(header *raw.InHeader, input *raw.ReadIn, buf []byte) (ReadResult, Status) {
	node := c.toInode(header.NodeId)
	opened := node.mount.getOpenedFile(input.Fh)

	return opened.WithFlags.File.Read(buf, int64(input.Offset))
}

func (c *FileSystemConnector) StatFs(out *StatfsOut, header *raw.InHeader) Status {
	node := c.toInode(header.NodeId)
	s := node.FsNode().StatFs()
	if s == nil {
		return ENOSYS
	}
	*out = *s
	return OK
}

func (c *FileSystemConnector) Flush(header *raw.InHeader, input *raw.FlushIn) Status {
	node := c.toInode(header.NodeId)
	opened := node.mount.getOpenedFile(input.Fh)
	return opened.WithFlags.File.Flush()
}