# Declare our package
package POE::Component::Fuse::AsyncFsV;
use strict; use warnings;

# Initialize our version
use vars qw( $VERSION );
$VERSION = '0.04';

# load the aio helper
use POE::Component::AIO { no_auto_bootstrap => 1, no_auto_export => 1 };

# we need access to the kernel
use POE;

# constants we need to interact with FUSE
use Errno qw( :POSIX );   	# ENOENT EISDIR etc
use Fcntl qw( :DEFAULT :mode );	# S_IFREG S_IFDIR, O_SYNC O_LARGEFILE etc

# get the refaddr of our FHs
use Scalar::Util qw( openhandle );

# creates a new instance
sub new {
	my $class = shift;
	my $fsv   = shift;

	# init PoCo-AIO
	POE::Component::AIO->new();

	# Create our obj and return it
	return bless {
		'fsv'	=> $fsv,
		'fhmap'	=> {},
	}, $class;
}

# shutdown PoCo-AIO
sub DESTROY {
	POE::Component::AIO->new()->shutdown();

	return;
}

# simple accessor
sub fsv {
	return shift->{'fsv'};
}

# simple callback that returns 0 for FUSE
sub _cb {
	my( $self, $name ) = @_;

	# make our custom wrapper
	my $cb = $poe_kernel->get_active_session->postback( 'reply', $name );
	my $callback = sub {
		if ( defined $_[0] ) {
			# IO::AIO returns 0 as successful...
			if ( $_[0] == 0 ) {
				# FUSE needs 0 as retval
				$cb->( 0 );
			} else {
				# error code
				$cb->( $_[0] );
			}
		} else {
			$cb->( -EINVAL() );
		}
	};

	return $callback;
}

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

	# make a special wrapper
	my $cb = $poe_kernel->get_active_session->postback( 'reply', 'getattr' );
	my $callback = sub {
		if ( defined $_[0] ) {
			if ( ref $_[0] ) {
				$cb->( @{ $_[0] } );
			} else {
				# error code
				$cb->( $_[0] );
			}
		} else {
			# not found
			$cb->( -ENOENT() );
		}
	};

	$self->fsv->stat( $path, $callback );
	return;
}

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

	# make a special wrapper
	my $cb = $poe_kernel->get_active_session->postback( 'reply', 'getdir' );
	my $callback = sub {
		if ( defined $_[0] ) {
			if ( ref $_[0] ) {
				# FUSE needs 0 at the end of list to signify success
				$cb->( @{ $_[0] }, 0 );
			} else {
				# error code or empty list = 0
				$cb->( $_[0] );
			}
		} else {
			$cb->( -EINVAL() );
		}
	};

	$self->fsv->readdir( $path, $callback );
	return;
}

sub getxattr {
	my ( $self, $context, $path, $attr ) = @_;

	# we don't have any extended attribute support
	$poe_kernel->yield( 'reply', [ 'getxattr' ], [ 0 ] );
	return;
}

sub setxattr {
	my ( $self, $context, $path, $attr, $value, $flags ) = @_;

	# we don't have any extended attribute support
	$poe_kernel->yield( 'reply', [ 'setxattr' ], [ -ENOSYS() ] );
	return;

}

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

	# we don't have any extended attribute support
	$poe_kernel->yield( 'reply', [ 'listxattr' ], [ 0 ] );
	return;
}

sub removexattr {
	my ( $self, $context, $path, $attr ) = @_;

	# we don't have any extended attribute support
	$poe_kernel->yield( 'reply', [ 'removexattr' ], [ -ENOSYS() ] );
	return;
}

sub open {
	my ( $self, $context, $path, $flags ) = @_;

	# make a special wrapper
	my $cb = $poe_kernel->get_active_session->postback( 'reply', 'open', $context );
	my $callback = sub {
		my $fh = shift;
		if ( defined $fh ) {
			if ( openhandle( $fh ) ) {
				# arg, generate our own ID
				my $id = 0;
				1 while exists $self->{'fhmap'}->{ ++$id };
				$self->{'fhmap'}->{ $id } = $fh;
				$context->{'fh'} = $id;
				$cb->( 0 );
			} else {
				# error code
				$cb->( $fh );
			}
		} else {
			$cb->( -EIO() );
		}
	};

	# figure out the mode and set it
	# FIXME we use 0666 as default, should we change this or something?
	my $mode = 0;
	if ( $flags & O_CREAT ) {
		$mode = oct( 666 );
	}

	# actually open it!
	$self->fsv->open( $path, $flags, $mode, $callback );
	return;
}

sub read {
	my ( $self, $context, $path, $len, $offset ) = @_;

	# get the fh
	my $fh = undef;
	if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
		$fh = $self->{'fhmap'}->{ $context->{'fh'} };
	}

	if ( ! defined $fh ) {
		$poe_kernel->yield( 'reply', [ 'read' ], [ -EINVAL() ] );
	} else {
		# make our custom wrapper
		my $cb = $poe_kernel->get_active_session->postback( 'reply', 'read' );
		my $buf = '';
		my $callback = sub {
			if ( ! defined $_[0] ) {
				$cb->( -EIO() );
			} else {
				$cb->( $buf );
			}
		};

		# aio_read $fh,$offset,$length, $data,$dataoffset, $callback->($retval)
		$self->fsv->read( $fh, $offset, $len, $buf, 0, $callback );
	}

	return;
}

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

	# get the fh
	my $fh = undef;
	if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
		$fh = $self->{'fhmap'}->{ $context->{'fh'} };
	}

	if ( ! defined $fh ) {
		$poe_kernel->yield( 'reply', [ 'flush' ], [ -EINVAL() ] );
	} else {
		# we map flush to fsync and hope all will be well :)
		$self->fsv->fsync( $fh, $self->_cb( 'flush' ) );
	}

	return;
}

sub release {
	my ( $self, $context, $path, $flags ) = @_;

	# get the fh
	my $fh = undef;
	if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
		$fh = $self->{'fhmap'}->{ $context->{'fh'} };
	}

	# FUSE sometimes calls release multiple times per file
	if ( ! defined $fh ) {
		# assume success
		$poe_kernel->yield( 'reply', [ 'release' ], [ 0 ] );
	} else {
		# make our custom wrapper
		my $cb = $poe_kernel->get_active_session->postback( 'reply', 'release' );
		my $callback = sub {
			# IO::AIO returns 0 as successful close...
			if ( $_[0] == 0 ) {
				# get rid of our mapping
				if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
					delete $self->{'fhmap'}->{ $context->{'fh'} };
				}

				# FUSE needs 0 as retval
				$cb->( 0 );
			} else {
				$cb->( $_[0] );
			}
		};

		$self->fsv->close( $fh, $callback );
	}

	return;
}

sub truncate {
	my ( $self, $context, $path, $offset ) = @_;

	$self->fsv->truncate( $path, $offset, $self->_cb( 'truncate' ) );
	return;
}

sub write {
	my ( $self, $context, $path, $buffer, $offset ) = @_;

	# get the fh
	my $fh = undef;
	if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
		$fh = $self->{'fhmap'}->{ $context->{'fh'} };
	}

	if ( ! defined $fh ) {
		$poe_kernel->yield( 'reply', [ 'write' ], [ -EINVAL() ] );
	} else {
		# make a special wrapper
		my $cb = $poe_kernel->get_active_session->postback( 'reply', 'write' );
		my $callback = sub {
			if ( defined $_[0] ) {
				$cb->( $_[0] );
			} else {
				# error
				$cb->( -EIO() );
			}
		};

		# aio_write $fh,$offset,$length, $data,$dataoffset, $callback->($retval)
		$self->fsv->write( $fh, $offset, length( $buffer ), $buffer, 0, $callback );
	}

	return;
}

sub mknod {
	my ( $self, $context, $path, $modes, $device ) = @_;

	$self->fsv->mknod( $path, $modes, $device, $self->_cb( 'mknod' ) );
	return;
}

sub mkdir {
	my ( $self, $context, $path, $modes ) = @_;

	$self->fsv->mkdir( $path, $modes, $self->_cb( 'mkdir' ) );
	return;
}

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

	$self->fsv->unlink( $path, $self->_cb( 'unlink' ) );
	return;
}

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

	$self->fsv->rmdir( $path, $self->_cb( 'rmdir' ) );
	return;
}

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

	$self->fsv->symlink( $path, $symlink, $self->_cb( 'symlink' ) );
	return;
}

sub rename {
	my ( $self, $context, $path, $newpath ) = @_;

	$self->fsv->rename( $path, $newpath, $self->_cb( 'rename' ) );
	return;
}

sub link {
	my ( $self, $context, $path, $hardlink ) = @_;

	$self->fsv->link( $path, $hardlink, $self->_cb( 'link' ) );
	return;
}

sub chmod {
	my ( $self, $context, $path, $modes ) = @_;

	$self->fsv->chmod( $path, $modes, $self->_cb( 'chmod' ) );
	return;
}

sub chown {
	my ( $self, $context, $path, $uid, $gid ) = @_;

	$self->fsv->chown( $path, $uid, $gid, $self->_cb( 'chown' ) );
	return;
}

sub utime {
	my ( $self, $context, $path, $atime, $mtime ) = @_;

	$self->fsv->utime( $path, $atime, $mtime, $self->_cb( 'utime' ) );
	return;
}

sub fsync {
	my ( $self, $context, $path, $fsync_mode ) = @_;

	# get the fh
	my $fh = undef;
	if ( exists $self->{'fhmap'}->{ $context->{'fh'} } ) {
		$fh = $self->{'fhmap'}->{ $context->{'fh'} };
	}

	if ( ! defined $fh ) {
		$poe_kernel->yield( 'reply', [ 'fsync' ], [ -EINVAL() ] );
	} else {
		# interesting, ::Async don't care about $fsync_mode...
		$self->fsv->fsync( $fh, $self->_cb( 'fsync' ) );
	}

	return;
}

sub statfs {
	my( $self, $context ) = @_;

	# FIXME fake the data...
	# $namelen, $files, $files_free, $blocks, $blocks_avail, $blocksize
	$poe_kernel->yield( 'reply', [ 'statfs' ], [ 255, 1, 1, 1, 1, 2 ] );
	return;
}

1;
__END__

=head1 NAME

POE::Component::Fuse::AsyncFsV - Wrapper for Filesys::Virtual::Async

=head1 SYNOPSIS

  Please do not use this module directly.

=head1 ABSTRACT

Please do not use this module directly.

=head1 DESCRIPTION

This module is responsible for "wrapping" L<Filesys::Virtual::Async> objects and making them communicate properly
with the FUSE API that L<POE::Component::Fuse> exposes. Please do not use this module directly.

=head1 EXPORT

None.

=head1 SEE ALSO

L<POE::Component::Fuse>

L<Filesys::Virtual::Async>

=head1 AUTHOR

Apocalypse E<lt>apocal@cpan.orgE<gt>

Props goes to xantus who wrote the L<Filesys::Virtual::Async> module!

Inspiration was taken from the L<Fuse::Filesys::Virtual> module too.

=head1 COPYRIGHT AND LICENSE

Copyright 2009 by Apocalypse

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

=cut
