package fuse

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

// SwitchFileSystem construct the union of a set of filesystems, and
// select them by prefix.  Consider the traditional Unix file system:
// different parts have different characteristics, eg. "/usr" is
// read-only, while "/home/user" is read/write.  Similarly, "/tmp"
// does not need to be persistent and "/dev" is totally unlike a file
// system.
//
// With SwitchFileSystem, you can write filesystems for each of these
// parts separately, and combine them using SwitchFileSystem.  This is
// a simpler and less efficient alternative to in-process mounts, but
// it also works if the encompassing file system already has the
// mounted directory.
type SwitchFileSystem struct {
	DefaultFileSystem
	fileSystems SwitchedFileSystems
}

// This is the definition of one member of SwitchedFileSystems.
type SwitchedFileSystem struct {
	Prefix string
	FileSystem
	StripPrefix bool
}

type SwitchedFileSystems []*SwitchedFileSystem

func (p SwitchedFileSystems) Len() int {
	return len(p)
}

func (p SwitchedFileSystems) Less(i, j int) bool {
	// Invert order, so we get more specific prefixes first.
	return p[i].Prefix > p[j].Prefix
}

func (p SwitchedFileSystems) Swap(i, j int) {
	swFs := p[i]
	p[i] = p[j]
	p[j] = swFs
}

func NewSwitchFileSystem(fsMap []SwitchedFileSystem) *SwitchFileSystem {
	me := &SwitchFileSystem{}
	for _, inSwFs := range fsMap {
		swFs := inSwFs
		swFs.Prefix = strings.TrimLeft(swFs.Prefix, string(filepath.Separator))
		swFs.Prefix = strings.TrimRight(swFs.Prefix, string(filepath.Separator))
		me.fileSystems = append(me.fileSystems, &swFs)
	}
	sort.Sort(me.fileSystems)
	return me
}

// TODO - use binary search.  This is inefficient if there are large
// numbers of switched filesystems.
func (me *SwitchFileSystem) findFileSystem(path string) (string, *SwitchedFileSystem) {
	for _, swFs := range me.fileSystems {
		if swFs.Prefix == "" || swFs.Prefix == path || strings.HasPrefix(path, swFs.Prefix+string(filepath.Separator)) {
			if swFs.StripPrefix {
				path = strings.TrimLeft(path[len(swFs.Prefix):], string(filepath.Separator))
			}

			return path, swFs
		}
	}
	return "", nil
}

func (me *SwitchFileSystem) GetAttr(name string, context *Context) (*os.FileInfo, Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.GetAttr(name, context)
}

func (me *SwitchFileSystem) Readlink(name string, context *Context) (string, Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return "", ENOENT
	}
	return fs.FileSystem.Readlink(name, context)
}

func (me *SwitchFileSystem) Mknod(name string, mode uint32, dev uint32, context *Context) Status {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Mknod(name, mode, dev, context)
}

func (me *SwitchFileSystem) Mkdir(name string, mode uint32, context *Context) Status {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Mkdir(name, mode, context)
}

func (me *SwitchFileSystem) Unlink(name string, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Unlink(name, context)
}

func (me *SwitchFileSystem) Rmdir(name string, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Rmdir(name, context)
}

func (me *SwitchFileSystem) Symlink(value string, linkName string, context *Context) (code Status) {
	linkName, fs := me.findFileSystem(linkName)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Symlink(value, linkName, context)
}

func (me *SwitchFileSystem) Rename(oldName string, newName string, context *Context) (code Status) {
	oldName, fs1 := me.findFileSystem(oldName)
	newName, fs2 := me.findFileSystem(newName)
	if fs1 != fs2 {
		return syscall.EXDEV
	}
	if fs1 == nil {
		return ENOENT
	}
	return fs1.Rename(oldName, newName, context)
}

func (me *SwitchFileSystem) Link(oldName string, newName string, context *Context) (code Status) {
	oldName, fs1 := me.findFileSystem(oldName)
	newName, fs2 := me.findFileSystem(newName)
	if fs1 != fs2 {
		return syscall.EXDEV
	}
	if fs1 == nil {
		return ENOENT
	}
	return fs1.Link(oldName, newName, context)
}

func (me *SwitchFileSystem) Chmod(name string, mode uint32, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Chmod(name, mode, context)
}

func (me *SwitchFileSystem) Chown(name string, uid uint32, gid uint32, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Chown(name, uid, gid, context)
}

func (me *SwitchFileSystem) Truncate(name string, offset uint64, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Truncate(name, offset, context)
}

func (me *SwitchFileSystem) Open(name string, flags uint32, context *Context) (file File, code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.Open(name, flags, context)
}

func (me *SwitchFileSystem) OpenDir(name string, context *Context) (stream chan DirEntry, status Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.OpenDir(name, context)
}

func (me *SwitchFileSystem) OnMount(nodeFs *PathNodeFs) {
	for _, fs := range me.fileSystems {
		fs.FileSystem.OnMount(nodeFs)
	}
}

func (me *SwitchFileSystem) OnUnmount() {
	for _, fs := range me.fileSystems {
		fs.FileSystem.OnUnmount()
	}
}

func (me *SwitchFileSystem) Access(name string, mode uint32, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Access(name, mode, context)
}

func (me *SwitchFileSystem) Create(name string, flags uint32, mode uint32, context *Context) (file File, code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.Create(name, flags, mode, context)
}

func (me *SwitchFileSystem) Utimens(name string, AtimeNs uint64, CtimeNs uint64, context *Context) (code Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Utimens(name, AtimeNs, CtimeNs, context)
}

func (me *SwitchFileSystem) GetXAttr(name string, attr string, context *Context) ([]byte, Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.GetXAttr(name, attr, context)
}

func (me *SwitchFileSystem) SetXAttr(name string, attr string, data []byte, flags int, context *Context) Status {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.SetXAttr(name, attr, data, flags, context)
}

func (me *SwitchFileSystem) ListXAttr(name string, context *Context) ([]string, Status) {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return nil, ENOENT
	}
	return fs.FileSystem.ListXAttr(name, context)
}

func (me *SwitchFileSystem) RemoveXAttr(name string, attr string, context *Context) Status {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.RemoveXAttr(name, attr, context)
}

func (me *SwitchFileSystem) Flush(name string) Status {
	name, fs := me.findFileSystem(name)
	if fs == nil {
		return ENOENT
	}
	return fs.FileSystem.Flush(name)
}