// Copyright 2016 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fuse

import (
	"fmt"
	"os"
	"strings"
	"syscall"
)

var (
	writeFlagNames = newFlagNames(map[int64]string{
		WRITE_CACHE:     "CACHE",
		WRITE_LOCKOWNER: "LOCKOWNER",
	})
	readFlagNames = newFlagNames(map[int64]string{
		READ_LOCKOWNER: "LOCKOWNER",
	})
	initFlagNames = newFlagNames(map[int64]string{
		CAP_ASYNC_READ:          "ASYNC_READ",
		CAP_POSIX_LOCKS:         "POSIX_LOCKS",
		CAP_FILE_OPS:            "FILE_OPS",
		CAP_ATOMIC_O_TRUNC:      "ATOMIC_O_TRUNC",
		CAP_EXPORT_SUPPORT:      "EXPORT_SUPPORT",
		CAP_BIG_WRITES:          "BIG_WRITES",
		CAP_DONT_MASK:           "DONT_MASK",
		CAP_SPLICE_WRITE:        "SPLICE_WRITE",
		CAP_SPLICE_MOVE:         "SPLICE_MOVE",
		CAP_SPLICE_READ:         "SPLICE_READ",
		CAP_FLOCK_LOCKS:         "FLOCK_LOCKS",
		CAP_IOCTL_DIR:           "IOCTL_DIR",
		CAP_AUTO_INVAL_DATA:     "AUTO_INVAL_DATA",
		CAP_READDIRPLUS:         "READDIRPLUS",
		CAP_READDIRPLUS_AUTO:    "READDIRPLUS_AUTO",
		CAP_ASYNC_DIO:           "ASYNC_DIO",
		CAP_WRITEBACK_CACHE:     "WRITEBACK_CACHE",
		CAP_NO_OPEN_SUPPORT:     "NO_OPEN_SUPPORT",
		CAP_PARALLEL_DIROPS:     "PARALLEL_DIROPS",
		CAP_POSIX_ACL:           "POSIX_ACL",
		CAP_HANDLE_KILLPRIV:     "HANDLE_KILLPRIV",
		CAP_ABORT_ERROR:         "ABORT_ERROR",
		CAP_MAX_PAGES:           "MAX_PAGES",
		CAP_CACHE_SYMLINKS:      "CACHE_SYMLINKS",
		CAP_NO_OPENDIR_SUPPORT:  "NO_OPENDIR_SUPPORT",
		CAP_EXPLICIT_INVAL_DATA: "EXPLICIT_INVAL_DATA",
	})
	releaseFlagNames = newFlagNames(map[int64]string{
		RELEASE_FLUSH: "FLUSH",
	})
	openFlagNames = newFlagNames(map[int64]string{
		int64(os.O_WRONLY):        "WRONLY",
		int64(os.O_RDWR):          "RDWR",
		int64(os.O_APPEND):        "APPEND",
		int64(syscall.O_ASYNC):    "ASYNC",
		int64(os.O_CREATE):        "CREAT",
		int64(os.O_EXCL):          "EXCL",
		int64(syscall.O_NOCTTY):   "NOCTTY",
		int64(syscall.O_NONBLOCK): "NONBLOCK",
		int64(os.O_SYNC):          "SYNC",
		int64(os.O_TRUNC):         "TRUNC",

		int64(syscall.O_CLOEXEC):   "CLOEXEC",
		int64(syscall.O_DIRECTORY): "DIRECTORY",
	})
	fuseOpenFlagNames = newFlagNames(map[int64]string{
		FOPEN_DIRECT_IO:   "DIRECT",
		FOPEN_KEEP_CACHE:  "CACHE",
		FOPEN_NONSEEKABLE: "NONSEEK",
		FOPEN_CACHE_DIR:   "CACHE_DIR",
		FOPEN_STREAM:      "STREAM",
	})
	accessFlagName = newFlagNames(map[int64]string{
		X_OK: "x",
		W_OK: "w",
		R_OK: "r",
	})
	getAttrFlagNames = newFlagNames(map[int64]string{
		FUSE_GETATTR_FH: "FH",
	})
)

// flagNames associate flag bits to their names.
type flagNames [64]flagNameEntry

// flagNameEntry describes one flag value.
//
// Usually a flag constitues only one bit, but, for example at least O_SYNC and
// O_TMPFILE are represented by a value with two bits set. To handle such
// situations we map all bits of a flag to the same flagNameEntry.
type flagNameEntry struct {
	bits int64
	name string
}

// newFlagNames creates flagNames from flag->name map.
func newFlagNames(names map[int64]string) *flagNames {
	var v flagNames
	for flag, name := range names {
		v.set(flag, name)
	}
	return &v
}

// set associates flag value with name.
func (names *flagNames) set(flag int64, name string) {
	entry := flagNameEntry{bits: flag, name: name}
	for i := 0; i < 64; i++ {
		if flag&(1<<i) != 0 {
			if ie := names[i]; ie.bits != 0 {
				panic(fmt.Sprintf("%s (%x) overlaps with %s (%x)", name, flag, ie.name, ie.bits))
			}
			names[i] = entry
		}
	}
}

func flagString(names *flagNames, fl int64, def string) string {
	s := []string{}
	// emit flags in their numeric order
	for i := range names {
		entry := &names[i]
		if entry.bits == 0 {
			continue
		}
		if fl&entry.bits == entry.bits {
			s = append(s, entry.name)
			fl ^= entry.bits
			if fl == 0 {
				break
			}
		}
	}
	if fl != 0 {
		s = append(s, fmt.Sprintf("0x%x", fl))
	}
	if len(s) == 0 && def != "" {
		return def
	}

	return strings.Join(s, ",")
}

func (in *ForgetIn) string() string {
	return fmt.Sprintf("{Nlookup=%d}", in.Nlookup)
}

func (in *_BatchForgetIn) string() string {
	return fmt.Sprintf("{Count=%d}", in.Count)
}

func (in *MkdirIn) string() string {
	return fmt.Sprintf("{0%o (0%o)}", in.Mode, in.Umask)
}

func (in *Rename1In) string() string {
	return fmt.Sprintf("{i%d}", in.Newdir)
}

func (in *RenameIn) string() string {
	return fmt.Sprintf("{i%d %x}", in.Newdir, in.Flags)
}

func (in *SetAttrIn) string() string {
	s := []string{}
	if in.Valid&FATTR_MODE != 0 {
		s = append(s, fmt.Sprintf("mode 0%o", in.Mode))
	}
	if in.Valid&FATTR_UID != 0 {
		s = append(s, fmt.Sprintf("uid %d", in.Uid))
	}
	if in.Valid&FATTR_GID != 0 {
		s = append(s, fmt.Sprintf("gid %d", in.Gid))
	}
	if in.Valid&FATTR_SIZE != 0 {
		s = append(s, fmt.Sprintf("size %d", in.Size))
	}
	if in.Valid&FATTR_ATIME != 0 {
		s = append(s, fmt.Sprintf("atime %d.%09d", in.Atime, in.Atimensec))
	}
	if in.Valid&FATTR_MTIME != 0 {
		s = append(s, fmt.Sprintf("mtime %d.%09d", in.Mtime, in.Mtimensec))
	}
	if in.Valid&FATTR_FH != 0 {
		s = append(s, fmt.Sprintf("fh %d", in.Fh))
	}
	// TODO - FATTR_ATIME_NOW = (1 << 7), FATTR_MTIME_NOW = (1 << 8), FATTR_LOCKOWNER = (1 << 9)
	return fmt.Sprintf("{%s}", strings.Join(s, ", "))
}

func (in *ReleaseIn) string() string {
	return fmt.Sprintf("{Fh %d %s %s L%d}",
		in.Fh, flagString(openFlagNames, int64(in.Flags), ""),
		flagString(releaseFlagNames, int64(in.ReleaseFlags), ""),
		in.LockOwner)
}

func (in *OpenIn) string() string {
	return fmt.Sprintf("{%s}", flagString(openFlagNames, int64(in.Flags), "O_RDONLY"))
}

func (in *OpenOut) string() string {
	return fmt.Sprintf("{Fh %d %s}", in.Fh,
		flagString(fuseOpenFlagNames, int64(in.OpenFlags), ""))
}

func (in *InitIn) string() string {
	return fmt.Sprintf("{%d.%d Ra %d %s}",
		in.Major, in.Minor, in.MaxReadAhead,
		flagString(initFlagNames, int64(in.Flags), ""))
}

func (o *InitOut) string() string {
	return fmt.Sprintf("{%d.%d Ra %d %s %d/%d Wr %d Tg %d MaxPages %d}",
		o.Major, o.Minor, o.MaxReadAhead,
		flagString(initFlagNames, int64(o.Flags), ""),
		o.CongestionThreshold, o.MaxBackground, o.MaxWrite,
		o.TimeGran, o.MaxPages)
}

func (s *FsyncIn) string() string {
	return fmt.Sprintf("{Fh %d Flags %x}", s.Fh, s.FsyncFlags)
}

func (in *SetXAttrIn) string() string {
	return fmt.Sprintf("{sz %d f%o}", in.Size, in.Flags)
}

func (in *GetXAttrIn) string() string {
	return fmt.Sprintf("{sz %d}", in.Size)
}

func (o *GetXAttrOut) string() string {
	return fmt.Sprintf("{sz %d}", o.Size)
}

func (in *AccessIn) string() string {
	return fmt.Sprintf("{u=%d g=%d %s}",
		in.Uid,
		in.Gid,
		flagString(accessFlagName, int64(in.Mask), ""))
}

func (in *FlushIn) string() string {
	return fmt.Sprintf("{Fh %d}", in.Fh)
}

func (o *AttrOut) string() string {
	return fmt.Sprintf(
		"{tA=%gs %v}",
		ft(o.AttrValid, o.AttrValidNsec), &o.Attr)
}

// ft converts (seconds , nanoseconds) -> float(seconds)
func ft(tsec uint64, tnsec uint32) float64 {
	return float64(tsec) + float64(tnsec)*1e-9
}

// Returned by LOOKUP
func (o *EntryOut) string() string {
	return fmt.Sprintf("{n%d g%d tE=%gs tA=%gs %v}",
		o.NodeId, o.Generation, ft(o.EntryValid, o.EntryValidNsec),
		ft(o.AttrValid, o.AttrValidNsec), &o.Attr)
}

func (o *CreateOut) string() string {
	return fmt.Sprintf("{n%d g%d %v %v}", o.NodeId, o.Generation, &o.EntryOut, &o.OpenOut)
}

func (o *StatfsOut) string() string {
	return fmt.Sprintf(
		"{blocks (%d,%d)/%d files %d/%d bs%d nl%d frs%d}",
		o.Bfree, o.Bavail, o.Blocks, o.Ffree, o.Files,
		o.Bsize, o.NameLen, o.Frsize)
}

func (o *NotifyInvalEntryOut) string() string {
	return fmt.Sprintf("{parent i%d sz %d}", o.Parent, o.NameLen)
}

func (o *NotifyInvalInodeOut) string() string {
	return fmt.Sprintf("{i%d [%d +%d)}", o.Ino, o.Off, o.Length)
}

func (o *NotifyInvalDeleteOut) string() string {
	return fmt.Sprintf("{parent i%d ch i%d sz %d}", o.Parent, o.Child, o.NameLen)
}

func (o *NotifyStoreOut) string() string {
	return fmt.Sprintf("{n%d [%d +%d)}", o.Nodeid, o.Offset, o.Size)
}

func (o *NotifyRetrieveOut) string() string {
	return fmt.Sprintf("{> %d: n%d [%d +%d)}", o.NotifyUnique, o.Nodeid, o.Offset, o.Size)
}

func (i *NotifyRetrieveIn) string() string {
	return fmt.Sprintf("{[%d +%d)}", i.Offset, i.Size)
}

func (f *FallocateIn) string() string {
	return fmt.Sprintf("{Fh %d [%d +%d) mod 0%o}",
		f.Fh, f.Offset, f.Length, f.Mode)
}

func (f *LinkIn) string() string {
	return fmt.Sprintf("{Oldnodeid: n%d}", f.Oldnodeid)
}

func (o *WriteOut) string() string {
	return fmt.Sprintf("{%db }", o.Size)

}
func (i *CopyFileRangeIn) string() string {
	return fmt.Sprintf("{Fh %d [%d +%d) => n%d Fh %d [%d, %d)}",
		i.FhIn, i.OffIn, i.Len, i.NodeIdOut, i.FhOut, i.OffOut, i.Len)
}

func (in *InterruptIn) string() string {
	return fmt.Sprintf("{ix %d}", in.Unique)
}

var seekNames = map[uint32]string{
	0: "SET",
	1: "CUR",
	2: "END",
	3: "DATA",
	4: "HOLE",
}

func (in *LseekIn) string() string {
	return fmt.Sprintf("{Fh %d [%s +%d)}", in.Fh,
		seekNames[in.Whence], in.Offset)
}

func (o *LseekOut) string() string {
	return fmt.Sprintf("{%d}", o.Offset)
}

// Print pretty prints FUSE data types for kernel communication
func Print(obj interface{}) string {
	t, ok := obj.(interface {
		string() string
	})
	if ok {
		return t.string()
	}
	return fmt.Sprintf("%T: %v", obj, obj)
}
