// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package fs

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

var _ FileSystem = &fsInMemory{}

// fsInMemory implements FileSystem using a in-memory filesystem
// primarily for use in tests.
type fsInMemory struct {
	m map[string]*fileInMemory
}

// MakeFsInMemory returns an instance of fsInMemory with no files in it.
func MakeFsInMemory() FileSystem {
	result := &fsInMemory{m: map[string]*fileInMemory{}}
	result.Mkdir(separator)
	return result
}

const (
	separator = string(filepath.Separator)
	doubleSep = separator + separator
)

// Create assures a fake file appears in the in-memory file system.
func (fs *fsInMemory) Create(name string) (File, error) {
	f := &fileInMemory{}
	f.open = true
	fs.m[name] = f
	return fs.m[name], nil
}

// Mkdir assures a fake directory appears in the in-memory file system.
func (fs *fsInMemory) Mkdir(name string) error {
	fs.m[name] = makeDir(name)
	return nil
}

// MkdirAll delegates to Mkdir
func (fs *fsInMemory) MkdirAll(name string) error {
	return fs.Mkdir(name)
}

// RemoveAll presumably does rm -r on a path.
// There's no error.
func (fs *fsInMemory) RemoveAll(name string) error {
	var toRemove []string
	for k := range fs.m {
		if strings.HasPrefix(k, name) {
			toRemove = append(toRemove, k)
		}
	}
	for _, k := range toRemove {
		delete(fs.m, k)
	}
	return nil
}

// Open returns a fake file in the open state.
func (fs *fsInMemory) Open(name string) (File, error) {
	if _, found := fs.m[name]; !found {
		return nil, fmt.Errorf("file %q cannot be opened", name)
	}
	return fs.m[name], nil
}

// CleanedAbs cannot fail.
func (fs *fsInMemory) CleanedAbs(path string) (ConfirmedDir, string, error) {
	if fs.IsDir(path) {
		return ConfirmedDir(path), "", nil
	}
	d := filepath.Dir(path)
	if d == path {
		return ConfirmedDir(d), "", nil
	}
	return ConfirmedDir(d), filepath.Base(path), nil
}

// Exists returns true if file is known.
func (fs *fsInMemory) Exists(name string) bool {
	_, found := fs.m[name]
	return found
}

// Glob returns the list of matching files
func (fs *fsInMemory) Glob(pattern string) ([]string, error) {
	var result []string
	for p := range fs.m {
		if fs.pathMatch(p, pattern) {
			result = append(result, p)
		}
	}
	sort.Strings(result)
	return result, nil
}

// IsDir returns true if the file exists and is a directory.
func (fs *fsInMemory) IsDir(name string) bool {
	f, found := fs.m[name]
	if found && f.dir {
		return true
	}
	if !strings.HasSuffix(name, separator) {
		name = name + separator
	}
	for k := range fs.m {
		if strings.HasPrefix(k, name) {
			return true
		}
	}
	return false
}

// ReadFile always returns an empty bytes and error depending on content of m.
func (fs *fsInMemory) ReadFile(name string) ([]byte, error) {
	if ff, found := fs.m[name]; found {
		return ff.content, nil
	}
	return nil, fmt.Errorf("cannot read file %q", name)
}

// WriteFile always succeeds and does nothing.
func (fs *fsInMemory) WriteFile(name string, c []byte) error {
	ff := &fileInMemory{}
	ff.Write(c)
	fs.m[name] = ff
	return nil
}

// Walk implements filepath.Walk using the fake filesystem.
func (fs *fsInMemory) Walk(path string, walkFn filepath.WalkFunc) error {
	info, err := fs.lstat(path)
	if err != nil {
		err = walkFn(path, info, err)
	} else {
		err = fs.walk(path, info, walkFn)
	}
	if err == filepath.SkipDir {
		return nil
	}
	return err
}

func (fs *fsInMemory) pathMatch(path, pattern string) bool {
	match, _ := filepath.Match(pattern, path)
	return match
}

func (fs *fsInMemory) lstat(path string) (*fileInfo, error) {
	f, found := fs.m[path]
	if !found {
		return nil, os.ErrNotExist
	}
	return &fileInfo{f}, nil
}

func (fs *fsInMemory) join(elem ...string) string {
	for i, e := range elem {
		if e != "" {
			return strings.Replace(
				strings.Join(elem[i:], separator), doubleSep, separator, -1)
		}
	}
	return ""
}

func (fs *fsInMemory) readDirNames(path string) []string {
	var names []string
	if !strings.HasSuffix(path, separator) {
		path += separator
	}
	pathSegments := strings.Count(path, separator)
	for name := range fs.m {
		if name == path {
			continue
		}
		if strings.Count(name, separator) > pathSegments {
			continue
		}
		if strings.HasPrefix(name, path) {
			names = append(names, filepath.Base(name))
		}
	}
	sort.Strings(names)
	return names
}

func (fs *fsInMemory) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
	if !info.IsDir() {
		return walkFn(path, info, nil)
	}

	names := fs.readDirNames(path)
	if err := walkFn(path, info, nil); err != nil {
		return err
	}
	for _, name := range names {
		filename := fs.join(path, name)
		fileInfo, err := fs.lstat(filename)
		if err != nil {
			if err := walkFn(filename, fileInfo, os.ErrNotExist); err != nil && err != filepath.SkipDir {
				return err
			}
		} else {
			err = fs.walk(filename, fileInfo, walkFn)
			if err != nil {
				if !fileInfo.IsDir() || err != filepath.SkipDir {
					return err
				}
			}
		}
	}
	return nil
}
