##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/Iterator.pm
## Version v1.1.1
## Copyright(c) 2022 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2021/03/20
## 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::Iterator;
BEGIN
{
    use common::sense;
    use warnings;
    use warnings::register;
    use parent qw( Module::Generic );
    use Module::Generic::Array;
    use Scalar::Util ();
    use Want;
    our( $VERSION ) = 'v1.1.1';
};

use strict;
no warnings 'redefine';

sub init
{
    my $self = CORE::shift( @_ );
    my $init = [];
    $init = CORE::shift( @_ ) if( @_ && ( ( Scalar::Util::blessed( $_[0] ) && $_[0]->isa( 'ARRAY' ) ) || ref( $_[0] ) eq 'ARRAY' ) );
    $self->{_init_strict_use_sub} = 1;
    $self->SUPER::init( @_ );
    my $elems = Module::Generic::Array->new;
    ## Wrap each element in an Iterator element to enable next, prev, etc
    foreach my $this ( @$init )
    {
        CORE::push( @$elems, Module::Generic::Iterator::Element->new( $this, { parent => $self, debug => $self->debug } ) );
    }
    $self->{elements} = $elems;
    $self->{pos} = 0;
    return( $self );
}

# This class does not convert to an HASH
sub as_hash { return( $_[0] ); }

sub elements { return( shift->_set_get_array_as_object( 'elements', @_ ) ); }

sub eof
{
    my $self = shift( @_ );
    my $pos;
    if( @_ )
    {
        $pos  = $self->_find_pos( @_ );
        return if( !CORE::defined( $pos ) );
    }
    else
    {
        $pos = $self->pos;
    }
    return( $pos >= ( $self->elements->length - 1 ) );
}

sub find
{
    my $self = shift( @_ );
    my $pos  = $self->_find_pos( @_ );
    return if( !CORE::defined( $pos ) );
    return( $self->elements->index( $pos ) );
}

sub first
{
    my $self = shift( @_ );
    $self->pos = 0;
    return( $self->elements->index( 0 ) );
}

sub has_next
{
    my $self = shift( @_ );
    my $pos  = $self->pos;
    return( $pos < ( $self->elements->length - 1 ) );
}

sub has_prev
{
    my $self = shift( @_ );
    my $pos  = $self->pos;
    return( $pos > 0 && $self->elements->length > 0 );
}

sub last
{
    my $self = shift( @_ );
    my $pos = $self->elements->length - 1;
    $self->pos = $pos;
    return( $self->elements->index( $pos ) );
}

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

sub next
{
    my $self = shift( @_ );
    my $pos;
    if( @_ )
    {
        $pos = $self->_find_pos( @_ );
        return if( !CORE::defined( $pos ) );
        return if( $pos >= ( $self->elements->length - 1 ) );
        $pos++;
    }
    else
    {
        return if( $self->eof );
        $self->pos++;
        $pos = $self->pos;
    }
    return( $self->elements->index( $pos ) );
}

sub pos : lvalue
{
    my $self = shift( @_ );
    if( want( qw( LVALUE ASSIGN ) ) )
    {
        my( $a ) = want( 'ASSIGN' );
        if( $a !~ /^\d+$/ )
        {
            CORE::warn( "Position provided \"$a\" is not an integer.\n" );
            lnoreturn;
        }
        $self->{pos} = $a;
        lnoreturn;
    }
    elsif( want( 'RVALUE' ) )
    {
        rreturn( $self->{pos} );
    }
    else
    {
        return( $self->{pos} );
    }
    return;
}

sub prev
{
    my $self = shift( @_ );
    my $pos;
    if( @_ )
    {
        $pos  = $self->_find_pos( @_ );
        return if( !CORE::defined( $pos ) );
        return if ( $pos <= 0 );
        $pos--;
    }
    else
    {
        $self->pos-- if( $self->pos > 0 );
        # Position of the given element is at the beginning of our array, there is nothing more
        $pos = $self->pos;
        return if( $pos <= 0 );
        # $self->pos--;
    }
    return( $self->elements->index( $pos ) );
}

sub reset
{
    my $self = shift( @_ );
    $self->pos = 0;
    return( $self );
}

sub _find_pos
{
    my $self = shift( @_ );
    my $this = shift( @_ );
    return if( !CORE::length( $this ) );
    my $is_ref = ref( $this );
    my $ref = $is_ref ? Scalar::Util::refaddr( $this ) : $this;
    my $elems = $self->elements;
    foreach my $i ( 0 .. $#$elems )
    {
        my $val = $elems->[$i]->value;
        if( ( $is_ref && Scalar::Util::refaddr( $elems->[$i] ) eq $ref ) ||
            ( !$is_ref && $val eq $this ) )
        {
            return( $i );
        }
    }
    return;
}

# NOTE: FREEZE is inherited

sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }

sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }

# NOTE: THAW is inherited

# NOTE: package Module::Generic::Iterator::Element
package Module::Generic::Iterator::Element;
BEGIN
{
    use common::sense;
    use warnings;
    use warnings::register;
    use parent qw( Module::Generic );
    use Want;
    our( $VERSION ) = 'v0.1.0';
};

sub init
{
    my $self = CORE::shift( @_ );
    ## This could be anything
    my $value = CORE::shift( @_ );
    $self->{value}      = '';
    $self->{parent}     = '';
    $self->{_init_strict_use_sub} = 1;
    $self->SUPER::init( @_ );
    $self->{value} = $value;
    return( $self );
}

# This class does not convert to an HASH
sub as_hash { return( $_[0] ); }

sub has_next
{
    my $self = shift( @_ );
    my $pos = $self->pos;
    return( $pos < ( $self->parent->elements->length - 1 ) );
}

sub has_prev
{
    my $self = shift( @_ );
    my $pos  = $self->pos;
    return( $pos > 0 && $self->parent->elements->length > 0 );
}

sub next
{
    my $self = shift( @_ );
    my $next = $self->parent->next( $self );
    if( want( 'OBJECT' ) )
    {
        return( $next );
    }
    else
    {
        return( $next->value );
    }
}

sub parent { return( shift->_set_get_object( 'parent', 'Module::Generic::Iterator', @_ ) ); }

sub pos { return( $_[0]->parent->_find_pos( $_[0] ) ); }

sub prev
{
    my $self = shift( @_ );
    my $prev = $self->parent->prev( $self );
    if( want( 'OBJECT' ) )
    {
        return( $prev );
    }
    else
    {
        return( $prev->value );
    }
}

sub value { return( shift->{value} ); }

sub FREEZE
{
    my $self = CORE::shift( @_ );
    my $serialiser = CORE::shift( @_ ) // '';
    my $class = CORE::ref( $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 STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }

sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }

# NOTE: CBOR will call the THAW method with the stored classname as first argument, the constant string CBOR as second argument, and all values returned by FREEZE as remaining arguments.
# NOTE: Storable calls it with a blessed object it created followed with $cloning and any other arguments initially provided by STORABLE_freeze
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;
    # Storable pattern requires to modify the object it created rather than returning a new one
    if( CORE::ref( $self ) )
    {
        foreach( CORE::keys( %$hash ) )
        {
            $self->{ $_ } = CORE::delete( $hash->{ $_ } );
        }
        $new = $self;
    }
    else
    {
        $new = CORE::bless( $hash => $class );
    }
    CORE::return( $new );
}

1;

__END__