package Mojo::Loader;
use Mojo::Base -base;

use File::Basename 'fileparse';
use File::Spec::Functions qw(catdir catfile splitdir);
use Mojo::Exception;
use Mojo::Util qw(b64_decode class_to_path);

# Cache
my %CACHE;

# "Homer no function beer well without."
sub data {
  my ($self, $class, $data) = @_;
  return $class ? $data ? _all($class)->{$data} : _all($class) : undef;
}

# "Olive oil? Asparagus? If your mother wasn't so fancy,
#  we could just shop at the gas station like normal people."
sub load {
  my ($self, $module) = @_;

  # Check module name
  return 1 if !$module || $module !~ /^\w(?:[\w:']*\w)?$/;

  # Load
  return if $module->can('new') || eval "require $module; 1";

  # Exists
  my $path = class_to_path $module;
  return 1 if $@ =~ /^Can't locate $path in \@INC/;

  # Real error
  return Mojo::Exception->new($@);
}

# "This is the worst thing you've ever done.
#  You say that so often that it lost its meaning."
sub search {
  my ($self, $namespace) = @_;

  # Check all directories
  my (@modules, %found);
  for my $directory (@INC) {
    next unless -d (my $path = catdir $directory, split(/::|'/, $namespace));

    # List "*.pm" files in directory
    opendir(my $dir, $path);
    for my $file (grep /\.pm$/, readdir $dir) {
      next if -d catfile splitdir($path), $file;

      # Module found
      my $class = "$namespace\::" . fileparse $file, qr/\.pm/;
      push @modules, $class unless $found{$class}++;
    }
  }

  return \@modules;
}

sub _all {
  my $class = shift;

  # Refresh or use cached data
  my $handle = do { no strict 'refs'; \*{"$class\::DATA"} };
  return $CACHE{$class} || {} unless fileno $handle;
  seek $handle, 0, 0;
  my $content = join '', <$handle>;
  close $handle;

  # Ignore everything before __DATA__ (Windows will seek to start of file)
  $content =~ s/^.*\n__DATA__\r?\n/\n/s;

  # Ignore everything after __END__
  $content =~ s/\n__END__\r?\n.*$/\n/s;

  # Split
  my @data = split /^@@\s*(.+?)\s*\r?\n/m, $content;
  shift @data;

  # Find data
  my $all = $CACHE{$class} = {};
  while (@data) {
    my ($name, $content) = splice @data, 0, 2;
    $content = b64_decode $content if $name =~ s/\s*\(\s*base64\s*\)$//;
    $all->{$name} = $content;
  }

  return $all;
}

1;

=head1 NAME

Mojo::Loader - Loader

=head1 SYNOPSIS

  use Mojo::Loader;

  # Find modules in a namespace
  my $loader = Mojo::Loader->new;
  for my $module (@{$loader->search('Some::Namespace')}) {

    # Load them safely
    my $e = $loader->load($module);
    warn qq{Loading "$module" failed: $e} and next if ref $e;

    # And extract files from the DATA section
    say $loader->data($module, 'some_file.txt');
  }

=head1 DESCRIPTION

L<Mojo::Loader> is a class loader and plugin framework.

=head1 METHODS

L<Mojo::Loader> inherits all methods from L<Mojo::Base> and implements the
following new ones.

=head2 C<data>

  my $all   = $loader->data('Foo::Bar');
  my $index = $loader->data('Foo::Bar', 'index.html');

Extract embedded file from the C<DATA> section of a class.

  say for keys %{$loader->data('Foo::Bar')};

=head2 C<load>

  my $e = $loader->load('Foo::Bar');

Load a class and catch exceptions. Note that classes are checked for a C<new>
method to see if they are already loaded.

  if (my $e = $loader->load('Foo::Bar')) {
    die ref $e ? "Exception: $e" : 'Already loaded!';
  }

=head2 C<search>

  my $modules = $loader->search('MyApp::Namespace');

Search for modules in a namespace non-recursively.

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.

=cut
