# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>

# Implements most Linux::Inotify2 functionality we need in pure Perl
# Anonymous sub support isn't supported since it's expensive in the
# best case and likely leaky in older Perls (e.g. 5.16.3)
package PublicInbox::Inotify3;
use v5.12;
use autodie qw(open);
use PublicInbox::Syscall ();
use Carp;
use Scalar::Util ();

# this fails if undefined no unsupported platforms
use constant $PublicInbox::Syscall::INOTIFY;
our %events;

# extracted from devel/sysdefs-list output, these should be arch-independent
BEGIN {
%events = (
	IN_ACCESS => 0x1,
	IN_ALL_EVENTS => 0xfff,
	IN_ATTRIB => 0x4,
	IN_CLOSE => 0x18,
	IN_CLOSE_NOWRITE => 0x10,
	IN_CLOSE_WRITE => 0x8,
	IN_CREATE => 0x100,
	IN_DELETE => 0x200,
	IN_DELETE_SELF => 0x400,
	IN_DONT_FOLLOW => 0x2000000,
	IN_EXCL_UNLINK => 0x4000000,
	IN_IGNORED => 0x8000,
	IN_ISDIR => 0x40000000,
	IN_MASK_ADD => 0x20000000,
	IN_MODIFY => 0x2,
	IN_MOVE => 0xc0,
	IN_MOVED_FROM => 0x40,
	IN_MOVED_TO => 0x80,
	IN_MOVE_SELF => 0x800,
	IN_ONESHOT => 0x80000000,
	IN_ONLYDIR => 0x1000000,
	IN_OPEN => 0x20,
	IN_Q_OVERFLOW => 0x4000,
	IN_UNMOUNT => 0x2000,
);
} # /BEGIN
use constant \%events;
require PublicInbox::In3Event; # uses %events
require PublicInbox::In3Watch; # uses SYS_inotify_rm_watch

use constant autocancel =>
	(IN_IGNORED|IN_UNMOUNT|IN_ONESHOT|IN_DELETE_SELF);

sub new {
	open my $fh, '+<&=', syscall(SYS_inotify_init1, IN_CLOEXEC);
	bless { fh => $fh }, __PACKAGE__;
}

sub read {
	my ($self) = @_;
	my (@ret, $wd, $mask, $len, $name, $size, $buf);
	my $r = sysread($self->{fh}, my $rbuf, 8192);
	if ($r) {
		while ($r) {
			($wd, $mask, undef, $len) = unpack('lLLL', $rbuf);
			$size = 16 + $len; # 16: sizeof(struct inotify_event)
			substr($rbuf, 0, 16, '');
			$name = $len ? unpack('Z*', substr($rbuf, 0, $len, ''))
					: undef;
			$r -= $size;
			next if $self->{ignore}->{$wd};
			my $ev = bless [$mask, $name], 'PublicInbox::In3Event';
			push @ret, $ev;
			if (my $w = $self->{w}->{$wd}) {
				$ev->[2] = $w;
				$w->cancel if $ev->mask & autocancel;
			} elsif ($mask & IN_Q_OVERFLOW) {
				carp 'E: IN_Q_OVERFLOW, too busy? (non-fatal)'
			} else {
				carp "BUG? wd:$wd unknown (non-fatal)";
			}
		}
	} elsif (defined($r) || ($!{EAGAIN} || $!{EINTR})) {
	} else {
		croak "inotify read: $!";
	}
	delete $self->{ignore};
	@ret;
}

sub fileno { CORE::fileno($_[0]->{fh}) }

sub fh { $_[0]->{fh} }

sub blocking { shift->{fh}->blocking(@_) }

sub watch {
	my ($self, $name, $mask, $cb) = @_;
	croak "E: $cb not supported" if $cb; # too much memory
	my $wd = syscall(SYS_inotify_add_watch, $self->fileno, $name, $mask);
	return if $wd < 0;
	my $w = bless [ $wd, $mask, $name, $self ], 'PublicInbox::In3Watch';
	$self->{w}->{$wd} = $w;
	Scalar::Util::weaken($w->[3]); # ugh
	$w;
}

sub rm_watch {
	my ($self, $wd) = @_;
	delete $self->{w}->{$wd};
	$self->{ignore}->{$wd} = 1; # is this needed?
	syscall(SYS_inotify_rm_watch, $self->fileno, $wd) < 0 ? undef : 1;
}

1;
