use strict;
use 5.010;

package FusqlFS;

=head1 NAME

FusqlFS - FUSE filesystem to work with database via DBI interface

=head1 SYNOPSIS

    use FusqlFS;

    FusqlFS::init(
        engine   => 'PgSQL',
        host     => 'localhost',
        database => 'postgres',
        user     => 'postgres',
        password => 'pas$w0rd',
        limit    => 100,
        debug    => 2,
        threaded => 0,
    );

    FusqlFS::mount(
        '/path/to/mount/point',
        'allow_other',
    );

=head1 DESCRIPTION

This package provides basic interface to FusqlFS to initialize L<DBI> backend
and mount it with L<Fuse>.

=head1 SUBROUTINES

=over

=cut

use POSIX qw(:fcntl_h :errno_h mktime);
use Fcntl qw(:mode);
use Carp;
use Fuse;

use FusqlFS::Cache;
use FusqlFS::Backend;

our $fusqlh;
our $def_time;
our $debug;
our $threaded;
our %cache;
our %inbuffer;

use FusqlFS::Version;
our $VERSION = $FusqlFS::Version::VERSION;

=item init

Initializes L<FusqlFS::Backend> and L<FusqlFS::Cache> subsystems, preparing
all resources to be actually used by L<Fuse> interface.

Input: %options.
Output: $fusqlfs_instance.

=cut
sub init
{
    my $class = shift;
    my %options = @_;

    $threaded = $options{threaded}||0;
    $debug    = $options{debug}||0;
    $def_time = mktime(localtime());

    $fusqlh   = FusqlFS::Backend->new(@_);
    croak "Unable to initialize database backend" unless defined $fusqlh;

    if ($threaded)
    {
        use threads;
        use threads::shared;
    }

    FusqlFS::Cache->init(\%cache, @options{qw(cache_strategy cache_threshold)});
    $SIG{USR1} = sub () { %cache = (); };
    $SIG{HUP} = sub () { %cache = (); $fusqlh->reconnect(); };

    return $class;
}

=item mount

Runs fuse main loop, configured with all required hooks and options to
interface with L<FusqlFS::Backend>.

Input: $mountpoint, $mountopts.

=cut
sub mount
{
    my $class = shift;
    my $mountpoint = shift;
    my $mountopts = shift||'';

    my $fusermount = (grep { -f "$_/fusermount" } split /:/, $ENV{PATH})[0] . '/fusermount';
    if (-x $fusermount) {
        carp "fusermount found at `$fusermount' and will be used to autounmount when daemon terminated.";
        $SIG{QUIT} = $SIG{INT} = $SIG{TERM} = sub () {
            carp "Running `$fusermount' to unmount `$mountpoint'...";
            chdir '/';
            $mountpoint =~ s/(['\\])/\\$1/g;
            system(sprintf(q{fusermount -u -z '%s'}, $mountpoint));
            exit(0);
        };
    } else {
        carp "fusermount is not found or non-executable, won't autounmount when daemon terminated!";
    }

    Fuse::main(
        mountpoint => $mountpoint,
        mountopts  => $mountopts,
        threaded   => $threaded,
        debug      => $debug > 2,

        getdir     => \&getdir,
        getattr    => \&getattr,
        readlink   => \&readlink,
        open       => \&open,
        read       => \&read,

        write      => \&write,
        truncate   => \&truncate,
        flush      => \&flush,
        rename     => \&rename,

        mkdir      => \&mkdir,
        mknod      => \&mknod,
        symlink    => \&symlink,

        unlink     => \&unlink,
        rmdir      => \&rmdir,

        fsync      => \&fsync,
        utime      => \&utime,
    );

}

=item Fuse hooks

L<Fuse/getdir>, L<Fuse/getattr>, L<Fuse/readlink>, L<Fuse/read>, L<Fuse/write>,
L<Fuse/flush>, L<Fuse/open>, L<Fuse/truncate>, L<Fuse/symlink>, L<Fuse/unlink>,
L<Fuse/mkdir>, L<Fuse/rmdir>, L<Fuse/mknod>, L<Fuse/rename>, L<Fuse/fsync>,
L<Fuse/utime>.

See L<Fuse> for details.

=cut
sub getdir
{

    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -ENOTDIR() unless $entry->isdir();
    return ('.', '..', @{$entry->list()}, 0);
}

sub getattr
{
    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return file_struct($entry);
}

sub readlink
{
    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EINVAL() unless $entry->islink();
    return $entry->read();
}

sub read
{
    my ($path, $size, $offset) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EINVAL() unless $entry->isfile();

    return $entry->read($offset, $size);
}

sub write
{
    my ($path, $buffer, $offset) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EISDIR() if $entry->isdir();
    return -EINVAL() unless $entry->isfile();
    return -EACCES() unless $entry->writable();

    $inbuffer{$path} ||= $entry->get();
    substr($inbuffer{$path}, $offset, length($buffer)) = $buffer;
    return length($buffer);
}

sub flush
{
    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;

    flush_inbuffer($path, $entry);
    clear_cache($path, $entry->depth()) unless $entry->ispipe();
    return 0;
}

sub open
{
    my ($path, $mode) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EISDIR() if $entry->isdir();
    return 0;
}

sub truncate
{
    my ($path, $offset) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EINVAL() unless $entry->isfile();
    return -EACCES() unless $entry->writable();

    $entry->write($offset);
    clear_cache($path, $entry->depth()) unless $entry->ispipe();
    return 0;
}

sub symlink
{
    my ($path, $symlink) = @_;
    return -EOPNOTSUPP() if $path =~ /^\//;

    $path = fold_path($symlink, '..', $path);
    my $origin = by_path($path);
    return -ENOENT() unless $origin;

    $path =~ s{^/}{};
    my $entry = $fusqlh->by_path($symlink, \$path);
    return -EEXIST() unless $entry->get() == \$path;

    $entry->store();
    say STDERR "depth = ",$entry->depth(), "; height = ", $entry->height();
    clear_cache($symlink, $entry->depth());
    return 0;
}

sub unlink
{
    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EACCES() unless $entry->writable();
    return -EISDIR() if $entry->isdir();

    clear_cache($path, $entry->depth() + 1);
    $entry->drop();
    return 0;
}

sub mkdir
{
    my ($path, $mode) = @_;
    my $newdir = {};
    my $entry = $fusqlh->by_path($path, $newdir);
    return -ENOENT() unless $entry;
    return -EEXIST() unless $entry->get() == $newdir;

    $entry->create();
    clear_cache($path, $entry->depth() + 1);
    return 0;
}

sub rmdir
{
    my ($path) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EACCES() unless $entry->writable();
    return -ENOTDIR() unless $entry->isdir();

    $entry->drop();
    clear_cache($path, $entry->depth() + 1);
    return 0;
}

sub mknod
{
    my ($path, $mode, $dev) = @_;
    my $entry = $fusqlh->by_path($path, '');
    return -ENOENT() unless $entry;
    return -EEXIST() unless $entry->get() eq '';

    $entry->create();
    clear_cache($path, $entry->depth());
    return 0;
}

sub rename
{
    my ($path, $name) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return -EACCES() unless $entry->writable();

    my $target = $fusqlh->by_path($name, $entry->get());
    return -ENOENT() unless $target;
    return -EEXIST() unless $entry->get() == $target->get();
    return -EACCES() unless $target->writable();
    return -EOPNOTSUPP() unless $entry->pkg()    == $target->pkg()
                             && $entry->depth()  == $target->depth()
                             && $entry->height() == $target->height();

    $entry->move($target);
    clear_cache($path, $entry->depth());
    return 0;
}

sub fsync
{
    my ($path, $flags) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;

    flush_inbuffer($path, $entry);
    clear_cache($path, $flags? $entry->depth(): undef);
    return 0;
}

sub utime
{
    my ($path, $atime, $mtime) = @_;
    my $entry = by_path($path);
    return -ENOENT() unless $entry;
    return 0;
}

=item fold_path

Folds path by removing "..", "." and other special sequences from it.

Input: $path.
Output $folded_path.

=cut
sub fold_path
{
    local $/ = '/';
    my $path = join '/', @_;
    $path =~ s{//+}{/}g;
    while ($path =~ s{/\./}{/}g) {}
    while ($path =~ s{[^/]+/\.\./}{}) {}
    #$path =~ s{^/\.\./}{/};
    chomp $path;
    return $path;
}

=item file_struct

Creates FUSE-exportable file structure for given entry.

Input: $entry.
Output: @file_struct.

=cut
sub file_struct
{
    my ($entry) = @_;
    my @fileinfo = (
        undef,     # 0 dev
        undef,     # 1 ino
        0644,      # 2 mode
        1,         # 3 nlink
        $>,        # 4 uid
        $),        # 5 gid
        undef,     # 6 rdev
        0,         # 7 size
        $def_time, # 8 atime
        $def_time, # 9 mtime
        $def_time, # 10 ctime
        512,       # 11 blksize
        undef,     # 12 blocks
    );
    if ($entry->isdir())
    {
        $fileinfo[2] |= (S_IFDIR|0111);
        $fileinfo[3] = 2 + $entry->size();
    }
    elsif ($entry->islink())
    {
        $fileinfo[2] |= S_IFLNK;
        $fileinfo[7] = $entry->size();
    }
    else
    {
        $fileinfo[2] |= S_IFREG;
        $fileinfo[2] |= S_ISVTX if $entry->ispipe();
        $fileinfo[7] = $entry->size();
    }

    unless ($entry->writable())
    {
        $fileinfo[2] &= ~0222;
    }

    return @fileinfo;
}

=item by_path

Gets entry by path, uses cache.

Input: $path.

=cut
sub by_path
{
    my ($path) = @_;
    return $cache{$path} if exists $cache{$path};
    my $entry = $fusqlh->by_path(@_);
    $cache{$path} = $entry if $entry;
    return $entry;
}

=item clear_cache

Invalidates entries cache for given path, optionally recursively invalidating
all paths up to given depth.

Input: $path, $depth=undef.

=cut
sub clear_cache
{
    delete $cache{$_[0]};
    if (defined $_[1])
    {
        my $key = $_[0];
        my $re = "/[^/]+" x $_[1];
        $key =~ s{$re$}{} if $re;
        my $lk  = length($key);
        while (my $_ = each %cache)
        {
            next unless substr($_, 0, $lk) eq $key;
            delete $cache{$_};
        }
    }
}

=item flush_inbuffer

Flushes input buffer for given $entry by given $path.

Input: $path, $entry.

=cut
sub flush_inbuffer
{
    my ($path, $entry) = @_;
    if (exists $inbuffer{$path})
    {
        $entry->write(0, $inbuffer{$path});
        delete $inbuffer{$path};
    }
}

1;

__END__

=back
