###############################################################################
## ----------------------------------------------------------------------------
## Hash helper class.
##
###############################################################################

package MCE::Shared::Hash;

use strict;
use warnings;

use 5.010001;

no warnings qw( threads recursion uninitialized numeric );

our $VERSION = '1.879';

## no critic (TestingAndDebugging::ProhibitNoStrict)

use MCE::Shared::Base ();
use base 'MCE::Shared::Base::Common';

use overload (
   q("")    => \&MCE::Shared::Base::_stringify,
   q(0+)    => \&MCE::Shared::Base::_numify,
   fallback => 1
);

###############################################################################
## ----------------------------------------------------------------------------
## Based on Tie::StdHash from Tie::Hash.
##
###############################################################################

sub TIEHASH {
   my $self = bless {}, shift;
   %{ $self } = @_ if @_;

   $self;
}

sub STORE    { $_[0]->{ $_[1] } = $_[2] }
sub FETCH    { $_[0]->{ $_[1] } }
sub DELETE   { delete $_[0]->{ $_[1] } }
sub FIRSTKEY { my $a = keys %{ $_[0] }; each %{ $_[0] } }
sub NEXTKEY  { each %{ $_[0] } }
sub EXISTS   { exists $_[0]->{ $_[1] } }
sub CLEAR    { %{ $_[0] } = () }
sub SCALAR   { scalar keys %{ $_[0] } }

###############################################################################
## ----------------------------------------------------------------------------
## _find, clone, flush, iterator, keys, pairs, values
##
###############################################################################

# _find ( { getkeys => 1 }, "query string" )
# _find ( { getvals => 1 }, "query string" )
# _find ( "query string" ) # pairs

sub _find {
   my $self   = shift;
   my $params = ref($_[0]) eq 'HASH' ? shift : {};
   my $query  = shift;

   MCE::Shared::Base::_find_hash( $self, $params, $query );
}

# clone ( key [, key, ... ] )
# clone ( )

sub clone {
   my $self = shift;
   my $params = ref($_[0]) eq 'HASH' ? shift : {};
   my %data;

   if ( @_ ) {
      @data{ @_ } = @{ $self }{ @_ };
   }
   else {
      %data = %{ $self };
   }

   $self->clear() if $params->{'flush'};

   bless \%data, ref $self;
}

# flush ( key [, key, ... ] )
# flush ( )

sub flush {
   shift()->clone( { flush => 1 }, @_ );
}

# iterator ( key [, key, ... ] )
# iterator ( "query string" )
# iterator ( )

sub iterator {
   my ( $self, @keys ) = @_;

   if ( ! @keys ) {
      @keys = CORE::keys %{ $self };
   }
   elsif ( @keys == 1 && $keys[0] =~ /^(?:key|val)[ ]+\S\S?[ ]+\S/ ) {
      @keys = $self->keys($keys[0]);
   }

   return sub {
      return unless @keys;
      my $key = shift @keys;
      return ( $key => $self->{ $key } );
   };
}

# keys ( key [, key, ... ] )
# keys ( "query string" )
# keys ( )

sub keys {
   my $self = shift;

   if ( @_ == 1 && $_[0] =~ /^(?:key|val)[ ]+\S\S?[ ]+\S/ ) {
      $self->_find({ getkeys => 1 }, @_);
   }
   elsif ( wantarray ) {
      @_ ? map { exists $self->{ $_ } ? $_ : undef } @_
         : CORE::keys %{ $self };
   }
   else {
      scalar CORE::keys %{ $self };
   }
}

# pairs ( key [, key, ... ] )
# pairs ( "query string" )
# pairs ( )

sub pairs {
   my $self = shift;

   if ( @_ == 1 && $_[0] =~ /^(?:key|val)[ ]+\S\S?[ ]+\S/ ) {
      $self->_find(@_);
   }
   elsif ( wantarray ) {
      @_ ? map { $_ => $self->{ $_ } } @_
         : %{ $self };
   }
   else {
      scalar CORE::keys %{ $self };
   }
}

# values ( key [, key, ... ] )
# values ( "query string" )
# values ( )

sub values {
   my $self = shift;

   if ( @_ == 1 && $_[0] =~ /^(?:key|val)[ ]+\S\S?[ ]+\S/ ) {
      $self->_find({ getvals => 1 }, @_);
   }
   elsif ( wantarray ) {
      @_ ? @{ $self }{ @_ }
         : CORE::values %{ $self };
   }
   else {
      scalar CORE::keys %{ $self };
   }
}

###############################################################################
## ----------------------------------------------------------------------------
## assign, mdel, mexists, mget, mset
##
###############################################################################

# assign ( key, value [, key, value, ... ] )

sub assign {
   $_[0]->clear; shift()->mset(@_);
}

# mdel ( key [, key, ... ] )

sub mdel {
   my $self = shift;
   my ( $cnt, $key ) = ( 0 );

   while ( @_ ) {
      $key = shift;
      $cnt++, delete($self->{ $key }) if ( exists $self->{ $key } );
   }

   $cnt;
}

# mexists ( key [, key, ... ] )

sub mexists {
   my $self = shift;
   my $key;

   while ( @_ ) {
      $key = shift;
      return '' unless ( exists $self->{ $key } );
   }

   1;
}

# mget ( key [, key, ... ] )

sub mget {
   my $self = shift;

   @_ ? @{ $self }{ @_ } : ();
}

# mset ( key, value [, key, value, ... ] )

sub mset {
   my ( $self, $key ) = ( shift );

   while ( @_ ) {
      $key = shift, $self->{ $key } = shift;
   }

   defined wantarray ? scalar CORE::keys %{ $self } : ();
}

###############################################################################
## ----------------------------------------------------------------------------
## Sugar API, mostly resembles https://redis.io/commands#string primitives.
##
###############################################################################

# append ( key, string )

sub append {
   length( $_[0]->{ $_[1] } .= $_[2] // '' );
}

# decr    ( key )
# decrby  ( key, number )
# incr    ( key )
# incrby  ( key, number )
# getdecr ( key )
# getincr ( key )

sub decr    { --$_[0]->{ $_[1] }               }
sub decrby  {   $_[0]->{ $_[1] } -= $_[2] || 0 }
sub incr    { ++$_[0]->{ $_[1] }               }
sub incrby  {   $_[0]->{ $_[1] } += $_[2] || 0 }
sub getdecr {   $_[0]->{ $_[1] }--        // 0 }
sub getincr {   $_[0]->{ $_[1] }++        // 0 }

# getset ( key, value )

sub getset {
   my $old = $_[0]->{ $_[1] };
   $_[0]->{ $_[1] } = $_[2];

   $old;
}

# setnx ( key, value )

sub setnx {
   return 0 if ( exists $_[0]->{ $_[1] } );
   $_[0]->{ $_[1] } = $_[2];

   1;
}

# len ( key )
# len ( )

sub len {
   ( defined $_[1] )
      ? length $_[0]->{ $_[1] }
      : scalar CORE::keys %{ $_[0] };
}

{
   no strict 'refs';

   *{ __PACKAGE__.'::new'    } = \&TIEHASH;
   *{ __PACKAGE__.'::set'    } = \&STORE;
   *{ __PACKAGE__.'::get'    } = \&FETCH;
   *{ __PACKAGE__.'::delete' } = \&DELETE;
   *{ __PACKAGE__.'::exists' } = \&EXISTS;
   *{ __PACKAGE__.'::clear'  } = \&CLEAR;
   *{ __PACKAGE__.'::del'    } = \&delete;
   *{ __PACKAGE__.'::merge'  } = \&mset;
   *{ __PACKAGE__.'::vals'   } = \&values;
}

1;

__END__

###############################################################################
## ----------------------------------------------------------------------------
## Module usage.
##
###############################################################################

=head1 NAME

MCE::Shared::Hash - Hash helper class

=head1 VERSION

This document describes MCE::Shared::Hash version 1.879

=head1 DESCRIPTION

A hash helper class for use as a standalone or managed by L<MCE::Shared>.

=head1 SYNOPSIS

 # non-shared or local construction for use by a single process

 use MCE::Shared::Hash;

 my $ha = MCE::Shared::Hash->new( @pairs );

 # construction for sharing with other threads and processes

 use MCE::Shared;

 my $ha = MCE::Shared->hash( @pairs );

 # hash-like dereferencing

 my $val = $ha->{$key};
 $ha->{$key} = $val;

 %{$ha} = ();

 # OO interface

 if ( !defined ( $val = $ha->get("some_key") ) ) {
    $val = $ha->set( some_key => "some_value" );
 }

 $val   = $ha->set( $key, $val );
 $ret   = $ha->setnx( $key, $val );         # set only if the key exists
 $val   = $ha->get( $key );
 $val   = $ha->delete( $key );              # del is an alias for delete
 $bool  = $ha->exists( $key );
 void   = $ha->clear();
 $len   = $ha->len();                       # scalar keys %{ $ha }
 $len   = $ha->len( $key );                 # length $ha->{ $key }

 $ha2   = $ha->clone( @keys );              # @keys is optional
 $ha3   = $ha->flush( @keys );
 $iter  = $ha->iterator( @keys );           # ($key, $val) = $iter->()
 @keys  = $ha->keys( @keys );
 %pairs = $ha->pairs( @keys );
 @vals  = $ha->values( @keys );             # vals is an alias for values

 $len   = $ha->assign( $key/$val pairs );   # equivalent to ->clear, ->mset
 $cnt   = $ha->mdel( @keys );
 @vals  = $ha->mget( @keys );
 $bool  = $ha->mexists( @keys );            # true if all keys exists
 $len   = $ha->mset( $key/$val pairs );     # merge is an alias for mset

 # included, sugar methods without having to call set/get explicitly

 $len   = $ha->append( $key, $string );     #   $val .= $string
 $val   = $ha->decr( $key );                # --$val
 $val   = $ha->decrby( $key, $number );     #   $val -= $number
 $val   = $ha->getdecr( $key );             #   $val--
 $val   = $ha->getincr( $key );             #   $val++
 $val   = $ha->incr( $key );                # ++$val
 $val   = $ha->incrby( $key, $number );     #   $val += $number
 $old   = $ha->getset( $key, $new );        #   $o = $v, $v = $n, $o

 # pipeline, provides atomicity for shared objects, MCE::Shared v1.09+

 @vals  = $ha->pipeline(                    # ( "a_a", "b_b", "c_c" )
    [ "set", foo => "a_a" ],
    [ "set", bar => "b_b" ],
    [ "set", baz => "c_c" ],
    [ "mget", qw/ foo bar baz / ]
 );

For normal hash behavior, the TIE interface is supported.

 # non-shared or local construction for use by a single process

 use MCE::Shared::Hash;

 tie my %ha, "MCE::Shared::Hash";

 # construction for sharing with other threads and processes

 use MCE::Shared;

 tie my %ha, "MCE::Shared";

 # usage

 my $val;

 if ( !defined ( $val = $ha{some_key} ) ) {
    $val = $ha{some_key} = "some_value";
 }

 $ha{some_key} = 0;

 tied(%ha)->incrby("some_key", 20);
 tied(%ha)->incrby(some_key => 20);

=head1 SYNTAX for QUERY STRING

Several methods take a query string for an argument. The format of the string
is described below. In the context of sharing, the query mechanism is beneficial
for the shared-manager process. It is able to perform the query where the data
resides versus the client-process grep locally involving lots of IPC.

 o Basic demonstration

   @keys = $ha->keys( "query string given here" );
   @keys = $ha->keys( "val =~ /pattern/" );

 o Supported operators: =~ !~ eq ne lt le gt ge == != < <= > >=
 o Multiple expressions delimited by :AND or :OR, mixed case allowed

   "key eq 'some key' :or (val > 5 :and val < 9)"
   "key eq some key :or (val > 5 :and val < 9)"
   "key =~ /pattern/i :And val =~ /pattern/i"
   "val eq foo baz :OR key !~ /pattern/i"

   * key matches on keys in the hash
   * likewise, val matches on values

 o Quoting is optional inside the string

   "key =~ /pattern/i :AND val eq 'foo bar'"   # val eq "foo bar"
   "key =~ /pattern/i :AND val eq foo bar"     # val eq "foo bar"

Examples.

 # search capability key/val: =~ !~ eq ne lt le gt ge == != < <= > >=
 # key/val means to match against actual key/val respectively

 @keys  = $ha->keys( "key eq 'some key' :or (val > 5 :and val < 9)" );
 @keys  = $ha->keys( "key eq some key :or (val > 5 :and val < 9)" );

 @keys  = $ha->keys( "key =~ /$pattern/i" );
 @keys  = $ha->keys( "key !~ /$pattern/i" );
 @keys  = $ha->keys( "val =~ /$pattern/i" );
 @keys  = $ha->keys( "val !~ /$pattern/i" );

 %pairs = $ha->pairs( "key == $number" );
 %pairs = $ha->pairs( "key != $number :and val > 100" );
 %pairs = $ha->pairs( "key <  $number :or key > $number" );
 %pairs = $ha->pairs( "val <= $number" );
 %pairs = $ha->pairs( "val >  $number" );
 %pairs = $ha->pairs( "val >= $number" );

 @vals  = $ha->vals( "key eq $string" );
 @vals  = $ha->vals( "key ne $string with space" );
 @vals  = $ha->vals( "key lt $string :or val =~ /$pat1|$pat2/" );
 @vals  = $ha->vals( "val le $string :and val eq 'foo bar'" );
 @vals  = $ha->vals( "val le $string :and val eq foo bar" );
 @vals  = $ha->vals( "val gt $string" );
 @vals  = $ha->vals( "val ge $string" );

=head1 API DOCUMENTATION

This module may involve TIE when accessing the object via hash-like behavior.
Only shared instances are impacted if doing so. Although likely fast enough for
many use cases, the OO interface is recommended for best performance.

=head2 MCE::Shared::Hash->new ( key, value [, key, value, ... ] )

=head2 MCE::Shared->hash ( key, value [, key, value, ... ] )

Constructs a new object, with an optional list of key-value pairs.

 # non-shared or local construction for use by a single process

 use MCE::Shared::Hash;

 $ha = MCE::Shared::Hash->new( @pairs );
 $ha = MCE::Shared::Hash->new( );

 # construction for sharing with other threads and processes

 use MCE::Shared;

 $ha = MCE::Shared->hash( @pairs );
 $ha = MCE::Shared->hash( );

=head2 assign ( key, value [, key, value, ... ] )

Clears the hash, then sets multiple key-value pairs and returns the number of
keys stored in the hash. This is equivalent to C<clear>, C<mset>.

 $len = $ha->assign( "key1" => "val1", "key2" => "val2" );  # 2
 $len = %{$ha} = ( "key1" => "val1", "key2" => "val2" );    # 4

API available since 1.007.

=head2 clear

Removes all key-value pairs from the hash.

 $ha->clear;
 %{$ha} = ();

=head2 clone ( key [, key, ... ] )

Creates a shallow copy, a C<MCE::Shared::Hash> object. It returns an exact
copy if no arguments are given. Otherwise, the object includes only the given
keys. Keys that do not exist in the hash will have the C<undef> value.

 $ha2 = $ha->clone( "key1", "key2" );
 $ha2 = $ha->clone;

=head2 delete ( key )

Deletes and returns the value by given key or C<undef> if the key does not
exists in the hash.

 $val = $ha->delete( "some_key" );
 $val = delete $ha->{ "some_key" };

=head2 del

C<del> is an alias for C<delete>.

=head2 exists ( key )

Determines if a key exists in the hash.

 if ( $ha->exists( "some_key" ) ) { ... }
 if ( exists $ha->{ "some_key" } ) { ... }

=head2 flush ( key [, key, ... ] )

Same as C<clone>. Though, clears all existing items before returning.

=head2 get ( key )

Gets the value of a hash key or C<undef> if the key does not exists.

 $val = $ha->get( "some_key" );
 $val = $ha->{ "some_key" };

=head2 iterator ( key [, key, ... ] )

Returns a code reference for iterating a list of key-value pairs stored in
the hash when no arguments are given. Otherwise, returns a code reference for
iterating the given keys in the same order. Keys that do not exist will have
the C<undef> value.

The list of keys to return is set when the closure is constructed. Later keys
added to the hash are not included. Subsequently, the C<undef> value is
returned for deleted keys.

 $iter = $ha->iterator;
 $iter = $ha->iterator( "key1", "key2" );

 while ( my ( $key, $val ) = $iter->() ) {
    ...
 }

=head2 iterator ( "query string" )

Returns a code reference for iterating a list of key-value pairs that match
the given criteria. It returns an empty list if the search found nothing.
The syntax for the C<query string> is described above.

 $iter = $ha->iterator( "val eq some_value" );
 $iter = $ha->iterator( "key eq some_key :AND val =~ /sun|moon|air|wind/" );
 $iter = $ha->iterator( "val eq sun :OR val eq moon :OR val eq foo" );
 $iter = $ha->iterator( "key =~ /$pattern/" );

 while ( my ( $key, $val ) = $iter->() ) {
    ...
 }

=head2 keys ( key [, key, ... ] )

Returns all keys in the hash when no arguments are given. Otherwise, returns
the given keys in the same order. Keys that do not exist will have the C<undef>
value. In scalar context, returns the size of the hash.

 @keys = $ha->keys( "key1", "key2" );

 @keys = $ha->keys;     # faster
 @keys = keys %{$ha};   # involves TIE overhead

 $len  = $ha->keys;     # ditto
 $len  = keys %{$ha};

=head2 keys ( "query string" )

Returns only keys that match the given criteria. It returns an empty list
if the search found nothing. The syntax for the C<query string> is described
above. In scalar context, returns the size of the resulting list.

 @keys = $ha->keys( "val eq some_value" );
 @keys = $ha->keys( "key eq some_key :AND val =~ /sun|moon|air|wind/" );
 @keys = $ha->keys( "val eq sun :OR val eq moon :OR val eq foo" );
 $len  = $ha->keys( "key =~ /$pattern/" );

=head2 len ( key )

Returns the size of the hash when no arguments are given. For the given key,
returns the length of the value stored at key or the C<undef> value if the
key does not exists.

 $size = $ha->len;
 $len  = $ha->len( "key1" );
 $len  = length $ha->{ "key1" };

=head2 mdel ( key [, key, ... ] )

Deletes one or more keys in the hash and returns the number of keys deleted.
A given key which does not exist in the hash is not counted.

 $cnt = $ha->mdel( "key1", "key2" );

=head2 mexists ( key [, key, ... ] )

Returns a true value if all given keys exists in the hash. A false value is
returned otherwise.

 if ( $ha->mexists( "key1", "key2" ) ) { ... }

=head2 mget ( key [, key, ... ] )

Gets the values of all given keys. It returns C<undef> for keys which do not
exists in the hash.

 ( $val1, $val2 ) = $ha->mget( "key1", "key2" );

=head2 mset ( key, value [, key, value, ... ] )

Sets multiple key-value pairs in a hash and returns the number of keys stored
in the hash.

 $len = $ha->mset( "key1" => "val1", "key2" => "val2" );

=head2 merge

C<merge> is an alias for C<mset>.

=head2 pairs ( key [, key, ... ] )

Returns key-value pairs in the hash when no arguments are given. Otherwise,
returns key-value pairs for the given keys in the same order. Keys that do not
exist will have the C<undef> value. In scalar context, returns the size of the
hash.

 @pairs = $ha->pairs( "key1", "key2" );

 @pairs = $ha->pairs;
 $len   = $ha->pairs;

=head2 pairs ( "query string" )

Returns only key-value pairs that match the given criteria. It returns an
empty list if the search found nothing. The syntax for the C<query string> is
described above. In scalar context, returns the size of the resulting list.

 @pairs = $ha->pairs( "val eq some_value" );
 @pairs = $ha->pairs( "key eq some_key :AND val =~ /sun|moon|air|wind/" );
 @pairs = $ha->pairs( "val eq sun :OR val eq moon :OR val eq foo" );
 $len   = $ha->pairs( "key =~ /$pattern/" );

=head2 pipeline ( [ func1, @args ], [ func2, @args ], ... )

Combines multiple commands for the object to be processed serially. For shared
objects, the call is made atomically due to single IPC to the shared-manager
process. The C<pipeline> method is fully C<wantarray>-aware and receives a list
of commands and their arguments. In scalar or list context, it returns data
from the last command in the pipeline.

 @vals = $ha->pipeline(                     # ( "a_a", "b_b", "c_c" )
    [ "set", foo => "a_a" ],
    [ "set", bar => "b_b" ],
    [ "set", baz => "c_c" ],
    [ "mget", qw/ foo bar baz / ]
 );

 $len = $ha->pipeline(                      # 3, same as $ha->len
    [ "set", foo => "i_i" ],
    [ "set", bar => "j_j" ],
    [ "set", baz => "k_k" ],
    [ "len" ]
 );

 $ha->pipeline(
    [ "set", foo => "m_m" ],
    [ "set", bar => "n_n" ],
    [ "set", baz => "o_o" ]
 );

Current API available since 1.809.

=head2 pipeline_ex ( [ func1, @args ], [ func2, @args ], ... )

Same as C<pipeline>, but returns data for every command in the pipeline.

 @vals = $ha->pipeline_ex(                  # ( "a_a", "b_b", "c_c" )
    [ "set", foo => "a_a" ],
    [ "set", bar => "b_b" ],
    [ "set", baz => "c_c" ]
 );

Current API available since 1.809.

=head2 set ( key, value )

Sets the value of the given hash key and returns its new value.

 $val = $ha->set( "key", "value" );
 $val = $ha->{ "key" } = "value";

=head2 setnx ( key, value )

Sets the value of a hash key, only if the key does not exist. Returns a 1
for new key or 0 if the key already exists and no operation was performed.

 $ret = $ha->setnx( "key", "value" );

Current API available since 1.872.

=head2 values ( key [, key, ... ] )

Returns all values in the hash when no arguments are given. Otherwise, returns
values for the given keys in the same order. Keys that do not exist will have
the C<undef> value. In scalar context, returns the size of the hash.

 @vals = $ha->values( "key1", "key2" );

 @vals = $ha->values;     # faster
 @vals = values %{$ha};   # involves TIE overhead

 $len  = $ha->values;     # ditto
 $len  = values %{$ha};

=head2 values ( "query string" )

Returns only values that match the given criteria. It returns an empty list
if the search found nothing. The syntax for the C<query string> is described
above. In scalar context, returns the size of the resulting list.

 @vals = $ha->values( "val eq some_value" );
 @vals = $ha->values( "key eq some_key :AND val =~ /sun|moon|air|wind/" );
 @vals = $ha->values( "val eq sun :OR val eq moon :OR val eq foo" );
 $len  = $ha->values( "key =~ /$pattern/" );

=head2 vals

C<vals> is an alias for C<values>.

=head1 SUGAR METHODS

This module is equipped with sugar methods to not have to call C<set>
and C<get> explicitly. In shared context, the benefit is atomicity and
reduction in inter-process communication.

The API resembles a subset of the Redis primitives
L<https://redis.io/commands#strings> with key representing the hash key.

=head2 append ( key, string )

Appends a value to a key and returns its new length.

 $len = $ha->append( $key, "foo" );

=head2 decr ( key )

Decrements the value of a key by one and returns its new value.

 $num = $ha->decr( $key );

=head2 decrby ( key, number )

Decrements the value of a key by the given number and returns its new value.

 $num = $ha->decrby( $key, 2 );

=head2 getdecr ( key )

Decrements the value of a key by one and returns its old value.

 $old = $ha->getdecr( $key );

=head2 getincr ( key )

Increments the value of a key by one and returns its old value.

 $old = $ha->getincr( $key );

=head2 getset ( key, value )

Sets the value of a key and returns its old value.

 $old = $ha->getset( $key, "baz" );

=head2 incr ( key )

Increments the value of a key by one and returns its new value.

 $num = $ha->incr( $key );

=head2 incrby ( key, number )

Increments the value of a key by the given number and returns its new value.

 $num = $ha->incrby( $key, 2 );

=head1 CREDITS

The implementation is inspired by L<Tie::StdHash>.

=head1 INDEX

L<MCE|MCE>, L<MCE::Hobo>, L<MCE::Shared>

=head1 AUTHOR

Mario E. Roy, S<E<lt>marioeroy AT gmail DOT comE<gt>>

=cut

