#!/usr/bin/env perl

use strict;
no strict qw(refs);

use threads;
use threads::shared;

use Carp;
local $SIG{'__WARN__'} = \&Carp::cluck;

use Filesys::Fuse3 qw(:all);
use Fcntl qw(:mode);
use POSIX;
use IO::Poll qw(POLLIN);
use Time::HiRes qw(sleep);
use Getopt::Long;

# $fsel_open_mask is used to limit the number of opens to 1 per file. This
# uses the file index (0-F) as $fh, as poll support requires a unique handle
# per open file. Lifting this would require more complete open file
# management.
my $fsel_open_mask :shared = 0;

# Maximum "file" size.
use constant FSEL_CNT_MAX   => 10;
use constant FSEL_FILES     => 16;

# Used only as a lock for $fsel_poll_notify_mask and @fsel_cnt.
my $fsel_mutex :shared;
# Mask indicating what FDs have poll notifications waiting.
my $fsel_poll_notify_mask :shared = 0;
# Poll notification handles.
my @fsel_poll_handle :shared;
# Number of bytes for each "file".
my @fsel_cnt :shared;
# Initialize all byte counts.
map { $fsel_cnt[$_] = 0 } (0 .. (FSEL_FILES - 1));

sub fsel_path_index {
    my ($path) = @_;
    print 'called ', (caller(0))[3], "\n";

    return -1 if $path !~ m{^/([0-9A-F])$};
    return hex($1);
}

sub fsel_getattr {
    my ($path) = @_;
    print 'called ', (caller(0))[3], "\n";
    my @stbuf = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);

    if ($path eq '/') {
        @stbuf[2, 3] = (S_IFDIR | 0555, 2);
        return @stbuf;
    }

    my $idx = fsel_path_index($path);
    return -&ENOENT if $idx < 0;

    @stbuf[2, 3, 7] = (S_IFREG | 0444, 1, $fsel_cnt[$idx]);
    return @stbuf;
}

sub fsel_readdir {
    my ($path, $offset) = @_;
    print 'called ', (caller(0))[3], "\n";

    return -&ENOENT if $path ne '/';

    return('.', '..', map { sprintf('%X', $_) } (0 .. (FSEL_FILES - 1)), 0);
}

sub fsel_open {
    my ($path, $flags, $info) = @_;
    print 'called ', (caller(0))[3], "\n";

    my $idx = fsel_path_index($path);
    return -&ENOENT if $idx < 0;
    return -&EACCES if $flags & O_ACCMODE != O_RDONLY;
    return -&EBUSY  if $fsel_open_mask & (1 << $idx);
    $fsel_open_mask |= (1 << $idx);

    # fsel files are nonseekable somewhat pipe-like files which get filled
    # up periodically by the producer thread, and consumed on read. Tell
    # FUSE to do this right.
    @{$info}{'direct_io', 'nonseekable'} = (1, 1);
    return (0, $idx);
}

sub fsel_release {
    my ($path, $flags, $fh) = @_;
    print 'called ', (caller(0))[3], "\n";

    $fsel_open_mask &= ~(1 << $fh);
    return 0;
}

sub fsel_read {
    my ($path, $size, $offset, $fh) = @_;
    print 'called ', (caller(0))[3], "\n";
    lock($fsel_mutex);

    if ($fsel_cnt[$fh] < $size) {
        $size = $fsel_cnt[$fh];
    }
    printf("READ   \%X transferred=\%u cnt=\%u\n", $fh, $size, $fsel_cnt[$fh]);
    $fsel_cnt[$fh] -= $size;

    return(chr($fh) x $size);
}

our $polled_zero :shared = 0;

sub fsel_poll {
    my ($path, $ph, $revents, $fh) = @_;
    print 'called ', (caller(0))[3], ", path = \"$path\", fh = $fh, revents = $revents\n";

    lock($fsel_mutex);

    if ($ph) {
        my $oldph = $fsel_poll_handle[$fh];
        pollhandle_destroy($oldph) if $oldph;
        $fsel_poll_notify_mask |= (1 << $fh);
        $fsel_poll_handle[$fh] = $ph;
    }

    if ($fsel_cnt[$fh]) {
        $revents |= POLLIN;
        printf("POLL   \%X cnt=\%u polled_zero=\%u\n", $fh, $fsel_cnt[$fh],
                $polled_zero);
        $polled_zero = 0;
    }
    else {
        $polled_zero++;
    }

    return(0, $revents);
}

sub fsel_producer {
    print 'called ', (caller(0))[3], "\n";
    local $SIG{'KILL'} = sub { threads->exit(); };
    my ($tv, $idx, $nr) = (0.25, 0, 1);

    while (1) {
        {
            my ($i, $t);
            lock($fsel_mutex);

            # This is the main producer loop which is executed every 250
            # msec. On each iteration, it adds one byte to 1, 2 or 4 files
            # and sends a poll notification if a poll handle is present.
            for (($i, $t) = (0, $idx); $i < $nr; $i++,
                    $t = (($t + int(FSEL_FILES / $nr)) % FSEL_FILES)) {
                next if $fsel_cnt[$t] == FSEL_CNT_MAX;

                $fsel_cnt[$t]++;
                if ($fsel_poll_notify_mask & (1 << $t)) {
                    printf("NOTIFY \%X\n", $t);
                    my $ph = $fsel_poll_handle[$t];
                    notify_poll($ph);
                    pollhandle_destroy($ph);
                    $fsel_poll_notify_mask &= ~(1 << $t);
                    $fsel_poll_handle[$t] = undef;
                }
            }

            $idx = ($idx + 1) % FSEL_FILES;
            if ($idx == 0) {
                # Cycle through 1, 2 and 4.
                $nr = ($nr * 2) % 7;
            }
        }

        sleep($tv);
    }
}

croak("Filesys::Fuse3 doesn't have poll") unless Filesys::Fuse3::fuse_version() >= 2.8;

my %fuseargs = (
    'getattr'   => 'main::fsel_getattr',
    'readdir'   => 'main::fsel_readdir',
    'open'      => 'main::fsel_open',
    'release'   => 'main::fsel_release',
    'read'      => 'main::fsel_read',
    'poll'      => 'main::fsel_poll',
);

GetOptions(
    'use-threads'       => sub {
        print STDERR "Warning: Filesys::Fuse3 currently has bugs related to threading which may cause misbehavior\n";
        $fuseargs{'threaded'} = 1;
    },
    'debug'             => sub {
        $fuseargs{'debug'} = 1;
    }
) || croak("Malformed options passed");

$fuseargs{'mountpoint'} = $ARGV[0];

my $thread = threads->create(\&fsel_producer);

Filesys::Fuse3::main(%fuseargs);

$thread->kill('KILL');
$thread->join();
