#!/usr/bin/perl
#
# Copyright 2014-2016 - Giovanni Simoni
#
# This file is part of PFT.
#
# PFT is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# PFT is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with PFT.  If not, see <http://www.gnu.org/licenses/>.

=encoding utf8

=head1 NAME

pft entry - Edit an entry

=head1 SYNOPSYS

    pft entry -P [options] title of your page
    pft entry -B [options] title of your blog page
    pft entry -M [options] title of your month page
    pft entry -T [options] title of your tag page

=head1 DESCRIPTION

The C<pft edit> command allows to conveniently edit a text entry in a
I<PFT> site. It takes care of creating a skeleton for the new entries, to
position them correctly within your I<PFT> site, and to maintain their
position consistent with the type of content.

All entries are stored under the C<ROOT/content> directory, where C<ROOT>
is the base path of your I<PFT> site. Each entry has the same format:
a unicode text file composed by a YAML header concatenated with Markdown
text content (more details later in this document).

There can be different kind of entries:

=over

=item Regular Pages

Regular pages do not declare a date in their header. They get positioned
in C<ROOT/content/pages>. Their file name depends on their title.

Example:

    C<ROOT/content/pages/hello-world>

=item Blog pages

Blog pages declare a full date (year, month and day) in their header. They
get positioned in the C<ROOT/content/blog/YYYY-MM> directory (where
C<YYYY> and C<MM> correspond to the declared year and month respectively).
Their file name starts with a C<DD-> prefix, where C<DD> corresponds to
the declared day. The remaining part of the file name depends on their
title.

Example:

    C<ROOT/content/blog/2015-09/12-hello-world>

=item Month pages

Month pages are meant as C<entries summarizing a whole month>. They are
like blog pages, but the date defined in their headers lack of the day
part (it is replaced by C<*>). They are stored as
C<ROOT/content/blog/YYYY-MM.month> (where C<YYYY> and C<MM> represent the
declared year and month).

Example:

    C<ROOT/content/blog/2016-09.month>

=item Tag pages

All pages can optionally declare one or more tags. A tag can optionally be
associated to a tag page.

For example, if your site has have a number of entries about cuisine, some
of them might be tagged with the C<chicken> tag. You may want to have a
tag page entitled C<chicken>, where you give a description of what
I<chicken> is (just in case someone doesn't know).

Tag pages are stored in C<ROOT/content/tags>, and their file name depends
on the tag title.

Example:

    C<ROOT/content/tags/chicken>

=back

=head1 OPTIONS

=over

=item -P

Edit a page

A title is mandatory.

=item -B

Edit a blog page

If no title is specified, it defaults to C<today>.

=item -M

Edit a month page

The title is optional.

=item -T

Edit a tag page

A title is mandatory.

=item --year=<y> | -y <y>

Defines a year for the edited entry. Implies -B unless -M is set.

This flag cannot be used if -P or -T are specified. It gets honored only
when generating a header for the entry, that is if the file does not exist
yet.

=item --month=<m> | -m <m>

Defines a month for the edited entry. Implies -B unless -M is set.

This flag cannot be used if -P or -T are specified. It gets honored only
when generating a header for the entry, that is if the file does not exist
yet.

=item --day=<d> | -d <d>

Defines a day for the edited entry. Implies -B unless -M is set.

This flag cannot be used if -P or -T are specified. It gets honored only
when generating a header for the entry, that is if the file does not exist
yet.

=item --author=<name> | -a <name>

Defines the author for the entry.

This flag gets honored only when generating a header for the entry, that
is if the file does not exist yet.

=item --tag=<tag> | -t <tag>

This flag gets honored only when generating a header for the entry, that
is if the file does not exist yet.

=item --resume | -r

Resume editing the most recent blog entry. Equivalent to C<--back 0>,
implies C<-B>.

=item --back=<count> | -b <count>

Resume editing an old entry. The supplied parameter defines how many steps
we should go back in history (0 means the most recent entry, 1 means the
second to last).

=item --editor <command>

Specify an editor to use.

The editor can be specified by name (e.g. C<vim>) or as a shell command,
where C<%s> is replaced with the file name (e.g. C<vim [options] %s>).

This flag overrides the C<$EDITOR> environment variable and the
C<system.editor> setting in the main configuration file.

=item --help | -h

Shows this help.

=back

=head2 Editing

Content entries are in encoded text format.

The expected encoding for the file corresponds to the C<locale> encoding.
On most modern Unix systems this is I<UTF-8>, but you can use the
C<locale> command in order to figure it out.

Each file starts with a header in I<YAML> format. The header is followed
by a line with three dashes (C<--->) which marks the beginning of the
actual content. The content will be parsed as I<Markdown> when the site is
compiled.

The header of a content entry is created automatically by C<pft edit> when
the accessed entry does not exist. The file gets then opened with a text
editor. The C<$EDITOR> environment variable will be honored unless an
editor is defined in the C<pft.yaml> configuration file (see the manual of
C<pft init> for further information). You may also specify a different
editor using the C<--editor> command line option.

After the editor is closed a warning will be issued if the header is
invalid. If the file completely empty (as in I<zero bytes>) it will be
removed from the filesystem. If the header is valid but it is not
consistent with the position in the filesystem (e.g. the date was manually
changed) the file position is updated to be consistent.

=head2 The content header

Upon compilation the content text is parsed as Markdown and gets
translated into HTML (see the manual of C<pft make> for additional
details). In this phase some special symbols are recognized:  HTML tags
such as C<E<lt>aE<gt>> and C<E<lt>imgE<gt>> are analyzed in search of
links to internal resources, like pictures or other pages within your
I<PFT> site.

This section contains a reference manual for the recognized special links.
Since Markdown is a super-set of HTML, direct HTML can be supplied in the
text too, so both Markdown and HTML syntaxes will be mentioned in the
following description.

The recognized syntax for special links is:

    :kind:param/param/...

Follows a list of valid C<kind> keywords. The meaning of each C<param>
depends on the specified C<kind>. Separation is done with the C</> symbol.

=head3 Pictures with C<:pic:path/to/picture.jpg>

Picture reference accept special links in the form

    ![alttext](:pic:filename)
    <img alt="alttext" src=":pic:filename"/>

This form will be resolved to an HTML link towards a file named named
I<filename> under the C<I<ROOT>/content/pics> directory.  The name
provided is used directly for lookup, so it must be complete of file
extension, if any.  The C</> symbol will work as path separator regardless
of the operating system.

HTML Example:

    <!-- ROOT/content/pics/test.png -->
    <img src=":pic:test.png"/>

Markdown Example:

    <!-- ROOT/content/pics/cars/golf.png -->
    ![](:pic:cars/golf.png)

=head3 URLs:

Regular URLs in C<E<lt>aE<gt>> tags accept the following special
prefixes:

=over

=item :page:I<pagename>

Resolve the link as the page having identified by C<pagename>.

=item :blog:date/I<yy>/I<mm>/I<dd>/I<slug>

Resolve the link as the blog entry published at date I<yy/mm/dd> and
having the slug I<slug>.

The I<slug> parameter is optional if only one entry was published in the
given date. The keyword C<date> can be shortened as C<d>.

=item :blog:back/I<N>

Only valid within a blog entry. The generated link refers to I<N + 1> blog
entries before the current one. The I<N> parameter is optional, and
defaults to zero (i.e. previous entry).

Examples:

    <a href=":blog:back/0">     (previous entry)

    <a href=":blog:back">       (equivalently, previous entry)

    <a href=":blog:back/1">     (before previous, two entries ago)

    <a href=":blog:back/5">     (six entries ago)

=item :web:I<service>/I<param>/I<param>/...

Generate an URL which points to a web site or service (e.g. search
engines, or specialized website) and passes data on the query. A number of
valid values are supported for the  I<service> argument. The list of
I<param> arguments depend on the specific service.

Supported services:

=over

=item Duck Duck Go

C<:web:ddg/I<bang>/I<param>/I<param>/...>

Search query on the I<Duck Duck Go> search engine. The first parameter is
used for Duck Duck Go's Bang syntax, and can be empty in order not to
use any Bang.

Example: search C<linux howto> on Duck Duck Go:

    :web:ddg//linux/howto

Example: search C<linux howto> with the C<!yt> bang (redirects search
on I<YouTube>):

    :web:ddg/yt/linux/howto

Example: search C<linux howto> with the C<!so> bang (redirects search
on I<StackOverflow>):

    :web:ddg/so/linux/howto

=item Manpages

C<:web:man/I<name>>

C<:web:man/I<name>/I<section>>

Point to an online Unix manual page. Manual section can be optionally
supplied.

Examples:

    :web:man/bash

    :web:man/signal/7

=back

=back

=head2 Examples

=head3 Blog about today:

    pft edit -B

=head3 Remove the page C<garbage> 

    pft edit --editor ':> %s' -P garbage

=cut

use strict;
use warnings;

use App::PFT;

use PFT::Tree;
use PFT::Date;

use Pod::Usage;

use Getopt::Long qw/GetOptionsFromArray/;
Getopt::Long::Configure qw/bundling/;

my %opts;
my %datespec;

GetOptions(
    'year|y=i'      => sub { $datespec{y}  = $_[1] },
    'month|m=s'     => sub { $datespec{m} = $_[1] },
    'day|d=i'       => sub { $datespec{d}  = $_[1] },
    'B!'            => \$opts{B},
    'M!'            => \$opts{M},
    'T!'            => \$opts{T},
    'P!'            => \$opts{P},
    'author|a=s'    => sub { $opts{author} = $_[1] },
    'tag|t=s@'      => sub { push @{$opts{tags}}, $_[1] },
    'resume|r!'     => sub { $opts{back} = 0 },
    'back=i'        => sub { $opts{back} = int($_[1]) },
    'editor=s'      => sub { $opts{editor} = $_[1] },
    'help|h!'       => sub {
        pod2usage
            -exitval => 1,
            -verbose => 2,
            -input => App::PFT::help_of 'edit',
    },
) or exit 1;

$opts{B} = 1 if !$opts{M} &&
    grep defined, $opts{back}, map $datespec{$_}, qw(y m d);

do {
    my @sel = grep $opts{$_}, qw(B M T P);
    if (@sel != 1) {
        local $, = ' -';
        say STDERR 'Select exactly one mode: -B -M -T -P. Currently:', @sel;
        exit 2
    }
};

my $tree = eval{ PFT::Tree->new } || do {
    say STDERR $@ =~ s/ at.*$//rs;
    exit 3
};

my $conf = eval{ $tree->conf } || do {
    say STDERR 'Configuration error: ', $@ =~ s/ at.*$//rs;
    exit 4
};

my $editor = $opts{editor} || $conf->{system}{editor} || $ENV{EDITOR} || do {
    say STDERR "Cannot edit: no editor selected";
    say STDERR "Try setting env EDITOR or to define it in configuration file";
    say STDERR "(system -> editor)";
    exit 5
};

my $entry = eval {;
    if (defined $opts{back}) {
        my $entry = $tree->content->blog_back($opts{back});
        defined $entry and $entry or
            die 'Cannot go back by ', $opts{back}, ': ', $@ =~ s/ at .*$//rs;
    } elsif ($opts{M}) {
        $tree->content->new_entry(PFT::Header->new(
            author => $conf->{site}{author},
            date => PFT::Date->from_spec(%datespec)->derive(d => undef),
        ))
    } elsif ($opts{B}) {
        $tree->content->new_entry(PFT::Header->new(
            title => join(' ', @ARGV) || 'Today',
            author => $conf->{site}{author},
            tags => $opts{tags} || [],
            date => eval{ PFT::Date->from_spec(%datespec) } || do {
                say STDERR 'Invalid date: ', $@ =~ s/ at.*$//rs;
            },
        ))
    } elsif ($opts{T}) {
        die 'Mandatory title' unless @ARGV;
        $tree->content->new_tag(PFT::Header->new(
            title => join(' ', @ARGV),
            author => $conf->{site}{author},
        ))
    } elsif ($opts{P}) {
        die 'Mandatory title' unless @ARGV;
        $tree->content->new_entry(PFT::Header->new(
            title => join(' ', @ARGV),
            author => $conf->{site}{author},
            tags => $opts{tags} || [],
        ))
    } else { die "Unhandled?" }
}
or do {
    say STDERR 'Cannot edit entry: ', $@ =~ s/ at.*$//rs;
    exit 6
};

eval {
    my $path = $entry->path;
    if ($editor =~ s/(?<!%)%s/$path/g) {
        system($editor)
    } else {
        system($editor, $entry->path)
    }
    if ($entry->exists) {
        if ($entry->empty) {
            $entry->unlink;
        } else {
            $entry->make_consistent;
        }
    }
} or $@ && do {
    say STDERR "After editing: ", $@ =~ s/at .*$//sr;
    exit 7
}
