The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Path::Extended::Entity;

use strict;
use warnings;
use Carp ();
use File::Spec;
use Log::Dump;
use Scalar::Util qw( blessed );

use overload
  '""'   => sub { shift->path },
  'cmp'  => sub { return "$_[0]" cmp "$_[1]" },
  'bool' => sub { shift->_boolify },
  '*{}'  => sub { shift->_handle };

sub new {
  my $class = shift;
  my $self  = bless {}, $class;

  $self->_initialize(@_) or return;

  $self;
}

sub _initialize {1}
sub _boolify {1}

sub _class {
  my ($self, $type) = @_;
  my $class = ref $self;
  $class =~ s/::(?:File|Dir|Entity)$//;
  return $class unless $type;
  return $class.'::'.($type eq 'file' ? 'File' : 'Dir');
}

sub _set_path {
    my ($self, $path) = @_;
    $self->{input_path} = $self->_unixify($path);
    $self->{abs_path}   = $self->_unixify( File::Spec->rel2abs($path) );

    # respect setting of _attribute when already done
    $self->{_stringify_absolute} ||= File::Spec->file_name_is_absolute($path);
}

sub _related {
  my ($self, $type, @parts) = @_;

  my $class = $self->_class($type);
  eval "require $class" or Carp::croak $@;
  my $item;
  if ( @parts && $parts[0] eq '..' ) { # parent
    require File::Basename;
    $item = $class->new( File::Basename::dirname($self->_absolute) );
  }
  elsif ( @parts && File::Spec->file_name_is_absolute($parts[0]) ) {
    $item = $class->new( @parts );
  }
  else {
    $item = $class->new( $self->_absolute, @parts );
  }
  foreach my $key ( grep /^_/, keys %{ $self } ) {
    $item->{$key} = $self->{$key};
  }
  $item;
}

sub _unixify {
  my ($self, $path) = @_;

  $path =~ s{\\}{/}g if $^O eq 'MSWin32';

  return $path;
}

sub _handle { shift->{handle} }

sub _stringify_absolute {
  my $self = shift;
  $self->{_stringify_absolute} && !$self->{_base} ? 1 : '';
}

# returns the string version of the path
sub path {
  my $self = shift;
  return ( $self->_stringify_absolute ) ? $self->_absolute : $self->_relative;
}

sub stringify { shift->path }

sub is_dir      { shift->{is_dir} }
sub is_open     { shift->{handle} ? 1 : 0 }


sub is_absolute {
  my $self = shift;
  File::Spec->file_name_is_absolute($self->{input_path});
}

sub resolve {
  my $self = shift;
  Carp::croak "$self: $!" unless -e $self->{abs_path};
  # WoP :
  # Cwd::realpath returns the resolved absolute path
  # calling File::Spec->file_name_is_absolute() not necessary
  $self->{abs_path}  = $self->_unixify(Cwd::realpath($self->{abs_path}));
  $self->{_stringify_absolute} = File::Spec->file_name_is_absolute($self->{abs_path});
  $self;
}

sub _absolute {
  my ($self, %options) = @_;

  my $path = File::Spec->canonpath( $self->{abs_path} );
  if ( $options{native} ) {
    return $path;
  }
  elsif ( $self->{_compat} ) {
    my ($vol, @parts) = File::Spec->splitpath( $path );
    $vol = '' if $Path::Extended::IgnoreVolume;
    return $self->_unixify( File::Spec->catpath($vol, File::Spec->catdir( @parts ), '') );
  }
  else {
    return $self->_unixify($path);
  }
}

sub _relative {
  my $self = shift;
  my $base = @_ % 2 ? shift : undef;
  my %options = @_;

  $base ||= $options{base} || $self->{_base};

  my $path = File::Spec->abs2rel( $self->{abs_path}, $base );
     $path = $self->_unixify($path) unless $options{native};

  $path;
}

sub absolute { shift->_absolute(@_) }
sub relative { shift->_relative(@_) }

sub parent { shift->_related( dir => '..' ); }

sub unlink {
  my $self = shift;

  $self->close if $self->is_open;
  unlink $self->_absolute if $self->exists;
}

sub exists {
  my $self = shift;

  -e $self->_absolute ? 1 : 0;
}

sub is_writable {
  my $self = shift;

  -w $self->_absolute ? 1 : 0;
}

sub is_readable {
  my $self = shift;

  -r $self->_absolute ? 1 : 0;
}

sub copy_to {
  my ($self, $destination) = @_;

  unless ( $destination ) {
    $self->log( fatal => 'requires destination' );
    return;
  }

  my $class = ref $self;
  $destination = $class->new( "$destination" );

  require File::Copy::Recursive;
  File::Copy::Recursive::rcopy( $self->_absolute, $destination->_absolute )
    or do { $self->log( error => "Can't copy $self to $destination: $!" ); return; };

  $self;
}

sub move_to {
  my ($self, $destination) = @_;

  unless ( $destination ) {
    $self->log( fatal => 'requires destination' );
    return;
  }

  my $class = ref $self;
  $destination = $class->new( "$destination" );

  $self->close if $self->is_open;

  require File::Copy::Recursive;
  File::Copy::Recursive::rmove( $self->_absolute, $destination->_absolute )
    or do { $self->log( error => "Can't move $self to $destination: $!" ); return; };

  $self->{abs_path} = $destination->_absolute;

  $self;
}

sub rename_to {
  my ($self, $destination) = @_;

  unless ( $destination ) {
    $self->log( fatal => 'requires destination' );
    return;
  }

  my $class = ref $self;
  $destination = $class->new( "$destination" );

  $self->close if $self->is_open;

  rename $self->_absolute => $destination->_absolute
    or do { $self->log( error => "Can't rename $self to $destination: $!" ); return; };

  $self->{abs_path} = $destination->_absolute;

  $self;
}

sub stat {
  my $self = shift;

  require File::stat;
  File::stat::stat( $self->{handle} || $self->{abs_path} );
}

sub lstat {
  my $self = shift;

  require File::stat;
  File::stat::lstat( $self->{handle} || $self->{abs_path} );
}

1;

__END__

=head1 NAME

Path::Extended::Entity

=head1 SYNOPSIS

  use Path::Extended::File;
  my $file = Path::Extended::File->new('path/to/some.file');

=head1 DESCRIPTION

This is a base class for L<Path::Extended::File> and L<Path::Extended::Dir>.

=head1 METHODS

=head2 new

creates an appropriate object. Note that this base class itself doesn't hold anything.

=head2 absolute

may take an optional hash, and returns an absolute path of the file/directory. Note that back slashes in the path will be converted to forward slashes unless you explicitly set a C<native> option to true.

=head2 relative

may take an optional hash, and returns a relative path of the file/directory (compared to the current directory (Cwd::cwd) by default, but you may change this bahavior by passing a C<base> option). Note that back slashes in the path will be converted to forward slashes unless you explicitly set a C<native> option to true.

=head2 is_absolute

returns if the path you passed to the constructor was absolute or not (note that the path stored in an object is always absolute).

=head2 is_dir

returns if the object represents directory or not.

=head2 resolve

does a physical cleanup of the path with L<Cwd::realpath>, that means, resolves a symbolic link if necessary. Note that this method may croak (when the path does not exist).

=head2 copy_to

copies the file/directory to the destination by C<File::Copy::copy>.

=head2 move_to

moves the file/directory to the destination by C<File::Copy::move>. If the file/directory is open, it'll automatically close.

=head2 rename_to

renames the file/directory. If the file/directory is open, it'll automatically close. If your OS allows rename of an open file, you may want to use built-in C<rename> function for better atomicity.

=head2 unlink

unlinks the file/directory. The same thing can be said as for the C<rename_to> method.

=head2 exists

returns true if the file/directory exists.

=head2 is_readable, is_writable

returns true if the file/directory is readable/writable.

=head2 is_open

returns true if the file/directory is open.

=head2 stat, lstat

returns a File::stat object for the file/directory.

=head2 parent

returns a L<Path::Extended::Dir> object that points to the parent directory of the file/directory.

=head2 path, stringify

explicitly returns a path string.

=head2 log, logger, logfile, logfilter

You may optionally pass a logger object with C<log> method that accepts C<< ( label => @log_messages ) >> array arguments to notifty when some (usually unfavorable) thing occurs. By default, a built-in L<Carp> logger will be used. If you want to disable logging, set a false value to C<logger>. See L<Log::Dump> for details, and for how to use C<logfile> and C<logfilter> methods.

=head1 SEE ALSO

L<Path::Extended>, L<Path::Extended::File>, L<Path::Extended::Dir>, L<Log::Dump>

=head1 AUTHOR

Kenichi Ishigaki, E<lt>ishigaki@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008 by Kenichi Ishigaki.

This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=cut