//go:build linux

// Copyright 2010 The Go 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 inotify implements a wrapper for the Linux inotify system.

Example:

	watcher, err := inotify.NewWatcher()
	if err != nil {
	    log.Fatal(err)
	}
	err = watcher.Watch("/tmp")
	if err != nil {
	    log.Fatal(err)
	}
	for {
	    select {
	    case ev := <-watcher.Event:
	        log.Println("event:", ev)
	    case err := <-watcher.Error:
	        log.Println("error:", err)
	    }
	}
*/
package inotify

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

// NewWatcher creates and returns a new inotify instance using inotify_init(2)
func NewWatcher() (*Watcher, error) {
	fd, errno := syscall.InotifyInit1(syscall.IN_CLOEXEC | syscall.IN_NONBLOCK)
	if fd == -1 {
		return nil, os.NewSyscallError("inotify_init", errno)
	}
	w := &Watcher{
		fd:         fd,
		notifyFile: os.NewFile(uintptr(fd), ""),
		watches:    make(map[string]*watch),
		paths:      make(map[int]string),
		Event:      make(chan *Event),
		Error:      make(chan error),
		done:       make(chan bool),
	}

	go w.readEvents()
	return w, nil
}

// Close closes an inotify watcher instance
// It sends a message to the reader goroutine to quit and removes all watches
// associated with the inotify instance
func (w *Watcher) Close() error {
	if w.isClosed {
		return nil
	}
	w.isClosed = true
	for path := range w.watches {
		w.RemoveWatch(path)
	}
	close(w.done)
	w.notifyFile.Close()
	return nil
}

// AddWatch adds path to the watched file set.
// The flags are interpreted as described in inotify_add_watch(2).
func (w *Watcher) AddWatch(path string, flags uint32) error {
	if w.isClosed {
		return errors.New("inotify instance already closed")
	}

	watchEntry, found := w.watches[path]
	if found {
		watchEntry.flags |= flags
		flags |= syscall.IN_MASK_ADD
	}

	w.mu.Lock() // synchronize with readEvents goroutine

	wd, err := syscall.InotifyAddWatch(w.fd, path, flags)
	if err != nil {
		w.mu.Unlock()
		return &os.PathError{
			Op:   "inotify_add_watch",
			Path: path,
			Err:  err,
		}
	}

	if !found {
		w.watches[path] = &watch{wd: uint32(wd), flags: flags}
		w.paths[wd] = path
	}
	w.mu.Unlock()
	return nil
}

// Watch adds path to the watched file set, watching all events.
func (w *Watcher) Watch(path string) error {
	return w.AddWatch(path, InAllEvents)
}

// RemoveWatch removes path from the watched file set.
func (w *Watcher) RemoveWatch(path string) error {
	watch, ok := w.watches[path]
	if !ok {
		return fmt.Errorf("can't remove non-existent inotify watch for: %s", path)
	}
	success, errno := syscall.InotifyRmWatch(w.fd, watch.wd)
	if success == -1 {
		// when file descriptor or watch descriptor not found, InotifyRmWatch syscall return EINVAL error
		// if return error, it may lead this path remain in watches and paths map, and no other event can trigger remove action.
		if errno != syscall.EINVAL {
			return os.NewSyscallError("inotify_rm_watch", errno)
		}
	}
	delete(w.watches, path)
	// Locking here to protect the read from paths in readEvents.
	w.mu.Lock()
	delete(w.paths, int(watch.wd))
	w.mu.Unlock()
	return nil
}

// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Event channel
func (w *Watcher) readEvents() {
	var buf [syscall.SizeofInotifyEvent * 4096]byte
	defer func() {
		// Close w.Event first.
		close(w.Event)
		close(w.Error)
	}()

	for {
		// wait until there are events from the kernel
		n, err := w.notifyFile.Read(buf[:])

		// If EOF is received
		if n == 0 {
			return
		}
		if n < 0 {
			select {
			case w.Error <- os.NewSyscallError("read", err):
				continue
			case <-w.done:
				return
			}
		}

		var offset uint32
		// We don't know how many events we just read into the buffer
		// While the offset points to at least one whole event...
		for offset <= uint32(n-syscall.SizeofInotifyEvent) {
			// Point "raw" to the event in the buffer
			raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset]))
			event := new(Event)
			event.Mask = uint32(raw.Mask)
			event.Cookie = uint32(raw.Cookie)
			nameLen := uint32(raw.Len)
			// The raw event received from the kernel contains only the file/folder name, but
			// not the full path. To fix this, and fill the Event.Name field with the complete
			// path of the file/folder, we retrieve it from the "paths" map using the Watch
			// Descriptor (which is the one returned by InotifyAddWatch when we added the
			// file/folder to being monitored).
			w.mu.Lock()
			name, ok := w.paths[int(raw.Wd)]
			w.mu.Unlock()
			if ok {
				event.Name = name
				if nameLen > 0 {
					// Point "bytes" at the first byte of the filename
					bytes := buf[offset+syscall.SizeofInotifyEvent : offset+syscall.SizeofInotifyEvent+nameLen]
					// The filename is padded with NUL bytes. TrimRight() gets rid of those.
					event.Name += "/" + strings.TrimRight(string(bytes), "\000")
				}
				// Send the event on the events channel
				select {
				case w.Event <- event:
				case <-w.done:
					return
				}
			}
			// Move to the next event in the buffer
			offset += syscall.SizeofInotifyEvent + nameLen
		}
	}
}

// String formats the event e in the form
// "filename: 0xEventMask = IN_ACCESS|IN_ATTRIB_|..."
func (e *Event) String() string {
	var events string

	m := e.Mask
	for _, b := range eventBits {
		if m&b.Value == b.Value {
			m &^= b.Value
			events += "|" + b.Name
		}
	}

	if m != 0 {
		events += fmt.Sprintf("|%#x", m)
	}
	if len(events) > 0 {
		events = " == " + events[1:]
	}

	return fmt.Sprintf("%q: %#x%s", e.Name, e.Mask, events)
}

const (
	// Options for inotify_init() are not exported
	// IN_CLOEXEC    uint32 = syscall.IN_CLOEXEC
	// IN_NONBLOCK   uint32 = syscall.IN_NONBLOCK

	// Options for AddWatch

	// InDontFollow : Don't dereference pathname if it is a symbolic link
	InDontFollow uint32 = syscall.IN_DONT_FOLLOW
	// InOneshot : Monitor the filesystem object corresponding to pathname for one event, then remove from watch list
	InOneshot uint32 = syscall.IN_ONESHOT
	// InOnlydir : Watch pathname only if it is a directory
	InOnlydir uint32 = syscall.IN_ONLYDIR

	// The "IN_MASK_ADD" option is not exported, as AddWatch
	// adds it automatically, if there is already a watch for the given path
	// IN_MASK_ADD      uint32 = syscall.IN_MASK_ADD

	// Events

	// InAccess : File was accessed
	InAccess uint32 = syscall.IN_ACCESS
	// InAllEvents : Bit mask for all notify events
	InAllEvents uint32 = syscall.IN_ALL_EVENTS
	// InAttrib : Metadata changed
	InAttrib uint32 = syscall.IN_ATTRIB
	// InClose : Equates to IN_CLOSE_WRITE | IN_CLOSE_NOWRITE
	InClose uint32 = syscall.IN_CLOSE
	// InCloseNowrite : File or directory not opened for writing was closed
	InCloseNowrite uint32 = syscall.IN_CLOSE_NOWRITE
	// InCloseWrite : File opened for writing was closed
	InCloseWrite uint32 = syscall.IN_CLOSE_WRITE
	// InCreate : File/directory created in watched directory
	InCreate uint32 = syscall.IN_CREATE
	// InDelete : File/directory deleted from watched directory
	InDelete uint32 = syscall.IN_DELETE
	// InDeleteSelf : Watched file/directory was itself deleted
	InDeleteSelf uint32 = syscall.IN_DELETE_SELF
	// InModify : File was modified
	InModify uint32 = syscall.IN_MODIFY
	// InMove : Equates to IN_MOVED_FROM | IN_MOVED_TO
	InMove uint32 = syscall.IN_MOVE
	// InMovedFrom : Generated for the directory containing the old filename when a file is renamed
	InMovedFrom uint32 = syscall.IN_MOVED_FROM
	// InMovedTo : Generated for the directory containing the new filename when a file is renamed
	InMovedTo uint32 = syscall.IN_MOVED_TO
	// InMoveSelf : Watched file/directory was itself moved
	InMoveSelf uint32 = syscall.IN_MOVE_SELF
	// InOpen : File or directory was opened
	InOpen uint32 = syscall.IN_OPEN

	// Special events

	// InIsdir : Subject of this event is a directory
	InIsdir uint32 = syscall.IN_ISDIR
	// InIgnored : Watch was removed explicitly or automatically
	InIgnored uint32 = syscall.IN_IGNORED
	// InQOverflow : Event queue overflowed
	InQOverflow uint32 = syscall.IN_Q_OVERFLOW
	// InUnmount : Filesystem containing watched object was unmounted
	InUnmount uint32 = syscall.IN_UNMOUNT
)

var eventBits = []struct {
	Value uint32
	Name  string
}{
	{InAccess, "IN_ACCESS"},
	{InAttrib, "IN_ATTRIB"},
	{InClose, "IN_CLOSE"},
	{InCloseNowrite, "IN_CLOSE_NOWRITE"},
	{InCloseWrite, "IN_CLOSE_WRITE"},
	{InCreate, "IN_CREATE"},
	{InDelete, "IN_DELETE"},
	{InDeleteSelf, "IN_DELETE_SELF"},
	{InModify, "IN_MODIFY"},
	{InMove, "IN_MOVE"},
	{InMovedFrom, "IN_MOVED_FROM"},
	{InMovedTo, "IN_MOVED_TO"},
	{InMoveSelf, "IN_MOVE_SELF"},
	{InOpen, "IN_OPEN"},
	{InIsdir, "IN_ISDIR"},
	{InIgnored, "IN_IGNORED"},
	{InQOverflow, "IN_Q_OVERFLOW"},
	{InUnmount, "IN_UNMOUNT"},
}
