// Copyright 2017 Google Inc. All rights reserved.
//
// 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 fs

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/user"
	"path/filepath"
	"sync"
	"time"
)

var OsFs FileSystem = osFs{}

func NewMockFs(files map[string][]byte) *MockFs {
	workDir := "/cwd"
	fs := &MockFs{
		Clock:   NewClock(time.Unix(2, 2)),
		workDir: workDir,
	}
	fs.root = *fs.newDir()
	fs.MkDirs(workDir)

	for path, bytes := range files {
		dir := filepath.Dir(path)
		fs.MkDirs(dir)
		fs.WriteFile(path, bytes, 0777)
	}

	return fs
}

type FileSystem interface {
	// getting information about files
	Open(name string) (file io.ReadCloser, err error)
	Lstat(path string) (stats os.FileInfo, err error)
	ReadDir(path string) (contents []DirEntryInfo, err error)

	InodeNumber(info os.FileInfo) (number uint64, err error)
	DeviceNumber(info os.FileInfo) (number uint64, err error)
	PermTime(info os.FileInfo) (time time.Time, err error)

	// changing contents of the filesystem
	Rename(oldPath string, newPath string) (err error)
	WriteFile(path string, data []byte, perm os.FileMode) (err error)
	Remove(path string) (err error)
	RemoveAll(path string) (err error)

	// metadata about the filesystem
	ViewId() (id string) // Some unique id of the user accessing the filesystem
}

// DentryInfo is a subset of the functionality available through os.FileInfo that might be able
// to be gleaned through only a syscall.Getdents without requiring a syscall.Lstat of every file.
type DirEntryInfo interface {
	Name() string
	Mode() os.FileMode // the file type encoded as an os.FileMode
	IsDir() bool
}

type dirEntryInfo struct {
	name       string
	mode       os.FileMode
	modeExists bool
}

var _ DirEntryInfo = os.FileInfo(nil)

func (d *dirEntryInfo) Name() string      { return d.name }
func (d *dirEntryInfo) Mode() os.FileMode { return d.mode }
func (d *dirEntryInfo) IsDir() bool       { return d.mode.IsDir() }
func (d *dirEntryInfo) String() string    { return d.name + ": " + d.mode.String() }

// osFs implements FileSystem using the local disk.
type osFs struct{}

var _ FileSystem = (*osFs)(nil)

func (osFs) Open(name string) (io.ReadCloser, error) { return os.Open(name) }

func (osFs) Lstat(path string) (stats os.FileInfo, err error) {
	return os.Lstat(path)
}

func (osFs) ReadDir(path string) (contents []DirEntryInfo, err error) {
	entries, err := readdir(path)
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		contents = append(contents, entry)
	}

	return contents, nil
}

func (osFs) Rename(oldPath string, newPath string) error {
	return os.Rename(oldPath, newPath)
}

func (osFs) WriteFile(path string, data []byte, perm os.FileMode) error {
	return ioutil.WriteFile(path, data, perm)
}

func (osFs) Remove(path string) error {
	return os.Remove(path)
}

func (osFs) RemoveAll(path string) error {
	return os.RemoveAll(path)
}

func (osFs) ViewId() (id string) {
	user, err := user.Current()
	if err != nil {
		return ""
	}
	username := user.Username

	hostname, err := os.Hostname()
	if err != nil {
		return ""
	}

	return username + "@" + hostname
}

type Clock struct {
	time time.Time
}

func NewClock(startTime time.Time) *Clock {
	return &Clock{time: startTime}

}

func (c *Clock) Tick() {
	c.time = c.time.Add(time.Microsecond)
}

func (c *Clock) Time() time.Time {
	return c.time
}

// given "/a/b/c/d", pathSplit returns ("/a/b/c", "d")
func pathSplit(path string) (dir string, leaf string) {
	dir, leaf = filepath.Split(path)
	if dir != "/" && len(dir) > 0 {
		dir = dir[:len(dir)-1]
	}
	return dir, leaf
}

// MockFs supports singlethreaded writes and multithreaded reads
type MockFs struct {
	// configuration
	viewId       string //
	deviceNumber uint64

	// implementation
	root            mockDir
	Clock           *Clock
	workDir         string
	nextInodeNumber uint64

	// history of requests, for tests to check
	StatCalls      []string
	ReadDirCalls   []string
	aggregatesLock sync.Mutex
}

var _ FileSystem = (*MockFs)(nil)

type mockInode struct {
	modTime     time.Time
	permTime    time.Time
	sys         interface{}
	inodeNumber uint64
	readErr     error
}

func (m mockInode) ModTime() time.Time {
	return m.modTime
}

func (m mockInode) Sys() interface{} {
	return m.sys
}

type mockFile struct {
	bytes []byte

	mockInode
}

type mockLink struct {
	target string

	mockInode
}

type mockDir struct {
	mockInode

	subdirs  map[string]*mockDir
	files    map[string]*mockFile
	symlinks map[string]*mockLink
}

func (m *MockFs) resolve(path string, followLastLink bool) (result string, err error) {
	if !filepath.IsAbs(path) {
		path = filepath.Join(m.workDir, path)
	}
	path = filepath.Clean(path)

	return m.followLinks(path, followLastLink, 10)
}

// note that followLinks can return a file path that doesn't exist
func (m *MockFs) followLinks(path string, followLastLink bool, count int) (canonicalPath string, err error) {
	if path == "/" {
		return path, nil
	}

	parentPath, leaf := pathSplit(path)
	if parentPath == path {
		err = fmt.Errorf("Internal error: %v yields itself as a parent", path)
		panic(err.Error())
	}

	parentPath, err = m.followLinks(parentPath, true, count)
	if err != nil {
		return "", err
	}

	parentNode, err := m.getDir(parentPath, false)
	if err != nil {
		return "", err
	}
	if parentNode.readErr != nil {
		return "", &os.PathError{
			Op:   "read",
			Path: path,
			Err:  parentNode.readErr,
		}
	}

	link, isLink := parentNode.symlinks[leaf]
	if isLink && followLastLink {
		if count <= 0 {
			// probably a loop
			return "", &os.PathError{
				Op:   "read",
				Path: path,
				Err:  fmt.Errorf("too many levels of symbolic links"),
			}
		}

		if link.readErr != nil {
			return "", &os.PathError{
				Op:   "read",
				Path: path,
				Err:  link.readErr,
			}
		}

		target := m.followLink(link, parentPath)
		return m.followLinks(target, followLastLink, count-1)
	}
	return path, nil
}

func (m *MockFs) followLink(link *mockLink, parentPath string) (result string) {
	return filepath.Clean(filepath.Join(parentPath, link.target))
}

func (m *MockFs) getFile(parentDir *mockDir, fileName string) (file *mockFile, err error) {
	file, isFile := parentDir.files[fileName]
	if !isFile {
		_, isDir := parentDir.subdirs[fileName]
		_, isLink := parentDir.symlinks[fileName]
		if isDir || isLink {
			return nil, &os.PathError{
				Op:   "open",
				Path: fileName,
				Err:  os.ErrInvalid,
			}
		}

		return nil, &os.PathError{
			Op:   "open",
			Path: fileName,
			Err:  os.ErrNotExist,
		}
	}
	if file.readErr != nil {
		return nil, &os.PathError{
			Op:   "open",
			Path: fileName,
			Err:  file.readErr,
		}
	}
	return file, nil
}

func (m *MockFs) getInode(parentDir *mockDir, name string) (inode *mockInode, err error) {
	file, isFile := parentDir.files[name]
	if isFile {
		return &file.mockInode, nil
	}
	link, isLink := parentDir.symlinks[name]
	if isLink {
		return &link.mockInode, nil
	}
	dir, isDir := parentDir.subdirs[name]
	if isDir {
		return &dir.mockInode, nil
	}
	return nil, &os.PathError{
		Op:   "stat",
		Path: name,
		Err:  os.ErrNotExist,
	}

}

func (m *MockFs) Open(path string) (io.ReadCloser, error) {
	path, err := m.resolve(path, true)
	if err != nil {
		return nil, err
	}

	if err != nil {
		return nil, err
	}

	parentPath, base := pathSplit(path)
	parentDir, err := m.getDir(parentPath, false)
	if err != nil {
		return nil, err
	}
	file, err := m.getFile(parentDir, base)
	if err != nil {
		return nil, err
	}
	return struct {
		io.Closer
		*bytes.Reader
	}{
		ioutil.NopCloser(nil),
		bytes.NewReader(file.bytes),
	}, nil

}

// a mockFileInfo is for exporting file stats in a way that satisfies the FileInfo interface
type mockFileInfo struct {
	path         string
	size         int64
	modTime      time.Time // time at which the inode's contents were modified
	permTime     time.Time // time at which the inode's permissions were modified
	isDir        bool
	inodeNumber  uint64
	deviceNumber uint64
}

func (m *mockFileInfo) Name() string {
	return m.path
}

func (m *mockFileInfo) Size() int64 {
	return m.size
}

func (m *mockFileInfo) Mode() os.FileMode {
	return 0
}

func (m *mockFileInfo) ModTime() time.Time {
	return m.modTime
}

func (m *mockFileInfo) IsDir() bool {
	return m.isDir
}

func (m *mockFileInfo) Sys() interface{} {
	return nil
}

func (m *MockFs) dirToFileInfo(d *mockDir, path string) (info *mockFileInfo) {
	return &mockFileInfo{
		path:         path,
		size:         1,
		modTime:      d.modTime,
		permTime:     d.permTime,
		isDir:        true,
		inodeNumber:  d.inodeNumber,
		deviceNumber: m.deviceNumber,
	}

}

func (m *MockFs) fileToFileInfo(f *mockFile, path string) (info *mockFileInfo) {
	return &mockFileInfo{
		path:         path,
		size:         1,
		modTime:      f.modTime,
		permTime:     f.permTime,
		isDir:        false,
		inodeNumber:  f.inodeNumber,
		deviceNumber: m.deviceNumber,
	}
}

func (m *MockFs) linkToFileInfo(l *mockLink, path string) (info *mockFileInfo) {
	return &mockFileInfo{
		path:         path,
		size:         1,
		modTime:      l.modTime,
		permTime:     l.permTime,
		isDir:        false,
		inodeNumber:  l.inodeNumber,
		deviceNumber: m.deviceNumber,
	}
}

func (m *MockFs) Lstat(path string) (stats os.FileInfo, err error) {
	// update aggregates
	m.aggregatesLock.Lock()
	m.StatCalls = append(m.StatCalls, path)
	m.aggregatesLock.Unlock()

	// resolve symlinks
	path, err = m.resolve(path, false)
	if err != nil {
		return nil, err
	}

	// special case for root dir
	if path == "/" {
		return m.dirToFileInfo(&m.root, "/"), nil
	}

	// determine type and handle appropriately
	parentPath, baseName := pathSplit(path)
	dir, err := m.getDir(parentPath, false)
	if err != nil {
		return nil, err
	}
	subdir, subdirExists := dir.subdirs[baseName]
	if subdirExists {
		return m.dirToFileInfo(subdir, path), nil
	}
	file, fileExists := dir.files[baseName]
	if fileExists {
		return m.fileToFileInfo(file, path), nil
	}
	link, linkExists := dir.symlinks[baseName]
	if linkExists {
		return m.linkToFileInfo(link, path), nil
	}
	// not found
	return nil, &os.PathError{
		Op:   "stat",
		Path: path,
		Err:  os.ErrNotExist,
	}
}

func (m *MockFs) InodeNumber(info os.FileInfo) (number uint64, err error) {
	mockInfo, ok := info.(*mockFileInfo)
	if ok {
		return mockInfo.inodeNumber, nil
	}
	return 0, fmt.Errorf("%v is not a mockFileInfo", info)
}
func (m *MockFs) DeviceNumber(info os.FileInfo) (number uint64, err error) {
	mockInfo, ok := info.(*mockFileInfo)
	if ok {
		return mockInfo.deviceNumber, nil
	}
	return 0, fmt.Errorf("%v is not a mockFileInfo", info)
}
func (m *MockFs) PermTime(info os.FileInfo) (when time.Time, err error) {
	mockInfo, ok := info.(*mockFileInfo)
	if ok {
		return mockInfo.permTime, nil
	}
	return time.Date(0, 0, 0, 0, 0, 0, 0, nil),
		fmt.Errorf("%v is not a mockFileInfo", info)
}

func (m *MockFs) ReadDir(path string) (contents []DirEntryInfo, err error) {
	// update aggregates
	m.aggregatesLock.Lock()
	m.ReadDirCalls = append(m.ReadDirCalls, path)
	m.aggregatesLock.Unlock()

	// locate directory
	path, err = m.resolve(path, true)
	if err != nil {
		return nil, err
	}
	results := []DirEntryInfo{}
	dir, err := m.getDir(path, false)
	if err != nil {
		return nil, err
	}
	if dir.readErr != nil {
		return nil, &os.PathError{
			Op:   "read",
			Path: path,
			Err:  dir.readErr,
		}
	}
	// describe its contents
	for name, subdir := range dir.subdirs {
		dirInfo := m.dirToFileInfo(subdir, name)
		results = append(results, dirInfo)
	}
	for name, file := range dir.files {
		info := m.fileToFileInfo(file, name)
		results = append(results, info)
	}
	for name, link := range dir.symlinks {
		info := m.linkToFileInfo(link, name)
		results = append(results, info)
	}
	return results, nil
}

func (m *MockFs) Rename(sourcePath string, destPath string) error {
	// validate source parent exists
	sourcePath, err := m.resolve(sourcePath, false)
	if err != nil {
		return err
	}
	sourceParentPath := filepath.Dir(sourcePath)
	sourceParentDir, err := m.getDir(sourceParentPath, false)
	if err != nil {
		return err
	}
	if sourceParentDir == nil {
		return &os.PathError{
			Op:   "move",
			Path: sourcePath,
			Err:  os.ErrNotExist,
		}
	}
	if sourceParentDir.readErr != nil {
		return &os.PathError{
			Op:   "move",
			Path: sourcePath,
			Err:  sourceParentDir.readErr,
		}
	}

	// validate dest parent exists
	destPath, err = m.resolve(destPath, false)
	destParentPath := filepath.Dir(destPath)
	destParentDir, err := m.getDir(destParentPath, false)
	if err != nil {
		return err
	}
	if destParentDir == nil {
		return &os.PathError{
			Op:   "move",
			Path: destParentPath,
			Err:  os.ErrNotExist,
		}
	}
	if destParentDir.readErr != nil {
		return &os.PathError{
			Op:   "move",
			Path: destParentPath,
			Err:  destParentDir.readErr,
		}
	}
	// check the source and dest themselves
	sourceBase := filepath.Base(sourcePath)
	destBase := filepath.Base(destPath)

	file, sourceIsFile := sourceParentDir.files[sourceBase]
	dir, sourceIsDir := sourceParentDir.subdirs[sourceBase]
	link, sourceIsLink := sourceParentDir.symlinks[sourceBase]

	// validate that the source exists
	if !sourceIsFile && !sourceIsDir && !sourceIsLink {
		return &os.PathError{
			Op:   "move",
			Path: sourcePath,
			Err:  os.ErrNotExist,
		}

	}

	// validate the destination doesn't already exist as an incompatible type
	_, destWasFile := destParentDir.files[destBase]
	_, destWasDir := destParentDir.subdirs[destBase]
	_, destWasLink := destParentDir.symlinks[destBase]

	if destWasDir {
		return &os.PathError{
			Op:   "move",
			Path: destPath,
			Err:  errors.New("destination exists as a directory"),
		}
	}

	if sourceIsDir && (destWasFile || destWasLink) {
		return &os.PathError{
			Op:   "move",
			Path: destPath,
			Err:  errors.New("destination exists as a file"),
		}
	}

	if destWasFile {
		delete(destParentDir.files, destBase)
	}
	if destWasDir {
		delete(destParentDir.subdirs, destBase)
	}
	if destWasLink {
		delete(destParentDir.symlinks, destBase)
	}

	if sourceIsFile {
		destParentDir.files[destBase] = file
		delete(sourceParentDir.files, sourceBase)
	}
	if sourceIsDir {
		destParentDir.subdirs[destBase] = dir
		delete(sourceParentDir.subdirs, sourceBase)
	}
	if sourceIsLink {
		destParentDir.symlinks[destBase] = link
		delete(destParentDir.symlinks, sourceBase)
	}

	destParentDir.modTime = m.Clock.Time()
	sourceParentDir.modTime = m.Clock.Time()
	return nil
}

func (m *MockFs) newInodeNumber() uint64 {
	result := m.nextInodeNumber
	m.nextInodeNumber++
	return result
}

func (m *MockFs) WriteFile(filePath string, data []byte, perm os.FileMode) error {
	filePath, err := m.resolve(filePath, true)
	if err != nil {
		return err
	}
	parentPath := filepath.Dir(filePath)
	parentDir, err := m.getDir(parentPath, false)
	if err != nil || parentDir == nil {
		return &os.PathError{
			Op:   "write",
			Path: parentPath,
			Err:  os.ErrNotExist,
		}
	}
	if parentDir.readErr != nil {
		return &os.PathError{
			Op:   "write",
			Path: parentPath,
			Err:  parentDir.readErr,
		}
	}

	baseName := filepath.Base(filePath)
	_, exists := parentDir.files[baseName]
	if !exists {
		parentDir.modTime = m.Clock.Time()
		parentDir.files[baseName] = m.newFile()
	} else {
		readErr := parentDir.files[baseName].readErr
		if readErr != nil {
			return &os.PathError{
				Op:   "write",
				Path: filePath,
				Err:  readErr,
			}
		}
	}
	file := parentDir.files[baseName]
	file.bytes = data
	file.modTime = m.Clock.Time()
	return nil
}

func (m *MockFs) newFile() *mockFile {
	newFile := &mockFile{}
	newFile.inodeNumber = m.newInodeNumber()
	newFile.modTime = m.Clock.Time()
	newFile.permTime = newFile.modTime
	return newFile
}

func (m *MockFs) newDir() *mockDir {
	newDir := &mockDir{
		subdirs:  make(map[string]*mockDir, 0),
		files:    make(map[string]*mockFile, 0),
		symlinks: make(map[string]*mockLink, 0),
	}
	newDir.inodeNumber = m.newInodeNumber()
	newDir.modTime = m.Clock.Time()
	newDir.permTime = newDir.modTime
	return newDir
}

func (m *MockFs) newLink(target string) *mockLink {
	newLink := &mockLink{
		target: target,
	}
	newLink.inodeNumber = m.newInodeNumber()
	newLink.modTime = m.Clock.Time()
	newLink.permTime = newLink.modTime

	return newLink
}
func (m *MockFs) MkDirs(path string) error {
	_, err := m.getDir(path, true)
	return err
}

// getDir doesn't support symlinks
func (m *MockFs) getDir(path string, createIfMissing bool) (dir *mockDir, err error) {
	cleanedPath := filepath.Clean(path)
	if cleanedPath == "/" {
		return &m.root, nil
	}

	parentPath, leaf := pathSplit(cleanedPath)
	if len(parentPath) >= len(path) {
		return &m.root, nil
	}
	parent, err := m.getDir(parentPath, createIfMissing)
	if err != nil {
		return nil, err
	}
	if parent.readErr != nil {
		return nil, &os.PathError{
			Op:   "stat",
			Path: path,
			Err:  parent.readErr,
		}
	}
	childDir, dirExists := parent.subdirs[leaf]
	if !dirExists {
		if createIfMissing {
			// confirm that a file with the same name doesn't already exist
			_, fileExists := parent.files[leaf]
			if fileExists {
				return nil, &os.PathError{
					Op:   "mkdir",
					Path: path,
					Err:  os.ErrExist,
				}
			}
			// create this directory
			childDir = m.newDir()
			parent.subdirs[leaf] = childDir
			parent.modTime = m.Clock.Time()
		} else {
			return nil, &os.PathError{
				Op:   "stat",
				Path: path,
				Err:  os.ErrNotExist,
			}
		}
	}
	return childDir, nil

}

func (m *MockFs) Remove(path string) (err error) {
	path, err = m.resolve(path, false)
	parentPath, leaf := pathSplit(path)
	if len(leaf) == 0 {
		return fmt.Errorf("Cannot remove %v\n", path)
	}
	parentDir, err := m.getDir(parentPath, false)
	if err != nil {
		return err
	}
	if parentDir == nil {
		return &os.PathError{
			Op:   "remove",
			Path: path,
			Err:  os.ErrNotExist,
		}
	}
	if parentDir.readErr != nil {
		return &os.PathError{
			Op:   "remove",
			Path: path,
			Err:  parentDir.readErr,
		}
	}
	_, isDir := parentDir.subdirs[leaf]
	if isDir {
		return &os.PathError{
			Op:   "remove",
			Path: path,
			Err:  os.ErrInvalid,
		}
	}
	_, isLink := parentDir.symlinks[leaf]
	if isLink {
		delete(parentDir.symlinks, leaf)
	} else {
		_, isFile := parentDir.files[leaf]
		if !isFile {
			return &os.PathError{
				Op:   "remove",
				Path: path,
				Err:  os.ErrNotExist,
			}
		}
		delete(parentDir.files, leaf)
	}
	parentDir.modTime = m.Clock.Time()
	return nil
}

func (m *MockFs) Symlink(oldPath string, newPath string) (err error) {
	newPath, err = m.resolve(newPath, false)
	if err != nil {
		return err
	}

	newParentPath, leaf := pathSplit(newPath)
	newParentDir, err := m.getDir(newParentPath, false)
	if newParentDir.readErr != nil {
		return &os.PathError{
			Op:   "link",
			Path: newPath,
			Err:  newParentDir.readErr,
		}
	}
	if err != nil {
		return err
	}
	newParentDir.symlinks[leaf] = m.newLink(oldPath)
	return nil
}

func (m *MockFs) RemoveAll(path string) (err error) {
	path, err = m.resolve(path, false)
	if err != nil {
		return err
	}
	parentPath, leaf := pathSplit(path)
	if len(leaf) == 0 {
		return fmt.Errorf("Cannot remove %v\n", path)
	}
	parentDir, err := m.getDir(parentPath, false)
	if err != nil {
		return err
	}
	if parentDir == nil {
		return &os.PathError{
			Op:   "removeAll",
			Path: path,
			Err:  os.ErrNotExist,
		}
	}
	if parentDir.readErr != nil {
		return &os.PathError{
			Op:   "removeAll",
			Path: path,
			Err:  parentDir.readErr,
		}

	}
	_, isFile := parentDir.files[leaf]
	_, isLink := parentDir.symlinks[leaf]
	if isFile || isLink {
		return m.Remove(path)
	}
	_, isDir := parentDir.subdirs[leaf]
	if !isDir {
		if !isDir {
			return &os.PathError{
				Op:   "removeAll",
				Path: path,
				Err:  os.ErrNotExist,
			}
		}
	}

	delete(parentDir.subdirs, leaf)
	parentDir.modTime = m.Clock.Time()
	return nil
}

func (m *MockFs) SetReadable(path string, readable bool) error {
	var readErr error
	if !readable {
		readErr = os.ErrPermission
	}
	return m.SetReadErr(path, readErr)
}

func (m *MockFs) SetReadErr(path string, readErr error) error {
	path, err := m.resolve(path, false)
	if err != nil {
		return err
	}
	parentPath, leaf := filepath.Split(path)
	parentDir, err := m.getDir(parentPath, false)
	if err != nil {
		return err
	}
	if parentDir.readErr != nil {
		return &os.PathError{
			Op:   "chmod",
			Path: parentPath,
			Err:  parentDir.readErr,
		}
	}

	inode, err := m.getInode(parentDir, leaf)
	if err != nil {
		return err
	}
	inode.readErr = readErr
	inode.permTime = m.Clock.Time()
	return nil
}

func (m *MockFs) ClearMetrics() {
	m.ReadDirCalls = []string{}
	m.StatCalls = []string{}
}

func (m *MockFs) ViewId() (id string) {
	return m.viewId
}

func (m *MockFs) SetViewId(id string) {
	m.viewId = id
}
func (m *MockFs) SetDeviceNumber(deviceNumber uint64) {
	m.deviceNumber = deviceNumber
}