The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

package Footprintless::Overlay;
$Footprintless::Overlay::VERSION = '1.27';
# ABSTRACT: An overlay manager
# PODNAME: Footprintless::Overlay

use parent qw(Footprintless::MixableBase);

use Carp;
use Footprintless::Mixins qw (
    _clean
    _command_options
    _entity
    _extract_resource
    _local_template
    _push_to_destination
    _sub_coordinate
    _sub_entity
);
use Footprintless::Util qw(
    dynamic_module_new
    invalid_entity
    temp_dir
);
use Log::Any;
use File::Find;
use File::Spec;
use Template::Resolver;
use Template::Overlay;

my $logger = Log::Any->get_logger();

sub clean {
    my ($self) = @_;
    $self->_clean();
}

sub _dirs_template {
    my ( $self, $to_dir, $with_dirs_work ) = @_;

    my $base_dir     = $self->_sub_entity('base_dir');
    my $template_dir = $self->_sub_entity('template_dir');

    my $unpack_dir;
    my $resource = $self->_sub_entity('resource');
    if ($resource) {
        $unpack_dir = temp_dir();
        $self->_extract_resource( $resource, $unpack_dir );

        if ($base_dir) {
            $base_dir = File::Spec->catdir( $unpack_dir, $base_dir );
        }
        if ($template_dir) {
            $template_dir =
                ref($template_dir) eq 'ARRAY'
                ? [ map { File::Spec->catdir( $unpack_dir, $_ ) } @$template_dir ]
                : File::Spec->catdir( $unpack_dir, $template_dir );
        }
    }

    &$with_dirs_work( $base_dir, $template_dir, $to_dir );
}

sub _dot_footprintless_resolver {
    my ($self) = @_;
    return sub {
        my ( $template, $destination ) = @_;
        if ( $template =~ /\/\.footprintless$/ ) {
            $self->_resolve_footprintless( $template, $destination );
            return 1;
        }
        return 0;
    };
}

sub initialize {
    my ( $self, %options ) = @_;

    $self->clean();

    if ( $options{to_dir} ) {
        $self->_dirs_template(
            $options{to_dir},
            sub {
                $self->_initialize(@_);
            }
        );
    }
    else {
        $self->_local_with_dirs_template(
            sub {
                $self->_initialize(@_);
            }
        );
    }
}

sub _initialize {
    my ( $self, $base_dir, $template_dir, $to_dir ) = @_;
    $self->_overlay($base_dir)->overlay(
        $template_dir,
        resolver => $self->_dot_footprintless_resolver(),
        to       => $to_dir
    );
}

sub _local_with_dirs_template {
    my ( $self, $local_work ) = @_;
    $self->_local_template(
        sub {
            $self->_dirs_template( $_[0], $local_work );
        }
    );
}

sub _overlay {
    my ( $self, $base_dir ) = @_;

    my @overlay_opts = ();
    my $key          = $self->_sub_entity('key');
    push( @overlay_opts, key => $key ) if ($key);

    return Template::Overlay->new( $base_dir, $self->_resolver(), @overlay_opts );
}

sub _resolver {
    my ($self) = @_;

    my @resolver_opts = ();
    my $os            = $self->_sub_entity('os');
    push( @resolver_opts, os => $os ) if ($os);

    my $resolver_coordinate = $self->_sub_entity('resolver_coordinate');
    my $resolver_spec =
          $resolver_coordinate
        ? $self->_entity($resolver_coordinate)
        : $self->_entity();

    my $resolver;
    my $resolver_factory = $self->_entity('footprintless.overlay.resolver_factory');
    if ($resolver_factory) {
        $logger->tracef( "using resolver_factory: %s", $resolver_factory );
        $resolver =
            dynamic_module_new($resolver_factory)->new_resolver( $resolver_spec, @resolver_opts );
    }
    else {
        $resolver = Template::Resolver->new( $resolver_spec, @resolver_opts );
    }
    return $resolver;
}

sub _resolve_footprintless {
    my ( $self, $template, $footprintless_path ) = @_;
    my $destination = ( File::Spec->splitpath($footprintless_path) )[1];
    $logger->debugf( "resolving [%s]->[%s]", $template, $destination );

    my $spec = do($template) || return;
    croak("invalid $template") unless ( ref($spec) eq 'HASH' );

    if ( $spec->{clean} ) {
        my @to_be_cleaned =
            map { File::Spec->catdir( $destination, $_ ) . ( /\/$/ ? '/' : '' ); }
            ref( $spec->{clean} ) ? @{ $spec->{clean} } : ( $spec->{clean} );

        Footprintless::Util::clean(
            \@to_be_cleaned,
            command_runner  => $self->{factory}->command_runner(),
            command_options => $self->_command_options()
        );
    }

    if ( $spec->{resources} ) {
        my $resource_manager = $self->{factory}->resource_manager();
        foreach my $resource ( keys( %{ $spec->{resources} } ) ) {
            $resource_manager->download( $spec->{resources}{$resource}, to => $destination );
        }
    }
}

sub update {
    my ( $self, %options ) = @_;

    if ( $options{to_dir} ) {
        $self->_dirs_template(
            $options{to_dir},
            sub {
                $self->_update(@_);
            }
        );
    }
    else {
        $self->_local_with_dirs_template(
            sub {
                $self->_update(@_);
            }
        );
    }
}

sub _update {
    my ( $self, $base_dir, $template_dir, $to_dir ) = @_;
    $logger->tracef( "update to=[%s], template=[%s]", $to_dir, $template_dir );
    $self->_overlay($to_dir)
        ->overlay( $template_dir, resolver => $self->_dot_footprintless_resolver() );
}

1;

__END__

=pod

=head1 NAME

Footprintless::Overlay - An overlay manager

=head1 VERSION

version 1.27

=head1 SYNOPSIS

    # Standard way of getting an overlay
    use Footprintless;
    my $overlay = Footprintless->new()->overlay('overlay');

    $overlay->clean();

    $overlay->initialize();

    $overlay->update();

=head1 DESCRIPTION

Overlays are a combination of a directory of static files and a directory 
of templated files that will be merged to an output directory.  This
is implemented in L<Template::Overlay>.  

Additionally, any folder under the C<template_dir> can contain a 
C<.footprintless> file containing a C<clean> and/or C<resources> entities:

    return {
        clean => [
            'foo.jar',
            'bar.jar',
            'ext/'
        ],
        resources => {
            foo => 'com.pastdev:foo:1.0.0',
            bar => 'com.pastdev:bar:1.0.0'
        }
    };

The C<clean> entity is an arrayref containing a list of paths to clean out.
These paths will be added to the path of the directory containing the
C<.footprintless> file.  The C<resources> entity is a list of resources to
download into the same directory as the C<.footprintless> file.

=head1 ENTITIES

A simple overlay: 

    overlay => {
        base_dir => "/home/me/foo/base",
        clean => [
            "/opt/tomcat/"
        ],
        hostname => 'localhost',
        key => 'T',
        os => 'linux',
        template_dir => "/home/me/foo/template",
        to_dir => '/opt/foo/tomcat'
    }

A more complex example:

    foo => {
        hostname => 'test.pastdev.com',
        overlay => {
            'Config::Entities::inherit' => ['hostname', 'sudo_username'],
            base_dir => '/home/me/foo/base',
            clean => [
                '/opt/foo/tomcat/'
            ],
            key => 'T',
            os => 'linux',
            resolver_coordinate => 'foo',
            template_dir => '/home/me/foo/template',
            to_dir => '/opt/foo/tomcat'
        },
        sudo_username => 'developer',
        tomcat => {
            'Config::Entities::inherit' => ['hostname', 'sudo_username'],
            catalina_base => '/opt/foo/tomcat',
            http => {
                port => 20080
            },
            service => {
                action => {
                    'kill' => { command_args => 'stop -force' },
                    'status' => { use_pid => 1 }
                },
                command => '/opt/foo/tomcat/bin/catalina.sh',
                pid_file => '/opt/foo/tomcat/bin/.catalina.pid',
            },
            shutdown => {
                port => 8505,
                password => $properties->{'foo.tomcat.shutdown.password'},
            },
            trust_store => {
                'Config::Entities::inherit' => ['hostname', 'sudo_username'],
                file => '/opt/foo/tomcat/certs/truststore.jks',
                include_java_home_cacerts => 1,
                password => $properties->{'foo.tomcat.trust_store.password'},
            }
        }
    }

An overlay can obtain base/template content from a resource.  When
initialize or update are called, the resource will be downloaded (if not
already local) and extracted to a temp folder.  The C<base_dir> and
C<template_dir> paths will be appended to the extract temp folder:

    overlay => {
        base_dir => 'base',
        clean => [
            '/opt/tomcat/'
        ],
        hostname => 'localhost',
        key => 'T',
        os => 'linux',
        resource => 'com.pastdev:app-overlay:zip:package:1.0.0',
        template_dir => 'template',
        to_dir => '/opt/foo/tomcat'
    }

An overlay can have multiple template folders.  If it does, they will
be processed in the order they are listed:

    overlay => {
        base_dir => 'base',
        clean => [
            '/opt/tomcat/'
        ],
        hostname => 'localhost',
        key => 'T',
        os => 'linux',
        template_dir => [
            'first/template_dir',
            'second/template_dir',
        ],
        to_dir => '/opt/foo/tomcat'
    }

=head1 CONSTRUCTORS

=head2 new($entity, $coordinate)

Constructs a new overlay configured by C<$entities> at C<$coordinate>.  

=head1 METHODS

=head2 clean()

Cleans the overlay.  Each path in the C<clean> entity, will be removed 
from the destination.  If the path ends in a C</>, then after being 
removed, the directory will be recreated.

=head2 initialize()

Will call C<clean>, then C<overlay> on an instance of L<Template::Overlay>
configured to this entity.  

=head2 update()

Will overlay I<ONLY> the templated files.  It will not C<clean>, nor copy 
any files from C<base_dir> like C<initialize> does.

=head1 AUTHOR

Lucas Theisen <lucastheisen@pastdev.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by Lucas Theisen.

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

=head1 SEE ALSO

Please see those modules/websites for more information related to this module.

=over 4

=item *

L<Footprintless|Footprintless>

=item *

L<Config::Entities|Config::Entities>

=item *

L<Footprintless|Footprintless>

=item *

L<Footprintless::Mixins|Footprintless::Mixins>

=item *

L<Template::Overlay|Template::Overlay>

=item *

L<Template::Resolver|Template::Resolver>

=back

=head1 CONFIGURATION

This module can optionally be configured to use a customized resolver.  To
do so, configure a resolver factory in your entities:

    footprintless => {
        overlay => {
            resolver_factory => 'My::ResolverFactory'
        }
    }

The resolver factory must have a C<new_resolver> method that takes a spec and
a list of options and returns a C<Template::Resolver>, for example:

    sub new_resolver {
        my ($self, $resolver_spec, %resolver_opts) = @_;
        return Template::Resolver->new(
            $resolver_spec,
            %resolver_opts,
            additional_transforms => {
                random => sub {
                    my ($resolver_self, $value) = @_;
                    return $value . rand();
                }
            });
    }

=cut