package fs

import (
	"io"
	"os"
	"path"
	"sync"
	"syscall"
	"time"

	"github.com/restic/restic/internal/errors"
)

// Reader is a file system which provides a directory with a single file. When
// this file is opened for reading, the reader is passed through. The file can
// be opened once, all subsequent open calls return syscall.EIO. For Lstat(),
// the provided FileInfo is returned.
type Reader struct {
	Name string
	io.ReadCloser

	Mode    os.FileMode
	ModTime time.Time
	Size    int64

	open sync.Once
}

// statically ensure that Local implements FS.
var _ FS = &Reader{}

// VolumeName returns leading volume name, for the Reader file system it's
// always the empty string.
func (fs *Reader) VolumeName(path string) string {
	return ""
}

// Open opens a file for reading.
func (fs *Reader) Open(name string) (f File, err error) {
	switch name {
	case fs.Name:
		fs.open.Do(func() {
			f = newReaderFile(fs.ReadCloser, fs.fi())
		})

		if f == nil {
			return nil, syscall.EIO
		}

		return f, nil
	case "/", ".":
		f = fakeDir{
			entries: []os.FileInfo{fs.fi()},
		}
		return f, nil
	}

	return nil, syscall.ENOENT
}

func (fs *Reader) fi() os.FileInfo {
	return fakeFileInfo{
		name:    fs.Name,
		size:    fs.Size,
		mode:    fs.Mode,
		modtime: fs.ModTime,
	}
}

// OpenFile is the generalized open call; most users will use Open
// or Create instead.  It opens the named file with specified flag
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable.  If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func (fs *Reader) OpenFile(name string, flag int, perm os.FileMode) (f File, err error) {
	if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 {
		return nil, errors.Errorf("invalid combination of flags 0x%x", flag)
	}

	fs.open.Do(func() {
		f = newReaderFile(fs.ReadCloser, fs.fi())
	})

	if f == nil {
		return nil, syscall.EIO
	}

	return f, nil
}

// Stat returns a FileInfo describing the named file. If there is an error, it
// will be of type *PathError.
func (fs *Reader) Stat(name string) (os.FileInfo, error) {
	return fs.Lstat(name)
}

// Lstat returns the FileInfo structure describing the named file.
// If the file is a symbolic link, the returned FileInfo
// describes the symbolic link.  Lstat makes no attempt to follow the link.
// If there is an error, it will be of type *PathError.
func (fs *Reader) Lstat(name string) (os.FileInfo, error) {
	switch name {
	case fs.Name:
		return fs.fi(), nil
	case "/", ".":
		fi := fakeFileInfo{
			name:    name,
			size:    0,
			mode:    0755,
			modtime: time.Now(),
		}
		return fi, nil
	}

	return nil, os.ErrNotExist
}

// Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls Clean on the result; in particular, all
// empty strings are ignored. On Windows, the result is a UNC path if and only
// if the first path element is a UNC path.
func (fs *Reader) Join(elem ...string) string {
	return path.Join(elem...)
}

// Separator returns the OS and FS dependent separator for dirs/subdirs/files.
func (fs *Reader) Separator() string {
	return "/"
}

// IsAbs reports whether the path is absolute. For the Reader, this is always the case.
func (fs *Reader) IsAbs(p string) bool {
	return true
}

// Abs returns an absolute representation of path. If the path is not absolute
// it will be joined with the current working directory to turn it into an
// absolute path. The absolute path name for a given file is not guaranteed to
// be unique. Abs calls Clean on the result.
//
// For the Reader, all paths are absolute.
func (fs *Reader) Abs(p string) (string, error) {
	return path.Clean(p), nil
}

// Clean returns the cleaned path. For details, see filepath.Clean.
func (fs *Reader) Clean(p string) string {
	return path.Clean(p)
}

// Base returns the last element of p.
func (fs *Reader) Base(p string) string {
	return path.Base(p)
}

// Dir returns p without the last element.
func (fs *Reader) Dir(p string) string {
	return path.Dir(p)
}

func newReaderFile(rd io.ReadCloser, fi os.FileInfo) readerFile {
	return readerFile{
		ReadCloser: rd,
		fakeFile: fakeFile{
			FileInfo: fi,
			name:     fi.Name(),
		},
	}
}

type readerFile struct {
	io.ReadCloser
	fakeFile
}

func (r readerFile) Read(p []byte) (int, error) {
	return r.ReadCloser.Read(p)
}

func (r readerFile) Close() error {
	return r.ReadCloser.Close()
}

// ensure that readerFile implements File
var _ File = readerFile{}

// fakeFile implements all File methods, but only returns errors for anything
// except Stat() and Name().
type fakeFile struct {
	name string
	os.FileInfo
}

// ensure that fakeFile implements File
var _ File = fakeFile{}

func (f fakeFile) Fd() uintptr {
	return 0
}

func (f fakeFile) Readdirnames(n int) ([]string, error) {
	return nil, os.ErrInvalid
}

func (f fakeFile) Readdir(n int) ([]os.FileInfo, error) {
	return nil, os.ErrInvalid
}

func (f fakeFile) Seek(int64, int) (int64, error) {
	return 0, os.ErrInvalid
}

func (f fakeFile) Write(p []byte) (int, error) {
	return 0, os.ErrInvalid
}

func (f fakeFile) Read(p []byte) (int, error) {
	return 0, os.ErrInvalid
}

func (f fakeFile) Close() error {
	return nil
}

func (f fakeFile) Stat() (os.FileInfo, error) {
	return f.FileInfo, nil
}

func (f fakeFile) Name() string {
	return f.name
}

// fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile.
type fakeDir struct {
	entries []os.FileInfo
	fakeFile
}

func (d fakeDir) Readdirnames(n int) ([]string, error) {
	if n >= 0 {
		return nil, errors.New("not implemented")
	}
	names := make([]string, 0, len(d.entries))
	for _, entry := range d.entries {
		names = append(names, entry.Name())
	}

	return names, nil
}

func (d fakeDir) Readdir(n int) ([]os.FileInfo, error) {
	if n >= 0 {
		return nil, errors.New("not implemented")
	}
	return d.entries, nil
}

// fakeFileInfo implements the bare minimum of os.FileInfo.
type fakeFileInfo struct {
	name    string
	size    int64
	mode    os.FileMode
	modtime time.Time
	sys     interface{}
}

func (fi fakeFileInfo) Name() string {
	return fi.name
}

func (fi fakeFileInfo) Size() int64 {
	return fi.size
}

func (fi fakeFileInfo) Mode() os.FileMode {
	return fi.mode
}

func (fi fakeFileInfo) ModTime() time.Time {
	return fi.modtime
}

func (fi fakeFileInfo) IsDir() bool {
	return fi.mode&os.ModeDir > 0
}

func (fi fakeFileInfo) Sys() interface{} {
	return fi.sys
}