// Package memfs provides a billy filesystem base on memory.
package memfs // import "github.com/go-git/go-billy/v6/memfs"

import (
	"errors"
	"fmt"
	gofs "io/fs"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"syscall"

	"github.com/go-git/go-billy/v6"
	"github.com/go-git/go-billy/v6/helper/chroot"
	"github.com/go-git/go-billy/v6/util"
)

const (
	separator       = filepath.Separator
	defaultUmask    = 0o022
	defaultDirMode  = 0o777
	defaultFileMode = 0o666
)

// Memory a very convenient filesystem based on memory files.
type Memory struct {
	s     *storage
	umask uint32
}

// New returns a new Memory filesystem.
func New(opts ...Option) billy.Filesystem {
	o := &options{}
	// Aligns default umask with general Windows behaviour.
	if runtime.GOOS != "windows" {
		o.umask = defaultUmask
	}

	for _, opt := range opts {
		opt(o)
	}

	fs := &Memory{
		s:     newStorage(),
		umask: o.umask,
	}
	_, err := fs.s.New("/", fs.applyUmask(defaultDirMode)|os.ModeDir, 0)
	if err != nil {
		log.Printf("failed to create root dir: %v", err)
	}
	return chroot.New(fs, string(separator))
}

func (fs *Memory) Create(filename string) (billy.File, error) {
	return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultFileMode)
}

func (fs *Memory) Open(filename string) (billy.File, error) {
	return fs.OpenFile(filename, os.O_RDONLY, 0)
}

func (fs *Memory) OpenFile(filename string, flag int, perm gofs.FileMode) (billy.File, error) {
	f, has := fs.s.Get(filename)
	if !has {
		if !isCreate(flag) {
			return nil, os.ErrNotExist
		}

		var err error
		f, err = fs.s.New(filename, fs.applyUmask(perm), flag)
		if err != nil {
			return nil, err
		}
	} else {
		if isExclusive(flag) {
			return nil, os.ErrExist
		}

		if target, isLink := fs.resolveLink(filename, f); isLink {
			if target != filename {
				return fs.OpenFile(target, flag, perm)
			}
		}
	}

	if f.mode.IsDir() {
		return nil, fmt.Errorf("cannot open directory: %s", filename)
	}

	return f.Duplicate(filename, perm, flag), nil
}

func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
	if !isSymlink(f.mode) {
		return fullpath, false
	}

	target = string(f.content.bytes)
	if !isAbs(target) {
		target = fs.Join(filepath.Dir(fullpath), target)
	}

	return target, true
}

// On Windows OS, IsAbs validates if a path is valid based on if stars with a
// unit (eg.: `C:\`)  to assert that is absolute, but in this mem implementation
// any path starting by `separator` is also considered absolute.
func isAbs(path string) bool {
	return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
}

func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
	f, has := fs.s.Get(filename)
	if !has {
		return nil, os.ErrNotExist
	}

	fi, _ := f.Stat()

	var err error
	if target, isLink := fs.resolveLink(filename, f); isLink {
		fi, err = fs.Stat(target)
		if err != nil {
			return nil, err
		}
	}

	// the name of the file should always the name of the stated file, so we
	// overwrite the Stat returned from the storage with it, since the
	// filename may belong to a link.
	if ffi, ok := fi.(*fileInfo); ok {
		ffi.name = filepath.Base(filename)
	}
	return fi, nil
}

func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
	f, has := fs.s.Get(filename)
	if !has {
		return nil, os.ErrNotExist
	}

	return f.Stat()
}

type ByName []gofs.DirEntry

func (a ByName) Len() int           { return len(a) }
func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
func (a ByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

func (fs *Memory) ReadDir(path string) ([]gofs.DirEntry, error) {
	if f, has := fs.s.Get(path); has {
		if target, isLink := fs.resolveLink(path, f); isLink {
			if target != path {
				return fs.ReadDir(target)
			}
		}
	} else {
		return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
	}

	var entries []gofs.DirEntry
	for _, f := range fs.s.Children(path) {
		fi, _ := f.Stat()
		entries = append(entries, gofs.FileInfoToDirEntry(fi))
	}

	sort.Sort(ByName(entries))

	return entries, nil
}

func (fs *Memory) MkdirAll(path string, perm gofs.FileMode) error {
	_, err := fs.s.New(path, fs.applyUmask(perm)|os.ModeDir, 0)
	return err
}

func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
	return util.TempFile(fs, dir, prefix)
}

func (fs *Memory) Rename(from, to string) error {
	return fs.s.Rename(from, to)
}

func (fs *Memory) Remove(filename string) error {
	return fs.s.Remove(filename)
}

func (fs *Memory) Chmod(path string, mode gofs.FileMode) error {
	return fs.s.Chmod(path, mode)
}

// Falls back to Go's filepath.Join, which works differently depending on the
// OS where the code is being executed.
func (fs *Memory) Join(elem ...string) string {
	return filepath.Join(elem...)
}

func (fs *Memory) Symlink(target, link string) error {
	_, err := fs.Lstat(link)
	if err == nil {
		return os.ErrExist
	}

	if !errors.Is(err, os.ErrNotExist) {
		return err
	}

	return util.WriteFile(fs, link, []byte(target), 0o777|os.ModeSymlink)
}

func (fs *Memory) Readlink(link string) (string, error) {
	f, has := fs.s.Get(link)
	if !has {
		return "", os.ErrNotExist
	}

	if !isSymlink(f.mode) {
		return "", &os.PathError{
			Op:   "readlink",
			Path: link,
			Err:  fmt.Errorf("not a symlink"),
		}
	}

	return string(f.content.bytes), nil
}

// Capabilities implements the Capable interface.
func (fs *Memory) Capabilities() billy.Capability {
	return billy.WriteCapability |
		billy.ReadCapability |
		billy.ReadAndWriteCapability |
		billy.SeekCapability |
		billy.TruncateCapability
}

// applyUmask applies the filesystem's umask to a mode by clearing the bits
// specified in the umask. For example, with umask 0o022, the mode 0o666
// becomes 0o644 (rw-r--r--).
func (fs *Memory) applyUmask(mode gofs.FileMode) gofs.FileMode {
	return mode &^ gofs.FileMode(fs.umask)
}

func (c *content) Truncate() {
	c.bytes = make([]byte, 0)
}

func (c *content) Len() int {
	return len(c.bytes)
}

func isCreate(flag int) bool {
	return flag&os.O_CREATE != 0
}

func isExclusive(flag int) bool {
	return flag&os.O_EXCL != 0
}

func isAppend(flag int) bool {
	return flag&os.O_APPEND != 0
}

func isTruncate(flag int) bool {
	return flag&os.O_TRUNC != 0
}

func isReadAndWrite(flag int) bool {
	return flag&os.O_RDWR != 0
}

func isReadOnly(flag int) bool {
	return flag == os.O_RDONLY
}

func isWriteOnly(flag int) bool {
	return flag&os.O_WRONLY != 0
}

func isSymlink(m gofs.FileMode) bool {
	return m&os.ModeSymlink != 0
}
