package Export::Lexical;

use 5.010;
use strict;
use warnings;
use B;
use Carp;

our $VERSION = '0.0.5';

my %exports_for  = ();
my %modifier_for = ();  # e.g., $modifier_for{$pkg} = 'silent'

sub MODIFY_CODE_ATTRIBUTES {
    my ( $package, $coderef, @attrs ) = @_;

    my @unused_attrs = ();

    while ( my $attr = shift @attrs ) {
        if ( $attr =~ /^Export_?Lexical$/i ) {
            push @{ $exports_for{$package} }, $coderef;
        }
        else {
            push @unused_attrs, $attr;
        }
    }

    return @unused_attrs;
}

sub import {
    my ($class) = @_;

    my $caller = caller;
    my $key    = _get_key($caller);
    my @params = ();

    {
        # Export our subroutines, if necessary.
        no strict 'refs';   ## no critic (ProhibitNoStrict)

        if ( !exists &{ $caller . '::MODIFY_CODE_ATTRIBUTES' } ) {
            *{ $caller . '::MODIFY_CODE_ATTRIBUTES' } = \&MODIFY_CODE_ATTRIBUTES;
        }

        if ( !exists &{ $caller . '::import' } ) {
            *{ $caller . '::import' } = sub {
                my ( $class, @args ) = @_;

                _export_all_to( $caller, scalar caller );

                $^H{$key} = @args ? ( join ',', @args ) : 1;   ## no critic (ProhibitPunctuationVars, RequireLocalizedPunctuationVars)
            };
        }

        if ( !exists &{ $caller . '::unimport' } ) {
            *{ $caller . '::unimport' } = sub {
                my ( $class, @args ) = @_;

                if ( @args ) {
                    # Leave the '1' on the front of the list from a previous 'use
                    # $module', as well as any subs previously imported.
                    $^H{$key} = join ',', $^H{$key}, map { "!$_" } @args;  ## no critic (ProhibitPunctuationVars, RequireLocalizedPunctuationVars)
                }
                else {
                    $^H{$key} = '';    ## no critic (ProhibitPunctuationVars, RequireLocalizedPunctuationVars)
                }
            };
        }
    }

    while ( my $modifier = shift ) {
        if ( $modifier =~ /^:(silent|warn)$/ ) {
            croak qq('$modifier' requested when '$modifier_for{$caller}' already in use)
                if $modifier_for{$caller};

            $modifier_for{$caller} = $modifier;
            next;
        }

        push @params, $modifier;
    }
}

sub _export_all_to {
    my ( $from, $caller ) = @_;

    return if !exists $exports_for{$from};

    for my $ref ( @{ $exports_for{$from} } ) {
        my $obj = B::svref_2object($ref);
        my $pkg = $obj->GV->STASH->NAME;
        my $sub = $obj->GV->NAME;
        my $key = _get_key($pkg);

        no strict 'refs';       ## no critic (ProhibitNoStrict)
        no warnings 'redefine'; ## no critic (ProhibitNoWarnings)

        next if exists &{ $caller . '::' . $sub };

        *{ $caller . '::' . $sub } = sub {
            my $hints = ( caller 0 )[10];

            return _fail( $pkg, $sub ) if $hints->{$key} =~ /(?:^$)|(?:!$sub\b)/; # no $module
                                                                                  # no $module '$sub'

            goto $ref if $hints->{$key} =~ /(?:^1\b)|(?:\b$sub\b)/;               # use $module
                                                                                  # use $module '$sub'
        };
    }
}

sub _fail {
    my ( $pkg, $sub ) = @_;

    if ( defined $modifier_for{$pkg} ) {
        if ( $modifier_for{$pkg} eq ':silent' ) {
            return;
        }

        if ( $modifier_for{$pkg} eq ':warn' ) {
            carp "$pkg\::$sub not allowed here";
            return;
        }
    }

    croak "$pkg\::$sub not allowed here";
}

sub _get_key {
    my ( $pkg ) = @_;

    return __PACKAGE__ . '/' . $pkg;
}

1;

__END__

=head1 NAME

Export::Lexical - Lexically scoped subroutine imports

=head1 SYNOPSIS

    package Foo;

    use Export::Lexical;

    sub foo :ExportLexical {
        # do something
    }

    sub bar :ExportLexical {
        # do something else
    }

    # In a nearby piece of code...

    use Foo;

    foo();    # calls foo()
    bar();    # calls bar()

    {
        no Foo 'bar';    # disables bar()

        foo();           # calls foo()
        bar();           # throws an exception
    }

=head1 DESCRIPTION

The Export::Lexical module provides a simple interface to the custom user
pragma interface in Perl 5.10. Simply by marking subroutines of a module with
the C<:ExportLexical> attribute, they will automatically be flagged for
lexically scoped import.

=head1 INTERFACE 

=head2 Import Modifiers

By default, subroutines not currently exported to the lexical scope will raise
exceptions when called. This behavior can be modified in two ways.

=over

=item C<< :silent >>

    use Export::Lexical ':silent';

Causes subroutines not imported into the current lexical scope to be no-ops.

=item C<< :warn >>

    use Export::Lexical ':warn';

Causes subroutines not imported into the current lexical scope to warn with
carp() instead of dying with croak().

=back

=head2 Subroutine Attributes

=over

=item C<< :ExportLexical >>

    package Foo;

    sub foo :ExportLexical {
        # do something
    }

This marks the foo() subroutine for lexically scoped import. When the Foo
module in this example is used, the foo() subroutine is available only in the
scope of the C<use> statement. The foo() subroutine can be made into a no-op
with the C<no> statement.

=back

=head1 DIAGNOSTICS

No diagnostics exist in this version of Export::Lexical.

=head1 CONFIGURATION AND ENVIRONMENT

Export::Lexical requires no configuration files or environment variables.

=head1 DEPENDENCIES

=over

=item *

Perl 5.10.0+

=back

=head1 INCOMPATIBILITIES

None reported.

=head1 BUGS AND LIMITATIONS

Do not define C<import()> or C<unimport()> subroutines when using
Export::Lexical. These will redefine the subroutines created by the
Export::Lexical module, disabling the special properties of the attributes. In
practice, this probably isn't a big deal.

Please report any bugs or feature requests to
L<https://github.com/sirhc/perl-Export-Lexical/issues/>.

=head1 AUTHOR

L<Christopher D. Grau|mailto:cgrau@cpan.org>

This module is an expansion of an idea presented by Damian Conway.

=head1 COPYRIGHT AND LICENSE

Copyright 2008–2015, Christopher D. Grau.

This is free software, licensed under the MIT (X11) License.

=head1 SEE ALSO

L<perlpragma>

=cut
