#!/usr/bin/env perl

#use utf8;
#use encoding 'utf8';
use warnings;
use strict;
use Carp;
use version;
use Getopt::Long;
use Pod::Usage;
use Publican;
use Publican::CreateBook;
use Publican::CreateBrand;
use Publican::Builder;
use Publican::XmlClean;
use Publican::TreeView;
use File::Path;
use File::Copy::Recursive qw(dircopy rcopy);
use File::Find::Rule;
use File::pushd;
use Archive::Tar;
use Term::ANSIColor qw(:constants);
use Cwd qw(abs_path);

#binmode STDOUT, ":utf8";

my $VERSION      = version->new($Publican::VERSION);
my $LANG_PATTERN = q|__LANG__|;
## TODO Consider making this a parameter
my $IMPORT_SCRIPT = '../common/cvs-import.sh';

=head1 NAME

publican - a DocBook XML publishing tool.

=head1 VERSION

This document describes publican version 1.0

=head1 SYNOPSIS

publican <global options>

publican <action> <options>

Global Options

    --help 		Display help message
    --man		Display the man page
    --help_actions	Display a list of valid actions
    -v			Display the version of Publican
    --config <file>	Use a nonstandard config file
    --nocolours Disable ANSI colourisation of logging
    --quiet     Disable all logging

Run: 'publican <action> --help' for details on action usage

Valid actions are:

    build         Transform XML to other formats (pdf, html, html-single, etc)
    clean         Remove all temporary files and directories
    clean_ids     Run clean ids for source XML
    cleanset      Remove local copies of remote set books
    create        Create a new book, set, or article
    create_brand  Create a new brand
    help_config   Display help text for the configuration file
    installbrand  Install a brand to the supplied location
    lang_stats    report PO statistics
    old2new       Create a publican.cfg file from the Makefile of an old book, set, or article
    package       Package a language for shipping
    printtree     Print a tree of the xi:includes
    update_po     Update the PO files
    update_pot    Update the POT files

=head1 DESCRIPTION

Publican is a DocBook publication system, not just a DocBook processing tool. As well as ensuring your DocBook XML is valid, publican works to ensure your XML is up to publishable standard.

=cut

### Code goes here
my $man          = 0;
my $help         = 0;
my $action       = "";
my $help_actions = 0;
my $show_version = 0;

#Getopt::Long::Configure("debug");

# Global options
my %opts = (
    'help'         => \$help,
    'man'          => \$man,
    'v'            => \$show_version,
    'help_actions' => \$help_actions,
);

#Action options
my %options = (
    'help'            => maketext('Display help message'),
    'man'             => maketext('Display full man page'),
    'v'               => maketext('Display the version of Publican'),
    'config=s'        => maketext('Use a nonstandard config file'),
    'binary'          => maketext('Build binary rpm when running package'),
    'brew'            => maketext('Push SRPM to brew'),
    'scratch'         => maketext('Use scratch instead of tag build'),
    'wait'            => maketext('Wait for brew to finish building'),
    'common_config=s' => maketext('Override path to Common_Config directory'),
    'common_content=s' =>
        maketext('Override path to Common_Content directory'),
    'formats=s' => maketext(
        'Comma-separated list of formats, for example: html,pdf,html-single,html-desktop,txt,epub'
    ),
    'langs=s' => maketext(
        'Comma-separated list of languages, for example: en-US,de-DE,all'),
    'embedtoc' =>
        maketext('Embed the web site TOC object in the generated HTML'),
    'name=s'    => maketext('The name of the book, article, set, or brand'),
    'version=s' => maketext('The version of the product'),
    'edition=s' => maketext('The edition of the book, article, or set'),
    'product=s' => maketext('The name of the product'),
    'brand=s'   => maketext('The brand to use'),
    'lang=s'    => maketext('The language the XML will be written in'),
    'type=s'    => maketext('The type (book, article, or set)'),
    'publish'   => maketext('Set up built content for publishing'),
    'desktop'   => maketext('Create desktop instead of web package'),
    'short_sighted' =>
        maketext('Create package without using version in package name'),
    'path=s'          => maketext('/path/to/install/to'),
    'distributed_set' => maketext(
        'This flag tells publican the data being processed is a distributed set. Note: do not use on the command line. Publican uses this flag when calling itself to process distributed sets. This is the only safe way this flag can be used.'
    ),
    'cvs' => maketext(
        'Import the SRPM in to CVS, the run make tag and make build.'),
    'nocolours' => maketext('Disable ANSI colourisation of logging.'),
    'quiet'     => maketext('Disable all logging.'),
);

# Options all actions use
my @utility_opts = (
    'help',            'help_actions',
    'man',             'config=s',
    'common_config=s', 'common_content=s',
    'v',               'nocolours',
    'quiet',
);

# Actions
my %actions = (
    'build' => {
        'brief' => maketext(
            'Transform XML to other formats (pdf, html, html-single, etc)'),
        'options' =>
            [ 'formats', 'langs', 'publish', 'embedtoc', 'distributed_set' ],
    },
    'clean' => {
        'brief'   => maketext('Remove all temporary files and directories'),
        'options' => [],
    },

    'cleanset' => {
        'brief'   => maketext('Remove local copies of remote set books'),
        'options' => [],
    },

    'clean_ids' => {
        'brief'   => maketext('Run clean ids for source XML'),
        'options' => [],
    },
    'old2new' => {
        'brief' => maketext(
            'Create a publican.cfg file from the Makefile of an old book, set, or article'
        ),
        'options' => [],
    },
    'printtree' => {
        'brief'   => maketext('Print a tree of the xi:includes'),
        'options' => [],
    },
    'create' => {
        'brief'   => maketext('Create a new book, set, or article'),
        'options' => [
            'name',  'version', 'edition', 'product',
            'brand', 'lang',    'type'
        ],
    },
    'create_brand' => {
        'brief'   => maketext('Create a new brand'),
        'options' => [ 'name', 'lang' ],
    },
    'package' => {
        'brief'   => maketext('Package a language for shipping'),
        'options' => [
            'lang',          'desktop', 'brew', 'scratch',
            'short_sighted', 'binary',  'wait', 'cvs'
        ],
    },
    'update_pot' => {
        'brief'   => maketext('Update the POT files'),
        'options' => [],
    },
    'update_po' => {
        'brief'   => maketext('Update the PO files'),
        'options' => ['langs'],
    },
    'installbrand' => {
        'brief'   => maketext('Install a brand to the supplied location'),
        'options' => ['path'],
    },
    'help_config' => {
        'brief'   => maketext('Display help text for the configuration file'),
        'options' => [],
    },
    'lang_stats' => {
        'brief'   => maketext('report PO statistics'),
        'options' => ['lang'],
    },
);

# Grab to limit options to the valid ones for this action
# TODO should we croak on more than one action? Handle more than one?
$action = ( $ARGV[0] || "" );

# Getopt: standard + the options for the supplied action
my @optns = @utility_opts;

if ( defined $actions{$action} ) {
    foreach my $opt ( @{ $actions{$action}->{options} } ) {
        if ( $options{$opt} ) {
            push( @optns, $opt );
        }

        # match options that take a String
        # this will need to be exapnded if other types are used
        elsif ( $options{"$opt=s"} ) {
            push( @optns, "$opt=s" );
        }
        else {

            # This should never happen
            croak(
                maketext(
                    "Invalid option '[_1]' in \$actions{\$action}->{options}",
                    $opt
                )
            );
        }
    }
}

GetOptions( \%opts, @optns, )
    or pod2usage( -msg => "\n", -verbose => 1, -exit => 1 );

# Getopt will remove the options leaving only the action
$action = ( $ARGV[0] || "" );

if ($show_version) {
    print("version=$VERSION\n");
    exit(0) if ( $action eq "" );
}

# Undocumented way of getting help for all actions
if ( $help and ( $action eq "all" ) ) {
    foreach my $cmd ( sort( keys(%actions) ) ) {
        _help_action($cmd);
        print("\n");
    }
    exit(0);
}

sub _help_actions {
    print( "\n", maketext("Valid actions are:"), "\n\n" );
    foreach my $cmd ( sort( keys(%actions) ) ) {
        printf( "    %-12s  %s\n", $cmd, $actions{$cmd}->{brief} );
    }
    print("\n\n");
    print(
        maketext(
            "Run: '[_1] <action> --help' for details on action usage", $0
        ),
        "\n\n"
    );

    return;
}

# catch no action set
if ( $action eq "" ) {
    pod2usage( -msg => "\n", -verbose => 1, -exit => 0 ) if $help;
    pod2usage( -verbose => 2, -exit => 0 ) if $man;

    if ($help_actions) {
        _help_actions();
        exit(0);
    }

    pod2usage(
        -msg     => "\n" . maketext("Action required!") . "\n",
        -verbose => 1,
        -exit    => 1
    );

    exit(1);
}

# Catch bogus action
if ( not defined( $actions{$action} ) ) {
    print( maketext( "'[_1]' is an unknown action!", $action ), "\n\n" );
    _help_actions();

    exit(1);

}

sub _help_action {
    my $cmd = shift || croak maketext( "[_1] is a required argument", 'cmd' );

    print( "$cmd\n    " . $actions{$cmd}->{brief} );
    print( "\n\n\t", maketext("Options:"),   "\n" );
    print( "\t\t",   maketext("No options"), "\n" )
        unless ( @{ $actions{$cmd}->{options} } );
    foreach my $option ( @{ $actions{$cmd}->{options} } ) {
        debug_msg("TODO: does printf work for right to left languages?\n");
        if ( $options{$option} ) {
            printf( "        --%-20s    %s\n", $option, $options{$option} );
        }
        else {
            printf(
                "        --%-20s    %s\n",
                "$option=<" . uc($option) . ">",
                $options{"$option=s"}
            );
        }
    }
    print("\n");

    return;
}

# $action must  be set to get here
if ($help) {
    _help_action($action);
    exit(0);
}

#######################################################################
#
# Start processing actions
#
#######################################################################

#pod2usage(1) if ( !$name || $type !~ /[Book|Set|Article]/);
if ( $action eq 'create' ) {

    my $docname = $opts{name}
        || croak( maketext("name is a required parameter") );
    $docname =~ s/\s/_/g;

    my $creator = Publican::CreateBook->new(
        {   name    => $docname,
            version => $opts{version},
            edition => $opts{edition},
            product => $opts{product},
            brand   => $opts{brand},
            lang    => $opts{lang},
            type    => $opts{type}
        }
    );
    $creator->create();

    my $dir = pushd($docname);

    my $publican = Publican->new(
        {   'configfile'   => $opts{config},
            common_config  => $opts{common_config},
            common_content => $opts{common_content},
            QUIET          => $opts{quiet},
            NOCOLOURS      => $opts{nocolours},
        }
    );

    my $builder = Publican::Builder->new();
    $builder->clean_ids();

    $dir = undef;

    exit(0);
}

if ( $action eq 'create_brand' ) {
    my $creator = Publican::CreateBrand->new(
        {   name => $opts{name},
            lang => $opts{lang},
        }
    );
    $creator->create();

    exit(0);
}

if ( $action eq 'old2new' ) {
    old2new();
    exit(0);
}

my %args = ( 'configfile' => $opts{config} );

my $publican = Publican->new(
    {   'configfile'   => $opts{config},
        common_config  => $opts{common_config},
        common_content => $opts{common_content},
        QUIET          => $opts{quiet},
        NOCOLOURS      => $opts{nocolours},
    }
);

if ( $action eq 'installbrand' ) {
    my $brand = $publican->param('brand') || croak("Can't find brand name");

    croak( maketext( "[_1] is a required argument", 'path' ) )
        unless ( $opts{path} );
    croak( maketext("you need to publish the brand first") )
        unless ( -d "publish" );
    croak( maketext("destination must exist") ) unless ( -d $opts{path} );

    rcopy( "publish/*",    "$opts{path}/." );
    rcopy( "publican.cfg", "$opts{path}/$brand/." );
    rcopy( "defaults.cfg", "$opts{path}/$brand/." )
        if ( -f "defaults.cfg" );
    rcopy( "overrides.cfg", "$opts{path}/$brand/." )
        if ( -f "overrides.cfg" );

    exit(0);
}

if ( $action eq 'help_config' ) {
    $publican->help_config();

    exit(0);
}

if ( $action eq 'printtree' ) {
    my $treeview = Publican::TreeView->new();
    $treeview->print_tree();
}

if ( $action eq 'clean' || $action eq 'package' ) {
    if ( $action eq 'package' ) {
        logger(
            maketext(
                "Running clean process to ensure stale content is not bundled in packages."
                )
                . "\n",
            RED
        );
    }
    logger( maketext("Clean: Removing temporary and publish content.") . "\n",
    );

    my $error;

    rmtree( $publican->param('tmp_dir') );
    rmtree("publish");

    my $books = $publican->param('books') || "";
    foreach my $book ( split( " ", $books ) ) {
        rmtree("$book/tmp");
        rmtree("$book/publish");
    }
}

if ( $action eq 'cleanset' ) {
    my $books = $publican->param('books') || "";
    foreach my $book ( split( " ", $books ) ) {
        rmtree("$book");
    }
}

if ( $action eq 'clean_ids' ) {
    my $builder = Publican::Builder->new();
    $builder->clean_ids();
}

if ( $action eq 'update_pot' ) {
    my $translater = Publican::Translate->new();
    $translater->update_pot();
}

if ( $action eq 'update_po' ) {
    my $translater = Publican::Translate->new();
    if ( $opts{langs} eq 'all' ) {
        $translater->update_po_all();
    }
    else {
        $translater->update_po( { langs => $opts{langs} } );
    }
}

if ( $action eq 'lang_stats' ) {
    my $translater = Publican::Translate->new();
    $translater->po_report( { lang => $opts{lang} } );
}

if ( $action eq 'build' ) {
    my $builder = Publican::Builder->new();
    $builder->build(
        {   formats         => $opts{formats},
            langs           => $opts{langs},
            publish         => $opts{publish},
            embedtoc        => $opts{embedtoc},
            distributed_set => $opts{distributed_set},
        }
    );
}

if ( $action eq 'package' ) {
    my $builder = Publican::Builder->new();
    if ( $publican->param('type') eq 'brand' ) {
        $builder->package_brand( { binary => $opts{binary} } );
    }
    else {
        $builder->package(
            {   lang          => $opts{lang},
                desktop       => $opts{desktop},
                short_sighted => $opts{short_sighted},
                binary        => $opts{binary}
            }
        );
    }

    if ( $opts{brew} ) {

        my @filelist = File::Find::Rule->file->name('*.src.rpm')
            ->in( $publican->param('tmp_dir') );

        my $srpm = pop(@filelist);

        my @brew_args = ( "brew", "build" );
        push( @brew_args, "--scratch" ) if ( $opts{scratch} );
        push( @brew_args, "--wait" )    if ( $opts{'wait'} );
        push( @brew_args, $publican->param('brew_dist') );

        if ( -f "$srpm" ) {
            system( @brew_args, "$srpm" );
        }
        else {
            croak( maketext("Can't locate srpm, packaging aborted") . "\n",
                RED );
        }
    }

    if ( $opts{cvs} ) {

        my $dir = pushd( $publican->param('tmp_dir') );
        print( "pushd " . $publican->param('tmp_dir') . "\n" );

        my @filelist = File::Find::Rule->file->name('*.src.rpm')->in('rpm');

        my $srpm = abs_path( pop(@filelist) );

        unless ( -f "$srpm" ) {
            croak( maketext("Can't locate srpm, CVS import aborted") . "\n" );
        }

        # TODO consider making this optional?
        my $cvs_root = $publican->param('cvs_root')
            || croak(
            maketext("--cvs requires cvs_root to be set in the cfg file") );

        my $cvs_pkg = $publican->param('cvs_pkg')
            || croak(
            maketext("--cvs requires cvs_pkg to be set in the cfg file") );
        $cvs_pkg =~ s/$LANG_PATTERN/$opts{lang}/g;

        my $cvs_branch = $publican->param('cvs_branch')
            || croak(
            maketext("--cvs requires cvs_branch to be set in the cfg file") );

        my $cvs_dir = "$cvs_pkg-$cvs_branch";

        my @cvs_args = ("cvs");
        push( @cvs_args, ( "-d", $cvs_root ) ) if ($cvs_root);
        push( @cvs_args, ( "co", $cvs_dir ) );

        print( "cvs command: " . join( " ", @cvs_args ), "\n" );
        system(@cvs_args);
        croak($@) if ($@);

        print(qq|pushd("$cvs_dir/$cvs_pkg"\n|);
        ## BUGBUG this breaks if you undef $dir first ...
        $dir = undef;
        $dir = pushd( $publican->param('tmp_dir') . "/$cvs_dir/$cvs_pkg" );

        system("$IMPORT_SCRIPT $srpm");
        croak($@) if ($@);

        system("cvs up");
        croak($@) if ($@);

        system("make tag");
        croak($@) if ($@);

        system("make build");
        croak($@) if ($@);

        $dir = undef;
    }
}

exit(0);

__END__

=head1 INTERFACE 

build
    Transform XML to other formats (pdf, html, html-single, etc)

	Options:
        --formats=<FORMATS>       Comma-separated list of formats, for example: html,pdf,html-single,html-desktop,txt,epub
        --langs=<LANGS>           Comma-separated list of languages, for example: en-US,de-DE,all
        --publish                 Set up built content for publishing
        --embedtoc                Embed the web site TOC object in the generated HTML
        --distributed_set         This flag tells publican the data being processed is a distributed set. Note: do not use on the command line. Publican uses this flag when calling itself to process distributed sets. This is the only safe way this flag can be used.


clean
    Remove all temporary files and directories

	Options:
		No options


clean_ids
    Run clean ids for source XML

	Options:
		No options


cleanset
    Remove local copies of remote set books

	Options:
		No options


create
    Create a new book, set, or article

	Options:
        --name=<NAME>             The name of the book, article, set, or brand
        --version=<VERSION>       The version of the product
        --edition=<EDITION>       The edition of the book, article, or set
        --product=<PRODUCT>       The name of the product
        --brand=<BRAND>           The brand to use
        --lang=<LANG>             The language the XML will be written in
        --type=<TYPE>             The type (book, article, or set)


create_brand
    Create a new brand

	Options:
        --name=<NAME>             The name of the book, article, set, or brand
        --lang=<LANG>             The language the XML will be written in


help_config
    Display help text for the configuration file

	Options:
		No options


installbrand
    Install a brand to the supplied location

	Options:
        --path=<PATH>             /path/to/install/to


lang_stats
    report PO statistics

	Options:
        --lang=<LANG>             The language the XML will be written in


old2new
    Create a publican.cfg file from the Makefile of an old book, set, or article

	Options:
		No options


package
    Package a language for shipping

	Options:
        --lang=<LANG>             The language the XML will be written in
        --desktop                 Create desktop instead of web package
        --brew                    Push SRPM to brew
        --scratch                 Use scratch instead of tag build
        --short_sighted           Create package without using version in package name
        --binary                  Build binary rpm when running package
        --wait                    Wait for brew to finish building
        --cvs                     Import the SRPM in to CVS, the run make tag and make build.


printtree
    Print a tree of the xi:includes

	Options:
		No options


update_po
    Update the PO files

	Options:
        --langs=<LANGS>           Comma-separated list of languages, for example: en-US,de-DE,all


update_pot
    Update the POT files

	Options:
		No options


=head1 CONFIGURATION AND ENVIRONMENT

Publican requires access to GetText msgmerge for merging updated POT files with PO files.

Publican requires access to Apache FOP for creating PDF files.


=head1 DEPENDENCIES

Archive::Tar
Carp
Config::Simple
Cwd
DateTime
DateTime::Format::DateParse
Encode
File::Copy::Recursive
File::Find
File::Find::Rule
File::Path
File::pushd
File::Spec
Getopt::Long
HTML::FormatText
HTML::TreeBuilder
I18N::LangTags::List
Image::Magick
Image::Size
Locale::PO
Makefile::Parser
Module::Build
Pod::Usage
Publican
Publican::Builder
Publican::CreateBook
Publican::CreateBrand
Publican::Localise
Publican::Translate
Publican::TreeView
Publican::XmlClean
Syntax::Highlight::Engine::Kate
Term::ANSIColor
Test::More
Text::Wrap
XML::LibXML
XML::LibXSLT
XML::TreeBuilder


=head1 INCOMPATIBILITIES

None reported.


=head1 BUGS AND LIMITATIONS

No bugs have been reported.

Please report any bugs or feature requests to
C<publican-list@redhat.com>, or through the web interface at
L<https://bugzilla.redhat.com/bugzilla/enter_bug.cgi?product=Fedora&amp;version=rawhide&amp;component=publican>.

=head1 AUTHOR

Jeff Fearn  C<< <jfearn@redhat.com> >>
