The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package RPC::ExtDirect::API::Hook;

use strict;
use warnings;
no  warnings 'uninitialized';           ## no critic

use B;

use RPC::ExtDirect::Util::Accessor;

### PUBLIC CLASS METHOD (ACCESSOR) ###
#
# Return the list of supported hook types
#

sub HOOK_TYPES { qw/ before instead after / }

### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
#
# Instantiate a new Hook object
#

sub new {
    my ($class, %arg) = @_;
    
    my ($type, $coderef) = @arg{qw/ type code /};
    
    # If we're passed an undef or 'NONE' instead of a coderef,
    # then the hook is not runnable. Otherwise, try resolving
    # package if we have a coderef.
    my $runnable = !('NONE' eq $coderef || !defined $coderef);
    
    my ($package, $sub_name);
    
    if ( 'CODE' eq ref $coderef ) {
        $package = _package_from_coderef($coderef);
    }
    else {
        my @parts = split /::/, $coderef;
        
        $sub_name = pop @parts;
        $package  = join '::', @parts;
        
        # We've got to have at least the sub_name part
        die "Can't resolve '$type' hook $coderef" unless $sub_name;
    }
    
    my $self = bless {
        package  => $package,
        type     => $type,
        code     => $coderef,
        sub_name => $sub_name,
        runnable => $runnable,
    }, $class;
    
    return $self;
}

### PUBLIC INSTANCE METHOD ###
#
# Run the hook
#

sub run {
    my ($self, %args) = @_;
    
    my ($api, $env, $arg, $result, $exception, $method_ref, $callee)
        = @args{qw/api env arg result exception method_ref callee/};
    
    my $action_name    = $method_ref->action;
    my $method_name    = $method_ref->name;
    my $method_pkg     = $method_ref->package;
    my $method_coderef = $method_ref->code;
    
    my %hook_arg = $method_ref->get_api_definition_compat();

    $hook_arg{arg}        = $arg;
    $hook_arg{env}        = $env;
    $hook_arg{code}       = $method_coderef;
    $hook_arg{method_ref} = $method_ref;

    # Result and exception are passed to "after" hook only
    @hook_arg{ qw/result   exception   method_called/ }
              = ($result, $exception, $callee)
        if $self->type eq 'after';

    for my $type ( $self->HOOK_TYPES ) {
        my $hook = $api->get_hook(
            action => $action_name,
            method => $method_name,
            type   => $type,
        );
        
        $hook_arg{ $type.'_ref' } = $hook;
        $hook_arg{ $type }        = $hook ? $hook->code : undef;
    }

    # A drop of sugar
    $hook_arg{orig} = sub { $method_coderef->($method_pkg, @$arg) };

    my $hook_coderef  = $self->code;
    my $hook_sub_name = $self->sub_name;
    my $hook_pkg      = $self->package;

    # By convention, hooks are called as class methods. If we were passed
    # a method name instead of a coderef, call it indirectly on the package
    # so that inheritance works properly
    return $hook_pkg && $hook_sub_name ? $hook_pkg->$hook_sub_name(%hook_arg)
         :                               $hook_coderef->($hook_pkg, %hook_arg)
         ;
}

### PUBLIC INSTANCE METHODS ###
#
# Simple read-write accessors
#

RPC::ExtDirect::Util::Accessor::mk_accessors(
    simple => [qw/ type code package sub_name runnable /],
);

############## PRIVATE METHODS BELOW ##############

### PRIVATE PACKAGE SUBROUTINE ###
#
# Return package name from coderef
#

sub _package_from_coderef {
    my ($code) = @_;

    my $pkg = eval { B::svref_2object($code)->GV->STASH->NAME };

    return defined $pkg && $pkg ne '' ? $pkg : undef;
}

1;