package fuse

import (
	"os"

	"bazil.org/fuse"
	"bazil.org/fuse/fs"
	"golang.org/x/net/context"

	"github.com/restic/restic"
	"github.com/restic/restic/repository"
)

// Statically ensure that *dir implement those interface
var _ = fs.HandleReadDirAller(&dir{})
var _ = fs.NodeStringLookuper(&dir{})

type dir struct {
	repo        *repository.Repository
	items       map[string]*restic.Node
	inode       uint64
	node        *restic.Node
	ownerIsRoot bool
}

func newDir(repo *repository.Repository, node *restic.Node, ownerIsRoot bool) (*dir, error) {
	tree, err := restic.LoadTree(repo, *node.Subtree)
	if err != nil {
		return nil, err
	}
	items := make(map[string]*restic.Node)
	for _, node := range tree.Nodes {
		items[node.Name] = node
	}

	return &dir{
		repo:        repo,
		node:        node,
		items:       items,
		inode:       node.Inode,
		ownerIsRoot: ownerIsRoot,
	}, nil
}

func newDirFromSnapshot(repo *repository.Repository, snapshot SnapshotWithId, ownerIsRoot bool) (*dir, error) {
	tree, err := restic.LoadTree(repo, *snapshot.Tree)
	if err != nil {
		return nil, err
	}
	items := make(map[string]*restic.Node)
	for _, node := range tree.Nodes {
		items[node.Name] = node
	}

	return &dir{
		repo: repo,
		node: &restic.Node{
			UID:        uint32(os.Getuid()),
			GID:        uint32(os.Getgid()),
			AccessTime: snapshot.Time,
			ModTime:    snapshot.Time,
			ChangeTime: snapshot.Time,
			Mode:       os.ModeDir | 0555,
		},
		items:       items,
		inode:       inodeFromBackendId(snapshot.ID),
		ownerIsRoot: ownerIsRoot,
	}, nil
}

func (d *dir) Attr(ctx context.Context, a *fuse.Attr) error {
	a.Inode = d.inode
	a.Mode = os.ModeDir | d.node.Mode

	if !d.ownerIsRoot {
		a.Uid = d.node.UID
		a.Gid = d.node.GID
	}
	a.Atime = d.node.AccessTime
	a.Ctime = d.node.ChangeTime
	a.Mtime = d.node.ModTime
	return nil
}

func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
	ret := make([]fuse.Dirent, 0, len(d.items))

	for _, node := range d.items {
		var typ fuse.DirentType
		switch node.Type {
		case "dir":
			typ = fuse.DT_Dir
		case "file":
			typ = fuse.DT_File
		case "symlink":
			typ = fuse.DT_Link
		}

		ret = append(ret, fuse.Dirent{
			Inode: node.Inode,
			Type:  typ,
			Name:  node.Name,
		})
	}

	return ret, nil
}

func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
	node, ok := d.items[name]
	if !ok {
		return nil, fuse.ENOENT
	}
	switch node.Type {
	case "dir":
		return newDir(d.repo, node, d.ownerIsRoot)
	case "file":
		return newFile(d.repo, node, d.ownerIsRoot)
	case "symlink":
		return newLink(d.repo, node, d.ownerIsRoot)
	default:
		return nil, fuse.ENOENT
	}
}
