//
// tardy - a tar post-processor
// Copyright (C) 1993-1996, 1998, 1999, 2001-2004, 2008, 2009, 2011 Peter Miller
//
// This program 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.
//
// This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
//

#include <libtardy/ac/grp.h>
#include <libtardy/ac/pwd.h>
#include <libexplain/output.h>
#include <vector>

#include <libtardy/file/input.h>
#include <libtardy/file/output/buffer.h>
#include <libtardy/file/output/gzip.h>
#include <libtardy/file/output/hexdump.h>
#include <tardy/ifmt.h>
#include <tardy/ofmt.h>
#include <libtardy/tar/input/filter/clean.h>
#include <libtardy/tar/input/filter/exclude.h>
#include <libtardy/tar/input/filter/group_name.h>
#include <libtardy/tar/input/filter/group_numbr.h>
#include <libtardy/tar/input/filter/gunzip.h>
#include <libtardy/tar/input/filter/mode_clear.h>
#include <libtardy/tar/input/filter/mode_set.h>
#include <libtardy/tar/input/filter/mtime.h>
#include <libtardy/tar/input/filter/prefix.h>
#include <libtardy/tar/input/filter/relative_paths.h>
#include <libtardy/tar/input/filter/remov_prefi.h>
#include <libtardy/tar/input/filter/remove_prefix_count.h>
#include <libtardy/tar/input/filter/suppr_direc.h>
#include <libtardy/tar/input/filter/user_name.h>
#include <libtardy/tar/input/filter/user_number.h>
#include <libtardy/tar/output/extract.h>
#include <libtardy/tar/output/filter/gzip.h>
#include <libtardy/tar/output/filter/list.h>
#include <tardy/tardy.h>

#define NOT_SET (-32767)

static long uid = NOT_SET;
static long gid = NOT_SET;
static int mode_set = NOT_SET;
static int mode_clear = NOT_SET;
static int list;
static const char *prefix;
static int no_directories;
static std::vector<rcstring> remove_prefix_list;
static long remove_prefix_count;
static int clean;
static time_t when = -1;
static rcstring uname;
static rcstring gname;
static input_opener_ty input_opener;
static output_opener_ty output_opener;
static bool extract;
static bool compress_flag;
static bool decompress_flag;
static int blocksize;
static std::vector<rcstring> exclude_list;
static bool hexdump;
static bool verbose;
static bool relative_paths;


void
tardy_user_name_by_string(const char *s)
{
    if (uname != "")
        explain_output_error_and_die("duplicate -User_NAme option");
    uname = s;
}


void
tardy_user_name_by_number(long n)
{
    if (uname != "")
        explain_output_error_and_die("duplicate -User_NAme option");
    if (n < 0 || n >= (1L << 16))
        explain_output_error_and_die("uid %ld out of range", n);
    struct passwd *pw = getpwuid(n);
    if (!pw)
        explain_output_error_and_die("uid %ld unknown", n);
    uname = pw->pw_name;
}


void
tardy_user_number_by_string(const char *s)
{
    if (uid != NOT_SET)
        explain_output_error_and_die("duplicate -User_NUmber option");
    struct passwd *pw = getpwnam(s);
    if (!pw)
    {
        explain_output_error_and_die
        (
            "user %s unknown",
            rcstring(s).quote_c().c_str()
        );
    }
    uid = pw->pw_uid;
}


void
tardy_user_number_by_number(long n)
{
    if (n < 0 || n >= (1L << 16))
        explain_output_error_and_die("user number %ld out of range", n);
    if (uid != NOT_SET)
        explain_output_error_and_die("duplicate -User_NUmber option");
    uid = n;
}


void
tardy_group_number_by_string(const char *s)
{
    if (gid != NOT_SET)
        explain_output_error_and_die("duplicate -Group_NUmber option");
    struct group *gr = getgrnam(s);
    if (!gr)
    {
        explain_output_error_and_die
        (
            "group %s unknown",
            rcstring(s).quote_c().c_str()
        );
    }
    gid = gr->gr_gid;
}


void
tardy_group_name_by_string(const char *s)
{
    if (gname != "")
        explain_output_error_and_die("duplicate -Group_NAme option");
    gname = s;
}


void
tardy_group_name_by_number(long n)
{
    if (gname != "")
        explain_output_error_and_die("duplicate -Group_NAme option");
    if (n < 0 || n >= (1L << 16))
        explain_output_error_and_die("gid %ld out of range", n);
    struct group *gr = getgrgid(n);
    if (!gr)
        explain_output_error_and_die("gid %ld unknown", n);
    gname = gr->gr_name;
}


void
tardy_group_number_by_number(long n)
{
    if (n < 0 || n >= (1L << 16))
        explain_output_error_and_die("group number %ld out of range", n);
    if (gid != NOT_SET)
        explain_output_error_and_die("duplicate -Group_NUmber option");
    gid = n;
}


void
tardy_no_fix_type(void)
{
    tardy_format_output("tar-v7");
}


void
tardy_format_input(const char *name)
{
    if (input_opener)
        explain_output_error_and_die("duplicate -Input_ForMaT option");
    input_opener = input_opener_by_name(name);
}


void
tardy_format_output(const char *name)
{
    if (extract)
    {
        explain_output_error_and_die
        (
            "the -eXtract and -Output-ForMaT options are mutually exclusive"
        );
    }
    if (output_opener)
        explain_output_error_and_die("duplicate -Output_ForMaT option");
    output_opener = output_opener_by_name(name);
}


void
tardy_extract(void)
{
    if (output_opener)
    {
        explain_output_error_and_die
        (
            "the -eXtract and -Output-ForMaT options are mutually exclusive"
        );
    }
    if (extract)
        explain_output_error_and_die("duplicate -eXtract option");
    extract = true;
}


void
tardy(const char *ifn, const char *ofn)
{
    //
    // open the input file
    //
    file_input::pointer ifpx = file_input::factory(ifn);
    tar_input::pointer ifp;
    if (input_opener)
        ifp = input_opener(ifpx);
    else
        ifp = tar_input::factory(ifpx);
    assert(ifp);
    if (verbose)
    {
        explain_output_error
        (
            "input format is %s\n",
            rcstring(ifp->get_format_name()).quote_c().c_str()
        );
    }

    if (decompress_flag)
    {
        ifp = tar_input_filter_gunzip::create(ifp);
        assert(ifp);
    }

    //
    // open the output file
    //
    // Where possible, if the user didn't say, and it's sensible, use
    // the same output format as the input format.
    //
    file_output::pointer ofpx = file_output::factory(ofn);
    if (hexdump)
        ofpx = file_output_hexdump::create(ofpx);
    ofpx = file_output_buffer::create(ofpx, blocksize);
    if (ofpx->filename_implies_gzip())
        ofpx = file_output_gzip::create(ofpx);
    tar_output::pointer ofp;
    if (extract)
    {
        ofp = tar_output_extract::create();
    }
    else if (output_opener)
    {
        //
        // Use the output format the user requested from the command line.
        //
        ofp = output_opener(ofpx);
    }
    else
    {
        //
        // To get the same output format as input format, we ask the
        // input instance to manufacture the "corresponding" output
        // instance.  The default implementation gives us a regular tar
        // output, but fancier formats are at liberty to do better.
        //
        format_family_t out_ff = format_family_from_filename(ofpx->filename());
        if (out_ff == format_family_other)
        {
            // No clue from filename, ask the input for "the same",
            // which will default to "tar" if it is "format family other".
            ofp = ifp->tar_output_factory(ofpx);
        }
        else
        {
            format_family_t in_ff = ifp->get_format_family();
            if (in_ff != out_ff)
            {
                //
                // From looking at the output file name, it seems that
                // different format families are in play, so use the
                // most generic output format in that family.
                //
                switch (out_ff)
                {
                case format_family_cpio:
                    output_opener = output_opener_by_name("cpio");
                    break;

                case format_family_ar:
                    output_opener = output_opener_by_name("ar");
                    break;

                case format_family_tar:
                case format_family_other:
                default:
                    output_opener = output_opener_by_name("tar");
                    break;
                }
                assert(output_opener);
                ofp = output_opener(ofpx);
            }
            else
            {
                //
                // Input format family agrees well enough with output
                // file name, so use the same format as the input.
                //
                ofp = ifp->tar_output_factory(ofpx);
            }
        }
    }
    assert(ofp);
    ofp->set_block_size(blocksize);
    if (verbose)
    {
        explain_output_error
        (
            "output format is %s\n",
            rcstring(ofp->get_format_name()).quote_c().c_str()
        );
    }

    if (compress_flag)
        ofp = tar_output_filter_gzip::create(ofp);

    //
    // Filter the input, if requested.
    //
    for
    (
        std::vector<rcstring>::iterator it = exclude_list.begin();
        it != exclude_list.end();
        ++it
    )
        ifp = tar_input_filter_exclude::create(ifp, *it);
    if (no_directories)
        ifp = tar_input_filter_suppress_directories::create(ifp);
    if (uid != NOT_SET)
        ifp = tar_input_filter_user_number::create(ifp, uid);
    if (uname != "")
        ifp = tar_input_filter_user_name::create(ifp, uname);
    if (gid != NOT_SET)
        ifp = tar_input_filter_group_number::create(ifp, gid);
    if (gname != "")
        ifp = tar_input_filter_group_name::create(ifp, gname);
    if (mode_set != NOT_SET)
        ifp = tar_input_filter_mode_set::create(ifp, mode_set);
    if (mode_clear != NOT_SET)
        ifp = tar_input_filter_mode_clear::create(ifp, mode_clear);
    if (remove_prefix_count > 0)
    {
        ifp =
            tar_input_filter_remove_prefix_count::create
            (
                ifp,
                remove_prefix_count
            );
    }
    for
    (
        std::vector<rcstring>::iterator it = remove_prefix_list.begin();
        it != remove_prefix_list.end();
        ++it
    )
        ifp = tar_input_filter_remove_prefix::create(ifp, *it);
    if (prefix)
        ifp = tar_input_filter_prefix::create(ifp, prefix);
    if (clean)
        ifp = tar_input_filter_clean::create(ifp, clean);
    if (when != (time_t)-1)
        ifp = tar_input_filter_mtime::create(ifp, when);
    if (relative_paths)
        ifp = tar_input_filter_relative_paths::create(ifp);

    //
    // List the output, if requested.
    //
    if (list)
        ofp = tar_output_filter_list::create(ofp);

    //
    // Allocate a large chunk of memory to play with.
    //
    size_t buffer_size = (size_t)1 << 16;
    char *buffer = new char[buffer_size];

    ifp->read_archive_begin();
    ofp->write_archive_begin();
    for (;;)
    {
        tar_header h;
        if (!ifp->read_header(h))
            break;
        ifp->read_header_padding();
        ofp->write_header(h);
        ofp->write_header_padding();

        int nbytes = 0;
        while (nbytes < h.size)
        {
            size_t chunk_max = h.size - nbytes;
            if (chunk_max > buffer_size)
                chunk_max = buffer_size;
            int chunk = ifp->read_data(buffer, chunk_max);
            if (chunk == 0)
                ifp->fatal("premature end of file");
            ofp->write_data(buffer, chunk);
            nbytes += chunk;
        }
        ifp->read_data_padding();
        ofp->write_data_padding();
    }
    ifp->read_archive_end();
    ofp->write_archive_end();

    delete buffer;
}


void
tardy_prefix(const char *s)
{
    if (prefix)
        explain_output_error_and_die("duplicate -Prefix option");
    prefix = s;
}


void
tardy_remove_prefix(const char *s)
{
    remove_prefix_list.push_back(rcstring(s));
}


void
tardy_remove_prefix(long n)
{
    remove_prefix_count = n;
}


void
tardy_mode_set(int n)
{
    if (mode_set != NOT_SET)
        explain_output_error_and_die("duplicate -Mode_Set option");
    mode_set = (n & 0777);
}


void
tardy_mode_clear(int n)
{
    if (mode_clear != NOT_SET)
        explain_output_error_and_die("duplicate -Mode_Clear option");
    mode_clear = (n & 07777);
}


void
tardy_list(void)
{
    if (list)
        explain_output_error_and_die("duplicate -List option");
    list = 1;
}


void
tardy_now(void)
{
    if (when != (time_t)-1)
        explain_output_error_and_die("duplicate -Now option");
    time(&when);
}


void
tardy_mtime(time_t value)
{
    if (when != (time_t)-1)
        explain_output_error_and_die("duplicate -MTime option");
    when = value;
}


void
tardy_no_directories(void)
{
    if (no_directories)
        explain_output_error_and_die("duplicate -No_Directories option");
    no_directories = 1;
}


void
tardy_clean_space(void)
{
    if (clean & tar_input_filter_clean::flag_space)
        explain_output_error_and_die("duplicate -Clean_Space option");
    clean |= tar_input_filter_clean::flag_space;
}


void
tardy_clean_print(void)
{
    if (clean & tar_input_filter_clean::flag_print)
        explain_output_error_and_die("duplicate -Clean_Print option");
    clean |= tar_input_filter_clean::flag_print;
}


void
tardy_clean_meta(void)
{
    if (clean & tar_input_filter_clean::flag_shell)
        explain_output_error_and_die("duplicate -Clean_Meta option");
    clean |= tar_input_filter_clean::flag_shell;
}


static void
check_case(void)
{
    if
    (
        (clean & tar_input_filter_clean::flag_up)
    &&
        (clean & tar_input_filter_clean::flag_down)
    )
    {
        explain_output_error_and_die
        (
            "the -DownCase and -UpCase options are mutually exclusive"
        );
    }
}


void
tardy_downcase(void)
{
    if (clean & tar_input_filter_clean::flag_down)
        explain_output_error_and_die("duplicate -DownCase option");
    clean |= tar_input_filter_clean::flag_down;
    check_case();
}


void
tardy_upcase(void)
{
    if (clean & tar_input_filter_clean::flag_up)
        explain_output_error_and_die("duplicate -UpCase option");
    clean |= tar_input_filter_clean::flag_up;
    check_case();
}


void
tardy_gzip(void)
{
    compress_flag = true;
}


void
tardy_gunzip(void)
{
    decompress_flag = true;
}


void
tardy_blocksize(int n)
{
    blocksize = (n > 0 ? (n << 9) : 0);
}


void
tardy_exclude(const char *pattern)
{
    exclude_list.push_back(rcstring(pattern));
}


void
tardy_hexdump(void)
{
    if (hexdump)
        explain_output_error_and_die("duplicate -HexDump option");
    hexdump = true;
}


void
tardy_relative_paths(void)
{
    if (relative_paths)
        explain_output_error_and_die("duplicate -RELative-paths option");
    relative_paths = true;
}
