package Dancer2::Plugin::LiteBlog;

=head1 NAME

Dancer2::Plugin::LiteBlog - A minimalist, file-based blog engine for Dancer2. 

=head1 DESCRIPTION

This Dancer2 plugin provides a lightweight blogging engine. Instead of relying
on a database, it utilizes flat files, primarily markdown and YAML, to store and
manage content. Through this plugin, Dancer2 applications can seamlessly
integrate a blog without the overhead of database management.

=head1 SYNOPSIS

First, you need to scaffold Liteblog's assets in your Dancer2 application directory:

   $ liteblog-scaffold . 

Then, in your Dancer2 PSGI startup script:

   # in your app.psgi 
   use Dancer2;
   use Dancer2::Plugin::LiteBlog;
   liteblog_init();

=head1 VERSION

Version 0.04

=cut

our $VERSION = '0.04';

use strict;
use warnings;
use File::Spec;
use Carp 'croak';

use Dancer2::Plugin;

=head1 METHODS

=head2 BUILD 

At build time, sets up essential configurations for the plugin and initializes
the default routes.

Template::Toolkit is forced (all scaffolded views are designed to be TT views). 
Similarly, views tags are forced to TT's defaults: '[%' and '%]'.

A C<before_template> hook is registered to populate tokens such as settings read 
from the liteblog config (C<liteblog> entry in Dancer2's config) or the widgets 
elements (see L<Dancer2::Plugin::LiteBlog::Widget>).

A default C<GET /> route is defined and handles the landing page of the liteblog 
site.

=cut

sub BUILD {
    my $plugin = shift;

    $plugin->dsl->info("LiteBlog Init: forcing template_toolkit with '[%', '%]'");
    $plugin->app->config->{template} = 'template_toolkit';
    $plugin->app->config->{engines}->{template}->{template_toolkit} = {
        start_tag => '[%',
        end_tag   => '%]',
    };

    # Prepare default template tokens with appropriate resources.
    $plugin->app->add_hook( Dancer2::Core::Hook->new(
        name => 'before_template',
        code => sub {
            my $tokens = shift;
            my $liteblog = $plugin->dsl->config->{'liteblog'};
            
            _init_default($liteblog);

            # Each app setting is fowarded to the tokens
            $plugin->dsl->info("LiteBlog Init: 'liteblog' loaded in the template tokens.");
            foreach my $k (keys %$liteblog) {
                $plugin->dsl->info("setting token '$k'");
                
                _init_favicon_token($tokens, $k, $liteblog) and next;
                _init_footer_token($tokens, $k, $liteblog) and next;

                $tokens->{$k} = $liteblog->{$k};
            }

            # Populate the loaded widgets in the tokens 
            my $widgets = _load_widgets($plugin, $liteblog);
            $tokens->{widgets} = $widgets;
            $tokens->{no_widgets} = scalar(@$widgets) == 0;

            # set a default title, if unset
            $tokens->{title} = $liteblog->{'title'} || "A Great Liteblog Site" 
                if !defined $tokens->{title};

            # Set the navigation elements for the nav bar
            my $navigation = $liteblog->{navigation};
            $tokens->{navigation} = $navigation if defined $navigation;
            return $tokens;
        }
    ));

    $plugin->dsl->info("LiteBlog Init: registering route GET /");
    $plugin->app->add_route(
        method => 'get',
        regexp => '/',
        code   => sub {
            $plugin->dsl->info("in the index route");
            return $plugin->dsl->template(
                'liteblog/index', {}, { layout => 'liteblog' }
            );
        });
}

sub _init_default {
    my ($liteblog) = @_;
    $liteblog->{footer} //= $liteblog->{title};
}

sub _init_favicon_token {
    my ($tokens, $k, $liteblog) = @_;

    if ($k eq 'favicon') {
        my $favicon = $liteblog->{$k};
        my $mime;
        if ($favicon =~ /\.ico$/) {
            $mime = 'image/x-icon'; 
        }
        elsif ($favicon =~ /\.png$/) {
            $mime = 'image/png'; 
        }
        elsif ($favicon =~/\.jpe?g$/) {
            $mime = 'image/jpeg'; 
        }
        else {
            return 0;
        }
        $tokens->{favicon}   = $favicon;
        $tokens->{mime_icon} = $mime;
        return 1;
    }
    return 0;
}

sub _init_footer_token {
    my ($tokens, $k, $liteblog) = @_;

    if ($k eq 'footer') {
        $tokens->{footer}   = $liteblog->{$k};
        $tokens->{footer}  .= ' &middot; Built with <a href="https://metacpan.org/pod/Dancer2::Plugin::LiteBlog">Liteblog</a>' 
            unless $liteblog->{no_liteblog_footer};
        return 1;
    }
    return 0;
}


=head2 liteblog_init

A Liteblog app must call this keyword right after having C<use>'ed Dancer2::Plugin::Liteblog.
This allows to declare widget-specific routes (defined in the Widget's classes) once the 
config is fully read by Dancer2 (which is not the case at BUILD time).

=cut

sub liteblog_init {
    my ($plugin) = @_;
    $plugin->dsl->info("Liteblog init");

    my $liteblog = $plugin->dsl->config->{'liteblog'};
    my $widgets = _load_widgets($plugin, $liteblog);

    # implement the declared routes of all registered widgets 
    foreach my $widget (@{ $widgets }) {
        my $w = $widget->{instance};
        $plugin->dsl->info("Widget '".$widget->{name}."' registered");
        next if ! $w->has_routes;

        $plugin->dsl->info("Widget '".$widget->{name}."' has routes to declare");
        $w->declare_routes($plugin, $widget);
    }
}

=head2 render_client_error($message)

Immediatly exits from the current route handler and render a 404
page with Liteblog's default templates.

=cut

sub render_client_error {
    my ($plugin, $message) = @_;
    
    # log the error
    $plugin->dsl->error('['.ref($plugin).
        "] Client Error: $message");

    $plugin->dsl->status('not_found');
    $plugin->dsl->template('liteblog/single-page', 
        {
            page_title => "Page Not Found",
            content => $message
        },
        {layout => 'liteblog'});
}

plugin_keywords 'liteblog_init';


# Private subs 

# Loads all widgets and initializes them. Each widget is responsible for a
# specific function or display within the blog. They are associated to stylesheets
# in public/css/liteblog/widgets/$widget.css and views in
# views/liteblog/$widget.
sub _load_widgets {
    my ($plugin, $liteblog) = @_;

    # Load all widgets and initialize them 
    my @widgets;
    my $id = 1;
    foreach my $w (@{ $liteblog->{widgets} }) {
        my $elements = [];
        my $widget;
        
        my $class = 'Dancer2::Plugin::LiteBlog::'.ucfirst($w->{name});
        $plugin->dsl->info("Initializing widget: $class");

        my $module;
        eval {
            $module = File::Spec->catfile(split /::/, $class) . '.pm';
            require $module;
        };
        if ($@) {
            $plugin->dsl->error("Unable to import '$module': $@");
            next;
        }
        else {
            $plugin->dsl->info("Widget '$module' successfully imported");
        }

        eval { 
            $widget = $class->new( 
                    root   => $plugin->dsl->config->{'appdir'}, 
                    dancer => $plugin->dsl,
                    %{$w->{params}}
                );
        };
        
        if ($@) {
            $plugin->dsl->error("Unable to initialized widget '".
            $w->{name}."' : $@");
            next;
        }
        else {
            $plugin->dsl->info("Widget '$class' successfully initialized");
        }

        $elements = $widget->elements;

        if (scalar(@$elements)) {
            push @widgets, { 
                id => $id++,
                name => $w->{name}, 
                %{$w->{params}},
                view => $w->{name}.'.tt',
                instance => $widget,
                elements => $elements,
            };
        }
    }
    return \@widgets;
}


1; # End of Dancer2::Plugin::LiteBlog

=head1 AUTHOR

Alexis Sukrieh, C<< <sukria at gmail.com> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-dancer2-plugin-liteblog at
rt.cpan.org>, or through the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Dancer2-Plugin-LiteBlog>.
I will be notified, and then you'll automatically be notified of progress on your
bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Dancer2::Plugin::LiteBlog

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Dancer2-Plugin-LiteBlog>

=item * CPAN Ratings

L<https://cpanratings.perl.org/d/Dancer2-Plugin-LiteBlog>

=item * Search CPAN

L<https://metacpan.org/release/Dancer2-Plugin-LiteBlog>

=item * GitHub Official Repository

L<https://github.com/sukria/Dancer2-Plugin-LiteBlog>

=item * The Author's personal site, built with Liteblog

L<https://alexissukrieh.com>

=back

=head1 ACKNOWLEDGEMENTS

=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2023 by Alexis Sukrieh.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut
