package Complete::Path;

our $DATE = '2015-09-22'; # DATE
our $VERSION = '0.19'; # VERSION

use 5.010001;
use strict;
use warnings;

use Complete::Setting;

require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(
                       complete_path
               );

sub _dig_leaf {
    my ($p, $list_func, $is_dir_func, $path_sep) = @_;
    my $num_dirs;
    my $listres = $list_func->($p, '', 0);
    return $p unless ref($listres) eq 'ARRAY' && @$listres == 1;
    my $e = $listres->[0];
    my $p2 = $p =~ m!\Q$path_sep\E\z! ? "$p$e" : "$p$path_sep$e";
    my $is_dir;
    if ($e =~ m!\Q$path_sep\E\z!) {
        $is_dir++;
    } else {
        $is_dir = $is_dir_func && $is_dir_func->($p2);
    }
    return _dig_leaf($p2, $list_func, $is_dir_func, $path_sep) if $is_dir;
    $p2;
}

our %SPEC;

$SPEC{complete_path} = {
    v => 1.1,
    summary => 'Complete path',
    description => <<'_',

Complete path, for anything path-like. Meant to be used as backend for other
functions like `Complete::File::complete_file` or
`Complete::Module::complete_module`. Provides features like case-insensitive
matching, expanding intermediate paths, and case mapping.

Algorithm is to split path into path elements, then list items (using the
supplied `list_func`) and perform filtering (using the supplied `filter_func`)
at every level.

_
    args => {
        word => {
            schema  => [str=>{default=>''}],
            pos     => 0,
        },
        list_func => {
            summary => 'Function to list the content of intermediate "dirs"',
            schema => 'code*',
            req => 1,
            description => <<'_',

Code will be called with arguments: ($path, $cur_path_elem, $is_intermediate).
Code should return an arrayref containing list of elements. "Directories" can be
marked by ending the name with the path separator (see `path_sep`). Or, you can
also provide an `is_dir_func` function that will be consulted after filtering.
If an item is a "directory" then its name will be suffixed with a path
separator by `complete_path()`.

_
        },
        is_dir_func => {
            summary => 'Function to check whether a path is a "dir"',
            schema  => 'code*',
            description => <<'_',

Optional. You can provide this function to determine if an item is a "directory"
(so its name can be suffixed with path separator). You do not need to do this if
you already suffix names of "directories" with path separator in `list_func`.

One reason you might want to provide this and not mark "directories" in
`list_func` is when you want to do extra filtering with `filter_func`. Sometimes
you do not want to suffix the names first (example: see `complete_file` in
`Complete::File`).

_
        },
        starting_path => {
            schema => 'str*',
            req => 1,
            default => '',
        },
        filter_func => {
            schema  => 'code*',
            description => <<'_',

Provide extra filtering. Code will be given path and should return 1 if the item
should be included in the final result or 0 if the item should be excluded.

_
        },

        path_sep => {
            schema  => 'str*',
            default => '/',
        },
        ci => {
            summary => 'Case-insensitive matching',
            schema  => 'bool',
        },
        fuzzy => {
            summary => 'Fuzzy matching',
            schema  => ['int*', min=>0],
        },
        map_case => {
            summary => 'Treat _ (underscore) and - (dash) as the same',
            schema  => 'bool',
            description => <<'_',

This is another convenience option like `ci`, where you can type `-` (without
pressing Shift, at least in US keyboard) and can still complete `_` (underscore,
which is typed by pressing Shift, at least in US keyboard).

This option mimics similar option in bash/readline: `completion-map-case`.

_
        },
        exp_im_path => {
            summary => 'Expand intermediate paths',
            schema  => 'bool',
            description => <<'_',

This option mimics feature in zsh where when you type something like `cd
/h/u/b/myscript` and get `cd /home/ujang/bin/myscript` as a completion answer.

_
        },
        dig_leaf => {
            summary => 'Dig leafs',
            schema => 'bool',
            description => <<'_',

This feature mimics what's seen on GitHub. If a directory entry only contains a
single entry, it will directly show the subentry (and subsubentry and so on) to
save a number of tab presses.

_
        },
        #result_prefix => {
        #    summary => 'Prefix each result with this string',
        #    schema  => 'str*',
        #},
    },
    result_naked => 1,
    result => {
        schema => 'array',
    },
};
sub complete_path {
    require Complete::Util;

    my %args   = @_;
    my $word   = $args{word} // "";
    my $path_sep = $args{path_sep} // '/';
    my $list_func   = $args{list_func};
    my $is_dir_func = $args{is_dir_func};
    my $filter_func = $args{filter_func};
    my $ci          = $args{ci} // $Complete::Setting::OPT_CI;
    my $fuzzy       = $args{fuzzy} // $Complete::Setting::OPT_FUZZY;
    my $map_case    = $args{map_case} // $Complete::Setting::OPT_MAP_CASE;
    my $exp_im_path = $args{exp_im_path} // $Complete::Setting::OPT_EXP_IM_PATH;
    my $dig_leaf    = $args{dig_leaf} // $Complete::Setting::OPT_DIG_LEAF;
    my $result_prefix = $args{result_prefix};
    my $starting_path = $args{starting_path} // '';

    my $re_ends_with_path_sep = qr!\A\z|\Q$path_sep\E\z!;

    # split word by into path elements, as we want to dig level by level (needed
    # when doing case-insensitive search on a case-sensitive tree).
    my @intermediate_dirs;
    {
        @intermediate_dirs = split qr/\Q$path_sep/, $word;
        @intermediate_dirs = ('') if !@intermediate_dirs;
        push @intermediate_dirs, '' if $word =~ $re_ends_with_path_sep;
    }

    # extract leaf path, because this one is treated differently
    my $leaf = pop @intermediate_dirs;
    @intermediate_dirs = ('') if !@intermediate_dirs;

    #say "D:starting_path=<$starting_path>";
    #say "D:intermediate_dirs=[",join(", ", map{"<$_>"} @intermediate_dirs),"]";
    #say "D:leaf=<$leaf>";

    # candidate for intermediate paths. when doing case-insensitive search,
    # there maybe multiple candidate paths for each dir, for example if
    # word='../foo/s' and there is '../foo/Surya', '../Foo/sri', '../FOO/SUPER'
    # then candidate paths would be ['../foo', '../Foo', '../FOO'] and the
    # filename should be searched inside all those dirs. everytime we drill down
    # to deeper subdirectories, we adjust this list by removing
    # no-longer-eligible candidates.
    my @candidate_paths;

    for my $i (0..$#intermediate_dirs) {
        my $intdir = $intermediate_dirs[$i];
        my $intdir_with_path_sep = "$intdir$path_sep";
        my @dirs;
        if ($i == 0) {
            # first path elem, we search starting_path first since
            # candidate_paths is still empty.
            @dirs = ($starting_path);
        } else {
            # subsequent path elem, we search all candidate_paths
            @dirs = @candidate_paths;
        }

        if ($i == $#intermediate_dirs && $intdir eq '') {
            @candidate_paths = @dirs;
            last;
        }

        my @new_candidate_paths;
        for my $dir (@dirs) {
            #say "D:  intdir list($dir)";
            my $listres = $list_func->($dir, $intdir, 1);
            next unless $listres && @$listres;
            #use DD; dd $listres;
            my $matches = Complete::Util::complete_array_elem(
                word => $intdir, array => $listres,
                ci=>$ci, fuzzy=>$fuzzy, map_case=>$map_case,
            );
            my $exact_matches = [grep {
                length($intdir)+length($path_sep) eq length($_)
            } @$matches];
            #print "D: word=<$intdir>, matches="; use DD; dd $matches; print ", exact_matches="; dd $exact_matches;

            # when doing exp_im_path, check if we have a single exact match. in
            # that case, don't use all the candidates because that can be
            # annoying, e.g. you have 'a/foo' and 'and/food', you won't be able
            # to complete 'a/f' because bash (e.g.) will always cut the answer
            # to 'a' because the candidates are 'a/foo' and 'and/foo' (it will
            # use the shortest common string which is 'a').
            #say "D:  num_exact_matches: $num_exact_matches";
            if ($exp_im_path && @$exact_matches == 1) {
                $matches = $exact_matches;
            }

            for (@$matches) {
                my $p = $dir =~ $re_ends_with_path_sep ?
                    "$dir$_" : "$dir$path_sep$_";
                push @new_candidate_paths, $p;
            }

        }
        #say "D:  candidate_paths=[",join(", ", map{"<$_>"} @new_candidate_paths),"]";
        return [] unless @new_candidate_paths;
        @candidate_paths = @new_candidate_paths;
    }

    my $cut_chars = 0;
    if (length($starting_path)) {
        $cut_chars += length($starting_path);
        unless ($starting_path =~ /\Q$path_sep\E\z/) {
            $cut_chars += length($path_sep);
        }
    }

    my @res;
    for my $dir (@candidate_paths) {
        #say "D:opendir($dir)";
        my $listres = $list_func->($dir, $leaf, 0);
        next unless $listres && @$listres;
        my $matches = Complete::Util::complete_array_elem(
            word => $leaf, array => $listres,
            ci=>$ci, fuzzy=>$fuzzy, map_case=>$map_case,
        );
        #use DD; dd $matches;

      L1:
        for my $e (@$matches) {
            my $p = $dir =~ $re_ends_with_path_sep ?
                "$dir$e" : "$dir$path_sep$e";
            {
                local $_ = $p; # convenience for filter func
                next L1 if $filter_func && !$filter_func->($p);
            }

            my $is_dir;
            if ($e =~ $re_ends_with_path_sep) {
                $is_dir = 1;
            } else {
                local $_ = $p; # convenience for is_dir_func
                $is_dir = $is_dir_func->($p);
            }

            if ($is_dir && $dig_leaf) {
                $p = _dig_leaf($p, $list_func, $is_dir_func, $path_sep);
                # check again
                if ($p =~ $re_ends_with_path_sep) {
                    $is_dir = 1;
                } else {
                    local $_ = $p; # convenience for is_dir_func
                    $is_dir = $is_dir_func->($p);
                }
            }

            # process into final result
            my $p0 = $p;
            substr($p, 0, $cut_chars) = '' if $cut_chars;
            $p = "$result_prefix$p" if length($result_prefix);
            unless ($p =~ /\Q$path_sep\E\z/) {
                $p .= $path_sep if $is_dir;
            }
            push @res, $p;
        }
    }

    \@res;
}
1;
# ABSTRACT: Complete path

__END__

=pod

=encoding UTF-8

=head1 NAME

Complete::Path - Complete path

=head1 VERSION

This document describes version 0.19 of Complete::Path (from Perl distribution Complete-Path), released on 2015-09-22.

=head1 DESCRIPTION

=head1 FUNCTIONS


=head2 complete_path(%args) -> array

Complete path.

Complete path, for anything path-like. Meant to be used as backend for other
functions like C<Complete::File::complete_file> or
C<Complete::Module::complete_module>. Provides features like case-insensitive
matching, expanding intermediate paths, and case mapping.

Algorithm is to split path into path elements, then list items (using the
supplied C<list_func>) and perform filtering (using the supplied C<filter_func>)
at every level.

Arguments ('*' denotes required arguments):

=over 4

=item * B<ci> => I<bool>

Case-insensitive matching.

=item * B<dig_leaf> => I<bool>

Dig leafs.

This feature mimics what's seen on GitHub. If a directory entry only contains a
single entry, it will directly show the subentry (and subsubentry and so on) to
save a number of tab presses.

=item * B<exp_im_path> => I<bool>

Expand intermediate paths.

This option mimics feature in zsh where when you type something like C<cd
/h/u/b/myscript> and get C<cd /home/ujang/bin/myscript> as a completion answer.

=item * B<filter_func> => I<code>

Provide extra filtering. Code will be given path and should return 1 if the item
should be included in the final result or 0 if the item should be excluded.

=item * B<fuzzy> => I<int>

Fuzzy matching.

=item * B<is_dir_func> => I<code>

Function to check whether a path is a "dir".

Optional. You can provide this function to determine if an item is a "directory"
(so its name can be suffixed with path separator). You do not need to do this if
you already suffix names of "directories" with path separator in C<list_func>.

One reason you might want to provide this and not mark "directories" in
C<list_func> is when you want to do extra filtering with C<filter_func>. Sometimes
you do not want to suffix the names first (example: see C<complete_file> in
C<Complete::File>).

=item * B<list_func>* => I<code>

Function to list the content of intermediate "dirs".

Code will be called with arguments: ($path, $cur_path_elem, $is_intermediate).
Code should return an arrayref containing list of elements. "Directories" can be
marked by ending the name with the path separator (see C<path_sep>). Or, you can
also provide an C<is_dir_func> function that will be consulted after filtering.
If an item is a "directory" then its name will be suffixed with a path
separator by C<complete_path()>.

=item * B<map_case> => I<bool>

Treat _ (underscore) and - (dash) as the same.

This is another convenience option like C<ci>, where you can type C<-> (without
pressing Shift, at least in US keyboard) and can still complete C<_> (underscore,
which is typed by pressing Shift, at least in US keyboard).

This option mimics similar option in bash/readline: C<completion-map-case>.

=item * B<path_sep> => I<str> (default: "/")

=item * B<starting_path>* => I<str> (default: "")

=item * B<word> => I<str> (default: "")

=back

Return value:  (array)

=head1 SEE ALSO

L<Complete>

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Complete-Path>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-Complete-Path>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Complete-Path>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by perlancar@cpan.org.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
