# $Id: Object.pm 59 2006-07-20 11:56:33Z shlomif $

#- Olivier Thauvin <olivier.thauvin@aerov.jussieu.fr>

# This program is free software, distributed under the same terms as 
# Parrot.

package File::Find::Object;

use strict;
use warnings;

use Carp;

use File::Find::Object::internal;

our $VERSION = '0.0.4';

sub new {
    my ($class, $options, @files) = @_;
    my $tree = {
        files => [ @files ],
        ind => -1,
        
        depth => $options->{depth},
        nocrossfs => $options->{nocrossfs},
        followlink => $options->{followlink},
        filter => $options->{filter},
        callback => $options->{callback},
        _dir_stack => [],
    };
    bless($tree, $class);
}

sub _dir_stack
{
    my $self = shift;
    if (@_)
    {
        $self->{_dir_stack} = shift;
    }
    return $self->{_dir_stack};
}

sub _curr_file
{
    my $self = shift;

    if (@_)
    {
        $self->{_curr_file} = shift;
    }

    return $self->{_curr_file};
}

sub DESTROY {
    my ($self) = @_;
#    print STDERR join(" ", caller)."\n";
#    printf STDERR "destroy `%s'\n", $self->{dir} || "--";
}

sub _current
{
    my $self = shift;
    return $self->_dir_stack()->[-1] || $self;
}

sub next {
    my ($self) = @_;
    while (1) {
        my $current = $self->_current();
        $self->_process_current($current) and return $self->{item} = $self->current_path($current);
        $current = $self->_current();
        if(!$self->movenext) {
            $self->me_die($current) and return $self->{item} = undef;
        }
    }
}

sub item {
    my ($self) = @_;
    $self->{item}
}

sub _father
{
    my ($self, $current) = @_;

    if (!defined($current->{idx}))
    {
        return undef;
    }
    elsif ($current->{idx} >= 1)
    {
        return $self->_dir_stack()->[$current->{idx}-1];
    }
    else
    {
        return $self;
    }
}

sub _movenext_with_current
{
    my $self = shift;
    if ($self->_current->_curr_file(
            shift(@{$self->_father($self->_current)->{_files}})
       ))
    {
        $self->_current->{_action} = {};
        return 1;
    } else {
        return 0;
    }
}

sub _movenext_wo_current
{
    my $self = shift;

    $self->{ind} > @{$self->{files}} and return;
    $self->{ind}++;
    $self->_curr_file(${$self->{files}}[$self->{ind}]);
    $self->{_action} = {};
    1;
}

sub movenext {
    my ($self) = @_;
    if (@{$self->_dir_stack()})
    {
        return $self->_movenext_with_current();
    }
    else
    {
        return $self->_movenext_wo_current();
    }
}

sub me_die {
    if (@_ != 2)
    {
        confess "Hello";
    }
    my ($self, $current) = @_;

    if ($self eq $current)
    {
        return 1;
    }

    $self->become_default($self->_father($current));
    return 0;
}

sub become_default
{
    my ($self, $current) = @_;

    if ($self eq $current)
    {
        @{$self->_dir_stack()} = ();
    }
    else
    {
        while (scalar(@{$self->_dir_stack()}) != $current->{idx} + 1)
        {
            pop(@{$self->_dir_stack()});
        }
    }

    return 0;
}

# Return true if there is somthing next
sub _process_current {
    my ($self, $current) = @_;
   
    $current->_curr_file() or return 0;

    $self->isdot($current) and return 0;
    $self->filter($current) or return 0;  

    foreach ($self->{depth} ? qw/b a/ : qw/a b/) {
        if ($current->{_action}{$_}) {
            next;
        }
        $current->{_action}{$_} = 1;
        if($_ eq 'a') {
            if ($self->{callback}) {
                $self->{callback}->($self->current_path($current));
            }
            return 1;
        }
            
        if ($_ eq 'b') {
            $self->check_subdir($current) or next;
            push @{$self->_dir_stack()}, 
                File::Find::Object::internal->new(
                    $self,
                    $current, 
                    scalar(@{$self->_dir_stack()})
                );
            return 0;
        }
    }
    return 0;
}

sub isdot
{
    my ($self, $current) = @_;

    my $file = $current->_curr_file();

    return ($file eq ".." || $file eq ".");
}

sub filter {
    my ($self, $current) = @_;
    return defined($self->{filter}) ?
        $self->{filter}->($self->current_path($current)) :
        1;
}

sub check_subdir 
{
    my ($self, $current) = @_;

    if ($self eq $current)
    {
        return 1;
    }
    my @st = stat($self->current_path($current));
    if (!-d _)
    {
        return 0;
    }
    if (-l $self->current_path($current) && !$self->{followlink})
    {
        return 0;
    }
    if ($st[0] != $self->_father($current)->{dev} && $self->{nocrossfs})
    {
        return 0;
    }
    my $ptr = $current; my $rc;
    while($self->_father($ptr)) {
        if($self->_father($ptr)->{inode} == $st[1] && $self->_father($ptr) == $st[0]) {
            $rc = 1;
            last;
        }
        $ptr = $self->_father($ptr);
    }
    if ($rc) {
        printf(STDERR "Avoid loop " . $self->_father($ptr)->{dir} . " => %s\n",
            $self->current_path($current));
        return 0;
    }
    return 1;
}

sub current_path {
    my ($self, $current) = @_;

    if ($self eq $current)
    {
        return $self->_curr_file;
    }

    my $p = $self->_father($current)->{dir};
    $p =~ s!/+$!!; #!
    $p .= '/' . $current->_curr_file;

    return $p;
}

sub open_dir {
    my ($self) = @_;
    opendir(my $handle, $self->{dir}) or return undef;
    $self->{_files} =
        [ sort { $a cmp $b } File::Spec->no_upwards(readdir($handle)) ];
    closedir($handle);
    my @st = stat($self->{dir});
    $self->{inode} = $st[1];
    $self->{dev} = $st[0];
    return 1;
}

__END__

=head1 NAME

File::Find::Object - An object oriented File::Find replacement

=head1 SYNOPSIS

    use File::Find::Object;
    my $tree = File::Find::Object->new({}, @dir);

    while (my $r = $tree->next()) {
        print $r ."\n";
    }

=head1 DESCRIPTION

File::Find::Object does same job as File::Find but works like an object and 
with an iterator. As File::Find is not object oriented, one cannot perform
multiple searches in the same application. The second problem of File::Find 
is its file processing: after starting its main loop, one cannot easilly wait 
for another event an so get the next result.

With File::Find::Object you can get the next file by calling the next() 
function, but setting a callback is still possible.

=head1 FUNCTIONS

=head2 new

    my $ffo = File::Find::Object->new( { options }, @files);

Create a new File::Find::Object object. @files is the list of directories
- or files - the object should explore.

=head3 options

=over 4

=item depth

Boolean - returns the directory content before the directory itself.

=item nocrossfs

Boolean - doesn't continue on filesystems different than the parent.

=item followlink

Boolean - follow symlinks when they point to a directory.

You can safely set this option to true as File::Find::Object does not follow 
the link if it detects a loop.

=item filter

Function reference - should point to a function returning TRUE or FALSE. This 
function is called with the filename to filter, if the function return FALSE, 
the file is skipped.

=item callback

Function reference - should point to a function, which would be called each 
time a new file is returned. The function is called with the current filename 
as an argument.

=back

=head2 next

Returns the next file found by the File::Find::Object. It returns undef once
the scan is completed.

=head2 item

Returns the current filename found by the File::Find::Object object, i.e: the
last value returned by next().

=head1 BUGS

Currently works only on UNIX as it uses '/' as a path separator.

=head1 SEE ALSO

L<File::Find>

=cut

