The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Smart::Dispatch::Table;

BEGIN {
	*_TYPES = $ENV{PERL_SMART_DISPATCH_TYPE_CHECKS}==42
		? sub () { 1 }
		: sub () { 0 };
};

use 5.010;
use Moo;
use Carp;
use Scalar::Util qw/ refaddr blessed /;
use if _TYPES, 'MooX::Types::MooseLike::Base', ':all';

sub _swap
{
	my ($x, $y, $swap) = @_;
	$swap ? ($y, $x) : ($x, $y);
}

use namespace::clean;

BEGIN {
	$Smart::Dispatch::Table::AUTHORITY = 'cpan:TOBYINK';
	$Smart::Dispatch::Table::VERSION   = '0.003';
}

use overload
	'&{}'    => sub { my $x=shift; sub { $x->action($_[0]) } },
	'+'      => sub { __PACKAGE__->make_combined(reverse _swap(@_)) },
	'.'      => sub { __PACKAGE__->make_combined(_swap(@_)) },
	'+='     => 'prepend',
	'.='     => 'append',
	'~~'     => 'exists',
	'bool'   => sub { 1 },
;

has match_list => (
	(_TYPES?(isa=>ArrayRef()):()),
	is        => 'rw',
	required  => 1,
);

sub BUILD
{
	my ($self) = @_;
	$self->validate_match_list;
}

sub make_combined
{
	my ($class, @all) = @_;
	my $self = $class->new(match_list => []);
	$self->append(@all);
}

sub validate_match_list
{
	my ($self) = @_;
	my @otherwise = $self->unconditional_matches;
	if (scalar @otherwise > 1)
	{
		carp "Too many 'otherwise' matches. Only one allowed.";
	}
	if (@otherwise and refaddr($otherwise[0]) != refaddr($self->match_list->[-1]))
	{
		carp "The 'otherwise' match is not the last match.";
	}
}

sub all_matches
{
	my ($self) = @_;
	@{ $self->match_list };
}

sub unconditional_matches
{
	my ($self) = @_;
	grep { $_->is_unconditional } @{ $self->match_list };
}

sub conditional_matches
{
	my ($self) = @_;
	grep { !$_->is_unconditional } @{ $self->match_list };
}

sub exists
{
	my ($self, $value, $allow_fails) = @_;
	foreach my $cond (@{ $self->match_list })
	{
		if ($cond->value_matches($value))
		{
			if ($allow_fails or not $cond->is_failover)
			{
				return $cond;
			}
			else
			{
				return;
			}
		}
	}
	return;
}

sub action
{
	my ($self, $value, @args) = @_;
	my $cond = $self->exists($value, 1);
	return $cond->conduct_dispatch($value, @args) if $cond;
	return;
}

sub append
{
	my $self = shift;
	foreach my $other (@_)
	{
		next unless defined $other;
		carp "Cannot add non-reference to dispatch table"
			unless ref $other;
		carp "Cannot add non-blessed reference to dispatch table"
			unless blessed $other;
		
		if ($other->isa(__PACKAGE__))
		{
			$self->match_list([
				$self->conditional_matches,
				$other->conditional_matches,
				($self->unconditional_matches ? $self->unconditional_matches : $other->unconditional_matches),
			]);
		}
		elsif ($other->isa('Smart::Dispatch::Match')
		and not $other->is_unconditional)
		{
			$self->match_list([
				$self->conditional_matches,
				$other,
				$self->unconditional_matches,
			]);
		}
		elsif ($other->isa('Smart::Dispatch::Match')
		and $other->is_unconditional)
		{
			$self->match_list([
				$self->conditional_matches,
				($self->unconditional_matches ? $self->conditional_matches : $other),
			]);
		}
		else
		{
			carp sprintf("Cannot add object of type '%s' to dispatch table", ref $other);
		}
	}
	
	$self->validate_match_list;
	return $self;
}

sub prepend
{
	my $self = shift;
	foreach my $other (@_)
	{
		next unless defined $other;
		carp "Cannot add non-reference to dispatch table"
			unless ref $other;
		carp "Cannot add non-blessed reference to dispatch table"
			unless blessed $other;
		
		if ($other->isa(__PACKAGE__))
		{
			$self->match_list([
				$other->conditional_matches,
				$self->conditional_matches,
				($other->unconditional_matches ? $other->unconditional_matches : $self->unconditional_matches),
			]);
		}
		elsif ($other->isa('Smart::Dispatch::Match')
		and not $other->is_unconditional)
		{
			$self->conditions([
				$other,
				$self->conditional_matches,
				$self->unconditional_matches,
			]);
		}
		elsif ($other->isa('Smart::Dispatch::Match')
		and $other->is_unconditional)
		{
			$self->conditions([
				$self->conditional_matches,
				$other,
			]);
		}
		else
		{
			carp sprintf("Cannot add object of type '%s' to dispatch table", ref $other);
		}
	}
	
	$self->validate_match_list;
	return $self;
}

__PACKAGE__
__END__

=head1 NAME

Smart::Dispatch::Table - a dispatch table

=head1 DESCRIPTION

Smart::Dispatch::Table is a Moose class.
(Well, L<Moo> actually, but close enough.)

=head2 Constructors

=over

=item * C<< new(%attributes) >>

Create a new dispatch table.

=item * C<< make_combined($table1, $table2, ...) >> 

Combine existing tables into a new one.

=back

=head2 Attributes

=over

=item * C<match_list>

is 'rw', isa 'ArrayRef[Smart::Dispatch::Match]'.

=back

=head2 Methods

=over

=item * C<< exists($value, $include_failovers) >>

Searches for a Smart::Dispatch::Match that matches C<$value>. Ignores
failover matches, unless optional argument C<$include_failovers> is
true. Returns Smart::Dispatch::Match if it finds a match; returns nothing
otherwise.

TL;DR: checks if value C<$value> can be dispatched.

=item * C<< action($value, @additional) >>

Calls C<exists> with C<$include_failovers> set to true, then, if there
is a result, calls C<< conduct_dispatch($value, @additional) >> on that
result.

TL;DR: dispatches value C<$value>.

=item * C<conditional_matches>

Returns a list of conditional matches. (Smart::Dispatch::Match objects.)

=item * C<unconditional_matches>

Returns a list of unconditional matches. (Smart::Dispatch::Match objects.)

Should only ever be zero or one items in the list.

=item * C<all_matches>

Returns the list which is the union of the above two lists.

=item * C<< append(@things) >>

Each thing must be a Smart::Dispatch::Table or a Smart::Dispatch::Match.

Handles conflicts between unconditional matches automatically.

=item * C<< prepend(@things) >>

Each thing must be a Smart::Dispatch::Table or a Smart::Dispatch::Match.

Handles conflicts between unconditional matches automatically.

=item * C<validate_match_list>

Checks that match_list looks OK (a maximum of unconditional match;
checks that all conditional matches preceed unconditional matches).

This is done automatically after construction, prepending and
appending, but if you've manipulated the match_list manually, it's
good practice to all this method to check you've not broken it.

=back

=begin private

=item BUILD

=end private

=head2 Overloads

Smart::Dispatch::Table overloads various operations. (See L<overload>.)

=over

=item * B<< code derefernce >> C<< &{} >> - funky stuff with C<action>.

=item * B<< concatenation >> C<< . >> - funky stuff with C<make_combined>.

=item * B<< addition >> C<< + >> - funky stuff with C<make_combined>.

=item * B<< concatenation assignment >> C<< .= >> - C<append>.

=item * B<< addition assignment >> C<< += >> - C<prepend>.

=item * B<< smart match >> C<< ~~ >> - C<exists> (with C<$ignore_failover> false).

=back

=head1 BUGS

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

=head1 SEE ALSO

L<Smart::Dispatch>.

=head1 AUTHOR

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

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2012 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.