##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/Scalar/IO.pm
## Version v0.2.1
## Copyright(c) 2022 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2022/04/24
## Modified 2022/08/05
## All rights reserved
## 
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
package Module::Generic::Scalar::IO;
BEGIN
{
    use strict;
    use warnings;
    use warnings::register;
    # For import of constants
    use Module::Generic::File::IO;
    use parent qw( Module::Generic::File::IO );
    use vars qw( $DEBUG $VERSION $ERROR @EXPORT );
    use Devel::StackTrace;
    no warnings 'once';
    our @EXPORT = @Module::Generic::File::IO;
    our $ERROR = '';
    our $VERSION = 'v0.2.1';
};

use strict;
use warnings;

sub new
{
    my $this = shift( @_ );
    my $class = ( ref( $this ) || $this );
    my $self;
    # try-catch
    local $@;
    eval
    {
        $self = $class->IO::File::new;
    };
    if( $@ )
    {
        return( $self->error( "Error trying to get a file handle: $@" ) );
    }
    *$self = {};
    if( Want::want( 'OBJECT' ) )
    {
        return( $self->init( @_ ) );
    }
    my $new = $self->init( @_ );
    if( !defined( $new ) )
    {
        # If we are called on an object, we hand it the error so the caller can check it using the object:
        # my $new = $old->new || die( $old->error );
        if( $self->_is_object( $this ) && $this->can( 'pass_error' ) )
        {
            return( $this->pass_error( $self->error ) );
        }
        else
        {
            return( $self->pass_error );
        }
    };
    return( $new );
}

sub init
{
    my( $p, $f, $l ) = caller;
    my $self = shift( @_ );
    my $class = ( ref( $self ) || $self );
    my( $ref, $mode );
    if( @_ )
    {
        $ref = shift( @_ );
        return( $self->error( "No scalar reference was provided." ) ) if( !defined( $ref ) );
        return( $self->error( "I was expecting a scalar reference, but got a string of ", CORE::length( $ref ), " bytes instead." ) ) if( !ref( $ref ) );
        return( $self->error( "I was expecting a scalar reference, but got instead '", overload::StrVal( $ref ), "'." ) ) if( !$self->_is_scalar( $ref ) );
        $mode = ( scalar( @_ ) && ( ref( $_[0] ) ne 'HASH' || ( @_ > 2 && ( @_ % 2 ) ) ) ) ? shift( @_ ) : '+<';
        $mode =~ s/^(.*?)\:$// if( substr( $mode, -1, 1 ) eq ':' );
    }
    else
    {
        my $str = '';
        $ref = \$str;
        $mode = '+<';
    }
    *$self->{sr} = $ref;
    my $opts = $self->_get_args_as_hash( @_ );
    my $core = {};
    $core->{binmode} = CORE::delete( $opts->{binmode} ) if( exists( $opts->{binmode} ) );
    $core->{autoflush} = CORE::delete( $opts->{autoflush} ) if( exists( $opts->{autoflush} ) );
    $self->SUPER::init( @_ ) || return( $self->pass_error );
    my $this = *$self;
    # See PerlIO man page
    if( defined( $core->{binmode} ) && length( $core->{binmode} ) )
    {
        # No need to specify the scalar layer, because we add it ourself
        if( $core->{binmode} eq 'scalar' )
        {
            # no-op
        }
        elsif( 
            $core->{binmode} eq 'bytes' ||
            $core->{binmode} eq 'crlf' ||
            $core->{binmode} eq 'perlio' ||
            $core->{binmode} eq 'raw' ||
            $core->{binmode} eq 'stdio' ||
            $core->{binmode} eq 'unix' ||
            $core->{binmode} eq 'win32'
            )
        {
            $mode .= ':' . $core->{binmode};
        }
        # others are encapsulated with :encoding() pragma, including utf8
        else
        {
            $mode .= ':encoding(' . $core->{binmode} . ')';
        }
    }
    $self->open( $ref => $mode ) || return( $self->pass_error );
    $self->autoflush( $core->{autoflush} ) if( exists( $core->{autoflush} ) );
    return( $self );
}

sub bit { return( *{shift( @_ )}->{bit} ); }

# Could also do: !( $_[0] & O_ACCMODE )
sub can_read { return( ( ( $_[0]->bit & O_RDONLY ) == O_RDONLY ) || ( $_[0]->bit & O_RDWR ) ); }

sub can_write { return( shift->bit & ( O_APPEND | O_WRONLY | O_CREAT | O_RDWR ) ); }

sub clearerr { return( shift->clear_error ); }

sub fcntl
{
    my $self = shift( @_ );
    my( $func, $bit ) = @_;
    return( $self->error( "Function bit value is not an integer." ) ) if( !$self->_is_integer( $func ) );
    if( $func & F_GETFL )
    {
        return( *$self->{bit} );
    }
    elsif( $func & F_SETFL )
    {
        return( $self->error( "Bitwise value provided '$bit' is not an integer." ) ) if( !$self->_is_integer( $bit ) );
        *$self->{bit} = $bit;
    }
    else
    {
        return( $self->error( "Unknown fcntl function provided Please use either F_GETFL or F_SETFL" ) );
    }
}

# Need to wrap the getline() method here, because it will not sto even when eof() 
# has been reached and leading to the error: "Inappropriate ioctl for device"
# Thus here we wrap the getline() call and check for eof()
sub getline
{
    my $self = shift( @_ );
    return if( $self->eof );
    return( $self->SUPER::getline() );
}

sub is_append { return( shift->bit & O_APPEND ); }

sub is_create { return( shift->bit & O_CREAT ); }

sub is_readonly { return( shift->bit == O_RDONLY ); }

sub is_readwrite { return( shift->bit & O_RDWR ); }

sub is_writeonly { return( shift->bit & O_WRONLY ); }

sub length
{
    my $self = shift( @_ );
    return( CORE::length( ${ *$self->{sr} } ) );
}

sub line
{
    my $self = shift( @_ );
    my $code = shift( @_ );
    return( $self->error( "No callback code was provided for line()" ) ) if( !defined( $code ) || ref( $code ) ne 'CODE' );
    my $opts = ref( $_[0] ) eq 'HASH' ? shift( @_ ) : { @_ };
    return if( !$self->can_read );
    $opts->{chomp} //= 0;
    $opts->{auto_next} //= 0;
    my $l;
    while( defined( $l = $self->getline ) )
    {
        chomp( $l ) if( $opts->{chomp} );
        local $_ = $l;
        my $rv = $code->( $l );
        if( !defined( $rv ) && !$opts->{auto_next} )
        {
            last;
        }
    }
    return( $self );
}

sub object { return( *{ $_[0] }->{sr} ) }

sub open
{
    my $self = shift( @_ );
    my $class = ( ref( $self ) || $self );
    return( $self->error( "open() is not a class function. You need to call it using a $class object." ) ) if( !ref( $self ) );
    my $ref = shift( @_ );
    return( $self->error( "No scalar reference was provided." ) ) if( !defined( $ref ) );
    return( $self->error( "I was expecting a scalar reference, but got a string of ", CORE::length( $ref ), " bytes instead." ) ) if( !ref( $ref ) );
    return( $self->error( "I was expecting a scalar reference, but got instead '", overload::StrVal( $ref ), "'." ) ) if( !$self->_is_scalar( $ref ) );
    my $mode = shift( @_ ) ||
        return( $self->error( "No mode was provided. Supported modes are: >, >>, +>, +>>, <, <+, r, r+, w, w+, a, a+" ) );
    my $equi =
    {
    'r'     => '<',
    'r+'    => '+<',
    'w'     => '>',
    'w+'    => '+>',
    'a'     => '>>',
    'a+'    => '+>>',
    };
    
    my $pl_mode = $mode;
    if( index( $mode, ':' ) != -1 )
    {
        my @parts = split( /:/, $mode );
        $mode = $parts[0];
        $parts[0] = $equi->{ $parts[0] } if( CORE::exists( $equi->{ $parts[0] } ) );
        # The order is important. :scalar needs to be the first IO layer
        splice( @parts, 1, 0, 'scalar' ) if( !scalar( grep( $_ eq 'scalar', @parts ) ) );
        # We only take the first part, i.e. the open mode and ignore the IO layer used for perl's open
        $pl_mode = join( ':', @parts );
    }
    else
    {
        $pl_mode = $equi->{ $pl_mode } if( CORE::exists( $equi->{ $pl_mode } ) );
        $pl_mode .= ':scalar';
    }
    no warnings 'uninitialized';
    local $@;
    my $rv = eval
    {
        open( $self, $pl_mode, $ref );
    };
    if( $@ )
    {
        return( $self->error( "Unable to open( $self, $pl_mode, ", overload::StrVal( $ref ), " ) scalar reference: $@" ) );
    }
    elsif( !$rv )
    {
        return( $self->error( "Unable to open( $self, $pl_mode, ", overload::StrVal( $ref ), " ) scalar reference: $!" ) );
    }

    my $bit;
    my $bitmap = 
    {
        '<'     => O_RDONLY,
        # Incorrect, but let's catch it anyway
        '<+'    => O_RDWR,
        '+<'    => O_RDWR,
        '>'     => ( O_CREAT | O_WRONLY ),
        '+>'    => ( O_CREAT | O_RDWR ),
        '>>'    => O_APPEND,
        '+>>'   => ( O_RDWR | O_APPEND ),
        'r'     => O_RDONLY,
        'r+'    => O_RDWR,
        'w'     => ( O_CREAT | O_WRONLY ),
        'w+'    => ( O_CREAT | O_RDWR ),
        'a'     => O_APPEND,
        'a+'    => ( O_RDWR | O_APPEND ),
    };
    
    # We set the bit for this glob, so fcntl works.
    if( $mode =~ /^(<|<\+|\+<|>|\+>|>>|\+>>|r|r\+|w|w\+|a|a\+)$/ )
    {
        die( "Unable to find mode '$1' in our bitmap!\n" ) if( !CORE::exists( $bitmap->{ $1 } ) );
        $bit = $bitmap->{ $1 };
        if( $bit & O_CREAT )
        {
            $$ref = '' unless( !defined( $$ref ) );
        }
    }
    else
    {
        return( $self->error( "Unsupported mode '$mode'" ) );
    }
    
    # If opened in read, even read/write mode, we position at the beginning of the string
    *$self->{sr}  = $ref;
    # We use the bits to check what the methods are allowed to do
    *$self->{bit} = $bit;
    return( $self );
}

sub setpos { return( shift->seek( $_[0], 0 ) ); }

sub size { return( shift->length ); }

sub sref { return( shift->object ); }

# Missing method in IO::Scalar and not working under perl native open with IO::Handle
# It throws 'Bad file descriptor'
sub truncate
{
    my $self = CORE::shift( @_ );
    return if( !$self->can_write );
    my $pos = $self->tell;
    return( CORE::length( CORE::substr( ${*$self->{sr}}, $pos, CORE::length( ${*$self->{sr}} ) - $pos, '' ) ) );
}

sub sysread { return( shift->read( @_ ) ); }

sub syswrite { return( shift->write( @_ ) ); }

sub write
{
    my $self = $_[0];
    my $n    = $_[2] // CORE::length( $_[1] );
    my $off  = $_[3] || 0;
    return( $self->error( "Wrong number of parameters. Usage: \$io->write( \$buffer, \$length, \$offset ); \$offset is optional." ) ) if( @_ < 2 || @_ > 4 ); 
    return if( !$self->can_write );

    if( @_ == 4 )
    {
        $n = ( CORE::length( $_[1] ) - $off ) if( ( $off + $n ) > CORE::length( $_[1] ) );
    }
    else
    {
        $n = CORE::length( $_[1] ) if( $n > CORE::length( $_[1] ) );
    }
    $self->print( substr( $_[1], $off, $n ) ) || return( $self->pass_error );
    return( $n );
}

sub DESTROY
{
    shift->close;
}

sub FREEZE
{
    my $self = CORE::shift( @_ );
    my $serialiser = CORE::shift( @_ ) // '';
    my $class = CORE::ref( $self ) || $self;
    my %hash  = %{*$self};
    # Return an array reference rather than a list so this works with Sereal and CBOR
    # On or before Sereal version 4.023, Sereal did not support multiple values returned
    CORE::return( [$class, \%hash] ) if( $serialiser eq 'Sereal' && Sereal::Encoder->VERSION <= version->parse( '4.023' ) );
    # But Storable want a list with the first element being the serialised element
    CORE::return( $class, \%hash );
}

sub THAW
{
    my( $self, undef, @args ) = @_;
    my $ref = ( CORE::scalar( @args ) == 1 && CORE::ref( $args[0] ) eq 'ARRAY' ) ? CORE::shift( @args ) : \@args;
    my $class = ( CORE::defined( $ref ) && CORE::ref( $ref ) eq 'ARRAY' && CORE::scalar( @$ref ) > 1 ) ? CORE::shift( @$ref ) : ( CORE::ref( $self ) || $self );
    my $hash = CORE::ref( $ref ) eq 'ARRAY' ? CORE::shift( @$ref ) : {};
    my $new = $class->new;
    foreach( CORE::keys( %$hash ) )
    {
        *$new->{ $_ } = CORE::delete( $hash->{ $_ } );
    }
    CORE::return( $new );
}

1;

__END__