use 5.006;
use strict;
use warnings;

package Role::Inspector;

our $AUTHORITY = 'cpan:TOBYINK';
our $VERSION   = '0.006';

use Exporter::Shiny qw( get_role_info learn does_role );
use Module::Runtime qw( use_package_optimistically );
use Scalar::Util qw( blessed );

BEGIN {
	*uniq = eval { require List::MoreUtils }
		? \&List::MoreUtils::uniq
		: sub { my %already; grep !$already{$_}++, @_ }
}

our @SCANNERS;

sub learn (&)
{
	push @SCANNERS, $_[0];
}

sub get_role_info
{
	my $me = shift;
	use_package_optimistically($_[0]);
	my ($info) = grep defined, map $_->(@_), @SCANNERS;
	$me->_canonicalize($info, @_);
	return $info;
}

sub _generate_get_role_info
{
	my $me = shift;
	my ($name, $args, $globals) = @_;
	return sub {
		my $info = $me->get_role_info(@_);
		delete($info->{meta}) if $args->{no_meta};
		return $info;
	};
}

sub _canonicalize
{
	my $me = shift;
	my ($info) = @_;
	
	if ( $info->{api} and not( $info->{provides} && $info->{requires} ) )
	{
		my @provides;
		my @requires;
		for my $method (@{ $info->{api} })
		{
			push @{
				$info->{name}->can($method) ? \@provides : \@requires
			}, $method;
		}
		$info->{provides} ||= \@provides;
		$info->{requires} ||= \@requires;
	}
	
	if ( not $info->{api} )
	{
		$info->{api} = [
			@{ $info->{provides} ||= [] },
			@{ $info->{requires} ||= [] },
		];
	}
	
	# if a method is in both `provides` and `requires`, remove from `requires`
	my %lookup;
	undef $lookup{$_} for @{$info->{provides}};
	@{$info->{requires}} = grep !exists($lookup{$_}), @{$info->{requires}};
	
	for my $k (qw/ api provides requires /) {
		@{ $info->{$k} } = sort(
			uniq(
				map ref($_) ? $_->{name} : $_,
				@{ $info->{$k} }
			)
		);
	}
}

sub _expand_attributes
{
	my $me = shift;
	my ($role, $meta) = @_;
	
	my @attrs = map {
		my $data = $meta->get_attribute($_);
		$data->{name} = $_ unless exists($data->{name});
		$data;
	} $meta->get_attribute_list;
	my %methods;
	
	for my $attr (@attrs)
	{
		my $is = blessed($attr) && $attr->can('is') ? $attr->is : $attr->{is};
		$methods{blessed($attr) && $attr->can('name') ? $attr->name : $attr->{name} }++
			if $is =~ /\A(ro|rw|lazy|rwp)\z/i;
		
		for my $method_type (qw(reader writer accessor clearer predicate))
		{
			my $method_name = blessed($attr) ? $attr->$method_type : $attr->{$method_type};
			($method_name) = %$method_name if ref($method_name); # HASH :-(
			$methods{$method_name}++ if defined $method_name;
		}
		
		my $handles;
		if (blessed($attr) and $attr->can('_canonicalize_handles'))
		{
			$handles =
				$attr->can('_canonicalize_handles') ? +{ $attr->_canonicalize_handles } :
				$attr->can('handles') ? $attr->handles :
				$attr->{handles};
		}
		else
		{
			$handles = $attr->{handles};
		}
		
		if (!defined $handles)
		{
			# no-op
		}
		elsif (not ref($handles))
		{
			$methods{$_}++ for @{ $me->get_info($handles)->{api} };
		}
		elsif (ref($handles) eq q(ARRAY))
		{
			$methods{$_}++ for @$handles;
		}
		elsif (ref($handles) eq q(HASH))
		{
			$methods{$_}++ for keys %$handles;
		}
		else
		{
			require Carp;
			Carp::carp(
				sprintf(
					"%s contains attribute with delegated methods, but %s cannot determine which methods are being delegated",
					$role,
					$me,
				)
			);
		}
	}
	
	return keys(%methods);
}

# Learn about mop
learn {
	my $role = shift;
	return unless $INC{'mop.pm'};
	
	my $meta = mop::meta($role);
	return unless $meta && $meta->isa('mop::role');
	
	return {
		name     => $role,
		type     => 'mop::role',
		provides => [ sort(map($_->name, $meta->methods)) ],
		requires => [ sort($meta->required_methods) ],
		meta     => $meta,
	};
};

# Learn about Role::Tiny and Moo::Role
learn {
	my $role = shift;
	return unless $INC{'Role/Tiny.pm'};
	
	# Moo 1.003000 added is_role, but that's too new to rely on.
	my @methods;
	return unless eval {
		@methods = 'Role::Tiny'->methods_provided_by($role);
		1;
	};
	
	no warnings qw(once);
	my $type =
		($INC{'Moo/Role.pm'} and $Moo::Role::INFO{$role}{accessor_maker})
		? 'Moo::Role'
		: 'Role::Tiny';
	
	@methods = $type->methods_provided_by($role)
		if $type ne 'Role::Tiny';
	
	my @requires = @{ $Role::Tiny::INFO{$role}{requires} or [] };
	
	my $modifiers = $Role::Tiny::INFO{$role}{modifiers} || [];
	foreach my $modifier (@$modifiers) {
		my @modified = @$modifier[ 1 .. $#$modifier - 1 ];
		# handle: before ['foo', 'bar'] => sub { ... }
		@modified = @{ $modified[0] } if ref $modified[0] eq 'ARRAY';
		push @requires, @modified;
	}
	
	return {
		name     => $role,
		type     => $type,
		api      => [ @methods, @requires ],
		provides => [ keys %{ $type->_concrete_methods_of($role) } ],
		requires => \@requires,
	};
};

# Learn about Moose
learn {
	my $role = shift;
	return unless $INC{'Moose.pm'};
	
	require Moose::Util;
	my $meta = Moose::Util::find_meta($role);
	return unless $meta && $meta->isa('Moose::Meta::Role');
	
	my (@provides, @requires);
	push @provides, $meta->get_method_list;
	push @provides, __PACKAGE__->_expand_attributes($role, $meta);
	push @requires, map($_->name, $meta->get_required_method_list);
	for my $kind (qw/before after around/) {
		my $accessor = "get_${kind}_method_modifiers_map";
		push @requires, keys %{ $meta->$accessor };
	}
	
	return {
		name     => $role,
		type     => 'Moose::Role',
		meta     => $meta,
		provides => \@provides,
		requires => \@requires,
	};
};

# Learn about Mouse
learn {
	my $role = shift;
	return unless $INC{'Mouse.pm'};
	
	require Mouse::Util;
	my $meta = Mouse::Util::find_meta($role);
	return unless $meta && $meta->isa('Mouse::Meta::Role');
	
	my (@provides, @requires);
	push @provides, $meta->get_method_list;
	push @provides, __PACKAGE__->_expand_attributes($role, $meta);
	push @requires, $meta->get_required_method_list;
	for my $kind (qw/before after around/) {
		push @requires, keys %{ $meta->{"${kind}_method_modifiers"} };
	}
	
	return {
		name     => $role,
		type     => 'Mouse::Role',
		meta     => $meta,
		provides => \@provides,
		requires => \@requires,
	};
};

# Learn about Role::Basic
learn {
	my $role = shift;
	return unless $INC{'Role/Basic.pm'};
	
	return unless eval { 'Role::Basic'->_load_role($role) };
	
	return {
		name     => $role,
		type     => 'Role::Basic',
		provides => [ keys %{ 'Role::Basic'->_get_methods($role) } ],
		requires => [ 'Role::Basic'->get_required_by($role) ],
	};
};

sub does_role
{
	my $me = shift;
	my ($thing, $role) = @_;
	
	return !!0 if !defined($thing);
	return !!0 if ref($thing) && !blessed($thing);
	
	ref($_) or use_package_optimistically($_) for @_;
	
	return !!1 if $thing->can('does') && $thing->does($role);
	return !!1 if $thing->can('DOES') && $thing->DOES($role);
	
	my $info = $me->get_role_info($role)
		or return !!0;
	
	if ($info->{type} eq 'Role::Tiny' or $info->{type} eq 'Moo::Role')
	{
		return !!1 if Role::Tiny::does_role($thing, $role);
	}
	
	if ($info->{type} eq 'Moose::Role')
	{
		require Moose::Util;
		return !!1 if Moose::Util::does_role($thing, $role);
	}
	
	if ($info->{type} eq 'Mouse::Role')
	{
		require Mouse::Util;
		return !!1 if Mouse::Util::does_role($thing, $role);
	}
	
	if (not ref $thing)
	{
		my $info2 = $me->get_role_info($thing) || { type => '' };
		
		if ($info2->{type} eq 'Role::Tiny' or $info2->{type} eq 'Moo::Role')
		{
			return !!1 if Role::Tiny::does_role($thing, $role);
		}
		
		if ($info2->{type} eq 'Moose::Role'
		or $INC{'Moose.pm'} && Moose::Util::find_meta($thing))
		{
			require Moose::Util;
			return !!1 if Moose::Util::does_role($thing, $role);
		}
		
		if ($info2->{type} eq 'Mouse::Role'
		or $INC{'Mouse.pm'} && Mouse::Util::find_meta($thing))
		{
			require Mouse::Util;
			return !!1 if Mouse::Util::does_role($thing, $role);
		}
	}
	
	# No special handling for Role::Basic, but hopefully checking
	# `DOES` worked!
	
	!!0;
}

# very simple class method curry
sub _generate_does_role
{
	my $me = shift;
	sub { $me->does_role(@_) };
}


1;

__END__

=pod

=encoding utf-8

=for stopwords metaobject

=head1 NAME

Role::Inspector - introspection for roles

=head1 SYNOPSIS

   use strict;
   use warnings;
   use feature qw(say);
   
   {
      package Local::Role;
      use Role::Tiny;   # or Moose::Role, Mouse::Role, etc...
      
      requires qw( foo );
      
      sub bar { ... }
   }
   
   use Role::Inspector qw( get_role_info );
   
   my $info = get_role_info('Local::Role');
   
   say $info->{name};          # Local::Role
   say $info->{type};          # Role::Tiny
   say for @{$info->{api}};    # bar
                               # foo

=head1 DESCRIPTION

This module allows you to retrieve a hashref of information about a
given role. The following role implementations are supported:

=over

=item *

L<Moose::Role>

=item *

L<Mouse::Role>

=item *

L<Moo::Role>

=item *

L<Role::Tiny>

=item *

L<Role::Basic>

=item *

L<p5-mop-redux|https://github.com/stevan/p5-mop-redux>

=back

=head2 Functions

=over

=item C<< get_role_info($package_name) >>

Returns a hashref of information about a role; returns C<undef> if the
package does not appear to be a role. Attempts to load the package
using L<Module::Runtime> if it's not already loaded.

The hashref may contain the following keys:

=over

=item *

C<name> - the package name of the role

=item *

C<type> - the role implementation used by the role

=item *

C<api> - an arrayref of method names required/provided by the role

=item *

C<provides> and C<requires> - the same as C<api>, but split into lists
of methods provided and required by the role

=item *

C<meta> - a metaobject for the role (e.g. a L<Moose::Meta::Role> object).
This key may be absent if the role implementation does not provide a
metaobject

=back

This function may be exported, but is not exported by default.

=item C<< does_role($thing, $role) >>

Returns a boolean indicating if C<< $thing >> does role C<< $role >>.
C<< $thing >> can be an object, a class name, or a role name.

This should mostly give the same answers as C<< $thing->DOES($role) >>,
but may be slightly more reliable in some cross-implementation (i.e.
Moose roles consuming Moo roles) cases.

This function may be exported, but is not exported by default.

=back

=head2 Methods

If you do not wish to export the functions provided by Role::Inspector,
you may call them as a class methods:

   my $info = Role::Inspector->get_role_info($package_name);

   $thing->blah() if Role::Inspector->does_role($thing, $role);

=head2 Extending Role::Inspector

=over

=item C<< Role::Inspector::learn { BLOCK } >>

In the unlikely situation that you have to deal with some other role
implementation that Role::Inspector doesn't know about, you can teach
it:

   use Role::Inspector qw( learn );
   
   learn {
      my $r = shift;
      return unless My::Implementation::is_role($r);
      return {
         name     => $r,
         type     => 'My::Implementation',
         provides => [ sort(@{My::Implementation::provides($r)}) ],
         requires => [ sort(@{My::Implementation::requires($r)}) ],
      };
   };

An alternative way to do this is:

   push @Role::Inspector::SCANNERS, sub {
      my $r = shift;
      ...;
   };

You can do the C<push> thing without having loaded Role::Inspector.
This makes it suitable for doing inside My::Implementation itself,
without introducing an additional dependency on Role::Inspector.

Note that if you don't provide all of C<provides>, C<requires>, and
C<api>, Role::Inspector will attempt to guess the missing parts.

=back

=head1 CAVEATS

=over

=item *

It is difficult to distinguish between L<Moo::Role> and L<Role::Tiny>
roles. (The distinction is not often important anyway.) Thus sometimes
the C<type> for a Moo::Role may say C<< "Role::Tiny" >>.

=item *

The way that Role::Basic roles are detected and introspected is a bit
dodgy, relying on undocumented methods.

=item *

Where Moose or Mouse roles define attributes, those attributes tend to
result in accessor methods being generated. However neither of these
frameworks provides a decent way of figuring out which accessor methods
will result from composing the role with the class.

Role::Inspector does its damnedest to figure out the list of likely
methods, but (especially in the case of unusual attribute traits) may
get things wrong from time to time.

=back

=head1 BUGS

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

=head1 SEE ALSO

L<Class::Inspector>.

=head1 AUTHOR

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

=head1 COPYRIGHT AND LICENCE

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