package fs

import (
	"io"
	"os"
	"path/filepath"
	"sync"
	"syscall"

	"github.com/barakmich/kubelwagen"
	"github.com/hanwen/go-fuse/fuse"
	"github.com/sirupsen/logrus"
)

type LocalFs struct {
	sync.Mutex
	base      string
	nextfile  int
	openfiles map[int]*os.File
}

func NewLocalFs(path string) *LocalFs {
	return &LocalFs{
		base:      path,
		nextfile:  1,
		openfiles: make(map[int]*os.File),
	}
}

func (fs *LocalFs) Handle(r *kubelwagen.Request) *kubelwagen.Response {
	switch r.Method {
	case kubelwagen.MethodOpenDir:
		return fs.openDir(r)
	case kubelwagen.MethodGetAttr:
		return fs.getAttr(r)
	case kubelwagen.MethodStatFs:
		return fs.statFs(r)
	case kubelwagen.MethodChmod:
		return fs.serveFromErr(r, os.Chmod(fs.getPath(r), os.FileMode(r.Mode)))
	case kubelwagen.MethodChown:
		return fs.serveFromErr(r, os.Chown(fs.getPath(r), int(r.UID), int(r.GID)))
	case kubelwagen.MethodTruncate:
		return fs.serveFromErr(r, os.Truncate(fs.getPath(r), int64(r.Offset)))
	case kubelwagen.MethodMknod:
		return fs.serveFromErr(r, syscall.Mknod(fs.getPath(r), r.Mode, int(r.Dev)))
	case kubelwagen.MethodMkdir:
		return fs.serveFromErr(r, os.Mkdir(fs.getPath(r), os.FileMode(r.Mode)))
	case kubelwagen.MethodUnlink:
		return fs.serveFromErr(r, syscall.Unlink(fs.getPath(r)))
	case kubelwagen.MethodRmdir:
		return fs.serveFromErr(r, syscall.Rmdir(fs.getPath(r)))
	case kubelwagen.MethodSymlink:
		return fs.serveFromErr(r, os.Symlink(r.NewPath, fs.getPath(r)))
	case kubelwagen.MethodRename:
		return fs.serveFromErr(r, os.Rename(fs.getPath(r), filepath.Join(fs.base, r.NewPath)))
	case kubelwagen.MethodLink:
		return fs.serveFromErr(r, os.Link(fs.getPath(r), filepath.Join(fs.base, r.NewPath)))
	case kubelwagen.MethodReadLink:
		f, err := os.Readlink(fs.getPath(r))
		if err != nil {
			return kubelwagen.ErrorResp(r, nil)
		}
		out := fs.serveFromErr(r, err)
		out.LinkStr = f
		return out
	case kubelwagen.MethodAccess:
		return fs.serveFromErr(r, syscall.Access(fs.getPath(r), r.Mode))
	default:
		return fs.handleFile(r)
	}
}

func (fs *LocalFs) getPath(r *kubelwagen.Request) string {
	return filepath.Join(fs.base, r.Path)
}

func (fs *LocalFs) openDir(r *kubelwagen.Request) *kubelwagen.Response {
	out := &kubelwagen.Response{
		ID:   r.ID,
		Code: fuse.OK,
	}
	// returns dirents and status

	f, err := os.Open(fs.getPath(r))
	if err != nil {
		logrus.Errorf("LocalFS openDir (%s): %s\n", r.Path, err)
		return kubelwagen.ErrorResp(r, err)
	}
	want := 100
	output := make([]fuse.DirEntry, 0, want)
	for {
		infos, err := f.Readdir(want)
		for i := range infos {
			// workaround for https://code.google.com/p/go/issues/detail?id=5960
			if infos[i] == nil {
				continue
			}
			n := infos[i].Name()
			d := fuse.DirEntry{
				Name: n,
			}
			if s := fuse.ToStatT(infos[i]); s != nil {
				d.Mode = uint32(s.Mode)
				d.Ino = s.Ino
			} else {
				logrus.Printf("ReadDir entry %q for %q has no stat info", n, r.Path)
			}
			output = append(output, d)
		}
		if len(infos) < want || err == io.EOF {
			break
		}
		if err != nil {
			logrus.Errorln("Readdir() returned err:", err)
			break
		}
	}
	f.Close()

	out.Dirents = output
	return out
}

func (fs *LocalFs) getAttr(r *kubelwagen.Request) *kubelwagen.Response {
	out := &kubelwagen.Response{
		ID:   r.ID,
		Code: fuse.OK,
	}
	fi, err := os.Stat(fs.getPath(r))
	if err != nil {
		logrus.Errorf("LocalFS getAttr (%s): %s\n", r.Path, err)
		return kubelwagen.ErrorResp(r, err)
	}
	out.Stat = fuse.ToAttr(fi)
	return out
}

func (fs *LocalFs) statFs(r *kubelwagen.Request) *kubelwagen.Response {
	out := &kubelwagen.Response{
		ID:   r.ID,
		Code: fuse.OK,
	}
	s := syscall.Statfs_t{}
	err := syscall.Statfs(fs.getPath(r), &s)
	if err != nil {
		return kubelwagen.ErrorResp(r, err)
	}
	g := &fuse.StatfsOut{}
	g.FromStatfsT(&s)
	out.Statfs = g
	return out
}

func (fs *LocalFs) serveFromErr(r *kubelwagen.Request, err error) *kubelwagen.Response {
	out := &kubelwagen.Response{
		ID:   r.ID,
		Code: fuse.OK,
	}
	if err != nil {
		return kubelwagen.ErrorResp(r, err)
	}
	return out

}
