use strict;
use warnings;

package fatfinger;
$fatfinger::VERSION = '0.000001';
use File::Spec;
use Path::Iterator::Rule;
use Text::Levenshtein::Damerau qw( edistance );

sub import {
    push @INC, sub {
        shift;
        my $name = shift;

        return if $name eq 'prefork.pm';

        # Don't recurse through directories if we're called inside an eval
        # Unfortunately, that's the interferes with our tests, so we'll
        # allow it when run under a test harness.

        return if !$ENV{FF_HARNESS_ACTIVE} && $^S;

        my @caller = caller(1);
        return
            if !$ENV{FF_HARNESS_ACTIVE}
            && ( ( $caller[3] && $caller[3] =~ m{eval} )
            || ( $caller[1] && $caller[1] =~ m{eval} ) );

        my $module = _maybe_find_module_in_INC($name);
        $module ||= _maybe_find_module_on_disk($name);
        return unless $module;

        $name =~ s{\.pm\z}{};
        $module =~ s{\.pm\z}{};

        my $msg = <<"EOF";

----------
The module "$name" could not be found. Perhaps you meant to "use $module"?
----------

EOF
        die $msg;
    };
}

sub _maybe_find_module_in_INC {
    my $module = shift;
    $module =~ s{::}{/}g;
    for my $file ( keys %INC ) {
        if ( edistance( lc($file), lc($module) ) <= 2 ) {
            $file =~ s{/}{::}g;
            return $file if $file;
        }
    }
}

sub _maybe_find_module_on_disk {
    my $module = shift;

    my $rule = Path::Iterator::Rule->new( depth_first => 1 );
    $rule->perl_module;

    my @module_parts = File::Spec->splitdir($module);
    my $module_depth = @module_parts;

    # don't iterate over any @INC hooks
    my @dirs = grep { !ref $_ } @INC;

    foreach my $inc_dir (@dirs) {
        _debug($inc_dir);
        my $this_rule = $rule->clone;

        $this_rule->and(
            sub {
                my $path = shift;
                my $file = shift;

                _debug($path);

                return \0 if $file =~ m{\A\.};

                $path =~ s{^$inc_dir/}{};

                # top level directory?
                return 0 if $path eq q{};

                my @path_parts = grep { m{\w} } File::Spec->splitdir($path);
                shift @path_parts if @path_parts && $path_parts[0] eq 'auto';

                my $path_depth = @path_parts;
                return \0 if $path_depth > $module_depth;
                return 0  if $path_depth < $module_depth;

                my $joined_path = join( '/', @path_parts );
                my $distance = edistance( lc($joined_path), lc($module) );

                return $distance <= 2 ? \1 : 0;
            }
        );

        my $next = $this_rule->iter($inc_dir);
        while ( defined( my $file = $next->() ) ) {
            $file =~ s{^$inc_dir/}{}g;

            my @parts = grep { m{\w} } File::Spec->splitdir($file);
            $file = join '::', @parts;
            $file =~ s{\.pm\z}{};
            return $file;
        }
    }
}

sub _debug {
    my $msg = shift;
    print "$msg\n" if $ENV{FF_DEBUG};
}

1;

# ABSTRACT: Catch typos in module names

__END__

=pod

=encoding UTF-8

=head1 NAME

fatfinger - Catch typos in module names

=head1 VERSION

version 0.000001

=head1 SYNOPSIS

    use strict;
    use warnings;
    use fatfinger;

=head1 DESCRIPTION

C<fatfinger> is a development tool which helps you spot typos in C<use>
statements.  It does this by adding an C<@INC> hook.  If a module which you
tried to C<use> cannot it be found, C<fatfinger> checks C<%INC> for similarly
named modules.  If this comes up empty C<fatfinger> will do some fuzzy matching
on the files in your C<@INC> directories.

In order for C<fatfinger> to be effective, C<use> it as early as possible in
your modules.

For example:

    use strict;
    use fatfinger;
    use warningz;

When running this code, you should get the following error message:

    ----------
    The module "warningz" could not be found. Perhaps you meant to "use warnings"?
    ----------

=head1 CAVEATS

This will add a (hopefully) small penalty to the run time of your code if a
module cannot be found.  Presently it seems to be fast enough for me, but it
may not be fast enough for you.

=head1 ACKNOWLEDGEMENTS

Thanks to Dave Rolsky, Greg Oschwald and Florian Ragwitz for helping me with
the logic behind this module.

=head1 AUTHOR

Olaf Alders <olaf@wundercounter.com>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2015 by MaxMind, Inc..

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut
