use 5.008;
use strict;
use warnings;

package Types::Set;

BEGIN {
	$Types::Set::AUTHORITY = 'cpan:TOBYINK';
	$Types::Set::VERSION   = '0.003';
}

use Set::Equivalence ();
use Type::Tiny 0.014;
use Type::Library -base, -declare => qw(Set AnySet MutableSet ImmutableSet);
use Types::Standard qw(ArrayRef InstanceOf HasMethods);
use Type::Utils -all;

declare Set,
	as InstanceOf['Set::Equivalence'],
	_params(Set);

declare AnySet,
	as HasMethods[qw( insert delete members contains )];

declare MutableSet,
	as Set,
	where { $_->is_mutable },
	inline_as { ( undef, "$_\->is_mutable" ) },
	_params(MutableSet);

declare ImmutableSet,
	as Set,
	where { $_->is_immutable },
	inline_as { ( undef, "$_\->is_immutable" ) },
	_params(ImmutableSet);

coerce Set,
	from ArrayRef, q{ 'Set::Equivalence'->new(members => $_) },
	from AnySet,   q{ 'Set::Equivalence'->new(members => [$_->members]) },
	;

coerce AnySet,
	from ArrayRef, q{ 'Set::Equivalence'->new(members => $_) },
	;

coerce MutableSet,
	from ImmutableSet, q{ $_->clone },
	from ArrayRef,     q{ 'Set::Equivalence'->new(members => $_) },
	from AnySet,       q{ 'Set::Equivalence'->new(members => [$_->members]) },
	;

coerce ImmutableSet,
	from MutableSet, q{ $_->clone->make_immutable },
	from ArrayRef,   q{ 'Set::Equivalence'->new(mutable => !!0, members => $_) },
	from AnySet,     q{ 'Set::Equivalence'->new(mutable => !!0, members => [$_->members]) },
	;

# Crazy stuff for parameterization...
sub _params
{
	my $basetype = shift;
	
	return(
		constraint_generator => sub {
			my $parameter = Types::TypeTiny::TypeTiny->(shift);
			return sub {
				my $tc = $_->type_constraint;
				Scalar::Util::blessed($tc) and $tc->can('is_a_type_of') and $tc->is_a_type_of($parameter);
			};
		},
		inline_generator => sub {
			our %REFADDR;
			my $parameter = Types::TypeTiny::TypeTiny->(shift);
			my $refaddr   = Scalar::Util::refaddr($parameter);
			$REFADDR{$refaddr} = $parameter;
			return sub {
				return (
					undef,
					"do { my \$tc = $_\->type_constraint; Scalar::Util::blessed(\$tc) and \$tc->can('is_a_type_of') and \$tc->is_a_type_of(\$Types::Set::REFADDR{$refaddr}) }",
				);
			};
		},
		coercion_generator => sub {
			my ($parent, $child, $parameter) = @_;
			my $coercions = 'Type::Coercion'->new( type_constraint => $child );
			my $immute = ($parent->name =~ /^Immutable/);
			
			if ($parameter->has_coercion) {
				$coercions->add_type_coercions(
					ArrayRef() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint      => $parameter,
							coerce               => 1,
							members              => [ map $parameter->coerce($_), @$in ],
						);
						$immute ? $set->make_immutable : $set;
					},
				);
				$coercions->add_type_coercions(
					Set() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint      => $parameter,
							coerce               => 1,
							equivalence_relation => $in->equivalence_relation,
							members              => [ map $parameter->coerce($_), $in->members ],
						);
						$immute ? $set->make_immutable : $set;
					},
				);
				$coercions->add_type_coercions(
					AnySet() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint      => $parameter,
							coerce               => 1,
							members              => [ map $parameter->coerce($_), $in->members ],
						);
						$immute ? $set->make_immutable : $set;
					},
				);
			}
			else {
				$coercions->add_type_coercions(
					ArrayRef() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint => $parameter,
							members         => $in,
						);
						$immute ? $set->make_immutable : $set;
					},
				);
				$coercions->add_type_coercions(
					Set() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint      => $parameter,
							equivalence_relation => $in->equivalence_relation,
							members              => [ $in->members ],
						);
						$immute ? $set->make_immutable : $set;
					},
				);
				$coercions->add_type_coercions(
					AnySet() => sub {
						my $in  = $_;
						my $set = 'Set::Equivalence'->new(
							type_constraint      => $parameter,
							members              => [ $in->members ],
						);
						$immute ? $set->make_immutable : $set;
					},
				);
			}
			
			$coercions->add_type_coercions(
				$parameter => sub {
					my $in  = $_;
					my $set = 'Set::Equivalence'->new(
						type_constraint      => $parameter,
						coerce               => $parameter->has_coercion,
						members              => [ $in ],
					);
					$immute ? $set->make_immutable : $set;
				},
			) unless $parameter->is_a_type_of(Set());
		},
	);
}

Set -> has_coercion

__END__

=pod

=encoding utf-8

=head1 NAME

Types::Set - Set::Equivalence-related type constraints

=head1 SYNOPSIS

   package Band {
      use Moose;
      use Types::Standard qw( InstanceOf );
      use Types::Set qw( Set );
      
      has members => (
         is          => 'ro',
         isa         => Set[ InstanceOf['Person'] ],
         coerce      => 1,
         default     => sub { +[] },
         handles     => {
            add_member     => 'insert',
            has_member     => 'contains',
            member_count   => 'size',
         }
      );
   }

=head1 DESCRIPTION

Types::Set is a type constraint library built using L<Type::Tiny>;
compatible with L<Moose>, L<Mouse>, L<Moo> and more.

=head2 Type constraints

=over

=item C<< AnySet >>

This type constraint is satisfied by any blessed object that provides
C<insert>, C<delete>, C<members> and C<contains> methods.

=item C<< Set >>

A blessed L<Set::Equivalence> object.

This may be parameterized with another type constraint; for example,
C<< Set[Num] >> is a set of numbers. In this case, not only must all
the set members be numbers, but also the set itself must have a type
constraint of C<Num> (or a subtype of C<Num>, such as C<Int>) attached,
which will prevent non-numeric values from being inserted into the
set later.

This type can coerce from C<ArrayRef> and C<AnySet>.

=item C<< MutableSet >>

Like C<Set>, but must be a mutable set. Similar parameterization.

This type can coerce from C<ImmutableSet>, C<ArrayRef> and C<AnySet>.

=item C<< ImmutableSet >>

Like C<Set>, but must not be a mutable set. Similar parameterization.

This type can coerce from C<MutableSet>, C<ArrayRef> and C<AnySet>.

=back

=head1 BUGS

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

=head1 SEE ALSO

L<Set::Equivalence>.

=head1 AUTHOR

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

=head1 COPYRIGHT AND LICENCE

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