use warnings;
use strict;

package Jifty::ClassLoader;

=head1 NAME

Jifty::ClassLoader - Loads the application classes

=head1 DESCRIPTION

C<Jifty::ClassLoader> loads all of the application's model and action
classes, generating classes on the fly for Collections of pre-existing
models.

=head2 new

Returns a new ClassLoader object.  Doing this installs a hook into
C<@INC> that allows L<Jifty::ClassLoader> to dynamically create
needed classes if they do not exist already.  This works because if
use/require encounters a blessed reference in C<@INC>, it will
invoke the INC method with the name of the module it is searching
for on the reference.

Takes one mandatory argument, C<base>, which should be the the
application's base path; all of the classes under this will be
automatically loaded.

=cut

sub new {
    my $class = shift;
    my %args = @_;

    my @exist = grep {ref $_ eq $class and $_->{base} eq $args{base}} @INC;
    return $exist[0] if @exist;

    my $self = bless {%args}, $class;
    push @INC, $self;
    return $self;
}

=head2 INC

The hook that is called when a module has been C<require>'d that
cannot be found on disk.  The following stub classes are
auto-generated:

=over

=item I<Application>

An empty application base class is created that doen't provide any
methods or inherit from anything.

=item I<Application>::Record

An empty class that descends from L<Jifty::Record> is created.

=item I<Application>::Event

An empty class that descends from L<Jifty::Event> is created.

=item I<Application>::Collection

An empty class that descends from L<Jifty::Collection> is created.

=item I<Application>::Notification

An empty class that descends from L<Jifty::Notification>.

=item I<Application>::Dispatcher

An empty class that descends from L<Jifty::Dispatcher>.

=item I<Application>::Handle

An empty class that descends from L<Jifty::Handle> is created.

=item I<Application>::Bootstrap

An empty class that descends from L<Jifty::Bootstrap>.

=item I<Application>::Upgrade

An empty class that descends from L<Jifty::Upgrade>.

=item I<Application>::CurrentUser

An empty class that descends from L<Jifty::CurrentUser>.

=item I<Application>::Model::I<Anything>Collection

If C<I<Application>::Model::I<Something>> is a valid model class, then
it creates a subclass of L<Jifty::Collection> whose C<record_class> is
C<I<Application>::Model::I<Something>>.

=item I<Application>::Action::(Create or Update or Delete or Search)I<Anything>

If C<I<Application>::Model::I<Something>> is a valid model class, then it
creates a subclass of L<Jifty::Action::Record::Create>,
L<Jifty::Action::Record::Update>, L<Jifty::Action::Record::Delete> or
L<Jifty::Action::Record::Search> whose I<record_class> is
C<I<Application>::Model::I<Something>>.

=back

=cut

# This subroutine's name is fully qualified, as perl will ignore a 'sub INC'
sub Jifty::ClassLoader::INC {
    my ( $self, $module ) = @_;
    my $base = $self->{base};
    return undef unless ( $module and $base );

    # Canonicalize $module to :: style rather than / and .pm style;
    $module =~ s/.pm$//;
    $module =~ s{/}{::}g;

    # The quick check. We only want to handle things for our app
    return undef unless $module =~ /^$base/;

    if ( $module =~ /^(?:$base)$/ ) {
        return $self->return_class( "package " . $base . ";\n");
    }
    elsif ( $module =~ /^(?:$base)::(Record|Collection|Notification|
                                      Dispatcher|Bootstrap|Upgrade|
                                      Handle|Event|Event::Model|Action|
                                      Action::Record::\w+)$/x ) {
        return $self->return_class(
                  "package $module;\n"
                . "use base qw/Jifty::$1/; sub _autogenerated { 1 };\n"
            );
    } elsif ( $module =~ /^(?:$base)::View/ ) {
        return $self->return_class(
                  "package $module;\n"
                . "use Jifty::View::Declare -base; sub _autogenerated { 1 };\n"
            );
    } elsif ( $module =~ /^(?:$base)::CurrentUser$/ ) {
        return $self->return_class(
                  "package $module;\n"
                . "use base qw/Jifty::CurrentUser/; sub _autogenerated { 1 };\n"
            );
    } elsif ( $module =~ /^(?:$base)::Model::(\w+)Collection$/ ) {
        return $self->return_class(
                  "package $module;\n"
                . "use base qw/@{[$base]}::Collection/;\n"
                . "sub record_class { '@{[$base]}::Model::$1' }\n"
            );
    } elsif ( $module =~ /^(?:$base)::Event::Model::([^\.]+)$/ ) {
        my $modelclass = $base . "::Model::" . $1;
        Jifty::Util->require($modelclass);

        return undef unless eval { $modelclass->table };

        return $self->return_class(
                  "package $module;\n"
                . "use base qw/${base}::Event::Model/;\n"
                . "sub record_class { '$modelclass' };\n"
                . "sub autogenerated { 1 };\n"
            );
    } elsif ( $module =~ /^(?:$base)::Action::
                        (Create|Update|Delete|Search)([^\.]+)$/x ) {
        my $modelclass = $base . "::Model::" . $2;

        Jifty::Util->require($modelclass);

        local $@;
            eval { $modelclass->table } ;
        if(!$@) {

        return $self->return_class(
                  "package $module;\n"
                . "use base qw/$base\::Action::Record::$1/;\n"
                . "sub record_class { '$modelclass' };\n"
                . "sub autogenerated { 1 };\n"
            );
        }

    }

    # This is if, not elsif because we might have $base::Action::Deleteblah 
    # that matches that last elsif clause but loses on the eval.
    if ( $module =~ /^(?:$base)::(Action|Notification)::(.*)$/x and not grep {$_ eq $base} map {ref} Jifty->plugins ) {
        my $type = $1; 
        my $item = $2;
        # If we don't have the action in our own app, let's try the plugins
        # the app has loaded.
        foreach my $plugin (map {ref} Jifty->plugins) {
            next if ($plugin eq $base);
            my $class = $plugin."::".$type."::".$item;
            if (Jifty::Util->try_to_require($class) ) {
        return $self->return_class(
                  "package $module;\n"
                . "use base qw/$class/;\n"
                . "sub autogenerated { 1 };\n"
            );


            }
        }

    }
    # Didn't find a match
    return undef;
}

=head2 return_class CODE

A helper method; takes CODE as a string and returns an open filehandle
containing that CODE.

=cut

sub return_class {
    my $self = shift;
    my $content = shift;

    $content = "use warnings; use strict; ". $content  . "\n1;";

    open my $fh, '<', \$content;
    return $fh;

}

=head2 require

Loads all of the application's Actions and Models.  It additionally
C<require>'s all Collections and Create/Update actions for each Model
base class -- which will auto-create them using the above code if they
do not exist on disk.

=cut

sub require {
    my $self = shift;
    
    my $base = $self->{base};
    # if we don't even have an application class, this trick will not work
    return unless ($base); 
    Jifty::Util->require($base);
    Jifty::Util->require($base."::CurrentUser");

    my %models;
    

    Jifty::Module::Pluggable->import(
        # $base goes last so we pull in the view class AFTER the model classes
        search_path => [map { $base . "::" . $_ } ('Model', 'Action', 'Notification', 'Event')],
        require => 1,
        except  => qr/\.#/,
        inner   => 0
    );
    $models{$_} = 1 for grep {/^($base)::Model::(.*)$/ and not /Collection$/} $self->plugins;
    $self->models(sort keys %models);
    for my $full ($self->models) {
        $self->_require_model_related_classes($full);
    }
        
}

sub _require_model_related_classes {
    my $self = shift;
    my $full = shift;
    my $base = $self->{base};
        my($short) = $full =~ /::Model::(.*)/;
        Jifty::Util->require($full . "Collection");
        Jifty::Util->require($base . "::Action::" . $_ . $short)
            for qw/Create Update Delete Search/;

}


=head2 require_classes_from_database

Jifty supports model classes that aren't files on disk but instead records
in your database. It's a little bit mind bending, but basically, you can
build an application entirely out of the database without ever writing a 
line of code(*). 

* As of early 2007, this forward looking statement is mostly a lie. But we're 
working on it.

This method finds all database-backed models and instantiates jifty classes for 
them it returns a list of classnames of the models it created.

=cut

sub require_classes_from_database {
    my $self = shift;
    my @instantiated;

    require Jifty::Model::ModelClassCollection;
    require Jifty::Model::ModelClass;
    my $models = Jifty::Model::ModelClassCollection->new(current_user => Jifty::CurrentUser->superuser);
    $models->unlimit();
    while (my $model = $models->next) {
        $model->instantiate();
        $self->_require_model_related_classes($model->qualified_class);
    }
}

=head2 require_views

Load up C<$appname::View>, the view class for the application.

=cut

sub require_views {
    my $self = shift;
    
    my $base = $self->{base};
    # if we don't even have an application class, this trick will not work
    return unless ($base); 
    Jifty::Util->require($base."::View");
}

=head2 models

Accessor to the list of models this application has loaded.

In scalar context, returns a mutable array reference; in list context,
return the content of the array.

=cut

sub models {
    my $self = shift;
    if (@_) {
        $self->{models} = ref($_[0]) ? $_[0] : \@_;
    }
    wantarray ? @{ $self->{models} ||= [] } : $self->{models};
}

=head2 DESTROY

When the ClassLoader gets garbage-collected, its entry in @INC needs
to be removed.

=cut

# The entries in @INC end up having SvTYPE == SVt_RV, but SvRV(sv) ==
# 0x0 and !SvROK(sv) (!?)  This may be something that perl should cope
# with more cleanly.
#
# We call this explictly in an END block in Jifty.pm, because
# otherwise the DESTROY block gets called *after* there's already a
# bogus entry in @INC

# This bug manifests itself as warnings that look like this:

# Use of uninitialized value in require at /tmp/7730 line 9 during global destruction.


sub DESTROY {
    my $self = shift;
    @INC = grep {defined $_ and $_ ne $self} @INC;
}

=head1 Writing your own classes

If you require more functionality than is provided by the classes
created by ClassLoader then you should create a class with the
appropriate name and add your extra logic to it.

For example you will almost certainly want to write your own
dispatcher, so something like:

 package MyApp::Dispatcher;
 use Jifty::Dispatcher -base;

If you want to add some application specific behaviour to a model's
collection class, say for the User model, create F<UserCollection.pm>
in your applications Model directory.

 package MyApp::Model::UserCollection;
 use base 'MyApp::Collection';

=cut

1;
