The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use 5.008;
use strict;
use warnings;

{
	package Tie::Hash::MultiValueOrdered;
	
	our $AUTHORITY = 'cpan:TOBYINK';
	our $VERSION   = '0.004';
	
	use constant {
		IDX_DATA  => 0,
		IDX_ORDER => 1,
		IDX_LAST  => 2,
		IDX_SEEN  => 3,
		IDX_MODE  => 4,
		NEXT_IDX  => 5,
	};
	use constant {
		MODE_LAST  => -1,
		MODE_FIRST => 0,
		MODE_REF   => 'ref',
		MODE_ITER  => 'iter',
	};
	
	sub fetch_first    { $_[0][IDX_MODE] = MODE_FIRST }
	sub fetch_last     { $_[0][IDX_MODE] = MODE_LAST }
	sub fetch_list     { $_[0][IDX_MODE] = MODE_REF }
	sub fetch_iterator { $_[0][IDX_MODE] = MODE_ITER }
	
	use Storable qw( dclone );
	sub TIEHASH {
		my $class = shift;
		bless [{}, [], 0, {}, -1], $class;
	}
	sub STORE {
		my ($tied, $key, $value) = @_;
		$key = "$key";
		push @{$tied->[IDX_ORDER]}, $key;
		push @{$tied->[IDX_DATA]{$key}}, $value;
	}
	sub FETCH {
		my ($tied, $key) = @_;
		my $mode = $tied->[IDX_MODE];
		if ($mode eq 'ref')
		{
			return $tied->[IDX_DATA]{$key} || [];
		}
		elsif ($mode eq 'iter')
		{
			my @values = @{ $tied->[IDX_DATA]{$key} || [] };
			return sub { shift @values };
		}
		else
		{
			return unless exists $tied->[IDX_DATA]{"$key"};
			return $tied->[IDX_DATA]{$key}[$mode];
		}
	}
	sub EXISTS {
		my ($tied, $key) = @_;
		return exists $tied->[IDX_DATA]{"$key"};
	}
	sub DELETE {
		my ($tied, $key) = @_;
		my $r = delete $tied->[IDX_DATA]{"$key"};
		return $r->[-1] if $r;
		return;
	}
	sub CLEAR {
		my $tied = shift;
		$tied->[IDX_DATA]  = {};
		$tied->[IDX_ORDER] = [];
		$tied->[IDX_LAST]  = 0;
		$tied->[IDX_SEEN]  = {};
		return;
	}
	sub FIRSTKEY {
		my $tied = shift;
		$tied->[IDX_LAST] = -1;
		$tied->[IDX_SEEN] = {};
		return $tied->NEXTKEY;
	}
	sub NEXTKEY {
		no warnings qw(uninitialized);
		my $tied = shift;
		my $i = ++$tied->[IDX_LAST];
		$i++ while $tied->[IDX_SEEN]{ $tied->[IDX_ORDER][$i] };
		$tied->[IDX_SEEN]{ $tied->[IDX_ORDER][$i] }++;
		my $key = $tied->[IDX_ORDER][$i];
		if (wantarray) {
			return (
				$tied->[IDX_ORDER][$i],
				$tied->FETCH( $tied->[IDX_ORDER][$i] ),
			);
		}
		return $tied->[IDX_ORDER][$i];
	}
	sub get {
		my ($tied, $key) = @_;
		return my @list = @{ $tied->[IDX_DATA]{"$key"} || [] };
	}
	sub pairs {
		my $tied = shift;
		my $clone = dclone( $tied->[IDX_DATA] );
		return map {
			$_, shift @{$clone->{$_}}
		} @{$tied->[IDX_ORDER]}
	}
	sub pair_refs {
		my $tied = shift;
		my $clone = dclone( $tied->[IDX_DATA] );
		return map {
			[ $_, shift @{$clone->{$_}} ]
		} @{$tied->[IDX_ORDER]}
	}
	sub all_keys {
		my $tied = shift;
		return @{$tied->[IDX_ORDER]};
	}
	sub keys {
		my $tied = shift;
		my %seen;
		return grep { not $seen{$_}++ } @{$tied->[IDX_ORDER]};
	}
	sub rr_keys {
		my $tied = shift;
		my %seen;
		return reverse grep { not $seen{$_}++ } reverse @{$tied->[IDX_ORDER]};
	}
	sub all_values {
		my $tied = shift;
		my $alt = 1;
		return grep { $alt=!$alt } $tied->pairs;
	}
	sub values {
		my $tied = shift;
		return map { $tied->[IDX_DATA]{$_}[-1] } $tied->keys;
	}
	sub rr_values {
		my $tied = shift;
		return map { $tied->[IDX_DATA]{$_}[0] } $tied->keys;
	}
}

1;


__END__

=head1 NAME

Tie::Hash::MultiValueOrdered - hash with multiple values per key, and ordered keys

=head1 SYNOPSIS

   use Test::More;
   use Tie::Hash::MultiValueOrdered;
   
   my $tied = tie my %hash, "Tie::Hash::MultiValueOrdered";
   
   $hash{a} = 1;
   $hash{b} = 2;
   $hash{a} = 3;
   $hash{b} = 4;
   
   # Order of keys is predictable
   is_deeply(
      [ keys %hash ],
      [ qw( a b ) ],
   );
   
   # Order of values is predictable
   # Note that the last values of 'a' and 'b' are returned.
   is_deeply(
      [ values %hash ],
      [ qw( 3 4 ) ],
   );
   
   # Can retrieve list of all key-value pairs
   is_deeply(
      [ $tied->pairs ],
      [ qw( a 1 b 2 a 3 b 4 ) ],
   );
   
   # Switch the retrieval mode for the hash.
   $tied->fetch_first;
   
   # Now the first values of 'a' and 'b' are returned.
   is_deeply(
      [ values %hash ],
      [ qw( 1 2 ) ],
   );
   
   # Switch the retrieval mode for the hash.
   $tied->fetch_list;
   
   # Now arrayrefs are returned.
   is_deeply(
      [ values %hash ],
      [ [1,3], [2,4] ],
   );
   
   # Restore the default retrieval mode for the hash.
   $tied->fetch_last;
   
   done_testing;

=head1 DESCRIPTION

A hash tied to this class acts more or less like a standard hash, except that
when you assign a new value to an existing key, the old value is retained
underneath. An explicit C<delete> deletes all values associated with a key.

By default, the old values are inaccessible through the hash interface, but
can be retrieved via the tied object:

   my @values = tied(%hash)->get($key);

However, the C<< fetch_* >> methods provide a means to alter the behaviour of
the hash.

=head2 Tied Object Methods

=over

=item C<< pairs >>

Returns all the hash's key-value pairs (including duplicates) as a flattened
list.

=item C<< pair_refs >>

Returns all the hash's key-value pairs (including duplicates) as a list of two
item arrayrefs.

=item C<< get($key) >>

Returns the list of all values associated with a key.

=item C<< keys >>

The list of all hash keys in their original order. Where a key is duplicated,
only the first occurance is returned.

=item C<< rr_keys >>

The list of all hash keys in their original order. Where a key is duplicated,
only the last occurance is returned.

=item C<< all_keys >>

The list of all hash keys in their original order, including duplicates.

=item C<< values >>

The values correponding to C<keys>.

=item C<< rr_values >>

The values correponding to C<rr_keys>.

=item C<< all_values >>

The values correponding to C<all_keys>.

=back

=head2 Fetch Styles

=over

=item C<< fetch_last >>

This is the default style of fetching.

   tie my %hash, "Tie::Hash::MultiValueOrdered";
   
   $hash{a} = 1;
   $hash{b} = 2;
   $hash{b} = 3;
   
   tied(%hash)->fetch_last;
   
   is($hash{a}, 1);
   is($hash{b}, 3);

=item C<< fetch_first >>

   tie my %hash, "Tie::Hash::MultiValueOrdered";
   
   $hash{a} = 1;
   $hash{b} = 2;
   $hash{b} = 3;
   
   tied(%hash)->fetch_first;
   
   is($hash{a}, 1);
   is($hash{b}, 2);

=item C<< fetch_list >>

   tie my %hash, "Tie::Hash::MultiValueOrdered";
   
   $hash{a} = 1;
   $hash{b} = 2;
   $hash{b} = 3;
   
   tied(%hash)->fetch_first;
   
   is_deeply($hash{a}, [1]);
   is_deeply($hash{b}, [2, 3]);

=item C<< fetch_iterator >>

This fetch style is experimental and subject to change.

   tie my %hash, "Tie::Hash::MultiValueOrdered";
   
   $hash{a} = 1;
   $hash{b} = 2;
   $hash{b} = 3;
   
   tied(%hash)->fetch_iterator;
   
   my $A = $hash{a};
   my $B = $hash{b};
   
   is($A->(), 1);
   is($B->(), 2);
   is($B->(), 3);

=back

=head1 BUGS

Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=JSON-MultiValueOrdered>.

=head1 SEE ALSO

L<JSON::Tiny::Subclassable>,
L<JSON::Tiny>,
L<Mojo::JSON>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2012-2013 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.