// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package p9

import (
	"fmt"
	"io"
	"sync/atomic"
	"syscall"

	"gvisor.dev/gvisor/pkg/fd"
	"gvisor.dev/gvisor/pkg/log"
)

// Attach attaches to a server.
//
// Note that authentication is not currently supported.
func (c *Client) Attach(name string) (File, error) {
	fid, ok := c.fidPool.Get()
	if !ok {
		return nil, ErrOutOfFIDs
	}

	rattach := Rattach{}
	if err := c.sendRecv(&Tattach{FID: FID(fid), Auth: Tauth{AttachName: name, AuthenticationFID: NoFID, UID: NoUID}}, &rattach); err != nil {
		c.fidPool.Put(fid)
		return nil, err
	}

	return c.newFile(FID(fid)), nil
}

// newFile returns a new client file.
func (c *Client) newFile(fid FID) *clientFile {
	return &clientFile{
		client: c,
		fid:    fid,
	}
}

// clientFile is provided to clients.
//
// This proxies all of the interfaces found in file.go.
type clientFile struct {
	DisallowServerCalls

	// client is the originating client.
	client *Client

	// fid is the FID for this file.
	fid FID

	// closed indicates whether this file has been closed.
	closed uint32
}

// Walk implements File.Walk.
func (c *clientFile) Walk(names []string) ([]QID, File, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, nil, syscall.EBADF
	}

	fid, ok := c.client.fidPool.Get()
	if !ok {
		return nil, nil, ErrOutOfFIDs
	}

	rwalk := Rwalk{}
	if err := c.client.sendRecv(&Twalk{FID: c.fid, NewFID: FID(fid), Names: names}, &rwalk); err != nil {
		c.client.fidPool.Put(fid)
		return nil, nil, err
	}

	// Return a new client file.
	return rwalk.QIDs, c.client.newFile(FID(fid)), nil
}

// WalkGetAttr implements File.WalkGetAttr.
func (c *clientFile) WalkGetAttr(components []string) ([]QID, File, AttrMask, Attr, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, nil, AttrMask{}, Attr{}, syscall.EBADF
	}

	if !versionSupportsTwalkgetattr(c.client.version) {
		qids, file, err := c.Walk(components)
		if err != nil {
			return nil, nil, AttrMask{}, Attr{}, err
		}
		_, valid, attr, err := file.GetAttr(AttrMaskAll())
		if err != nil {
			file.Close()
			return nil, nil, AttrMask{}, Attr{}, err
		}
		return qids, file, valid, attr, nil
	}

	fid, ok := c.client.fidPool.Get()
	if !ok {
		return nil, nil, AttrMask{}, Attr{}, ErrOutOfFIDs
	}

	rwalkgetattr := Rwalkgetattr{}
	if err := c.client.sendRecv(&Twalkgetattr{FID: c.fid, NewFID: FID(fid), Names: components}, &rwalkgetattr); err != nil {
		c.client.fidPool.Put(fid)
		return nil, nil, AttrMask{}, Attr{}, err
	}

	// Return a new client file.
	return rwalkgetattr.QIDs, c.client.newFile(FID(fid)), rwalkgetattr.Valid, rwalkgetattr.Attr, nil
}

// StatFS implements File.StatFS.
func (c *clientFile) StatFS() (FSStat, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return FSStat{}, syscall.EBADF
	}

	rstatfs := Rstatfs{}
	if err := c.client.sendRecv(&Tstatfs{FID: c.fid}, &rstatfs); err != nil {
		return FSStat{}, err
	}

	return rstatfs.FSStat, nil
}

// FSync implements File.FSync.
func (c *clientFile) FSync() error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Tfsync{FID: c.fid}, &Rfsync{})
}

// GetAttr implements File.GetAttr.
func (c *clientFile) GetAttr(req AttrMask) (QID, AttrMask, Attr, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return QID{}, AttrMask{}, Attr{}, syscall.EBADF
	}

	rgetattr := Rgetattr{}
	if err := c.client.sendRecv(&Tgetattr{FID: c.fid, AttrMask: req}, &rgetattr); err != nil {
		return QID{}, AttrMask{}, Attr{}, err
	}

	return rgetattr.QID, rgetattr.Valid, rgetattr.Attr, nil
}

// SetAttr implements File.SetAttr.
func (c *clientFile) SetAttr(valid SetAttrMask, attr SetAttr) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Tsetattr{FID: c.fid, Valid: valid, SetAttr: attr}, &Rsetattr{})
}

// GetXattr implements File.GetXattr.
func (c *clientFile) GetXattr(name string, size uint64) (string, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return "", syscall.EBADF
	}
	if !versionSupportsGetSetXattr(c.client.version) {
		return "", syscall.EOPNOTSUPP
	}

	rgetxattr := Rgetxattr{}
	if err := c.client.sendRecv(&Tgetxattr{FID: c.fid, Name: name, Size: size}, &rgetxattr); err != nil {
		return "", err
	}

	return rgetxattr.Value, nil
}

// SetXattr implements File.SetXattr.
func (c *clientFile) SetXattr(name, value string, flags uint32) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}
	if !versionSupportsGetSetXattr(c.client.version) {
		return syscall.EOPNOTSUPP
	}

	return c.client.sendRecv(&Tsetxattr{FID: c.fid, Name: name, Value: value, Flags: flags}, &Rsetxattr{})
}

// ListXattr implements File.ListXattr.
func (c *clientFile) ListXattr(size uint64) (map[string]struct{}, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, syscall.EBADF
	}
	if !versionSupportsListRemoveXattr(c.client.version) {
		return nil, syscall.EOPNOTSUPP
	}

	rlistxattr := Rlistxattr{}
	if err := c.client.sendRecv(&Tlistxattr{FID: c.fid, Size: size}, &rlistxattr); err != nil {
		return nil, err
	}

	xattrs := make(map[string]struct{}, len(rlistxattr.Xattrs))
	for _, x := range rlistxattr.Xattrs {
		xattrs[x] = struct{}{}
	}
	return xattrs, nil
}

// RemoveXattr implements File.RemoveXattr.
func (c *clientFile) RemoveXattr(name string) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}
	if !versionSupportsListRemoveXattr(c.client.version) {
		return syscall.EOPNOTSUPP
	}

	return c.client.sendRecv(&Tremovexattr{FID: c.fid, Name: name}, &Rremovexattr{})
}

// Allocate implements File.Allocate.
func (c *clientFile) Allocate(mode AllocateMode, offset, length uint64) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}
	if !versionSupportsTallocate(c.client.version) {
		return syscall.EOPNOTSUPP
	}

	return c.client.sendRecv(&Tallocate{FID: c.fid, Mode: mode, Offset: offset, Length: length}, &Rallocate{})
}

// Remove implements File.Remove.
//
// N.B. This method is no longer part of the file interface and should be
// considered deprecated.
func (c *clientFile) Remove() error {
	// Avoid double close.
	if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) {
		return syscall.EBADF
	}

	// Send the remove message.
	if err := c.client.sendRecv(&Tremove{FID: c.fid}, &Rremove{}); err != nil {
		return err
	}

	// "It is correct to consider remove to be a clunk with the side effect
	// of removing the file if permissions allow."
	// https://swtch.com/plan9port/man/man9/remove.html

	// Return the FID to the pool.
	c.client.fidPool.Put(uint64(c.fid))
	return nil
}

// Close implements File.Close.
func (c *clientFile) Close() error {
	// Avoid double close.
	if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) {
		return syscall.EBADF
	}

	// Send the close message.
	if err := c.client.sendRecv(&Tclunk{FID: c.fid}, &Rclunk{}); err != nil {
		// If an error occurred, we toss away the FID. This isn't ideal,
		// but I'm not sure what else makes sense in this context.
		log.Warningf("Tclunk failed, losing FID %v: %v", c.fid, err)
		return err
	}

	// Return the FID to the pool.
	c.client.fidPool.Put(uint64(c.fid))
	return nil
}

// SetAttrClose implements File.SetAttrClose.
func (c *clientFile) SetAttrClose(valid SetAttrMask, attr SetAttr) error {
	if !versionSupportsTsetattrclunk(c.client.version) {
		setAttrErr := c.SetAttr(valid, attr)

		// Try to close file even in case of failure above. Since the state of the
		// file is unknown to the caller, it will not attempt to close the file
		// again.
		if err := c.Close(); err != nil {
			return err
		}

		return setAttrErr
	}

	// Avoid double close.
	if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) {
		return syscall.EBADF
	}

	// Send the message.
	if err := c.client.sendRecv(&Tsetattrclunk{FID: c.fid, Valid: valid, SetAttr: attr}, &Rsetattrclunk{}); err != nil {
		// If an error occurred, we toss away the FID. This isn't ideal,
		// but I'm not sure what else makes sense in this context.
		log.Warningf("Tsetattrclunk failed, losing FID %v: %v", c.fid, err)
		return err
	}

	// Return the FID to the pool.
	c.client.fidPool.Put(uint64(c.fid))
	return nil
}

// Open implements File.Open.
func (c *clientFile) Open(flags OpenFlags) (*fd.FD, QID, uint32, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, QID{}, 0, syscall.EBADF
	}

	rlopen := Rlopen{}
	if err := c.client.sendRecv(&Tlopen{FID: c.fid, Flags: flags}, &rlopen); err != nil {
		return nil, QID{}, 0, err
	}

	return rlopen.File, rlopen.QID, rlopen.IoUnit, nil
}

// Connect implements File.Connect.
func (c *clientFile) Connect(flags ConnectFlags) (*fd.FD, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, syscall.EBADF
	}

	if !VersionSupportsConnect(c.client.version) {
		return nil, syscall.ECONNREFUSED
	}

	rlconnect := Rlconnect{}
	if err := c.client.sendRecv(&Tlconnect{FID: c.fid, Flags: flags}, &rlconnect); err != nil {
		return nil, err
	}

	return rlconnect.File, nil
}

// chunk applies fn to p in chunkSize-sized chunks until fn returns a partial result, p is
// exhausted, or an error is encountered (which may be io.EOF).
func chunk(chunkSize uint32, fn func([]byte, uint64) (int, error), p []byte, offset uint64) (int, error) {
	// Some p9.Clients depend on executing fn on zero-byte buffers. Handle this
	// as a special case (normally it is fine to short-circuit and return (0, nil)).
	if len(p) == 0 {
		return fn(p, offset)
	}

	// total is the cumulative bytes processed.
	var total int
	for {
		var n int
		var err error

		// We're done, don't bother trying to do anything more.
		if total == len(p) {
			return total, nil
		}

		// Apply fn to a chunkSize-sized (or less) chunk of p.
		if len(p) < total+int(chunkSize) {
			n, err = fn(p[total:], offset)
		} else {
			n, err = fn(p[total:total+int(chunkSize)], offset)
		}
		total += n
		offset += uint64(n)

		// Return whatever we have processed if we encounter an error. This error
		// could be io.EOF.
		if err != nil {
			return total, err
		}

		// Did we get a partial result? If so, return it immediately.
		if n < int(chunkSize) {
			return total, nil
		}

		// If we received more bytes than we ever requested, this is a problem.
		if total > len(p) {
			panic(fmt.Sprintf("bytes completed (%d)) > requested (%d)", total, len(p)))
		}
	}
}

// ReadAt proxies File.ReadAt.
func (c *clientFile) ReadAt(p []byte, offset uint64) (int, error) {
	return chunk(c.client.payloadSize, c.readAt, p, offset)
}

func (c *clientFile) readAt(p []byte, offset uint64) (int, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return 0, syscall.EBADF
	}

	rread := Rread{Data: p}
	if err := c.client.sendRecv(&Tread{FID: c.fid, Offset: offset, Count: uint32(len(p))}, &rread); err != nil {
		return 0, err
	}

	// The message may have been truncated, or for some reason a new buffer
	// allocated. This isn't the common path, but we make sure that if the
	// payload has changed we copy it. See transport.go for more information.
	if len(p) > 0 && len(rread.Data) > 0 && &rread.Data[0] != &p[0] {
		copy(p, rread.Data)
	}

	// io.EOF is not an error that a p9 server can return. Use POSIX semantics to
	// return io.EOF manually: zero bytes were returned and a non-zero buffer was used.
	if len(rread.Data) == 0 && len(p) > 0 {
		return 0, io.EOF
	}

	return len(rread.Data), nil
}

// WriteAt proxies File.WriteAt.
func (c *clientFile) WriteAt(p []byte, offset uint64) (int, error) {
	return chunk(c.client.payloadSize, c.writeAt, p, offset)
}

func (c *clientFile) writeAt(p []byte, offset uint64) (int, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return 0, syscall.EBADF
	}

	rwrite := Rwrite{}
	if err := c.client.sendRecv(&Twrite{FID: c.fid, Offset: offset, Data: p}, &rwrite); err != nil {
		return 0, err
	}

	return int(rwrite.Count), nil
}

// ReadWriterFile wraps a File and implements io.ReadWriter, io.ReaderAt, and io.WriterAt.
type ReadWriterFile struct {
	File   File
	Offset uint64
}

// Read implements part of the io.ReadWriter interface.
func (r *ReadWriterFile) Read(p []byte) (int, error) {
	n, err := r.File.ReadAt(p, r.Offset)
	r.Offset += uint64(n)
	if err != nil {
		return n, err
	}
	if n == 0 && len(p) > 0 {
		return n, io.EOF
	}
	return n, nil
}

// ReadAt implements the io.ReaderAt interface.
func (r *ReadWriterFile) ReadAt(p []byte, offset int64) (int, error) {
	n, err := r.File.ReadAt(p, uint64(offset))
	if err != nil {
		return 0, err
	}
	if n == 0 && len(p) > 0 {
		return n, io.EOF
	}
	return n, nil
}

// Write implements part of the io.ReadWriter interface.
func (r *ReadWriterFile) Write(p []byte) (int, error) {
	n, err := r.File.WriteAt(p, r.Offset)
	r.Offset += uint64(n)
	if err != nil {
		return n, err
	}
	if n < len(p) {
		return n, io.ErrShortWrite
	}
	return n, nil
}

// WriteAt implements the io.WriteAt interface.
func (r *ReadWriterFile) WriteAt(p []byte, offset int64) (int, error) {
	n, err := r.File.WriteAt(p, uint64(offset))
	if err != nil {
		return n, err
	}
	if n < len(p) {
		return n, io.ErrShortWrite
	}
	return n, nil
}

// Rename implements File.Rename.
func (c *clientFile) Rename(dir File, name string) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	clientDir, ok := dir.(*clientFile)
	if !ok {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Trename{FID: c.fid, Directory: clientDir.fid, Name: name}, &Rrename{})
}

// Create implements File.Create.
func (c *clientFile) Create(name string, openFlags OpenFlags, permissions FileMode, uid UID, gid GID) (*fd.FD, File, QID, uint32, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, nil, QID{}, 0, syscall.EBADF
	}

	msg := Tlcreate{
		FID:         c.fid,
		Name:        name,
		OpenFlags:   openFlags,
		Permissions: permissions,
		GID:         NoGID,
	}

	if versionSupportsTucreation(c.client.version) {
		msg.GID = gid
		rucreate := Rucreate{}
		if err := c.client.sendRecv(&Tucreate{Tlcreate: msg, UID: uid}, &rucreate); err != nil {
			return nil, nil, QID{}, 0, err
		}
		return rucreate.File, c, rucreate.QID, rucreate.IoUnit, nil
	}

	rlcreate := Rlcreate{}
	if err := c.client.sendRecv(&msg, &rlcreate); err != nil {
		return nil, nil, QID{}, 0, err
	}

	return rlcreate.File, c, rlcreate.QID, rlcreate.IoUnit, nil
}

// Mkdir implements File.Mkdir.
func (c *clientFile) Mkdir(name string, permissions FileMode, uid UID, gid GID) (QID, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return QID{}, syscall.EBADF
	}

	msg := Tmkdir{
		Directory:   c.fid,
		Name:        name,
		Permissions: permissions,
		GID:         NoGID,
	}

	if versionSupportsTucreation(c.client.version) {
		msg.GID = gid
		rumkdir := Rumkdir{}
		if err := c.client.sendRecv(&Tumkdir{Tmkdir: msg, UID: uid}, &rumkdir); err != nil {
			return QID{}, err
		}
		return rumkdir.QID, nil
	}

	rmkdir := Rmkdir{}
	if err := c.client.sendRecv(&msg, &rmkdir); err != nil {
		return QID{}, err
	}

	return rmkdir.QID, nil
}

// Symlink implements File.Symlink.
func (c *clientFile) Symlink(oldname string, newname string, uid UID, gid GID) (QID, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return QID{}, syscall.EBADF
	}

	msg := Tsymlink{
		Directory: c.fid,
		Name:      newname,
		Target:    oldname,
		GID:       NoGID,
	}

	if versionSupportsTucreation(c.client.version) {
		msg.GID = gid
		rusymlink := Rusymlink{}
		if err := c.client.sendRecv(&Tusymlink{Tsymlink: msg, UID: uid}, &rusymlink); err != nil {
			return QID{}, err
		}
		return rusymlink.QID, nil
	}

	rsymlink := Rsymlink{}
	if err := c.client.sendRecv(&msg, &rsymlink); err != nil {
		return QID{}, err
	}

	return rsymlink.QID, nil
}

// Link implements File.Link.
func (c *clientFile) Link(target File, newname string) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	targetFile, ok := target.(*clientFile)
	if !ok {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Tlink{Directory: c.fid, Name: newname, Target: targetFile.fid}, &Rlink{})
}

// Mknod implements File.Mknod.
func (c *clientFile) Mknod(name string, mode FileMode, major uint32, minor uint32, uid UID, gid GID) (QID, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return QID{}, syscall.EBADF
	}

	msg := Tmknod{
		Directory: c.fid,
		Name:      name,
		Mode:      mode,
		Major:     major,
		Minor:     minor,
		GID:       NoGID,
	}

	if versionSupportsTucreation(c.client.version) {
		msg.GID = gid
		rumknod := Rumknod{}
		if err := c.client.sendRecv(&Tumknod{Tmknod: msg, UID: uid}, &rumknod); err != nil {
			return QID{}, err
		}
		return rumknod.QID, nil
	}

	rmknod := Rmknod{}
	if err := c.client.sendRecv(&msg, &rmknod); err != nil {
		return QID{}, err
	}

	return rmknod.QID, nil
}

// RenameAt implements File.RenameAt.
func (c *clientFile) RenameAt(oldname string, newdir File, newname string) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	clientNewDir, ok := newdir.(*clientFile)
	if !ok {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Trenameat{OldDirectory: c.fid, OldName: oldname, NewDirectory: clientNewDir.fid, NewName: newname}, &Rrenameat{})
}

// UnlinkAt implements File.UnlinkAt.
func (c *clientFile) UnlinkAt(name string, flags uint32) error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	return c.client.sendRecv(&Tunlinkat{Directory: c.fid, Name: name, Flags: flags}, &Runlinkat{})
}

// Readdir implements File.Readdir.
func (c *clientFile) Readdir(offset uint64, count uint32) ([]Dirent, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return nil, syscall.EBADF
	}

	rreaddir := Rreaddir{}
	if err := c.client.sendRecv(&Treaddir{Directory: c.fid, Offset: offset, Count: count}, &rreaddir); err != nil {
		return nil, err
	}

	return rreaddir.Entries, nil
}

// Readlink implements File.Readlink.
func (c *clientFile) Readlink() (string, error) {
	if atomic.LoadUint32(&c.closed) != 0 {
		return "", syscall.EBADF
	}

	rreadlink := Rreadlink{}
	if err := c.client.sendRecv(&Treadlink{FID: c.fid}, &rreadlink); err != nil {
		return "", err
	}

	return rreadlink.Target, nil
}

// Flush implements File.Flush.
func (c *clientFile) Flush() error {
	if atomic.LoadUint32(&c.closed) != 0 {
		return syscall.EBADF
	}

	if !VersionSupportsTflushf(c.client.version) {
		return nil
	}

	return c.client.sendRecv(&Tflushf{FID: c.fid}, &Rflushf{})
}