#
package Pod::Weaver::Plugin::Include::Finder;

our $VERSION = 'v0.1.9';

# ABSTRACT: Finds source Pods in .pod files or modules.


use Pod::Find qw<pod_where>;
use File::Find::Rule;
use Pod::Elemental;
use Pod::Elemental::Transformer::Pod5;

use Moose;
use namespace::autoclean;


has cache => (
    is      => 'rw',
    isa     => 'HashRef[HashRef]',
    builder => 'init_cache',
);


has maps => (
    is      => 'rw',
    isa     => 'HashRef[Str]',
    lazy    => 1,
    builder => 'init_maps',
);


has callerPlugin => (
    is  => 'ro',
    isa => 'Pod::Weaver::Plugin::Include',
);


has pod_path => (
    is      => 'rw',
    lazy    => 1,
    isa     => 'ArrayRef[Str]',
    builder => 'init_pod_path',
);

has logger => (
    is      => 'rw',
    lazy    => 1,
    builder => 'init_logger',
    handles => [qw<log log_debug log_fatal>],
);


has _tmplSource => (
    is      => 'rw',
    clearer => '_clear_tmplSource',
    isa     => 'Str',
);

has _tmplName => (
    is      => 'rw',
    clearer => '_clear_tmplName',
    isa     => 'Str',
);

has _tmplContent => (
    is      => 'rw',
    isa     => 'ArrayRef',
    clearer => '_clear_tmplContent',
    lazy    => 1,
    default => sub { [] },
);


sub find_source {
    my $this = shift;
    my ($source) = @_;

    my $podFile = pod_where( { -dirs => $this->pod_path }, $source );

    return undef unless defined $podFile;

    $this->maps->{$source} = $podFile;

    return $podFile;
}


sub register_alias {
    my $this = shift;
    my ( $alias, $source ) = @_;

    my $podFile = $this->find_source($source);

    if ( defined $podFile ) {
        $this->maps->{$alias} = $podFile;
    }

    return $podFile;
}


sub _store_template {
    my $this = shift;

    return unless defined $this->_tmplName;
    
    $this->log_debug("Caching template", $this->_tmplName);

    $this->cache->{ $this->_tmplSource }{ $this->_tmplName } =
      $this->_tmplContent;

    $this->_clear_tmplName;
    $this->_clear_tmplContent;
}

sub _add2tmpl {
    my $this = shift;
    my $para = shift;
    
    if ($this->_tmplName) {
        push @{$this->_tmplContent}, $para;
    }
}


sub parse_tmpl {
    my $this = shift;
    my $str  = shift;

    my $attrs = {};

    if ($str) {
        $str =~ m/
                ^\s*
                (?<hidden>-)?
                (?<name>
                    [\p{XPosixAlpha}_]
                    ([\p{XPosixAlnum}_])*
                )
                \s*$
## Please see file perltidy.ERR
            /x;

        if ( $+{name} ) {
            $attrs->{name}   = $+{name};
            $attrs->{hidden} = defined $+{hidden};
        }
        else {
            # $str is not empty but no valid name found.
            $attrs->{badName} = 1;
        }
    }

    return $attrs;
}


sub load_file {
    my $this = shift;
    my ( $file, %opts ) = @_;

    $this->log_debug( "Loading file " . $file );

    my $showContent = 0;

    my $doc = Pod::Elemental->read_file($file);
    if ($doc) {
        Pod::Elemental::Transformer::Pod5->new->transform_node($doc);

        $this->_tmplSource($file);

        my $children = $doc->children;
      ELEM: for ( my $i = 0 ; $i < @$children ; $i++ ) {
            my $para = $children->[$i];
            if ( $para->isa('Pod::Elemental::Element::Pod5::Command') ) {
                if ( $para->command eq 'tmpl' ) {
                    $this->log_debug("Closing template by =tmpl");
                    $this->_store_template;

                    my $attrs = $this->parse_tmpl( $para->content );
                    $showContent = $attrs->{name} eq 'test' if $attrs->{name};
                    $this->_tmplName( $attrs->{name} ) if $attrs->{name};
                }
                else {
                    $this->_add2tmpl($para);
                }
                next ELEM;
            }
            elsif ( defined $this->_tmplName ) {
                $this->_add2tmpl($para);
            }
        }

        # If any template was declared at the document end.
        $this->log_debug("Closing any remaining template");
        $this->_store_template;
        $this->_clear_tmplSource;
    }
    else {
        die "Failed to load doc from $file";
    }

    return defined $doc;
}


sub get_template {
    my $this = shift;
    my %opts = @_;

    my $fullName = $this->maps->{ $opts{source} };

    my $template;

    unless ( defined $fullName ) {

        # Find file if specified by short name or module name.
        $fullName = $this->find_source( $opts{source} );
    }

    $this->log( "Cannot find source file for [" . $opts{source} . "]" )
      unless defined $fullName;

    return undef unless defined $fullName;

    $this->log_debug(
        "Found file $fullName for source [" . $opts{source} . "]" );

    unless ( $template = $this->cache->{$fullName}{ $opts{template} } ) {
        if ( my $doc = $this->load_file( $fullName, %opts ) ) {

            $template = $this->cache->{$fullName}{ $opts{template} };
        }
    }
    return $template;
}

sub init_cache {
    return {};
}

sub init_maps {
    return {};
}

sub init_pod_path {
    my $this = shift;

    return defined $this->callerPlugin
      ? $this->callerPlugin->pod_path
      : [qw<./lib>];
}

sub init_logger {
    my $this = shift;

    my $logger;

    if ( defined $this->callerPlugin ) {
        $logger = $this->callerPlugin->logger;
    }
    else {
        require Log::Dispatchouli;
        $logger = Log::Dispatchouli->new(
            {
                ident     => '-Include::Finder',
                to_stdout => 1,
                log_pid   => 0,
                debug     => 1,
            }
        );
    }
    return $logger;
}

__PACKAGE__->meta->make_immutable;
no Moose;

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Pod::Weaver::Plugin::Include::Finder - Finds source Pods in .pod files or modules.

=head1 VERSION

version v0.1.9

=head1 SYNOPSIS

    use Pod::Weaver::Plugin::Include::Finder;
    
    my $finder = Pod::Weaver::Plugin::Include::Finder->new;
    my $template = $finder->get_template(
        template => 'tmplName',
        source => 'source.pod',
    );

=head1 DESCRIPTION

This module loads sources, parses them and caches templates found.

=head1 ATTRIBUTES

=head2 B<cache>

Cache of templates by sources. Hash of hashes where first level keys are
sources by their full file names; and second level keys are template names.
Each cache entry is an array of Pod nodes.

=head2 B<maps>

Mapping of short names into full path names. Short names are either aliases
or what is used with a C<=include> command. For example:

    =srcAlias alias Some::Module
    =include template@templates/src.pod

With these commands the map will contain keys I<alias> and I<templates/src.pod>.    

=head2 callerPlugin

Back reference to a L<Pod::Weaver::Plugin::Include> instance.

=head2 pod_path

List of entries from C<pod_path> configuration variable.

=head1 METHODS

=head2 B<find_source( $source )>

Takes a short source name (not alias!) and returns full path name for it or
I<undef> if not found.

Successful search is stored into C<maps> attribute.

=head2 B<register_alias( $alias, $source )>

Finds out the full path name for C<$source> and stores a new entry for C<$alias>
in C<maps> attribute. Does nothing if source is not found.

B<NOTE:> This method will result in two C<maps> entries: one for the C<$source>
and one for the C<$alias>.

Returns full path name of the C<$source>.

=head2 B<parse_tmpl( $str )>

Parses argument of C<=tmpl> command. Returns a profile hash with two keys:

=over 4

=item C<hidden>

Boolean, I<true> if template is declared hidden.

=item C<name>

Template name.

=back

=head2 C<load_file( $file )>

Loads and parses a source file defined by C<$file>. The result is stored into
C<cache>.

Returns I<true> if file has been successully read by L<Pod::Elemental>.

=head2 C<get_template( %opts )>

Returns a cached template. C<%opts> profile can have two keys:

=over 4

=item C<template>

Template name

=item C<source>

Source in short form including aliases.

=back

If a template is missing in the C<cache> then tries to C<load_file()>.

Returns I<undef> if failed.

=head2 init_cache

Initializer for cache attribute.

=head2 init_maps

Initilizer for maps attribute.

=head2 init_pod_path

Initializer for pod_path attribute.

=head2 init_logger

Initializer for logger attribute. Takes logger object either from
callerPlugin or creates a new one.

=head1 PRIVATE ATTRIBUTES

=head2 B<_tmplSource, _tmplName, _tmplContent>

Solely for use by C<load_file()> and C<_store_template()> methods.

=head1 PRIVATE METHODS

=head2 _store_template

Records a new template into the C<cache>.

=head1 AUTHOR

Vadim Belman <vrurg@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Vadim Belman.

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
