// Portions copyright 2009 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.

// This file was originally copied from Go, see:
// https://github.com/golang/go/blob/master/src/os/file.go
//
// Some of the code inherited from Go is not used anymore in Tinygo, but we keep
// changes to a minimum to help simplify bringing changes (e.g. the lstat global
// is not used here anymore, but we might need it if we add tests from Go in
// this package).

// Package os implements a subset of the Go "os" package. See
// https://godoc.org/os for details.
//
// Note that the current implementation is blocking. This limitation should be
// removed in a future version.
package os

import (
	"errors"
	"io"
	"io/fs"
	"runtime"
	"syscall"
	"time"
)

// Seek whence values.
//
// Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
	SEEK_SET int = io.SeekStart
	SEEK_CUR int = io.SeekCurrent
	SEEK_END int = io.SeekEnd
)

// lstat is overridden in tests.
var lstat = Lstat

// Mkdir creates a directory. If the operation fails, it will return an error of
// type *PathError.
func Mkdir(path string, perm FileMode) error {
	fs, suffix := findMount(path)
	if fs == nil {
		return &PathError{Op: "mkdir", Path: path, Err: ErrNotExist}
	}
	err := fs.Mkdir(suffix, perm)
	if err != nil {
		return &PathError{Op: "mkdir", Path: path, Err: err}
	}
	return nil
}

// Many functions in package syscall return a count of -1 instead of 0.
// Using fixCount(call()) instead of call() corrects the count.
func fixCount(n int, err error) (int, error) {
	if n < 0 {
		n = 0
	}
	return n, err
}

// Remove removes a file or (empty) directory. If the operation fails, it will
// return an error of type *PathError.
func Remove(path string) error {
	fs, suffix := findMount(path)
	if fs == nil {
		return &PathError{Op: "remove", Path: path, Err: ErrNotExist}
	}
	err := fs.Remove(suffix)
	if err != nil {
		return err
	}
	return nil
}

// Name returns the name of the file with which it was opened.
func (f *File) Name() string {
	return f.name
}

// OpenFile opens the named file. If the operation fails, the returned error
// will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	fs, suffix := findMount(name)
	if fs == nil {
		return nil, &PathError{Op: "open", Path: name, Err: ErrNotExist}
	}
	handle, err := fs.OpenFile(suffix, flag, perm)
	if err != nil {
		return nil, &PathError{Op: "open", Path: name, Err: err}
	}
	f := NewFile(handle, name)
	f.appendMode = (flag & O_APPEND) != 0
	return f, nil
}

// Open opens the file named for reading.
func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

// Create creates the named file, overwriting it if it already exists.
func Create(name string) (*File, error) {
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

// Read reads up to len(b) bytes from the File. It returns the number of bytes
// read and any error encountered. At end of file, Read returns 0, io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		n, err = f.handle.Read(b)
	}
	// TODO: want to always wrap, like upstream, but ReadFile() compares against exactly io.EOF?
	if err != nil && err != io.EOF {
		err = &PathError{Op: "read", Path: f.name, Err: err}
	}
	return
}

var errNegativeOffset = errors.New("negative offset")

// ReadAt reads up to len(b) bytes from the File at the given absolute offset.
// It returns the number of bytes read and any error encountered, possible io.EOF.
// At end of file, Read returns 0, io.EOF.
func (f *File) ReadAt(b []byte, offset int64) (n int, err error) {
	if offset < 0 {
		return 0, &PathError{Op: "readat", Path: f.name, Err: errNegativeOffset}
	}
	if f.handle == nil {
		return 0, &PathError{Op: "readat", Path: f.name, Err: ErrClosed}
	}

	for len(b) > 0 {
		m, e := f.handle.ReadAt(b, offset)
		if e != nil {
			// TODO: want to always wrap, like upstream, but TestReadAtEOF compares against exactly io.EOF?
			if e != io.EOF {
				err = &PathError{Op: "readat", Path: f.name, Err: e}
			} else {
				err = e
			}
			break
		}
		n += m
		b = b[m:]
		offset += int64(m)
	}

	return
}

// Write writes len(b) bytes to the File. It returns the number of bytes written
// and an error, if any. Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		n, err = f.handle.Write(b)
	}
	if err != nil {
		err = &PathError{Op: "write", Path: f.name, Err: err}
	}
	return
}

// WriteString is like Write, but writes the contents of string s rather than a
// slice of bytes.
func (f *File) WriteString(s string) (n int, err error) {
	return f.Write([]byte(s))
}

var errWriteAtInAppendMode = errors.New("os: invalid use of WriteAt on file opened with O_APPEND")

// WriteAt writes len(b) bytes to the File starting at byte offset off.
// It returns the number of bytes written and an error, if any.
// WriteAt returns a non-nil error when n != len(b).
//
// If file was opened with the O_APPEND flag, WriteAt returns an error.
func (f *File) WriteAt(b []byte, offset int64) (n int, err error) {
	switch {
	case offset < 0:
		return 0, &PathError{Op: "writeat", Path: f.name, Err: errNegativeOffset}
	case f.handle == nil:
		return 0, &PathError{Op: "writeat", Path: f.name, Err: ErrClosed}
	case f.appendMode:
		// Go does not wrap this error but it would be more consistent
		// if it did.
		return 0, errWriteAtInAppendMode
	}
	for len(b) > 0 {
		m, e := f.handle.WriteAt(b, offset)
		if e != nil {
			err = &PathError{Op: "writeat", Path: f.name, Err: e}
			break
		}
		n += m
		b = b[m:]
		offset += int64(m)
	}
	return
}

// Close closes the File, rendering it unusable for I/O.
func (f *File) Close() (err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		// Some platforms manage extra state other than the system handle which
		// needs to be released when the file is closed. For example, darwin
		// files have a DIR object holding a dup of the file descriptor, and
		// linux files hold a buffer which needs to be released to a pool.
		//
		// These platform-specific logic is provided by the (*file).close method
		// which is why we do not call the handle's Close method directly.
		err = f.file.close()
		if err == nil {
			f.handle = nil
		}
	}
	if err != nil {
		err = &PathError{Op: "close", Path: f.name, Err: err}
	}
	return
}

// Seek sets the offset for the next Read or Write on file to offset, interpreted
// according to whence: 0 means relative to the origin of the file, 1 means
// relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
// The behavior of Seek on a file opened with O_APPEND is not specified.
//
// If f is a directory, the behavior of Seek varies by operating
// system; you can seek to the beginning of the directory on Unix-like
// operating systems, but not on Windows.
func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		ret, err = f.handle.Seek(offset, whence)
	}
	if err != nil {
		err = &PathError{Op: "seek", Path: f.name, Err: err}
	}
	return
}

func (f *File) SyscallConn() (conn syscall.RawConn, err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		err = ErrNotImplemented
	}
	return
}

// SetDeadline sets the read and write deadlines for a File.
// Calls to SetDeadline for files that do not support deadlines will return ErrNoDeadline
// This stub always returns ErrNoDeadline.
// A zero value for t means I/O operations will not time out.
func (f *File) SetDeadline(t time.Time) error {
	if f.handle == nil {
		return ErrClosed
	}
	return f.setDeadline(t)
}

// SetReadDeadline sets the deadline for future Read calls and any
// currently-blocked Read call.
func (f *File) SetReadDeadline(t time.Time) error {
	return f.setReadDeadline(t)
}

// SetWriteDeadline sets the deadline for any future Write calls and any
// currently-blocked Write call.
func (f *File) SetWriteDeadline(t time.Time) error {
	return f.setWriteDeadline(t)
}

// fd is an internal interface that is used to try a type assertion in order to
// call the Fd() method of the underlying file handle if it is implemented.
type fd interface {
	Fd() uintptr
}

// Fd returns the file handle referencing the open file.
func (f *File) Fd() uintptr {
	handle, ok := f.handle.(fd)
	if ok {
		return handle.Fd()
	}
	return ^uintptr(0)
}

// Sync commits the current contents of the file to stable storage.
// Typically, this means flushing the file system's in-memory copy of recently
// written data to disk.
func (f *File) Sync() (err error) {
	if f.handle == nil {
		err = ErrClosed
	} else {
		err = f.handle.Sync()
	}
	return
}

// LinkError records an error during a link or symlink or rename system call and
// the paths that caused it.
type LinkError struct {
	Op  string
	Old string
	New string
	Err error
}

func (e *LinkError) Error() string {
	return e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error()
}

func (e *LinkError) Unwrap() error {
	return e.Err
}

// OpenFile flag values.
const (
	O_RDONLY int = syscall.O_RDONLY
	O_WRONLY int = syscall.O_WRONLY
	O_RDWR   int = syscall.O_RDWR
	O_APPEND int = syscall.O_APPEND
	O_CREATE int = syscall.O_CREAT
	O_EXCL   int = syscall.O_EXCL
	O_SYNC   int = syscall.O_SYNC
	O_TRUNC  int = syscall.O_TRUNC
)

func Getwd() (string, error) {
	return syscall.Getwd()
}

// TempDir returns the default directory to use for temporary files.
//
// On Unix systems, it returns $TMPDIR if non-empty, else /tmp.
// On Windows, it uses GetTempPath, returning the first non-empty
// value from %TMP%, %TEMP%, %USERPROFILE%, or the Windows directory.
//
// The directory is neither guaranteed to exist nor have accessible
// permissions.
func TempDir() string {
	return tempDir()
}

// UserHomeDir returns the current user's home directory.
//
// On Unix, including macOS, it returns the $HOME environment variable.
// On Windows, it returns %USERPROFILE%.
// On Plan 9, it returns the $home environment variable.
func UserHomeDir() (string, error) {
	env, enverr := "HOME", "$HOME"
	switch runtime.GOOS {
	case "windows":
		env, enverr = "USERPROFILE", "%userprofile%"
	case "plan9":
		env, enverr = "home", "$home"
	}
	if v := Getenv(env); v != "" {
		return v, nil
	}
	// On some geese the home directory is not always defined.
	switch runtime.GOOS {
	case "android":
		return "/sdcard", nil
	case "ios":
		return "/", nil
	}
	return "", errors.New(enverr + " is not defined")
}

type (
	FileMode = fs.FileMode
	FileInfo = fs.FileInfo
)

// The followings are copied from Go 1.16 or 1.17 official implementation:
// https://github.com/golang/go/blob/go1.16/src/os/file.go

// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir.
//
// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the
// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the
// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside
// the /prefix tree, then using DirFS does not stop the access any more than using
// os.Open does. DirFS is therefore not a general substitute for a chroot-style security
// mechanism when the directory tree contains arbitrary content.
func DirFS(dir string) fs.FS {
	return dirFS(dir)
}

func containsAny(s, chars string) bool {
	for i := 0; i < len(s); i++ {
		for j := 0; j < len(chars); j++ {
			if s[i] == chars[j] {
				return true
			}
		}
	}
	return false
}

type dirFS string

func (dir dirFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
	}
	f, err := Open(string(dir) + "/" + name)
	if err != nil {
		return nil, err // nil fs.File
	}
	return f, nil
}

func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
	if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
		return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
	}
	f, err := Stat(string(dir) + "/" + name)
	if err != nil {
		return nil, err
	}
	return f, nil
}

// ReadFile reads the named file and returns the contents.
// A successful call returns err == nil, not err == EOF.
// Because ReadFile reads the whole file, it does not treat an EOF from Read
// as an error to be reported.
func ReadFile(name string) ([]byte, error) {
	f, err := Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var size int
	if info, err := f.Stat(); err == nil {
		size64 := info.Size()
		if int64(int(size64)) == size64 {
			size = int(size64)
		}
	}
	size++ // one byte for final read at EOF

	// If a file claims a small size, read at least 512 bytes.
	// In particular, files in Linux's /proc claim size 0 but
	// then do not work right if read in small pieces,
	// so an initial read of 1 byte would not work correctly.
	if size < 512 {
		size = 512
	}

	data := make([]byte, 0, size)
	for {
		if len(data) >= cap(data) {
			d := append(data[:cap(data)], 0)
			data = d[:len(data)]
		}
		n, err := f.Read(data[len(data):cap(data)])
		data = data[:len(data)+n]
		if err != nil {
			if err == io.EOF {
				err = nil
			}
			return data, err
		}
	}
}

// WriteFile writes data to the named file, creating it if necessary.
// If the file does not exist, WriteFile creates it with permissions perm (before umask);
// otherwise WriteFile truncates it before writing, without changing permissions.
func WriteFile(name string, data []byte, perm FileMode) error {
	f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm)
	if err != nil {
		return err
	}
	_, err = f.Write(data)
	if err1 := f.Close(); err1 != nil && err == nil {
		err = err1
	}
	return err
}

// The defined file mode bits are the most significant bits of the FileMode.
// The nine least-significant bits are the standard Unix rwxrwxrwx permissions.
// The values of these bits should be considered part of the public API and
// may be used in wire protocols or disk representations: they must not be
// changed, although new bits might be added.
const (
	// The single letters are the abbreviations
	// used by the String method's formatting.
	ModeDir        = fs.ModeDir        // d: is a directory
	ModeAppend     = fs.ModeAppend     // a: append-only
	ModeExclusive  = fs.ModeExclusive  // l: exclusive use
	ModeTemporary  = fs.ModeTemporary  // T: temporary file; Plan 9 only
	ModeSymlink    = fs.ModeSymlink    // L: symbolic link
	ModeDevice     = fs.ModeDevice     // D: device file
	ModeNamedPipe  = fs.ModeNamedPipe  // p: named pipe (FIFO)
	ModeSocket     = fs.ModeSocket     // S: Unix domain socket
	ModeSetuid     = fs.ModeSetuid     // u: setuid
	ModeSetgid     = fs.ModeSetgid     // g: setgid
	ModeCharDevice = fs.ModeCharDevice // c: Unix character device, when ModeDevice is set
	ModeSticky     = fs.ModeSticky     // t: sticky
	ModeIrregular  = fs.ModeIrregular  // ?: non-regular file; nothing else is known about this file

	// Mask for the type bits. For regular files, none will be set.
	ModeType = fs.ModeType

	ModePerm = fs.ModePerm // Unix permission bits, 0o777
)
