#!/usr/pkg/bin/perl -CDAS

use warnings;
use strict;
use 5.008;
use Carp;
use Getopt::Long qw(:config no_auto_abbrev);
use Pod::Usage;
use Publican;
use Publican::CreateBook;
use Publican::CreateBrand;
use Publican::Builder;
use Publican::Builder::DocBook4;
use Publican::Builder::DocBook5;
use Publican::XmlClean;
use Publican::TreeView;
use Publican::WebSite;
use File::Path;
use File::Find::Rule;
use File::pushd;
use Archive::Tar;
use Term::ANSIColor qw(:constants);
use Cwd qw(abs_path cwd);
use File::Basename;
use File::HomeDir;
use Publican::ConfigData;
use Encode qw(is_utf8 decode_utf8 encode_utf8);
use Lingua::EN::Fathom;

#binmode STDIN, ":encoding(UTF-8)";
#binmode STDOUT, ":encoding(UTF-8)";

my $VERSION           = "$Publican::VERSION";
my $LANG_PATTERN      = q|__LANG__|;
my $WEB_TEMPLATE_PATH = Publican::ConfigData->config('web');
my $DATA_DIR          = Publican::ConfigData->config('datadir');

# Try to catch SRPMS created using outdated versions of publican in brew/koji/mock
if (   ( $ENV{RPM_BUILD_DIR} )
    && ( $ENV{RPM_BUILD_DIR} ne "" )
    && ( $ENV{RPM_PACKAGE_NAME} ne "publican" )
    && (-f "/builddir/build/SRPMS/$ENV{RPM_PACKAGE_NAME}-$ENV{RPM_PACKAGE_VERSION}-$ENV{RPM_PACKAGE_RELEASE}.src.rpm"
    )
    )
{

    my $pkg
        = qq|/builddir/build/SRPMS/$ENV{RPM_PACKAGE_NAME}-$ENV{RPM_PACKAGE_VERSION}-$ENV{RPM_PACKAGE_RELEASE}.src.rpm|;
    my $req_version
        = qx|rpm --requires -qp $pkg \| grep 'publican-API' \| sed -e 's/^[^0-9\.]*//'|;

    system("rpm -Uvh $pkg");
    my $spec = qq|/builddir/build/SPECS/$ENV{RPM_PACKAGE_NAME}.spec|;

    my $db_count = qx|grep -c 'publican update_db' $spec|;
    my $pm_count = qx|grep -c 'use Publican' $spec|;

    chomp($req_version);
    if (( $db_count > 0 or $pm_count > 0 )
        and ( !$req_version
            || $req_version eq ""
            || $req_version != $Publican::SPEC_VERSION )
        )
    {
        croak(
            "\n\n\nERROR: The version of publican used to create the SRPM is not compatible with this version of publican. Upgrade or downgrade publican on your local machine to a version that supplies API version $Publican::SPEC_VERSION and try again.\n\n\n"
        );
    }
}

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

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

my $ver = $VERSION;
$ver =~ s/v//g;
my @pod_paths
    = ( './pod1', Publican::ConfigData->config('docdir') . "/publican-$ver", Publican::ConfigData->config('docdir') . "/publican" );

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

#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(
        qq|This flag tells publican the data being processed is a distributed set. Note: do not use distributed_set 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.|
    ),
    nocolours => maketext('Disable ANSI colourisation of logging.'),
    quiet     => maketext('Disable all logging.'),
## WebSite options
    'db_file=s'   => maketext('Override default database file.'),
    'toc_path=s'  => maketext('Override the default TOC path.'),
    'tmpl_path=s' => maketext('Override the default template path.'),
    'site_config=s' =>
        maketext('WebSite configuration file to use or create.'),
    novalid           => maketext('Do not run the DTD validation'),
    add               => maketext('Add a database entry'),
    del               => maketext('Delete a database entry'),
    'subtitle=s'      => maketext('Sub title for a book'),
    'abstract=s'      => maketext('Abstract for a book'),
    'product_label=s' => maketext('product label for a book'),
    'version_label=s' => maketext('version label for a book'),
    'name_label=s'    => maketext('name label for a book'),
## Revisions
    'revnumber=s' => maketext('Revision number to use for a revision.'),
    'date=s'      => maketext('Date to use for a revision.'),
    'member=s@'   => maketext(
        'An entry to be added to the revision. Can be specified multiple times.'
    ),
    'firstname=s'  => maketext('firstname to use for a revision.'),
    'surname=s'    => maketext('surname to use for a revision.'),
    'email=s'      => maketext('email to use for a revision.'),
    msgmerge       => maketext(q|Use gettext's msgmerge for POT/PO merging.|),
    web            => maketext(q|Install the web content for a brand.|),
    'src_dir=s'    => maketext(q|Directory to source publican files from.|),
    'brand_dir=s'  => maketext(q|Directory to source brand files from.|),
    'sort_order=s' => maketext('Order to sort a book'),
    'pdftool=s'    => maketext(
        'Override the tool to use when creating PDFs. Valid options are wkhtmltopdf and fop.'
    ),
    'pub_dir=s' =>
        maketext(q|Directory to publish files to. Defaults to publish.|),
    'dtdver=s' => maketext('The version of the DocBook DTD to use'),
    'book_version=s' =>
        maketext('The version number of the book being installed.'),
    'book_src_lang=s' =>
        maketext('The language this translation is based on.'),
    'old_site_config=s' => maketext(
        'WebSite configuration file to use when copying content between sites.'
    ),
    'previous' => maketext(
        'Keep previous msgid when fuzzy matches are detected in PO updates.'),
    showfuzzy     => maketext('Show fuzzy translation entries in output. Defaults off.'),
    'allow_network' => maketext(q|Allow the XML and XSLT processing to access the network. Defaults off.|),
    no_clean => maketext(q|Do not use the XML cleaner, use the XML as provided. WARNING: Will be ignored for translations!|),
);

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

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

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

    'clean_ids' => {
        'brief'   => maketext('Run clean ids for source XML'),
        'options' => [],
    },
    'print_tree' => {
        'brief'   => maketext('Print a tree of the xi:includes'),
        'options' => [],
    },
    'print_banned' => {
        'brief'   => maketext('Print a list of banned DocBook tags'),
        'options' => [],
    },
    'print_known' => {
        'brief'   => maketext("Print a list of QA'd DocBook tags"),
        'options' => [],
    },
    'print_unused' => {
        'brief'   => maketext('Print a list of unused XML files'),
        'options' => [],
    },
    'print_unused_images' => {
        'brief'   => maketext('Print a list of unused Image files'),
        'options' => [],
    },
    'create' => {
        'brief'   => maketext('Create a new book, set, or article'),
        'options' => [
            'name',  'version', 'edition', 'product',
            'brand', 'lang',    'type',    'dtdver'
        ],
    },
    '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', 'pub_dir'
        ],
    },
    'update_pot' => {
        'brief'   => maketext('Update the POT files'),
        'options' => [],
    },
    'update_po' => {
        'brief'   => maketext('Update the PO files'),
        'options' => [
            'langs', 'msgmerge', 'firstname', 'surname',
            'email', 'previous'
        ],
    },
    'install_brand' => {
        'brief'   => maketext('Install a brand to the supplied location'),
        'options' => [ 'path', 'web', 'pub_dir' ],
    },
    'copy_web_brand' => {
        'brief' =>
            maketext("Copy a brand's installed web content to another site"),
        'options' => [ 'brand', 'site_config', 'old_site_config' ],
    },
    'help_config' => {
        'brief'   => maketext('Display help text for the configuration file'),
        'options' => [],
    },
    'lang_stats' => {
        'brief'   => maketext('report PO statistics'),
        'options' => ['lang'],
    },
## WebSite actions
    'create_site' => {
        'brief' => maketext('Create a new WebSite in the supplied location.'),
        'options' =>
            [ 'site_config', 'db_file', 'toc_path', 'tmpl_path', 'lang' ],
    },
    'update_site' => {
        'brief'   => maketext('Update an existing sites templates.'),
        'options' => ['site_config'],
    },
    'install_book' => {
        'brief'   => maketext('Install a book in to a WebSite.'),
        'options' => [ 'site_config', 'lang' ],
    },
    'remove_book' => {
        'brief'   => maketext('Remove a book from a WebSite.'),
        'options' => [ 'site_config', 'lang' ],
    },
    'site_stats' => {
        'brief'   => maketext('Report on the contents of a WebSite'),
        'options' => ['site_config'],
    },
    'update_db' => {
        'brief' => maketext(
            'Add or remove database entries. Used for processing pre-build books, such as when building packages.'
        ),
        'options' => [
            'site_config',   'add',
            'del',           'lang',
            'product',       'version',
            'name',          'formats',
            'subtitle',      'abstract',
            'product_label', 'version_label',
            'name_label',    'sort_order',
            'book_version',  'book_src_lang'
        ],
    },
    'add_revision' => {
        'brief'   => maketext('Add an entry to the revision history'),
        'options' => [
            'lang',      'revnumber', 'date', 'member',
            'firstname', 'surname',   'email'
        ],
    },
    'rename' => {
        'brief'   => maketext('Rename a publican book'),
        'options' => [ 'name', 'product', 'version' ],
    },
    'migrate_site' => {
        'brief' => maketext(
            'Migrate a website DataBase from Publican < 3 to Publican 3.'),
        'options' => ['site_config'],
    },
    'trans_drop' => {
        'brief' =>
            maketext('Snapshot the source language for use in translation.'),
        'options' => [],
    },
    'report' => {
        'brief' =>
            maketext('Print a readability report for the source text.'),
        'options' => [],
    },
    'zt_pull' => {
        'brief'   => maketext('Pull translations from Zanata.'),
        'options' => [],
    },
    'zt_push' => {
        'brief'   => maketext('Push translations to Zanata.'),
        'options' => [],
    },
);

# 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 = ( keys(%opts), @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" );
        }
        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(
    -verbose  => 1,
    -exit     => 1,
    -input    => 'publican',
    -pathlist => \@pod_paths
    );

# catch multiple actions
if ( scalar @ARGV > 1 ) {
    croak(
        maketext(
            "publican only accepts one action you supplied '[_1]': [_2]\n",
            scalar @ARGV, join( ' & ', @ARGV )
        )
    );
}

# 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;
}

if ($bash_comp) {
    _bash_completion();
    exit(0);
}

# Undocumented way of getting help for all actions as DocBook 5.0 XML
# TODO should spit out the docs in every language ...
if ( $help and ( $action eq "docbook" ) ) {

    my $xml_doc = XML::Element->new_from_lol(
        [ 'appendix', {id=>'Command_summary'}, [ 'info', [ 'title', maketext('Command summary') ], ] ] );

    my $section = XML::Element->new_from_lol(
        [   'section',
            { id => 'Auto_Docs-Command_options' },
            [ 'info', [ 'title', maketext('Command options') ], ],
        ]
    );

    my $var_list = XML::Element->new_from_lol(
        [   'variablelist' ]
    );

    $section->push_content($var_list);
    $xml_doc->push_content($section);

    foreach my $option ( sort(@utility_opts) ) {
        my $entry = XML::Element->new_from_lol(
            [   'varlistentry',
                [ 'term', "--$option" ],
                [ 'listitem', [ 'para', $options{$option} ] ]
            ]
        );

        $var_list->push_content($entry);
    }

    $section = XML::Element->new_from_lol(
        [   'section',
            { id => 'Auto_Docs-Command_actions' },
            [ 'info', [ 'title', maketext('Command actions') ], ],
        ]
    );

    $var_list = XML::Element->new_from_lol(
        [   'variablelist' ]
    );

    $section->push_content($var_list);
    $xml_doc->push_content($section);

    foreach my $cmd ( sort( keys(%actions) ) ) {
        $var_list->push_content( _help_action_asdb($cmd) );

    }

    my $OUTDOC;
    my $out_file = 'en-US/Auto_Docs.xml';
    open( $OUTDOC, ">:encoding(UTF-8)", $out_file )
        || croak( maketext( "Could not open [_1] for output!", $out_file ) );

    print( $OUTDOC Publican::Builder::dtd_string(
            { tag => 'appendix', dtdver => '5.0', cleaning => 1 }
        )
    );
    print( $OUTDOC $xml_doc->as_XML() );
    close($OUTDOC);

    $xml_doc->root()->delete();

    my $cleaner = Publican::XmlClean->new( { exclude_ent => 1 } );
    $cleaner->process_file( { file => $out_file, out_file => $out_file } );

    # all the parameters

    $xml_doc = XML::Element->new_from_lol(
        [ 'appendix', {id => 'Configuration_summary'}, [ 'info', [ 'title', maketext('Configuration summary') ], ] ] );

    $section = XML::Element->new_from_lol(
        [   'section',
            { id => 'Auto_Docs_Configs-General_options' },
            [ 'info', [ 'title', maketext('General options') ], ],
	    ['para', maketext("These are set in the books or brands configuration files.") ],
        ]
    );

    my $gen_list = XML::Element->new_from_lol(
        [   'variablelist']
    );
    $section->push_content($gen_list);
    $xml_doc->push_content($section);

    my $brand_list = XML::Element->new_from_lol(
        [   'variablelist'],
 );

    $section = XML::Element->new_from_lol(
        [   'section',
            { id => 'Auto_Docs_Configs-Brand_options' },
            [ 'info', [ 'title', maketext('Brand options') ], ],
	    ['para', maketext("These are set in the brands configuration files.") ],
        ]
    );
    $section->push_content($brand_list);
    $xml_doc->push_content($section);

    my $web_list = XML::Element->new_from_lol(
        [   'variablelist'],
 );

    $section = XML::Element->new_from_lol(
        [   'section',
            { id => 'Auto_Docs_Configs-Site_option' },
            [ 'info', [ 'title', maketext('Site options') ], ],
	    ['para', maketext("These are set in the sites configuration file.") ],
        ]
    );
    $section->push_content($web_list);
    $xml_doc->push_content($section);

    $xml_doc->push_content(Publican::params_as_docbook($gen_list,$brand_list,$web_list));
    $xml_doc->push_content(Publican::WebSite::site_params_as_docbook($web_list));

    $out_file = 'en-US/Auto_Docs_Configs.xml';
    open( $OUTDOC, ">:encoding(UTF-8)", $out_file )
        || croak( maketext( "Could not open [_1] for output!", $out_file ) );

    print( $OUTDOC Publican::Builder::dtd_string(
            { tag => 'appendix', dtdver => '5.0', cleaning => 1 }
        )
    );
    print( $OUTDOC $xml_doc->as_XML() );
    close($OUTDOC);

    $xml_doc->root()->delete();
    $cleaner->process_file( { file => $out_file, out_file => $out_file } );

    exit(0);
}

sub _help_action_asdb {
    my $cmd = shift || croak maketext( "[_1] is a required argument", 'cmd' );
    my $brief = $actions{$cmd}->{brief};

    my $varentry = XML::Element->new_from_lol(
        [   'varlistentry', {id => $cmd},
            [   'term', [ 'prompt', '$' ], [ 'command', ' publican' ],
                " $cmd"
            ],
        ]
    );

    my $item
        = XML::Element->new_from_lol( [ 'listitem', [ 'para', $brief ] ], );

    $varentry->push_content($item);
    my $varlist = XML::Element->new_from_lol( [ 'variablelist', ] );

    foreach my $option ( sort( @{ $actions{$cmd}->{options} } ) ) {
        my $entry;

        if ( $options{$option} ) {
            $entry = XML::Element->new_from_lol(
                [   'varlistentry',
                    [ 'term', "--$option" ],
                    [ 'listitem', [ 'para', $options{$option} ] ]
                ]
            );
        }
        else {
            my $text
                = maketext( $options{"$option=s"} || $options{"$option=s@"} );
            $entry = XML::Element->new_from_lol(
                [   'varlistentry',
                    [ 'term', "--$option=", [ 'replaceable', uc($option) ] ],
                    [ 'listitem', [ 'para', $text ] ]
                ]
            );
        }

        $varlist->push_content($entry);
    }
    $item->push_content($varlist) if(!$varlist->is_empty());

    return ($varentry);
}

# catch no action set
if ( $action eq "" ) {
    pod2usage(
        -verbose  => 1,
        -exit     => 0,
        -input    => 'publican',
        -pathlist => \@pod_paths
    ) if $help;
    pod2usage(
        -verbose  => 2,
        -exit     => 0,
        -input    => 'publican',
        -pathlist => \@pod_paths
    ) if $man;

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

    pod2usage(
        -msg      => "\n" . maketext("Action required!") . "\n",
        -verbose  => 1,
        -exit     => 1,
        -input    => 'publican',
        -pathlist => \@pod_paths
    );

    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" );
    foreach my $option ( @utility_opts, @{ $actions{$cmd}->{options} } ) {
        debug_msg("TODO: does printf work for right to left languages?\n");
        if ( $options{$option} ) {
            printf( "        --%-20s    %s\n",
                $option, maketext( $options{$option} ) );
        }
        else {
            printf(
                "        --%-20s    %s\n",
                "$option=<" . uc($option) . ">",
                maketext( $options{"$option=s"} || $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},
            dtdver  => $opts{dtdver},
        }
    );
    $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;
    if ( $opts{dtdver} && $opts{dtdver} =~ /^5/ ) {
        $builder = Publican::Builder::DocBook5->new();
    }
    else {
        $builder = Publican::Builder::DocBook4->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 'create_site' ) {
    my $site_config = $opts{site_config}
        || croak( maketext( "[_1] is a required argument", 'site_config' ) );
    my $db_file = $opts{db_file}
        || croak( maketext( "[_1] is a required argument", 'db_file' ) );
    my $toc_path = $opts{toc_path}
        || croak( maketext( "[_1] is a required argument", 'toc_path' ) );
    my $tmpl_path = $opts{tmpl_path} || undef;
    my $def_lang  = $opts{lang}      || undef;

    if ( -f $site_config ) {
        croak(
            maketext(
                "Config file exists, you must supply a non-existent filename."
            )
        );
    }

    my $config = new Config::Simple();
    $config->syntax('http');

    my ( $filename, $directories, $suffix ) = fileparse($db_file);
    mkpath($directories) unless -d $directories;
    $config->param( 'db_file', abs_path($db_file) ) if ($db_file);

    if ($toc_path) {
        mkpath($toc_path) unless -d $toc_path;
        $config->param( 'toc_path', abs_path($toc_path) );
        rcopy( "$WEB_TEMPLATE_PATH/*", "$toc_path/." );
    }

    $config->param( 'tmpl_path', abs_path($tmpl_path) ) if ($tmpl_path);
    $config->param( 'def_lang',  $def_lang )            if ($def_lang);
    $config->write($site_config);

    my $ws = Publican::WebSite->new(
        { create => 1, site_config => $site_config } );
    $ws->regen_all_toc();

    exit(0);
}

if ( $action eq 'update_site' ) {
    my $site_config = $opts{site_config} || undef;
    local $File::Copy::Recursive::RMTrgFil = 1;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );
    rcopy( "$WEB_TEMPLATE_PATH/*", $ws->toc_path() . "/." );
    $ws->regen_all_toc( { force => 1 } );

    exit(0);
}

if ( $action eq 'migrate_site' ) {
    my $site_config = $opts{site_config} || undef;
    local $File::Copy::Recursive::RMTrgFil = 1;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );

    $ws->MigrateDB();
    $ws->regen_all_toc( { force => 1 } );

    print(
        maketext(
            "Now that your site is migrated you will need to rebuild your brands and install the brands web payload on your site.\n\nIt is also recommended that you rebuild and reinstall all books to ensure the layout is properly migrated."
        )
    );
    print( maketext('$ cd path/to/brand'), "\n" );
    print( maketext('$ publican build --formats=xml --langs=all --publish'),
        "\n" );
## BUGBUG should this use siteconfig?
    print(
        maketext(
            '$ publican install_brand --web --path=/path/to/site/%{brand}'),
        "\n"
    );
    print(
        maketext('$ publican update_site --site_config /path/to/site/config'),
        "\n"
    );

    exit(0);
}

if ( $action eq 'site_stats' ) {
    my $site_config = $opts{site_config} || undef;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );
    print( $ws->report() );

    exit(0);
}

# install/remove book for pre-build content (RPM, DEB, etc)
# BUGBUG formats isn't required for del
if ( $action eq 'update_db' ) {
    my $site_config = $opts{site_config} || undef;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );

    my $add = $opts{add} || undef;
    my $del = $opts{del} || undef;

    croak(
        maketext("One of add or del is required when updating the database") )
        unless ( $add || $del );

    my $lang = $opts{lang}
        || croak( maketext( "[_1] is a required argument", 'lang' ) );
    my $product = $opts{product}
        || croak( maketext( "[_1] is a required argument", 'product' ) );

    my $version
        = defined $opts{version}
        ? $opts{version}
        : croak( maketext( "[_1] is a required argument", 'version' ) );

    my $name = $opts{name}
        || croak( maketext( "[_1] is a required argument", 'name' ) );
    my $formats = $opts{formats}
        || croak( maketext( "[_1] is a required argument", 'formats' ) );

    # required for add
    my $subtitle = $opts{subtitle} || undef;
    my $abstract = $opts{abstract} || undef;

    my $book_version  = $opts{book_version}  || undef;
    my $book_src_lang = $opts{book_src_lang} || undef;

    if ($add) {
        croak( maketext( "[_1] is a required argument", 'subtitle' ) )
            unless ($subtitle);
        croak( maketext( "[_1] is a required argument", 'abstract' ) )
            unless ($abstract);

        croak( maketext( "[_1] is a required argument", 'book_version' ) )
            unless ($book_version);
    }

    # optional for add
    my $product_label = $opts{product_label} || undef;
    my $version_label = $opts{version_label} || undef;
    my $name_label    = $opts{name_label}    || undef;
    my $sort_order    = $opts{sort_order}    || undef;

    if ($add) {
        $ws->update_or_add_entry(
            {   language      => $lang,
                product       => $product,
                version       => $version,
                name          => $name,
                formats       => $formats,
                product_label => $product_label,
                version_label => $version_label,
                name_label    => $name_label,
                subtitle      => $subtitle,
                abstract      => $abstract,
                sort_order    => $sort_order,
                book_version  => $book_version,
                book_src_lang => $book_src_lang,
            }
        );
    }
    else {
        $ws->del_entry(
            {   language => $lang,
                product  => $product,
                version  => $version,
                name     => $name,
            }
        );
    }

## BUGBUG TODO only update langauges that have been updated
    # make sure OPDS is correct...
    $ws->regen_all_toc();

    exit(0);
}

if ( $action eq 'copy_web_brand' ) {
    my $brand = $opts{brand}
        || croak( maketext( "[_1] is a required argument", 'brand' ) );
    my $site_config = $opts{site_config}
        || croak( maketext( "[_1] is a required argument", 'site_config' ) );
    my $old_site_config = $opts{old_site_config}
        || croak(
        maketext( "[_1] is a required argument", 'old_site_config' ) );

    my $old_config = new Config::Simple();
    $old_config->syntax('http');
    $old_config->read($old_site_config)
        || croak("Failed to load config file: $old_site_config");

    my $old_path = $old_config->param('toc_path')
        || croak(
        maketext(
            "[_1] is a mandatory field in a site configuration file",
            'toc_path'
        )
        );

    my $new_config = new Config::Simple();
    $new_config->syntax('http');
    $new_config->read($site_config)
        || croak("Failed to load config file: $site_config");

    my $new_path = $new_config->param('toc_path')
        || croak(
        maketext(
            "[_1] is a mandatory field in a site configuration file",
            'toc_path'
        )
        );

    rcopy( "$old_path/$brand", "$new_path/$brand" );

    exit(0);
}

## NOTE: All targets here MUST have a valid publican.cfg file
if ( $action eq 'print_known' ) {
    Publican::XmlClean::print_known_tags();
    exit(0);
}
my %args = ( 'configfile' => $opts{config} );
my $src_dir;
my $cwd = cwd();

if ( defined( $opts{src_dir} ) ) {
    $src_dir = pushd( $opts{src_dir} );
}

if ( !defined( $opts{pub_dir} ) ) {
    $opts{pub_dir} = 'publish';
}

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

my $configfile = File::HomeDir->home() . "/.publican.cfg";
if ( -f $configfile ) {
    my $user_cfg = new Config::Simple();
    $user_cfg->syntax('http');
    $user_cfg->read($configfile)
        || croak(
        maketext( "Failed to load user config file: [_1]", $configfile ) );

    $opts{firstname} = decode_utf8( $user_cfg->param('firstname') )
        unless ( $opts{firstname} );
    $opts{surname} = decode_utf8( $user_cfg->param('surname') )
        unless ( $opts{surname} );
    $opts{email} = decode_utf8( $user_cfg->param('email') )
        unless ( $opts{email} );
    $opts{lang} = decode_utf8( $user_cfg->param('lang') )
        unless ( $opts{lang} );
    $opts{formats} = decode_utf8( $user_cfg->param('formats') )
        unless ( $opts{formats} );
    $opts{langs} = decode_utf8( $user_cfg->param('langs') )
        unless ( $opts{langs} );
}

if ( defined( $opts{src_dir} ) ) {
    $publican->{config}->param( 'tmp_dir',
        abs_path( "$cwd/" . $publican->param('tmp_dir') ) );
}

if ( $action eq 'print_banned' ) {
    $publican->print_banned_tags();
    exit(0);
}

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

    my $xml_lang = $publican->param('xml_lang');

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

    if ( $opts{web} ) {
        foreach my $ln ( split( ',', get_all_langs() ) ) {
            mkpath("$opts{path}/$ln/css") unless ( -f "$opts{path}/$ln/css" );

            rcopy( "$xml_lang/css/brand.css", "$opts{path}/$ln/css/brand.css" )
                if ( -f "$xml_lang/css/brand.css" );
            rcopy( "$ln/css/lang.css", "$opts{path}/$ln/css/lang.css" )
                if ( -f "$ln/css/lang.css" );

            my $images = $publican->param('img_dir');
            mkpath("$opts{path}/$ln/$images")
                unless ( -f "$opts{path}/$ln/$images" );
            rcopy( "$xml_lang/$images/*", "$opts{path}/$ln/$images/." )
                if ( -d "$xml_lang/$images" );
            rcopy( "$ln/$images/*", "$opts{path}/$ln/$images/." )
                if ( -d "$ln/$images" );

            if ( -d "$xml_lang/scripts" ) {
                mkpath("$opts{path}/$ln/scripts")
                    unless ( -f "$opts{path}/$ln/scripts" );
                rcopy( "$xml_lang/scripts/*", "$opts{path}/$ln/scripts/." );
            }

            if ( -d "$ln/scripts" ) {
                mkpath("$opts{path}/$ln/scripts")
                    unless ( -f "$opts{path}/$ln/scripts" );
                rcopy( "$ln/scripts/*", "$opts{path}/$ln/scripts/." );
            }
        }
    }
    else {
        rcopy( "$opts{pub_dir}/*", "$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 'print_tree' ) {
    my $treeview = Publican::TreeView->new();
    $treeview->print_tree();
    exit(0);
}

if ( $action eq 'print_unused' ) {
    my $treeview = Publican::TreeView->new();
    $treeview->print_unused();
    exit(0);
}

if ( $action eq 'print_unused_images' ) {
    my $treeview = Publican::TreeView->new();
    $treeview->print_unused_images();
    exit(0);
}

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 [_1] and publish directories.",
            $publican->param('tmp_dir') )
            . "\n",
    );

    my $error;

    rmtree( $publican->param('tmp_dir') );
    rmtree( $opts{pub_dir} );

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

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

if ( $action eq 'clean_ids' ) {
    my $dtdver = $publican->param('dtdver');
    my $builder;
    if ( $dtdver =~ /^5/ ) {
        $builder = Publican::Builder::DocBook5->new();
    }
    else {
        $builder = Publican::Builder::DocBook4->new();
    }
    $builder->clean_ids();
}

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

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

if ( $action eq 'update_po' ) {
    my $translater = Publican::Translate->new();
    if ( !defined $opts{langs} || $opts{langs} eq 'all' ) {
        $translater->update_po_all(
            {   msgmerge  => $opts{msgmerge},
                email     => $opts{email},
                firstname => $opts{firstname},
                surname   => $opts{surname},
                previous  => $opts{previous},
            }
        );
    }
    else {
        $translater->update_po(
            {   langs     => $opts{langs},
                msgmerge  => $opts{msgmerge},
                email     => $opts{email},
                firstname => $opts{firstname},
                surname   => $opts{surname},
                previous  => $opts{previous},
            }
        );
    }
}

if ( $action eq 'lang_stats' ) {
    my $translater = Publican::Translate->new();
    if ( $opts{lang} eq 'all' ) {
        foreach my $lang ( split( /,/, get_all_langs(1) ) ) {
            $translater->po_report( { lang => $lang } );
        }
    }
    else {
        $translater->po_report( { lang => $opts{lang} } );
    }
}

if ( $action eq 'build' ) {

    my $dtdver = $publican->param('dtdver');
    my $builder;

    if ( $dtdver =~ m/^5/ ) {
        $builder = Publican::Builder::DocBook5->new(
            { novalid => $opts{novalid}, showfuzzy => $opts{showfuzzy} } );

    }
    else {
        $builder = Publican::Builder::DocBook4->new(
            { novalid => $opts{novalid}, showfuzzy => $opts{showfuzzy} } );
    }

    $builder->build(
        {   formats         => $opts{formats},
            langs           => $opts{langs},
            publish         => $opts{publish},
            embedtoc        => $opts{embedtoc},
            distributed_set => $opts{distributed_set},
            pdftool         => $opts{pdftool},
            pub_dir         => $opts{pub_dir},
        }
    );
}

if ( $action eq 'add_revision' ) {

    if ( $publican->param('type') eq 'brand' ) {
        croak( maketext("add_revision does not work for brands.") );
    }

    $publican->add_revision(
        {   lang      => $opts{lang},
            revnumber => $opts{revnumber},
            date      => $opts{date},
            members   => $opts{member},
            email     => $opts{email},
            firstname => $opts{firstname},
            surname   => $opts{surname},
        }
    );
}

if ( $action eq 'package' ) {
    my $builder = Publican::Builder::DocBook4->new();
    if ( $publican->param('type') eq 'brand' ) {
        $builder->package_brand( { binary => $opts{binary} } );
    }
    elsif ( $publican->param('web_home') || $publican->param('web_type') ) {
        $builder->package_home( { 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} );
        if ( $opts{'wait'} ) {
            push( @brew_args, "--wait" );
        }
        else {
            push( @brew_args, "--nowait" );
        }

        push( @brew_args, $publican->param('brew_dist') );

        if ( -f "$srpm" ) {
            my $result = system( @brew_args, "$srpm" );
            if ($result) {
                croak( maketext("Brew died, error $result: '$!'") );
            }
        }
        else {
            croak( maketext("Can't locate srpm, packaging aborted") );
        }
    }
}

if ( $action eq 'install_book' ) {
    my $site_config = $opts{site_config} || undef;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );

    if ( $publican->param('web_home') || $publican->param('web_type') ) {
        croak( maketext("can't find 'publish' directory") )
            unless ( -d "$opts{pub_dir}/home" );
        rcopy( "$opts{pub_dir}/home/*", $ws->toc_path() . "/." );
    }
    else {
        my $lang = $opts{lang}
            || croak( maketext( "[_1] is a required argument", 'lang' ) );
        my $product       = $publican->param('product');
        my $version       = $publican->param('version');
        my $name          = $publican->param('docname');
        my $product_label = $publican->param('web_product_label');
        my $version_label = $publican->param('web_version_label');
        my $name_label    = $publican->param('web_name_label');
        my $sort_order    = $publican->param('sort_order');

        my $builder;
        if ( $opts{dtdver} && $opts{dtdver} =~ /^5/ ) {
            $builder = Publican::Builder::DocBook5->new();
        }
        else {
            $builder = Publican::Builder::DocBook4->new();
        }

        my $subtitle = $builder->get_subtitle( { lang => $lang } );
        my $abstract = $builder->get_abstract( { lang => $lang } );

        croak(
            maketext(
                "can't find publish directory '[_1]/[_2]'", $opts{pub_dir},
                $lang
            )
        ) unless ( -d "$opts{pub_dir}/$lang" );

        my $type     = $publican->param('type');
        my $xml_lang = $publican->param('xml_lang');
        my $tmp_dir  = $publican->param('tmp_dir');

        # get translated labels
## BUGBUG this is copied from Publican.pm, maybe a sub for these?
        if ( $lang ne $xml_lang ) {
            my $xml_file = "$tmp_dir/$lang/xml/$type" . '_Info.xml';
            $xml_file = "$tmp_dir/$lang/xml/" . $publican->param('info_file')
                if ( $publican->param('info_file') );

            croak( maketext( "Can't locate required file: [_1]", $xml_file ) )
                if ( !-f $xml_file );

            my $xml_doc = XML::TreeBuilder->new();
            $xml_doc->parse_file($xml_file);

            # BUGBUG can't translate overridden labels :(
            unless ($product_label) {
                $product_label = eval {
                    $xml_doc->root()->look_down( "_tag", "productname" )
                        ->as_text();
                };
                if ($@) {

    #                    croak maketext("productname not found in Info file");
                }
                else {
                    $product_label =~ s/\s/_/g;

                    $product_label = undef if ( $product_label eq $product );
                }
            }

            unless ($name_label) {
                $name_label = eval {
                    $xml_doc->root()->look_down( "_tag", "title" )->as_text();
                };
                if ($@) {

          #                    croak maketext("title not found in Info file");
                }
                else {
                    $name_label =~ s/\s/_/g;
                    $name_label = undef if ( $name_label eq $name );
                }
            }
        }

        mkpath( $ws->toc_path() . "/$lang" )
            unless ( -d $ws->toc_path() . "/$lang" );
        rcopy( "$opts{pub_dir}/$lang/*", $ws->toc_path() . "/$lang/." );

        my @formats;
        foreach my $format (glob("publish/$lang/$product/$version/*")) {
            $format =~ s{publish/$lang/$product/$version/}{}g;
            push(@formats, $format);
        }

        $ws->update_or_add_entry(
            {   language      => $lang,
                product       => $product,
                version       => $version,
                name          => $name,
                formats       => join(',',@formats),
                product_label => $product_label,
                version_label => $version_label,
                name_label    => $name_label,
                subtitle      => $subtitle,
                abstract      => $abstract,
                sort_order    => $sort_order,
            }
        );
    }

    $ws->regen_all_toc();
}

if ( $action eq 'remove_book' ) {
    my $site_config = $opts{site_config} || undef;
    my $ws = Publican::WebSite->new( { site_config => $site_config } );

    my $lang = $opts{lang}
        || croak( maketext( "[_1] is a required argument", 'lang' ) );
    my $product = $publican->param('product');
    my $version = $publican->param('version');
    my $name    = $publican->param('docname');

    $ws->del_entry(
        {   language => $lang,
            product  => $product,
            version  => $version,
            name     => $name,
        }
    );

    # Since every format uses the name, this finds all the formats even if they aren't in the DB.
    foreach my $fdir (File::Find::Rule->name($name)->in("$ws->{toc_path}/$lang/$product/$version")) {
        rmtree($fdir);
        my $format = $fdir;
        $format =~ s{$ws->{toc_path}/$lang/$product/$version/}{};
        $format =~ s{/$name}{};
        next if($format eq "");

        # if we delete the last of this format, then delete the format dir.
        # BUGBUG might need to check for non-template created files? e.g. index.html
        my $dir = "$ws->{toc_path}/$lang/$product/$version/$format";
        unless ( File::Find::Rule->not_name(qr/(\.|\.\.)/)->in($dir) ) {
            rmtree($dir);
            $dir = "$ws->{toc_path}/$lang/$product/$version";
            # if we delete the last format for this vrerison., delete the version dir.
            unless ( File::Find::Rule->not_name(qr/(\.|\.\.)/)->in($dir) ) {
                rmtree($dir);
                $dir = "$ws->{toc_path}/$lang/$product";
                # if we delete the last version for this product, delete the product dir.
                unless ( File::Find::Rule->not_name(qr/(\.|\.\.)/)->in($dir) )
                {
                    rmtree($dir);
                }
            }
        }
    }

    # Have to regenerate all languages as translations have untranslated and source newer tags.
    $ws->regen_all_toc();
}

if ( $action eq 'rename' ) {

    my $conf_file = ( $opts{config} || 'publican.cfg' );
    my $config = new Config::Simple($conf_file);

    $config->syntax('http');

    # load in $type_Info.xml
    my $file
        = $config->param('xml_lang') . '/'
        . ( $config->param('type') || 'Book' )
        . '_Info.xml';
    $file = $config->param('xml_lang') . "/" . $config->param('info_file')
        if ( $config->param('info_file') );

    my $cleaner = Publican::XmlClean->new( { clean_id => 1 } );

    my $xml_doc = XML::TreeBuilder->new(
        { 'NoExpand' => "1", 'ErrorContext' => "2" } );
    $xml_doc->store_comments(1);
    $xml_doc->parse_file($file)
        || croak( maketext( "Can't open file '[_1]' [_2]", $file, $@ ) );

    if ( $opts{name} ) {

        # set mainfile in publican.cfg if it's not set
        unless ( $config->param('mainfile') ) {
            $config->param( 'mainfile',
                $publican->{config}->param('docname') );
        }

        # delete docname
        $config->delete('docname');

        # Change Title
        my $title = $xml_doc->root()->look_down( "_tag", "title" )
            || croak( maketext('Cannot locate title tag in Info file!') );
        $title->delete_content();
        $title->push_content( $opts{name} );
    }

    # Change Product Name
    if ( $opts{product} ) {
        my $prod = $xml_doc->root()->look_down( "_tag", "productname" )
            || croak(
            maketext('Cannot locate productname tag in Info file!') );
        $prod->delete_content();
        $prod->push_content( $opts{product} );
    }

    # Change Product Number
    if ( $opts{version} ) {
        my $prod_num = $xml_doc->root()->look_down( "_tag", "productnumber" )
            || croak(
            maketext('Cannot locate productnumber tag in Info file!') );
        $prod_num->delete_content();
        $prod_num->push_content( $opts{version} );
    }

    # write out new files
    $config->write($conf_file);
    $cleaner->print_xml( { xml_doc => $xml_doc, out_file => $file } );

    # clean up memory
    $xml_doc->root()->delete();
}

if ( $action eq 'report' ) {
    my $dtdver = $publican->param('dtdver');
    my $builder;
    my $lang    = $publican->param('xml_lang');
    my $tmp_dir = $publican->param('tmp_dir');
    my $docname = $publican->param('docname');

    if ( $dtdver =~ m/^5/ ) {
        $builder = Publican::Builder::DocBook5->new(
            { novalid => $opts{novalid} } );

    }
    else {
        $builder = Publican::Builder::DocBook4->new(
            { novalid => $opts{novalid} } );
    }

    $builder->build(
        {   formats => 'txt',
            langs   => $publican->param('xml_lang'),
            pub_dir => $opts{pub_dir},
        }
    );

    my $file = "$tmp_dir/$lang/txt/$docname.txt";

    my $text = new Lingua::EN::Fathom;
    $text->analyse_file($file);
    print( $text->report );
}

if ( $action eq 'zt_pull' ) {
    my @zanata_cmd
        = qw(zanata-cli pull --src-dir pot --trans-dir . --include-fuzzy --locales);
    push( @zanata_cmd, get_all_langs(1) );
    my $result = system(@zanata_cmd);
    if ($result) {
        croak(
            maketext( "Zanata call failed, error [_1]: '[2]'", $result, $! )
        );
    }
}

if ( $action eq 'zt_push' ) {
    my @zanata_cmd
        = qw(zanata-cli push --src-dir pot --trans-dir . --push-type both --copy-trans 0 --locales );
    push( @zanata_cmd, get_all_langs(1) );
    my $result = system(@zanata_cmd);
    if ($result) {
        croak(
            maketext( "Zanata call failed, error [_1]: '[2]'", $result, $! )
        );
    }
}

exit(0);

# Secret action to create bash completion file.
# Nothing special just don't want to confuse people by discussing
# it in the general help

sub _bash_completion {

    my $actions = join( " ", sort( keys(%actions) ) );
    my $util_opts = "";
    foreach my $opto ( sort(@utility_opts) ) {
        $opto =~ s/=.*$//g;
        $util_opts .= "--$opto ";
    }

    my $top = <<TOP;
# publican completion

_publican()
{
	local cur prev commands options command

	COMPREPLY=()
	cur=`_get_cword`
        brands=`ls $DATA_DIR/Common_Content`

	commands='$actions'

	if [[ \$COMP_CWORD -eq 1 ]] ; then
		if [[ "\$cur" == -* ]]; then
			COMPREPLY=( \$( compgen -W '--help' -- \$cur ) )
		else
			COMPREPLY=( \$( compgen -W "\$commands" -- \$cur ) )
		fi
	else
		prev=\${COMP_WORDS[COMP_CWORD-1]}
		case \$prev in
			@(--site_config|--config))
				_filedir 'cfg'
				return 0;
				;;
			@(--lang|--langs))
				COMPREPLY=( \$( compgen -d -- "\$cur" ) )
				return 0;
				;;
			--formats)
				COMPREPLY=( \$( compgen -W "eclipse epub drupal-book html html-single html-desktop man pdf txt xml" -- \$cur ) )
				return 0;
				;;
			--type)
				COMPREPLY=( \$( compgen -W "Article Book Set" -- \$cur ) )
				return 0;
				;;
			--brand)
				COMPREPLY=( \$( compgen -W "\$brands" -- \$cur ) )
				return 0;
				;;
		esac

		command=\${COMP_WORDS[1]}

		if [[ "\$cur" == -* ]]; then
			# possible options for the command
			case \$command in
TOP

    my $options = "";

    foreach my $action ( sort( keys(%actions) ) ) {
        my $opt = "";
        foreach my $option ( sort( @{ $actions{$action}{'options'} } ) ) {
            $opt .= "--$option ";
        }

        $options .= <<EOO;
				$action)
					options='$opt'
					;;
EOO

    }

    my $bottom = <<BOTTOM;
			esac
			options="\$options $util_opts"
			COMPREPLY=( \$( compgen -W "\$options" -- \$cur ) )
		else
			if [[ "\$command" == @(--help) ]]; then
				COMPREPLY=( \$( compgen -W "\$commands" -- \$cur ) )
			else
				_filedir
			fi
		fi
	fi

	return 0
}
complete -F _publican publican
BOTTOM

    my $COMP_FILE;
    open( $COMP_FILE, ">", "completion/_publican" )
        || croak("file open fail: @_");
    print( $COMP_FILE $top );
    print( $COMP_FILE $options );
    print( $COMP_FILE $bottom );
    close($COMP_FILE);
    return;
}

