#+##############################################################################
#                                                                              #
# File: Net/STOMP/Client/IO.pm                                                 #
#                                                                              #
# Description: Input/Output support for Net::STOMP::Client                     #
#                                                                              #
#-##############################################################################

#
# module definition
#

package Net::STOMP::Client::IO;
use 5.005; # need the four-argument form of substr()
use strict;
use warnings;
our $VERSION  = "2.5";
our $REVISION = sprintf("%d.%02d", q$Revision: 2.2 $ =~ /(\d+)\.(\d+)/);

#
# used modules
#

use List::Util qw(min);
use No::Worries::Die qw(dief);
use No::Worries::Log qw(log_debug);
use POSIX qw(:errno_h);
use Time::HiRes qw();

#
# constants
#

use constant READ_LENGTH  => 32_768;  # chunk size for sysread()
use constant WRITE_LENGTH => 32_768;  # minimum length for syswrite()

#+++############################################################################
#                                                                              #
# private helpers                                                              #
#                                                                              #
#---############################################################################

#
# attempt to read data from the socket to the buffer
#
# note: we read at least once even if the buffer contains enough data
#
# common scenarios:
#  - timeout=undef minlen=undef: loop until we successfully read once
#  - timeout=undef minlen=N: loop until we read at least N bytes
#  - timeout=0     minlen=undef: read only once (successful or not)
#  - timeout=0     minlen=N: loop until we read >=N bytes or fail once
#  - timeout=T     minlen=undef: loop until timeout
#  - timeout=T     minlen=N: loop until we read >=N bytes or timeout
#

sub _try_to_read ($$$) {  ## no critic 'ProhibitExcessComplexity'
    my($self, $timeout, $minlen) = @_;
    my($maxtime, $total, $count, $sleeptime, $remaining);

    $self->{incoming_buflen} = length($self->{incoming_buffer});
    # boundary conditions
    if ($timeout) {
        return(0) unless $timeout > 0;
        # timer starts now
        $maxtime = Time::HiRes::time() + $timeout;
    }
    # try to read, in a loop, until we are done
    $total = 0;
    while (1) {
        # attempt to read once
        $count = sysread($self->{socket}, $self->{incoming_buffer},
                         READ_LENGTH, $self->{incoming_buflen});
        if (defined($count)) {
            # we could read this time
            unless ($count) {
                # ... but we hit the EOF
                $self->{error} = "cannot sysread(): EOF";
                return($total);
            }
            # this is a normal successful read
            $self->{incoming_time} = Time::HiRes::time();
            $self->{incoming_buflen} += $count;
            $total += $count;
            # check if we have worked enough
            return($total) unless $minlen and $total < $minlen;
        } else {
            # we could not read this time
            if ($! != EAGAIN and $! != EWOULDBLOCK) {
                # unexpected error
                $self->{error} = "cannot sysread(): $!";
                return(undef);
            }
        }
        # check time
        if (not defined($timeout)) {
            # timeout = undef => loop forever until we are done
            $sleeptime = 0.01;
        } elsif ($timeout) {
            # timeout > 0 => try again only if not too late
            $remaining = $maxtime - Time::HiRes::time();
            return($total) unless $remaining > 0;
            $sleeptime = min($remaining, 0.01);
        } else {
            # timeout = 0 => try again unless last read failed
            return($total) unless $count;
        }
        # sleep a bit...
        Time::HiRes::sleep($sleeptime) unless $count;
    }
}

#
# attempt to write data from the queue and buffer to the socket
#
# common scenarios:
#  - timeout=undef minlen=undef: loop until we successfully write once
#  - timeout=undef minlen=N: loop until we write at least N bytes
#  - timeout=0     minlen=undef: write only once (successful or not)
#  - timeout=0     minlen=N: loop until we write >=N bytes or fail once
#  - timeout=T     minlen=undef: loop until timeout
#  - timeout=T     minlen=N: loop until we write >=N bytes or timeout
#

sub _try_to_write ($$$) {  ## no critic 'ProhibitExcessComplexity'
    my($self, $timeout, $minlen) = @_;
    my($maxtime, $total, $count, $sleeptime, $remaining, $data);

    $self->{outgoing_buflen} = length($self->{outgoing_buffer});
    # boundary conditions
    return(0) unless $self->{outgoing_buflen} or @{ $self->{outgoing_queue} };
    if ($timeout) {
        return(0) unless $timeout > 0;
        # timer starts now
        $maxtime = Time::HiRes::time() + $timeout;
    }
    # try to write, in a loop, until we are done
    $total = 0;
    while (1) {
        # make sure there is enough data in the outgoing buffer
        while ($self->{outgoing_buflen} < WRITE_LENGTH
               and @{ $self->{outgoing_queue} }) {
            $data = shift(@{ $self->{outgoing_queue} });
            $self->{outgoing_buffer} .= ${ $data };
            $self->{outgoing_buflen} += length(${ $data });
        }
        return($total) unless $self->{outgoing_buflen};
        # attempt to write once
        $count = syswrite($self->{socket}, $self->{outgoing_buffer},
                          $self->{outgoing_buflen});
        if (defined($count)) {
            # we could write this time
            if ($count) {
                # this is a normal successful write
                $self->{outgoing_time} = Time::HiRes::time();
                $self->{outgoing_buflen} -= $count;
                $total += $count;
                substr($self->{outgoing_buffer}, 0, $count, "");
                $self->{outgoing_length} -= $count;
                # check if we have worked enough
                return($total) unless $self->{outgoing_buflen}
                                or @{ $self->{outgoing_queue} };
                return($total) unless $minlen and $total < $minlen;
            }
        } else {
            # we could not write this time
            if ($! != EAGAIN and $! != EWOULDBLOCK) {
                # unexpected error
                $self->{error} = "cannot syswrite(): $!";
                return(undef);
            }
        }
        # check time
        if (not defined($timeout)) {
            # timeout = undef => loop forever until we are done
            $sleeptime = 0.01;
        } elsif ($timeout) {
            # timeout > 0 => try again only if not too late
            $remaining = $maxtime - Time::HiRes::time();
            return($total) unless $remaining > 0;
            $sleeptime = min($remaining, 0.01);
        } else {
            # timeout = 0 => try again unless last write failed
            return($total) unless $count;
        }
        # sleep a bit...
        Time::HiRes::sleep($sleeptime) unless $count;
    }
}

#+++############################################################################
#                                                                              #
# object oriented interface                                                    #
#                                                                              #
#---############################################################################

#
# constructor
#

sub new : method {
    my($class, $socket) = @_;
    my($self);

    dief("missing or invalid socket")
        unless $socket and ref($socket) and $socket->isa("IO::Socket");
    $socket->blocking(0);
    $self = {};
    $self->{socket} = $socket;
    $self->{incoming_buffer} = "";
    $self->{incoming_buflen} = 0;
    $self->{outgoing_buffer} = "";
    $self->{outgoing_buflen} = 0; # buffer length only
    $self->{outgoing_queue} = [];
    $self->{outgoing_length} = 0; # buffer + queue length
    return(bless($self, $class));
}

#
# queue the given data (a scalar reference!)
#

sub queue_data : method {
    my($self, $data) = @_;
    my($length);

    dief("unexpected data: %s", $data) unless ref($data) eq "SCALAR";
    $length = length(${ $data });
    if ($length) {
        push(@{ $self->{outgoing_queue} }, $data);
        $self->{outgoing_length} += $length;
    }
    return($self->{outgoing_length});
}

#
# send the queued data
#

sub send_data : method {
    my($self, %option) = @_;
    my($minlen, $count);

    unless ($self->{error}) {
        # send some data
        $minlen = $self->{outgoing_length};
        $count = _try_to_write($self, $option{timeout}, $minlen);
    }
    dief($self->{error}) unless defined($count);
    # so far so good
    log_debug("sent %d bytes", $count)
        if $option{debug} and $option{debug} =~ /\b(io|all)\b/;
    return($count);
}

#
# receive some data
#

sub receive_data : method {
    my($self, %option) = @_;
    my($minlen, $count);

    unless ($self->{error}) {
        # receive some data
        $minlen = $option{timeout} ? 1 : undef;
        $count = _try_to_read($self, $option{timeout}, $minlen);
    }
    dief($self->{error}) unless defined($count);
    # so far so good
    log_debug("received %d bytes", $count)
        if $option{debug} and $option{debug} =~ /\b(io|all)\b/;
    return($count);
}

1;

__END__

=head1 NAME

Net::STOMP::Client::IO - Input/Output support for Net::STOMP::Client

=head1 DESCRIPTION

This module provides Input/Output (I/O) support. It is used internally by
L<Net::STOMP::Client> and should not be directly used elsewhere.

It uses non-blocking I/O: the socket is in non-blocking mode and errors like
C<EAGAIN> or C<EWOULDBLOCK> are trapped.

=head1 FUNCTIONS

This module provides the following internal methods:

=over

=item new(SOCKET)

return a new Net::STOMP::Client::IO object (class method)

=item queue_data(DATA)

queue (append to the internal outgoing buffer) the given data (a binary
string reference); return the length of DATA in bytes

=item send_data([OPTIONS])

send some queued data to the socket; return the total number of bytes
written

=item receive_data([OPTIONS])

receive some data from the socket and put it in the internal incoming
buffer; return the total number of bytes read

=back

=head1 SEE ALSO

L<Net::STOMP::Client>.

=head1 AUTHOR

Lionel Cons L<http://cern.ch/lionel.cons>

Copyright (C) CERN 2010-2021
