package Workflow::Inotify::Handler;

use strict;
use warnings;

use Config::IniFiles;
use Data::Dumper;
use List::Util qw(any);
use Linux::Inotify2;

__PACKAGE__->follow_best_practice;
__PACKAGE__->mk_accessors(qw(config events masks));

use parent qw(Exporter Class::Accessor::Fast);

use Readonly;

Readonly our $TRUE  => 1;
Readonly our $FALSE => 0;

our @EXPORT_OK = qw(boolean %EVENTS %MASKS);

our $VERSION = '1.0.7';  ## no critic (RequireInterpolation)

our %EVENTS = (
  IN_ACCESS        => IN_ACCESS,
  IN_ATTRIB        => IN_ATTRIB,
  IN_CLOSE_WRITE   => IN_CLOSE_WRITE,
  IN_CLOSE_NOWRITE => IN_CLOSE_NOWRITE,
  IN_CREATE        => IN_CREATE,
  IN_DELETE        => IN_DELETE,
  IN_DELETE_SELF   => IN_DELETE_SELF,
  IN_MODIFY        => IN_MODIFY,
  IN_MOVE_SELF     => IN_MOVE_SELF,
  IN_MOVED_FROM    => IN_MOVED_FROM,
  IN_MOVED_TO      => IN_MOVED_TO,
  IN_OPEN          => IN_OPEN,
);

our %MASKS = reverse %EVENTS;

for ( keys %EVENTS ) {
  $EVENTS{IN_ALL} |= $EVENTS{$_};
}

########################################################################
sub boolean {
########################################################################
  my ( $value, @default ) = @_;

  my $default_value;

  if (@default) {
    $default_value = $default[0];
  }

  $value =~ s/\s*([^ ]+)\s*/$1/xsm;

  return $default_value
    if !defined $value
    && defined $default_value;

  return $FALSE
    if !defined $value || any { $value eq $_ } qw( 0 false off no );

  return $TRUE
    if any { $value eq $_ } qw( 1 true on yes );

  die "invalid value ($value) for boolean variable";
}

########################################################################
sub new {
########################################################################
  my ( $class, $config ) = @_;

  $class = ref($class) || $class;

  if ( $config && !ref $config ) {
    if ( -e $config ) {
      $config = Config::IniFiles->new( -file => $config );
    }
  }

  my $self = $class->SUPER::new(
    { config => $config,
      events => \%EVENTS,
      masks  => \%MASKS
    }
  );

  $self->get_app_config();

  return $self;
}

########################################################################
sub get_app_config {
########################################################################
  my ($self) = @_;

  my $section_name = ref $self;
  $section_name =~ s/::/_/xsmg;

  my $config = $self->get_config;
  $section_name = lc $section_name;

  return
    if !$config->SectionExists($section_name);

  my %section_config;

  foreach ( $self->get_config->Parameters($section_name) ) {
    $section_config{$_} = $self->get_config->val( $section_name => $_ );
  }

  my @extra_vars = keys %section_config;

  if (@extra_vars) {
    no strict 'refs';  ## no critic (ProhibitNoStrict)

    for (@extra_vars) {
      die "attempt to redefine $_\n"
        if defined *{ ref($self) . q{::} . "get_$_" }{CODE};
    }

    $self->mk_accessors(@extra_vars);

    for (@extra_vars) {
      $self->set( $_, $section_config{$_} );
    }
  }

  return $self;
}

########################################################################
sub handler {
########################################################################
  my ( $self, $event ) = @_;

  return print {*STDERR} sprintf "event: %s file: %s\n", $event->mask, $event->fullname;
}

1;

## no critic (RequirePodSections)

__END__

=pod

=head1 NAME

Workflow::Inotify::Handler - base class for creating
L<Linux::Inotify2> handlers

=head1 SYNOPSIS

 package MyHandler;

 use parent qw(Workflow::Inotify::Handler);

 sub handler {
   my ($self, $e) = @_;

   print {*STDERR} sprintf("event: %s name: %s\n", $e->mask, $e->fullname); 
 }

 1;

=head1 DESCRIPTION

Base class for creating C<Linux::Inotify2> event handlers.  You can
use this base class to implement a handler that responds to events
generated by I<inotify> events. Your event handlers can do pretty much
anything they want, including C<fork()> a new process.  In general
however, you want your handlers to be fast and lightweight.

A typical implementation will unload, possibly filter or interpret,
then queue the event for another process to handle.  This technique
encourages a high degree of decoupling of your architecture and
ensures your handlers can process all events.

Note that using C<Workflow::Inotify::Handler> as a base class is not
strictly required when using the C<inotify.pl> script...as long as
your class contains a C<handler()> method. What you get from this
class is an object that sub-classes L<Class::Accessor::Fast> and
creates accessors for all your configuration values.

=head2 new

 new( config, [app config] )

The class is instantiated by the C<inotify.pl> script and is passed a
L<Config::IniFiles> object. Override the C<handler()> or the C<new()>
method if you choose.

I<HINT>: You can add additional application specific values to the
configuration file and access their values using the
C<Config::IniFiles> object passed in the constructor.

 my $user_name = $self->get_config()->val(application => 'user_name');

I<BONUS>: If you add a section to the configuration file using the
canonical form (replace all ':: with '_') of your handler's name the
configuration variables defined in that section will be used by the
default C<new()> method to create accessors for each value. Accessors
are named using the best practice of separate setters and getters for
each value (e.g. C<get_foo()>, C<set_foo()>).

 [workflow_s3_uploader]
 bucket        = foo
 bucket_prefix = /biz
 region        = us-east-1
 host          = s3.amazonaws.com

...

 sub handler {
   my ($event) = @_;

   my $host = $self->get_host();
   ...
 }

=head1 INOTIFY EVENTS

I<....from C<man 7 inotify>...>

The inotify_add_watch(2) mask argument and the mask
field of the inotify_event structure returned when read(2)ing an
inotify file descriptor are both bit masks identifying inotify events.
The following bits can be specified in mask when calling
inotify_add_watch(2) and may be returned in the mask field returned by
read(2):

=over 

=item * IN_ACCESS (+)

File was accessed (e.g., read(2), execve(2)).

=item * IN_ATTRIB (*)

Metadata changed for example, permissions (e.g., chmod(2)), timestamps
(e.g., utimensat(2)), extended attributes (setxattr(2)), link count
(since Linux 2.6.25; e.g., for the target of link(2) and for
unlink(2)), and user/group ID (e.g., chown(2)).

=item * IN_CLOSE_WRITE (+)

File opened for writing was closed.

=item * IN_CLOSE_NOWRITE (*)

File or directory not opened for writing was closed.

=item * IN_CREATE (+)

File/directory created in watched directory (e.g., open(2) O_CREAT,
mkdir(2), link(2), symlink(2), bind(2) on a UNIX domain socket).

=item * IN_DELETE (+)

File/directory deleted from watched directory.

=item * IN_DELETE_SELF

Watched file/directory was itself deleted.  (This event also occurs if
an object is moved to another filesystem, since mv(1) in effect copies
the file to the other filesystem and then deletes it from the original
filesystem.)  In addition, an=item * IN_IGNORED event will
subsequently begenerated for the watch descriptor.

=item * IN_MODIFY (+)

File was modified (e.g., write(2), truncate(2)).

=item * IN_MOVE_SELF

Watched file/directory was itself moved.

=item * IN_MOVED_FROM (+)

Generated for the directory containing the old filename when a file is renamed.

=item * IN_MOVED_TO (+)

Generated for the directory containing the new filename when a file is renamed.

=item * IN_OPEN (*)

File or directory was opened.

=back

When monitoring a directory:

=over

=item * the events marked above with an asterisk (*) can occur both
for the directory itself and for objects inside the directory; and

=item * the events marked with a plus sign (+) occur only for objects
inside the directory (not for the directory itself).

=back

=head2 boolean

 boolean(value)

Return a boolean value by converting anything that smells like a boolean.

 1, 0
 on, off
 true, false
 yes, no

=head2 handler

 handler( event )

=over 5

=item event

An instance of L<Linux::Inotify::Event>.  See L<Linux::Inotify2>

=back

=head2 get_config

 get_config()

Returns a L<Config::IniFiles> object initialized from your
configuration file.

=head1 AUTHOR

Rob Lauer - <rlauer6@comcast.net>

=head1 SEE ALSO

L</Linux::Inotify2>

=cut
