#!/usr/bin/perl -w
use strict;
use warnings;

# TODO trap -> cleanup

# buf - use a file as a large fifo

# It's hard to believe how difficult I found it
# to write this little program!

# It uses a file like an unlimited pipe,
# to avoid pipe congestion e.g. on input to cat.

# TODO multi buffer version of this?
# TODO option to somehow free disk space for (most) data that has already been copied, perhaps using multiple files
# TODO option to limit buffer size
# TODO track count of data written and read to avoid unnecessary extra read when file has not grown?
# TODO tail -f version using (portable) inotify

use File::Temp;
use IO::Select;
use Fcntl;

my ($buf_file) = @ARGV;
my ($fh_buf_write, $fh_buf_read);

if (defined $buf_file) {
    open $fh_buf_write, ">", $buf_file;
}
else {
	$fh_buf_write = File::Temp->new;
	$buf_file = $fh_buf_write->filename;
}
open $fh_buf_read, "<", $buf_file;

my $block_size = 64*1024;

my $buf1 = '';
my $buf2 = '';

nonblock($_) for \*STDIN, \*STDOUT;

my $sel_r = IO::Select->new(\*STDIN);
my $sel_w = IO::Select->new();
my $sel_e = IO::Select->new(\*STDIN, \*STDOUT);

my $closing = 0;

while(1) {
	my ($r, $w, $e) = IO::Select->select($sel_r, $sel_w, $sel_e);
	if (@$r) {
        my $count = sysread STDIN, $buf1, $block_size;
        if (!defined $count && $! eq 'EAGAIN') {
            # try again later
        }
        elsif (!defined $count) {
            die "sysread failed: $!";
        }
        elsif ($count == 0) {
            $closing = 1;
            $sel_w->add(\*STDOUT);
        }
        else {
            my $count = syswrite $fh_buf_write, $buf1;
            if (!defined $count) {
                die "failed: syswrite to buf file: $!";
            }
            if ($count != length $buf1) {
                die "failed: syswrite to buf file, returned short count";
            }
            $buf1 = '';
            $sel_w->add(\*STDOUT);
        }
    }
    if (@$w) {
        if ($closing != 2) {
            my $count = sysread $fh_buf_read, $buf2, $block_size, length $buf2;
            if (!defined $count) {
                die "read failed: $!";
            }
            elsif ($count == 0) {
                if ($closing) {
                    $closing = 2;
                }
                $sel_w->remove(\*STDOUT);
            }
        }
        if (length $buf2) {
            my $count = syswrite STDOUT, $buf2;
            if (!defined $count) {
                die "failed: syswrite to STDOUT: $!";
            }
            substr $buf2, 0, $count, '';
        }
        if ($closing == 2 && !length $buf2) {
            last;
        }
    }
}

sub nonblock {
    my ($fh) = @_;
    my $flags = '';
    $flags = fcntl($fh, F_GETFL, 0)
        or die "Couldn't get flags for HANDLE : $!\n";
    $flags |= O_NONBLOCK;
    fcntl($fh, F_SETFL, $flags)
        or die "Couldn't set flags for HANDLE: $!\n";
    return;
}

