# ABSTRACT: Role for template engines

package Dancer2::Core::Role::Template;
$Dancer2::Core::Role::Template::VERSION = '2.0.1';
use Dancer2::Core::Types;
use Dancer2::FileUtils 'path';
use Carp 'croak';
use Ref::Util qw< is_ref >;

use Moo::Role;
with 'Dancer2::Core::Role::Engine';

sub hook_aliases {
    {
        before_template_render => 'engine.template.before_render',
        after_template_render  => 'engine.template.after_render',
        before_layout_render   => 'engine.template.before_layout_render',
        after_layout_render    => 'engine.template.after_layout_render',
    }
}

sub supported_hooks { values %{ shift->hook_aliases } }

sub _build_type {'Template'}

requires 'render';

has log_cb => (
    is      => 'ro',
    isa     => CodeRef,
    default => sub { sub {1} },
);

has name => (
    is      => 'ro',
    lazy    => 1,
    builder => 1,
);

sub _build_name {
    ( my $name = ref shift ) =~ s/^Dancer2::Template:://;
    $name;
}

has charset => (
    is      => 'ro',
    isa     => Str,
    default => sub {'UTF-8'},
);

has default_tmpl_ext => (
    is      => 'ro',
    isa     => Str,
    default => sub { shift->config->{extension} || 'tt' },
);

has engine => (
    is      => 'ro',
    isa     => Object,
    lazy    => 1,
    builder => 1,
);

has settings => (
    is      => 'ro',
    isa     => HashRef,
    lazy    => 1,
    default => sub { +{} },
    writer  => 'set_settings',
);

# The attributes views, layout and layout_dir have triggers in
# Dancer2::Core::App that enable their values to be modified by
# the `set` keyword. As such, these are defined as read-write attrs.

has views => (
    is  => 'rw',
    isa => Maybe [Str],
);

has layout => (
    is  => 'rw',
    isa => Maybe [Str],
);

has layout_dir => (
    is  => 'rw',
    isa => Maybe [Str],
);

sub _template_name {
    my ( $self, $view ) = @_;
    my $def_tmpl_ext = $self->default_tmpl_ext();
    $view .= ".$def_tmpl_ext" if $view !~ /\.\Q$def_tmpl_ext\E$/;
    return $view;
}

sub view_pathname {
    my ( $self, $view ) = @_;

    $view = $self->_template_name($view);
    return path( $self->views, $view );
}

sub layout_pathname {
    my ( $self, $layout ) = @_;

    return path(
        $self->views,
        $self->layout_dir,
        $self->_template_name($layout),
    );
}

sub pathname_exists {
    my ( $self, $pathname ) = @_;
    return -f $pathname;
}

sub render_layout {
    my ( $self, $layout, $tokens, $content ) = @_;

    $layout = $self->layout_pathname($layout);

    # FIXME: not sure if I can "just call render"
    $self->render( $layout, { %$tokens, content => $content } );
}

sub apply_renderer {
    my ( $self, $view, $tokens ) = @_;
    $view = $self->view_pathname($view) if !is_ref($view);
    $tokens = $self->_prepare_tokens_options( $tokens );

    $self->execute_hook( 'engine.template.before_render', $tokens );

    my $content = $self->render( $view, $tokens );
    $self->execute_hook( 'engine.template.after_render', \$content );

    # make sure to avoid ( undef ) in list context return
    defined $content and return $content;
    return;
}

sub apply_layout {
    my ( $self, $content, $tokens, $options ) = @_;

    $tokens = $self->_prepare_tokens_options( $tokens );

   # If 'layout' was given in the options hashref, use it if it's a true value,
   # or don't use a layout if it was false (0, or undef); if layout wasn't
   # given in the options hashref, go with whatever the current layout setting
   # is.
    my $layout =
      exists $options->{layout}
      ? ( $options->{layout} ? $options->{layout} : undef )
      : ( $self->layout || $self->config->{layout} );

    # that should only be $self->config, but the layout ain't there ???

    defined $content or return;
    defined $layout  or return $content;

    $self->execute_hook(
        'engine.template.before_layout_render',
        $tokens, \$content
    );

    my $full_content = $self->render_layout( $layout, $tokens, $content );

    $self->execute_hook( 'engine.template.after_layout_render',
        \$full_content );

    # make sure to avoid ( undef ) in list context return
    defined $full_content and return $full_content;
    return;
}

sub _prepare_tokens_options {
    my ( $self, $tokens ) = @_;

    # these are the default tokens provided for template processing
    $tokens ||= {};
    $tokens->{perl_version}   = $^V;
    $tokens->{dancer_version} = Dancer2->VERSION;
    $tokens->{settings}       = $self->settings;

    # no request when template is called as a global keyword
    if ( $self->has_request ) {
        $tokens->{request}  = $self->request;
        $tokens->{params}   = $self->request->params;
        $tokens->{vars}     = $self->request->vars;

        # a session can not exist if there is no request
        $tokens->{session} = $self->session->data
          if $self->has_session;
    }

    return $tokens;
}

sub process {
    my ( $self, $view, $tokens, $options ) = @_;
    my ( $content, $full_content );

    # it's important that $tokens is not undef, so that things added to it via
    # a before_template in apply_renderer survive to the apply_layout. GH#354
    $tokens  ||= {};
    $options ||= {};

    ## FIXME - Look into PR 654 so we fix the problem here as well!

    $content =
        $view
      ? $self->apply_renderer( $view, $tokens )
      : delete $options->{content};

    defined $content
      and $full_content = $self->apply_layout( $content, $tokens, $options );

    defined $full_content
      and return $full_content;

    croak "Template did not produce any content";
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Dancer2::Core::Role::Template - Role for template engines

=head1 VERSION

version 2.0.1

=head1 DESCRIPTION

Any class that consumes this role will be able to be used as a template engine
under Dancer2.

In order to implement this role, the consumer B<must> implement the method C<render>. This method will receive three arguments:

=over 4

=item $self

=item $template

=item $tokens

=back

Any template receives the following tokens, by default:

=over 4

=item * C<perl_version>

Current version of perl, effectively C<$^V>.

=item * C<dancer_version>

Current version of Dancer2, effectively C<< Dancer2->VERSION >>.

=item * C<settings>

A hash of the application configuration.

=item * C<request>

The current request object.

=item * C<params>

A hash reference of all the parameters.

Currently the equivalent of C<< $request->params >>.

=item * C<vars>

The list of request variables, which is what you would get if you
called the C<vars> keyword.

=item * C<session>

The current session data, if a session exists.

=back

=head1 ATTRIBUTES

=head2 name

The name of the template engine (e.g.: Simple).

=head2 charset

The charset.  The default value is B<UTF-8>.

=head2 default_tmpl_ext

The default file extension.  If not provided, B<tt> is used.

=head2 views

Path to the directory containing the views.

=head2 layout

Path to the directory containing the layouts.

=head2 layout_dir

Relative path to the layout directory.

Default: B<layouts>.

=head2 engine

Contains the engine.

=head1 METHODS

=head2 view_pathname($view)

Returns the full path to the requested view.

=head2 layout_pathname($layout)

Returns the full path to the requested layout.

=head2 pathname_exists($pathname)

Returns true if the requested pathname exists. Can be used for either views
or layouts:

    $self->pathname_exists( $self->view_pathname( 'some_view' ) );
    $self->pathname_exists( $self->layout_pathname( 'some_layout' ) );

=head2 render_layout($layout, \%tokens, \$content)

Render the layout with the applied tokens

=head2 apply_renderer($view, \%tokens)

=head2 apply_layout($content, \%tokens, \%options)

=head2 process($view, \%tokens, \%options)

=head2 template($view, \%tokens, \%options)

=head1 AUTHOR

Dancer Core Developers

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2025 by Alexis Sukrieh.

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
