The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

package UR::ModuleLoader;

use strict;
use warnings;
require UR;
our $VERSION = "0.45"; # UR $VERSION;

Class::Autouse->autouse(\&dynamically_load_class);
Class::Autouse->autouse(\&dynamically_load_role);
Class::Autouse->sugar(\&define_class);

our @CARP_NOT = qw(Class::Autouse UR::Namespace);

sub define_class {
    my ($class,$func,@params) = @_;
    return unless $UR::initialized;
    return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get");

    # Handle the special case of defining a new class
    # This lets us have the effect of a UNIVERSAL::class method, w/o mucking with UNIVERSAL
    if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") {
        my @class_params;
        if (@params == 2 and ref($params[1]) eq 'HASH') {
            @class_params = %{ $params[1] };
        }
        elsif (@params == 2 and ref($params[1]) eq 'ARRAY') {
            @class_params = @{ $params[1] };
        }
        else {
            @class_params = @params[1..$#params];
        }
        my $class_meta = UR::Object::Type->define(class_name => $class, @class_params);
        unless ($class_meta) {
            Carp::croak "error defining class $class!";
        }
        return sub { $class };
    }

    else {
        return;
    }
}

sub _should_dynamically_load_package {
    my $package = shift;
    # Don't even try to load unless we're done boostrapping somewhat.
    return unless $UR::initialized;
    return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get");

    # Some modules (Class::DBI, recently) call UNIVERSAL::can directly with things which don't even resemble
    # class names.  Skip doing any work on anything which isn't at least a two-part class name.
    # We refuse explicitly to handle top-level namespaces below anyway, and this will keep us from 
    # slowing down other modules just to fail late.

    my ($namespace) = ($package =~ /^(.*?)::/);
    return unless $namespace;

    unless ($namespace->isa("UR::Namespace")) {
        return;
    }

    unless ($namespace->should_dynamically_load_class($package)) {
        return;
    }

    return $namespace;
}

our %loading;

sub dynamically_load_class {
    my ($class,$func,@params) = @_;

    return unless my $namespace = _should_dynamically_load_package($class);

    if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") {
        # a "class" statement caught by the above define_class call
        return;
    }

    return if $loading{$class};
    local %loading = ( %loading, $class => 1 );

    # Attempt to get a class object, loading it as necessary (probably).
    # TODO: this is a non-standard accessor
    my $meta = $namespace->get_member_class($class);
    unless ($meta) {
        return;
    }

    # Handle the case in which the class is not "generated".
    # These are generated by default when used, so this is a corner case.
    unless ($meta->generated())
    {
        # we have a new class
        # attempt to auto-generate it
        unless ($meta->generate)
        {
            Carp::confess("failed to auto-generate $class");
        }
    }

    # Return a descriptive error message for the caller.
    my $fref;
    if (defined $func) {
        $fref = $class->can($func);
        unless ($fref) {
            Carp::confess("$class was auto-generated successfully but cannot find method $func");
        }
        return $fref;
    }

    return 1;
};

sub dynamically_load_role {
    my($role_name, $func, @params) = @_;

    return unless _should_dynamically_load_package($role_name);

    return if $loading{$role_name};
    local %loading = ( %loading, $role_name => 1 );

    # The module may have actually been loaded by dynamically_load_class(),
    # but failed the check for class-ness
    if (UR::Role::Prototype->is_loaded($role_name)
        &&
        $role_name->can($func)
    ) {
        return 1;
    }

    if (UR::Util::use_package_optimistically($role_name)) {
        if (UR::Role::Prototype->is_loaded($role_name)
            &&
            $role_name->can($func)
        ) {
            return 1;
        }
    }

    return;
}

1;


=pod

=head1 NAME

UR::ModuleLoader - UR hooks into Class::Autouse

=head1 DESCRIPTION

UR uses Class::Autouse to handle automagic loading for modules.  As long
as some part of an application "use"s a Namespace module, the autoloader
will handle loading modules under that namespace when they are needed.

=head1 SEE ALSO

UR, UR::Namespace

=cut