#!/usr/bin/perl
use warnings;
use strict;

=head1 NAME

daizu - command line interface to Daizu CMS

=head1 SYNOPSIS

    export DAIZU_CONFIG=/etc/my-daizu-config.xml

    # Get a working copy
    daizu checkout

    # Bring it up to date with new revisions
    daizu update

    # Publish any new stuff
    daizu publish

    # Load new revisions (done automatically when updating
    # a working copy, so not normally needed)
    daizu load-revisions

    # Manually reload articles (not normally necessary)
    daizu update-article example.com/blog/article.html
    daizu update-all-articles

    # Manual URL updates (not normally necessary)
    daizu update-urls example.com/blog
    daizu update-all-urls

    # Manual publishing (not normally necessary)
    daizu publish-url http://example.com/page.html
    daizu publish-site http://example.com/

    # Publish to stdout (for testing)
    daizu url-content http://example.com/ >homepage.html

=head1 DESCRIPTION

This program allows you to operate Daizu, getting it to load content
from the Subversion repository (by checking out or updating working
copies), and publish the URLs generated by that content.

A Daizu configuration file is required.  You can specify
where yours is by setting the C<DAIZU_CONFIG> environment variable
to its path.  You can also provide the value in the C<-c> option when
you run C<daizu>.

The following subcommands are available:

=over

=cut

use Getopt::Std qw( getopts );
use DateTime;
use Carp::Assert qw( assert DEBUG );
use Daizu;
use Daizu::Wc;
use Daizu::File;
use Daizu::Publish qw(
    update_live_sites
    publish_urls
    publish_redirect_map publish_gone_map
);
use Daizu::Util qw(
    like_escape
    db_row_exists db_row_id db_select db_insert transactionally
    instantiate_generator
    update_all_file_urls
);

my %opt;
getopts('c:r:', \%opt) or usage();
usage() unless @ARGV;
my $command = shift @ARGV;

my %COMMANDS = (
    #add => \&cmd_add,
    #mkdir => \&cmd_mkdir,
    #replace => \&cmd_replace,
    'checkout' => \&cmd_checkout,
    'load-revisions' => \&cmd_load_revisions,
    'publish' => \&cmd_publish,
    'publish-site' => \&cmd_publish_site,
    'publish-url' => \&cmd_publish_url,
    'update' => \&cmd_update,
    'update-all-articles' => \&cmd_update_all_articles,
    'update-all-urls' => \&cmd_update_all_urls,
    'update-article' => \&cmd_update_article,
    'update-urls' => \&cmd_update_urls,
    'url-content' => \&cmd_url_content,
);

usage() unless exists $COMMANDS{$command};

my $cms = Daizu->new($opt{c});
$COMMANDS{$command}->($cms, \%opt, @ARGV);


=item load-revisions

Load new revisions from the content repository, up to the latest revision.
If the C<-r> option is given then it specifies a revision number to
load up to instead.

Revisions are automatically loaded when working copies are checked out and
updated, so you won't normally need to do this.

=cut

sub cmd_load_revisions
{
    my ($cms, $opt) = @_;
    my $revnum = $cms->load_revision($opt{r});
    print "Loaded revisions up to r$revnum\n";
}

=item checkout [branch]

Create a new working copy in the database and bring it up to the
latest revision of the content repository, or to the revision specified
by the C<-r> option.

An additional argument can be specified, which should identify a branch
to check out from.  It can be either the path of the branch in the
repository (something like I<branches/redesign>) or the ID number of a
branch in the database.  The default is I<trunk>.

=cut

sub cmd_checkout
{
    my ($cms, $opt, $branch) = @_;
    $branch = 'trunk' unless defined $branch;
    my $wc = Daizu::Wc->checkout($cms, $branch, $opt{r});
    print "Checked out working copy ", $wc->id, "\n";
}

=item update [wc-id]

Bring a working copy up to date with the latest revision, or the revision
specified by the C<-r> option.  If the extra argument is given then
the working copy specified by that ID number is updated (it should be the
C<id> column of the C<working_copy> table).  By default the live working
copy is updated.

=cut

sub cmd_update
{
    my ($cms, $opt, $wc_id) = @_;
    my $wc = Daizu::Wc->new($cms, $wc_id);
    my $new_revnum = $wc->update($opt{r});
    print "Updated working copy ", $wc->id, " to revision $new_revnum\n";
}

=item publish

Bring the live website (or sites) up to date with the latest changes
in the live working copy in the database.  After committing a new
revision and using the C<update> command to update the live WC, running
this should do everything necessary to update the sites (except in
exceptional circumstances like when you've changed custom templates).

If you provide a C<-r> option with a revision number, then that revision
will be used as the starting point for figuring out what changes need
to be published.  The default is to start from the last revision which
was published in this way.  There is no way to change the revision which
the output is updated I<to>, it's always whatever is in the live working
copy.

=cut

sub cmd_publish
{
    my ($cms, $opt) = @_;
    update_live_sites($cms, $opt{r});
}

=item publish-url url [wc-id]

Publish the given URL, writing its output however is specified by the
configuration file.  This will fail if there isn't a suitable C<output>
element in the configuration file to specify the document root.

The extra argument gives the ID number of the working copy to get the
content from, and defaults to the live working copy.

=cut

sub cmd_publish_url
{
    my ($cms, $opt, $url, $wc_id) = @_;
    my $db = $cms->db;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    $wc_id = $wc->id;

    my $url_info = $db->selectrow_hashref(q{
        select *
        from url
        where wc_id = ?
          and url = ?
          and status = 'A'
    }, undef, $wc_id, $url);
    die "no url '$url' found\n" unless defined $url_info;
    $url_info = { %$url_info };

    publish_guid_urls($cms, $wc_id, $url_info->{guid_id},
                      $url_info->{generator}, $url_info->{method},
                      [ $url_info ]);
}

# This has never worked. Working copies can't be edited without corrupting them.
sub cmd_add
{
    my ($cms, $opt, $wc_id, $path, $filename) = @_;
    my $data = load_file($filename);

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my $file_id = $wc->add_file($path, \$data);

    print "Saved new file with ID $file_id\n";
}

# This has never worked. Working copies can't be edited without corrupting them.
sub cmd_mkdir
{
    my ($cms, $opt, $wc_id, $path) = @_;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my $file_id = $wc->add_directory($path);

    print "Created new directory with ID $file_id\n";
}

# This has never worked. Working copies can't be edited without corrupting them.
sub cmd_replace
{
    my ($cms, $opt, $wc_id, $path, $filename) = @_;
    my $data = load_file($filename);

    my $wc = Daizu::Wc->new($cms, $wc_id);

    my $file_id = db_row_id($cms->{db}, 'wc_file', path => $path, is_dir => 0);
    die "$0: file '$path' doesn't exist, or is a directory\n"
        unless defined $file_id;
    $wc->change_file_content($file_id, \$data);

    print "Replaced content for file with ID $file_id\n";
}

=item update-urls path [wc-id]

Generate the URLs for file at the given path.
The resulting URLs are stored
in the database and assumed to have been published, so you'd better actually
publish any new ones straight after doing this.

The extra argument specifies the ID number of the working copy to generate
URLs for, and defaults to the live working copy.

=cut

sub cmd_update_urls
{
    my ($cms, $opt, $path, $wc_id) = @_;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my $file = $wc->file_at_path($path);
    $file->update_urls_in_db;
}

=item update-all-urls [wc-id]

Same as C<update-urls> above, but generates URLs for all files which
currently exist in the working copy.

Note that currently URLs which are no longer attached to an extant file
will not be marked 'gone' as they should be.  Also note that if this
fails half-way through it may leave the database partially updated.

=cut

sub cmd_update_all_urls
{
    my ($cms, $opt, $wc_id) = @_;
    my $db = $cms->db;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my ($new_redirects, $new_gone) = update_all_file_urls($cms, $wc->{id});
    # TODO - new return vals for update_all_file_urls
    _update_redirect_maps($cms, $wc->id) if $new_redirects;
    _update_gone_maps($cms, $wc->id) if $new_gone;
}

sub _update_redirect_maps
{
    my ($cms, $wc_id) = @_;

    while (my ($url, $config) = each %{$cms->{output}}) {
        next unless defined $config->{redirect_map};
        publish_redirect_map($cms, $wc_id, $config);
    }
}

sub _update_gone_maps
{
    my ($cms, $wc_id) = @_;

    while (my ($url, $config) = each %{$cms->{output}}) {
        next unless defined $config->{gone_map};
        publish_gone_map($cms, $wc_id, $config);
    }
}

=item update-article path [wc-id]

Reload the article at the specified path, using the appropriate article
loader plugin, and cache the resulting content and metadata.  This should
normally be done automatically when a working copy is updated.

The extra argument specifies the ID number of the working copy in which
to look for the path, and defaults to the live working copy.

=cut

sub cmd_update_article
{
    my ($cms, $opt, $path, $wc_id) = @_;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my $file = $wc->file_at_path($path);
    die "Can't update '$path', not an article\n" unless $file->{article};

    $file->update_loaded_article_in_db;
}

=item update-all-articles [wc-id]

Same as C<update-article> above, but reloads all article files which
currently exist in the working copy.

The whole thing is done in a single database transaction.

=cut

sub cmd_update_all_articles
{
    my ($cms, $opt, $wc_id) = @_;

    transactionally($cms->db, sub {
        my $wc = Daizu::Wc->new($cms, $wc_id);

        my $sth = $cms->db->prepare(q{
            select id
            from wc_file
            where wc_id = ?
              and article
            order by path
        });
        $sth->execute($wc->id);

        while (my ($file_id) = $sth->fetchrow_array) {
            Daizu::File->new($cms, $file_id)->update_loaded_article_in_db;
        }
    });
}

=item url-content url [wc-id]

Generate the content for the specified URL, and print it to the standard
output.  Doesn't update the database or publish the content anywhere.

Takes the content from the specified working copy, or the live
working copy by default.

=cut

sub cmd_url_content
{
    my ($cms, $opt, $url, $wc_id) = @_;
    my $db = $cms->db;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    my ($guid_id, $method, $argument, $type, $status, $redir_id) =
        db_select($db,
            url => { wc_id => $wc->id, url => $url },
            qw( guid_id method argument content_type status redirect_to_id ),
        );
    die "$0: URL '$url' does not exist in working copy " . $wc->id . "\n"
        unless defined $guid_id;
    die "$0: URL '$url' previously existed but no longer has content\n"
        if $status eq 'G';
    if ($status eq 'R') {
        my ($redir_url) = db_select($db, url => $redir_id, 'url');
        die "$0: URL '$url' redirects to '$redir_url'\n";
    }

    my ($file_id) = db_row_id($db, 'wc_file',
        wc_id => $wc->id, guid_id => $guid_id,
    );
    die "$0: URL '$url' marked active, but it's content no longer exists\n"
        unless defined $file_id;

    my $file = Daizu::File->new($cms, $file_id);

    my $generator = $file->generator;
    die "$0: URL '$url' has generator 'none', so it shouldn't exist\n"
        unless defined $generator;

    die "$0: generator for '$url' is missing method '$method'\n"
        unless $generator->can($method);
    binmode STDOUT
        or die "error setting binmode on STDOUT: $!";
    $generator->$method($file, [ {
        url => URI->new($url),
        generator => $file->{generator},
        method => $method,
        argument => $argument,
        type => $type,
        fh => \*STDOUT,
    } ]);
}

sub publish_guid_urls
{
    my ($cms, $wc_id, $guid_id, $gen_class, $method, $urls) = @_;
    my $db = $cms->db;

    my ($file_id, $root_file_id) = db_select($db, 'wc_file',
        { wc_id => $wc_id, guid_id => $guid_id },
        qw( id root_file_id ),
    );
    die "$0: URLs of GUID $guid_id marked active, but file no longer exists\n"
        unless defined $file_id;
    my $file = Daizu::File->new($cms, $file_id);

    my $root_file = $file;
    $root_file = Daizu::File->new($cms, $root_file_id)
        if defined $root_file_id;

    my $generator = instantiate_generator($cms, $gen_class, $root_file);
    die "$0: generator '$gen_class' is missing method '$method'\n"
        unless $generator->can($method);

    publish_urls($cms, $file, $generator, $method, $urls);
}

=item publish-site base-url [wc-id]

Generates content for all URLs which start with the base URL given.
For example, if base-url is L<http://www.daizucms.org/> then all of
the URLs on that domain will be generated.  The content is written
to the proper output location as for the C<publish> command.

=cut

sub cmd_publish_site
{
    my ($cms, $opt, $base_url, $wc_id) = @_;
    my $db = $cms->db;

    my $wc = Daizu::Wc->new($cms, $wc_id);
    $wc_id = $wc->id;

    my $sth = $db->prepare(q{
        select *
        from url
        where wc_id = ?
          and url like ?
          and status = 'A'
        order by guid_id, generator, method
    });
    $sth->execute($wc_id, like_escape($base_url) . '%');

    my $cur_guid_id;
    my $cur_generator;
    my $cur_method;
    my @urls;
    while (my $r = $sth->fetchrow_hashref) {
        if (@urls && ($r->{guid_id} != $cur_guid_id ||
                      $r->{generator} ne $cur_generator ||
                      $r->{method} ne $cur_method))
        {
            publish_guid_urls($cms, $wc_id, $cur_guid_id,
                              $cur_generator, $cur_method, \@urls);
            @urls = ();
        }

        $cur_guid_id = $r->{guid_id};
        $cur_generator = $r->{generator};
        $cur_method = $r->{method};
        push @urls, { %$r };
    }

    publish_guid_urls($cms, $wc_id, $cur_guid_id, $cur_generator,
                      $cur_method, \@urls)
        if @urls;

    _update_redirect_maps($cms, $wc->id);
    _update_gone_maps($cms, $wc->id);
}

sub load_file
{
    my ($filename) = @_;
    return do {
        open my $fh, '<', $filename
            or die "$0: error opening input file '$filename': $!\n";
        binmode $fh;
        local $/;
        <$fh>;
    };
}

sub usage
{
    print STDERR "Usage: $0 [OPTIONS] COMMAND [ARGS...]\n";
    exit 1;
}

=back

=head1 COPYRIGHT

This software is copyright 2006 Geoff Richards E<lt>geoff@laxan.comE<gt>.
For licensing information see this page:

L<http://www.daizucms.org/license/>

=cut

# vi:ts=4 sw=4 expandtab