// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// A simulated Unix-like file system for use within NaCl.
//
// The simulation is not particularly tied to NaCl other than the reuse
// of NaCl's definition for the Stat_t structure.
//
// The file system need never be written to disk, so it is represented as
// in-memory Go data structures, never in a serialized form.
//
// TODO: Perhaps support symlinks, although they muck everything up.

package syscall

import (
	"io"
	"sync"
	"unsafe"
)

// Provided by package runtime.
func now() (sec int64, nsec int32)

// An fsys is a file system.
// Since there is no I/O (everything is in memory),
// the global lock mu protects the whole file system state,
// and that's okay.
type fsys struct {
	mu   sync.Mutex
	root *inode                    // root directory
	cwd  *inode                    // process current directory
	inum uint64                    // number of inodes created
	dev  []func() (devFile, error) // table for opening devices
}

// A devFile is the implementation required of device files
// like /dev/null or /dev/random.
type devFile interface {
	pread([]byte, int64) (int, error)
	pwrite([]byte, int64) (int, error)
}

// An inode is a (possibly special) file in the file system.
type inode struct {
	Stat_t
	data []byte
	dir  []dirent
}

// A dirent describes a single directory entry.
type dirent struct {
	name  string
	inode *inode
}

// An fsysFile is the fileImpl implementation backed by the file system.
type fsysFile struct {
	defaultFileImpl
	fsys     *fsys
	inode    *inode
	openmode int
	offset   int64
	dev      devFile
}

// newFsys creates a new file system.
func newFsys() *fsys {
	fs := &fsys{}
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip := fs.newInode()
	ip.Mode = 0555 | S_IFDIR
	fs.dirlink(ip, ".", ip)
	fs.dirlink(ip, "..", ip)
	fs.cwd = ip
	fs.root = ip
	return fs
}

var fs = newFsys()
var fsinit = func() {}

func init() {
	// do not trigger loading of zipped file system here
	oldFsinit := fsinit
	defer func() { fsinit = oldFsinit }()
	fsinit = func() {}
	Mkdir("/dev", 0555)
	Mkdir("/tmp", 0777)
	mkdev("/dev/null", 0666, openNull)
	mkdev("/dev/random", 0444, openRandom)
	mkdev("/dev/urandom", 0444, openRandom)
	mkdev("/dev/zero", 0666, openZero)
	chdirEnv()
}

func chdirEnv() {
	pwd, ok := Getenv("NACLPWD")
	if ok {
		chdir(pwd)
	}
}

// Except where indicated otherwise, unexported methods on fsys
// expect fs.mu to have been locked by the caller.

// newInode creates a new inode.
func (fs *fsys) newInode() *inode {
	fs.inum++
	ip := &inode{
		Stat_t: Stat_t{
			Ino:     fs.inum,
			Blksize: 512,
		},
	}
	return ip
}

// atime sets ip.Atime to the current time.
func (fs *fsys) atime(ip *inode) {
	sec, nsec := now()
	ip.Atime, ip.AtimeNsec = sec, int64(nsec)
}

// mtime sets ip.Mtime to the current time.
func (fs *fsys) mtime(ip *inode) {
	sec, nsec := now()
	ip.Mtime, ip.MtimeNsec = sec, int64(nsec)
}

// dirlookup looks for an entry in the directory dp with the given name.
// It returns the directory entry and its index within the directory.
func (fs *fsys) dirlookup(dp *inode, name string) (de *dirent, index int, err error) {
	fs.atime(dp)
	for i := range dp.dir {
		de := &dp.dir[i]
		if de.name == name {
			fs.atime(de.inode)
			return de, i, nil
		}
	}
	return nil, 0, ENOENT
}

// dirlink adds to the directory dp an entry for name pointing at the inode ip.
// If dp already contains an entry for name, that entry is overwritten.
func (fs *fsys) dirlink(dp *inode, name string, ip *inode) {
	fs.mtime(dp)
	fs.atime(ip)
	ip.Nlink++
	for i := range dp.dir {
		if dp.dir[i].name == name {
			dp.dir[i] = dirent{name, ip}
			return
		}
	}
	dp.dir = append(dp.dir, dirent{name, ip})
	dp.dirSize()
}

func (dp *inode) dirSize() {
	dp.Size = int64(len(dp.dir)) * (8 + 8 + 2 + 256) // Dirent
}

// skipelem splits path into the first element and the remainder.
// the returned first element contains no slashes, and the returned
// remainder does not begin with a slash.
func skipelem(path string) (elem, rest string) {
	for len(path) > 0 && path[0] == '/' {
		path = path[1:]
	}
	if len(path) == 0 {
		return "", ""
	}
	i := 0
	for i < len(path) && path[i] != '/' {
		i++
	}
	elem, path = path[:i], path[i:]
	for len(path) > 0 && path[0] == '/' {
		path = path[1:]
	}
	return elem, path
}

// namei translates a file system path name into an inode.
// If parent is false, the returned ip corresponds to the given name, and elem is the empty string.
// If parent is true, the walk stops at the next-to-last element in the name,
// so that ip is the parent directory and elem is the final element in the path.
func (fs *fsys) namei(path string, parent bool) (ip *inode, elem string, err error) {
	// Reject NUL in name.
	for i := 0; i < len(path); i++ {
		if path[i] == '\x00' {
			return nil, "", EINVAL
		}
	}

	// Reject empty name.
	if path == "" {
		return nil, "", EINVAL
	}

	if path[0] == '/' {
		ip = fs.root
	} else {
		ip = fs.cwd
	}

	for len(path) > 0 && path[len(path)-1] == '/' {
		path = path[:len(path)-1]
	}

	for {
		elem, rest := skipelem(path)
		if elem == "" {
			if parent && ip.Mode&S_IFMT == S_IFDIR {
				return ip, ".", nil
			}
			break
		}
		if ip.Mode&S_IFMT != S_IFDIR {
			return nil, "", ENOTDIR
		}
		if len(elem) >= 256 {
			return nil, "", ENAMETOOLONG
		}
		if parent && rest == "" {
			// Stop one level early.
			return ip, elem, nil
		}
		de, _, err := fs.dirlookup(ip, elem)
		if err != nil {
			return nil, "", err
		}
		ip = de.inode
		path = rest
	}
	if parent {
		return nil, "", ENOTDIR
	}
	return ip, "", nil
}

// open opens or creates a file with the given name, open mode,
// and permission mode bits.
func (fs *fsys) open(name string, openmode int, mode uint32) (fileImpl, error) {
	dp, elem, err := fs.namei(name, true)
	if err != nil {
		return nil, err
	}
	var (
		ip  *inode
		dev devFile
	)
	de, _, err := fs.dirlookup(dp, elem)
	if err != nil {
		if openmode&O_CREATE == 0 {
			return nil, err
		}
		ip = fs.newInode()
		ip.Mode = mode
		fs.dirlink(dp, elem, ip)
		if ip.Mode&S_IFMT == S_IFDIR {
			fs.dirlink(ip, ".", ip)
			fs.dirlink(ip, "..", dp)
		}
	} else {
		ip = de.inode
		if openmode&(O_CREATE|O_EXCL) == O_CREATE|O_EXCL {
			return nil, EEXIST
		}
		if openmode&O_TRUNC != 0 {
			if ip.Mode&S_IFMT == S_IFDIR {
				return nil, EISDIR
			}
			ip.data = nil
		}
		if ip.Mode&S_IFMT == S_IFCHR {
			if ip.Rdev < 0 || ip.Rdev >= int64(len(fs.dev)) || fs.dev[ip.Rdev] == nil {
				return nil, ENODEV
			}
			dev, err = fs.dev[ip.Rdev]()
			if err != nil {
				return nil, err
			}
		}
	}

	switch openmode & O_ACCMODE {
	case O_WRONLY, O_RDWR:
		if ip.Mode&S_IFMT == S_IFDIR {
			return nil, EISDIR
		}
	}

	switch ip.Mode & S_IFMT {
	case S_IFDIR:
		if openmode&O_ACCMODE != O_RDONLY {
			return nil, EISDIR
		}

	case S_IFREG:
		// ok

	case S_IFCHR:
		// handled above

	default:
		// TODO: some kind of special file
		return nil, EPERM
	}

	f := &fsysFile{
		fsys:     fs,
		inode:    ip,
		openmode: openmode,
		dev:      dev,
	}
	if openmode&O_APPEND != 0 {
		f.offset = ip.Size
	}
	return f, nil
}

// fsysFile methods to implement fileImpl.

func (f *fsysFile) stat(st *Stat_t) error {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	*st = f.inode.Stat_t
	return nil
}

func (f *fsysFile) read(b []byte) (int, error) {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	n, err := f.preadLocked(b, f.offset)
	f.offset += int64(n)
	return n, err
}

func ReadDirent(fd int, buf []byte) (int, error) {
	f, err := fdToFsysFile(fd)
	if err != nil {
		return 0, err
	}
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	if f.inode.Mode&S_IFMT != S_IFDIR {
		return 0, EINVAL
	}
	n, err := f.preadLocked(buf, f.offset)
	f.offset += int64(n)
	return n, err
}

func (f *fsysFile) write(b []byte) (int, error) {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	n, err := f.pwriteLocked(b, f.offset)
	f.offset += int64(n)
	return n, err
}

func (f *fsysFile) seek(offset int64, whence int) (int64, error) {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	switch whence {
	case io.SeekCurrent:
		offset += f.offset
	case io.SeekEnd:
		offset += f.inode.Size
	}
	if offset < 0 {
		return 0, EINVAL
	}
	if offset > f.inode.Size {
		return 0, EINVAL
	}
	f.offset = offset
	return offset, nil
}

func (f *fsysFile) pread(b []byte, offset int64) (int, error) {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	return f.preadLocked(b, offset)
}

func (f *fsysFile) pwrite(b []byte, offset int64) (int, error) {
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	return f.pwriteLocked(b, offset)
}

func (f *fsysFile) preadLocked(b []byte, offset int64) (int, error) {
	if f.openmode&O_ACCMODE == O_WRONLY {
		return 0, EINVAL
	}
	if offset < 0 {
		return 0, EINVAL
	}
	if f.dev != nil {
		f.fsys.atime(f.inode)
		f.fsys.mu.Unlock()
		defer f.fsys.mu.Lock()
		return f.dev.pread(b, offset)
	}
	if offset > f.inode.Size {
		return 0, nil
	}
	if int64(len(b)) > f.inode.Size-offset {
		b = b[:f.inode.Size-offset]
	}

	if f.inode.Mode&S_IFMT == S_IFDIR {
		if offset%direntSize != 0 || len(b) != 0 && len(b) < direntSize {
			return 0, EINVAL
		}
		fs.atime(f.inode)
		n := 0
		for len(b) >= direntSize {
			src := f.inode.dir[int(offset/direntSize)]
			dst := (*Dirent)(unsafe.Pointer(&b[0]))
			dst.Ino = int64(src.inode.Ino)
			dst.Off = offset
			dst.Reclen = direntSize
			for i := range dst.Name {
				dst.Name[i] = 0
			}
			copy(dst.Name[:], src.name)
			n += direntSize
			offset += direntSize
			b = b[direntSize:]
		}
		return n, nil
	}

	fs.atime(f.inode)
	n := copy(b, f.inode.data[offset:])
	return n, nil
}

func (f *fsysFile) pwriteLocked(b []byte, offset int64) (int, error) {
	if f.openmode&O_ACCMODE == O_RDONLY {
		return 0, EINVAL
	}
	if offset < 0 {
		return 0, EINVAL
	}
	if f.dev != nil {
		f.fsys.atime(f.inode)
		f.fsys.mu.Unlock()
		defer f.fsys.mu.Lock()
		return f.dev.pwrite(b, offset)
	}
	if offset > f.inode.Size {
		return 0, EINVAL
	}
	f.fsys.mtime(f.inode)
	n := copy(f.inode.data[offset:], b)
	if n < len(b) {
		f.inode.data = append(f.inode.data, b[n:]...)
		f.inode.Size = int64(len(f.inode.data))
	}
	return len(b), nil
}

// Standard Unix system calls.

func Open(path string, openmode int, perm uint32) (fd int, err error) {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	f, err := fs.open(path, openmode, perm&0777|S_IFREG)
	if err != nil {
		return -1, err
	}
	return newFD(f), nil
}

func Mkdir(path string, perm uint32) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	_, err := fs.open(path, O_CREATE|O_EXCL, perm&0777|S_IFDIR)
	return err
}

func Getcwd(buf []byte) (n int, err error) {
	// Force package os to default to the old algorithm using .. and directory reads.
	return 0, ENOSYS
}

func Stat(path string, st *Stat_t) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	*st = ip.Stat_t
	return nil
}

func Lstat(path string, st *Stat_t) error {
	return Stat(path, st)
}

func unlink(path string, isdir bool) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	dp, elem, err := fs.namei(path, true)
	if err != nil {
		return err
	}
	if elem == "." || elem == ".." {
		return EINVAL
	}
	de, _, err := fs.dirlookup(dp, elem)
	if err != nil {
		return err
	}
	if isdir {
		if de.inode.Mode&S_IFMT != S_IFDIR {
			return ENOTDIR
		}
		if len(de.inode.dir) != 2 {
			return ENOTEMPTY
		}
	} else {
		if de.inode.Mode&S_IFMT == S_IFDIR {
			return EISDIR
		}
	}
	de.inode.Nlink--
	*de = dp.dir[len(dp.dir)-1]
	dp.dir = dp.dir[:len(dp.dir)-1]
	dp.dirSize()
	return nil
}

func Unlink(path string) error {
	return unlink(path, false)
}

func Rmdir(path string) error {
	return unlink(path, true)
}

func Chmod(path string, mode uint32) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	ip.Mode = ip.Mode&^0777 | mode&0777
	return nil
}

func Fchmod(fd int, mode uint32) error {
	f, err := fdToFsysFile(fd)
	if err != nil {
		return err
	}
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	f.inode.Mode = f.inode.Mode&^0777 | mode&0777
	return nil
}

func Chown(path string, uid, gid int) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	if uid != -1 {
		ip.Uid = uint32(uid)
	}
	if gid != -1 {
		ip.Gid = uint32(gid)
	}
	return nil
}

func Fchown(fd int, uid, gid int) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	f, err := fdToFsysFile(fd)
	if err != nil {
		return err
	}
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	f.inode.Uid = uint32(uid)
	f.inode.Gid = uint32(gid)
	return nil
}

func Lchown(path string, uid, gid int) error {
	return Chown(path, uid, gid)
}

func UtimesNano(path string, ts []Timespec) error {
	if len(ts) != 2 {
		return EINVAL
	}
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	ip.Atime = ts[0].Sec
	ip.AtimeNsec = int64(ts[0].Nsec)
	ip.Mtime = ts[1].Sec
	ip.MtimeNsec = int64(ts[1].Nsec)
	return nil
}

func Link(path, link string) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	dp, elem, err := fs.namei(link, true)
	if err != nil {
		return err
	}
	if ip.Mode&S_IFMT == S_IFDIR {
		return EPERM
	}
	_, _, err = fs.dirlookup(dp, elem)
	if err == nil {
		return EEXIST
	}
	fs.dirlink(dp, elem, ip)
	return nil
}

func Rename(from, to string) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	fdp, felem, err := fs.namei(from, true)
	if err != nil {
		return err
	}
	fde, _, err := fs.dirlookup(fdp, felem)
	if err != nil {
		return err
	}
	tdp, telem, err := fs.namei(to, true)
	if err != nil {
		return err
	}
	fs.dirlink(tdp, telem, fde.inode)
	fde.inode.Nlink--
	*fde = fdp.dir[len(fdp.dir)-1]
	fdp.dir = fdp.dir[:len(fdp.dir)-1]
	fdp.dirSize()
	return nil
}

func (fs *fsys) truncate(ip *inode, length int64) error {
	if length > 1e9 || ip.Mode&S_IFMT != S_IFREG {
		return EINVAL
	}
	if length < int64(len(ip.data)) {
		ip.data = ip.data[:length]
	} else {
		data := make([]byte, length)
		copy(data, ip.data)
		ip.data = data
	}
	ip.Size = int64(len(ip.data))
	return nil
}

func Truncate(path string, length int64) error {
	fsinit()
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	return fs.truncate(ip, length)
}

func Ftruncate(fd int, length int64) error {
	f, err := fdToFsysFile(fd)
	if err != nil {
		return err
	}
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	return f.fsys.truncate(f.inode, length)
}

func Chdir(path string) error {
	fsinit()
	return chdir(path)
}

func chdir(path string) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	ip, _, err := fs.namei(path, false)
	if err != nil {
		return err
	}
	fs.cwd = ip
	return nil
}

func Fchdir(fd int) error {
	f, err := fdToFsysFile(fd)
	if err != nil {
		return err
	}
	f.fsys.mu.Lock()
	defer f.fsys.mu.Unlock()
	if f.inode.Mode&S_IFMT != S_IFDIR {
		return ENOTDIR
	}
	fs.cwd = f.inode
	return nil
}

func Readlink(path string, buf []byte) (n int, err error) {
	return 0, ENOSYS
}

func Symlink(path, link string) error {
	return ENOSYS
}

func Fsync(fd int) error {
	return nil
}

// Special devices.

func mkdev(path string, mode uint32, open func() (devFile, error)) error {
	f, err := fs.open(path, O_CREATE|O_RDONLY|O_EXCL, S_IFCHR|mode)
	if err != nil {
		return err
	}
	ip := f.(*fsysFile).inode
	ip.Rdev = int64(len(fs.dev))
	fs.dev = append(fs.dev, open)
	return nil
}

type nullFile struct{}

func openNull() (devFile, error)                               { return &nullFile{}, nil }
func (f *nullFile) close() error                               { return nil }
func (f *nullFile) pread(b []byte, offset int64) (int, error)  { return 0, nil }
func (f *nullFile) pwrite(b []byte, offset int64) (int, error) { return len(b), nil }

type zeroFile struct{}

func openZero() (devFile, error)                               { return &zeroFile{}, nil }
func (f *zeroFile) close() error                               { return nil }
func (f *zeroFile) pwrite(b []byte, offset int64) (int, error) { return len(b), nil }

func (f *zeroFile) pread(b []byte, offset int64) (int, error) {
	for i := range b {
		b[i] = 0
	}
	return len(b), nil
}

type randomFile struct{}

func openRandom() (devFile, error) {
	return randomFile{}, nil
}

func (f randomFile) close() error {
	return nil
}

func (f randomFile) pread(b []byte, offset int64) (int, error) {
	if err := naclGetRandomBytes(b); err != nil {
		return 0, err
	}
	return len(b), nil
}

func (f randomFile) pwrite(b []byte, offset int64) (int, error) {
	return 0, EPERM
}

func fdToFsysFile(fd int) (*fsysFile, error) {
	f, err := fdToFile(fd)
	if err != nil {
		return nil, err
	}
	impl := f.impl
	fsysf, ok := impl.(*fsysFile)
	if !ok {
		return nil, EINVAL
	}
	return fsysf, nil
}

// create creates a file in the file system with the given name, mode, time, and data.
// It is meant to be called when initializing the file system image.
func create(name string, mode uint32, sec int64, data []byte) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	f, err := fs.open(name, O_CREATE|O_EXCL, mode)
	if err != nil {
		if mode&S_IFMT == S_IFDIR {
			ip, _, err := fs.namei(name, false)
			if err == nil && (ip.Mode&S_IFMT) == S_IFDIR {
				return nil // directory already exists
			}
		}
		return err
	}
	ip := f.(*fsysFile).inode
	ip.Atime = sec
	ip.Mtime = sec
	ip.Ctime = sec
	if len(data) > 0 {
		ip.Size = int64(len(data))
		ip.data = data
	}
	return nil
}
