package IndexData::Utils::PersistentCounter;

use 5.010001;
use strict;
use warnings;

use IO::File;


=head1 NAME

IndexData::Utils::PersistentCounter - Perl extension imnlementing persistent counters

=head1 SYNOPSIS

  use IndexData::Utils::PersistentCounter;
  $counter = new IndexData::Utils::PersistentCounter($file, 1);
  $n = $counter->next();
  $n = $counter->next();
  # ...
  $n = $counter->delete();

=head1 DESCRIPTION

This library provides a simple persistent counter class for
maintaining a counter on disk across multiple runs of a program. It is
safe against multiple concurrent accesses (i.e. will not issue the
same value twice to different processes). It can be used for
applications such as generating unique record IDs.

=head1 METHODS

=head2 new()

  $old = new IndexData::Utils::PersistentCounter($file1);
  $new = new IndexData::Utils::PersistentCounter($file2, 1);

Creates a new counter object associated with a file which contains the
persistent state of the counter. The purpose of the counter is to
return consecutive integers on consecutive calls, even if those calls
are made from multiple concurrent processes. The file stores the state
across invocations.

In the usual case (no second argument), the file must already exist;
if it does not, it is not created, but an undefined value is returned.

If a second argument is provided and its value is true, then a new
counter file is created with initial value 1. Note that B<this will
overwrite any existing file>, so use with caution.

=cut

sub new {
    my $class = shift();
    my($file, $create) = @_;

    if (! -f $file) {
	return undef if !$create;
	#   ###	There is a bit of a race condition here, but it's not
	#	something that's going to crop up in real life.
	my $fh = new IO::File(">$file") || return undef;
	$fh->print("1\n");
	$fh->close() or return undef;
    }

    my $this = bless {
	file => $file,
    }, $class;

    return $this;
}


=head2 next()

  $n = $counter->next();

Returns the next available integer from the specified counter, and
increments the counter ready for the next invocation (whether that
invocation is in this process or a different one).

The first call of C<next()> on a newly created counter returns 1, not
0. Each subsequent call returns a value one higher than the previous
call.

=cut

sub next {
    my $this = shift();

    my $fh = new IO::File('+<' . $this->{file}) || return undef;
    flock($fh, 2) || die "can't lock file";
    my $n = <$fh>;
    $fh->seek(0, 0);
    $fh->print($n+1, "\n");
    $fh->close() or return undef;
    return $n+0;
}


=head2 delete()

  $ok = $counter->delete();

Permanently deletes a counter file. Returns true if the deletion was
successful, false otherwise.

=cut

sub delete {
    my $this = shift();

    unlink $this->{file} or return 0;
    return 1;
}


=head1 SEE ALSO

IndexData::Utils

=head1 AUTHOR

Mike Taylor, E<lt>mike@indexdata.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2014 by Index Data.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.4 or,
at your option, any later version of Perl 5 you may have available.


=cut

1;
