use 5.010001;
use strict;
use warnings;

package Mite::Trait::HasMethods;
use Mite::Miteception -role, -all;

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

requires qw( _function_for_croak );

BEGIN {
    *_CONSTANTS_DEFLATE = "$]" >= 5.012 && "$]" < 5.020 ? \&true : \&false;
};

has method_signatures =>
  is            => ro,
  isa           => Map[ MethodName, MiteSignature ],
  builder       => sub { {} };

sub add_method_signature {
    my ( $self, $method_name, %opts ) = @_;

    defined $self->method_signatures->{ $method_name }
        and croak( 'Method signature for %s already exists', $method_name );

    require Mite::Signature;
    $self->method_signatures->{ $method_name } = 'Mite::Signature'->new(
        method_name => $method_name,
        class => $self,
        %opts,
    );

    return;
}

sub _all_subs {
    my $self = shift;
    my $package = $self->name;
    no strict 'refs';
    my $stash = \%{"$package\::"};
    return {
        map {;
          # this is an ugly hack to populate the scalar slot of any globs, to
          # prevent perl from converting constants back into scalar refs in the
          # stash when they are used (perl 5.12 - 5.18). scalar slots on their own
          # aren't detectable through pure perl, so this seems like an acceptable
          # compromise.
          ${"${package}::${_}"} = ${"${package}::${_}"}
            if _CONSTANTS_DEFLATE;
          $_ => \&{"${package}::${_}"}
        }
        grep exists &{"${package}::${_}"},
        grep !/::\z/,
        keys %$stash
    };
}

sub native_methods {
    my $self = shift;
    my %methods = %{ $self->_all_subs };

    require B;
    for my $name ( sort keys %methods ) {
        my $cv        = B::svref_2object( $methods{$name} );
        my $stashname = eval { $cv->GV->STASH->NAME };
        $stashname eq $self->name
            or $stashname eq 'constant'
            or delete $methods{$name};
    }

    delete $methods{meta};

    return \%methods;
}

before inject_mite_functions => sub {
    my ( $self, $file, $arg ) = ( shift, @_ );

    my $requested = sub { $arg->{$_[0]} ? 1 : $arg->{'!'.$_[0]} ? 0 : $arg->{'-all'} ? 1 : $_[1]; };
    my $defaults  = ! $arg->{'!-defaults'};
    my $shim      = $self->shim_name;
    my $package   = $self->name;
    my $kind      = $self->kind;
    my $parse_mm_args = $shim->can( '_parse_mm_args' ) || \&Mite::Shim::_parse_mm_args;

    no strict 'refs';

    if ( $requested->( 'signature_for', $defaults ) ) {

        *{ $package .'::signature_for' } = sub {
            my $name = shift;
            if ( $name =~ /^\+/ ) {
                $name =~ s/^\+//;
                $self->extend_method_signature( $name, @_ );
            }
            else {
                $self->add_method_signature( $name, @_ );
            }
            return;
        };

        $self->imported_keywords->{signature_for} =
            sprintf 'sub { $SHIM->HANDLE_signature_for( $CALLER, %s, @_ ) }',
            B::perlstring( $kind );
    }

    for my $modifier ( qw( before after around ) ) {

        $requested->( $modifier, $defaults ) or next;

        *{ $package .'::'. $modifier } = sub {
            my ( $names, $coderef ) = &$parse_mm_args;
            CodeRef->check( $coderef )
                or croak "Expected a coderef method modifier";
            ArrayRef->of(Str)->check( $names ) && @$names
                or croak "Expected a list of method names to modify";
            $self->add_required_methods( @$names ) if $kind eq 'role';
            return;
        };

        $self->imported_keywords->{$modifier} =
            sprintf 'sub { $SHIM->HANDLE_%s( $CALLER, %s, @_ ) }',
            $modifier, B::perlstring( $kind );
    }
};

around compilation_stages => sub {
    my ( $next, $self ) = ( shift, shift );
    my @stages = $self->$next( @_ );
    push @stages, '_compile_method_signatures';
    return @stages;
};

sub _compile_method_signatures {
    my $self = shift;
    my %sigs = %{ $self->method_signatures } or return;

    my $code = "# Method signatures\n"
        . "our \%SIGNATURE_FOR;\n\n";

    for my $name ( sort keys %sigs ) {
        my $guard = $sigs{$name}->locally_set_compiling_class( $self );

        $code .= sprintf(
            '$SIGNATURE_FOR{%s} = %s;' . "\n\n",
            B::perlstring( $name ),
            $sigs{$name}->_compile_coderef,
        );

        if ( my $support = $sigs{$name}->_compile_support ) {
            $code .= "$support\n\n";
        }
    }

    return $code;
}

1;
