/*
 * Command line utility for Base 64
 *
 * SPDX-FileType: SOURCE
 * SPDX-FileCopyrightText: Michael Bäuerle
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "libbasexx-0/base64_encode.h"
#include "libbasexx-0/base64_decode.h"
#include "libbasexx-0/basexx_ebcdic.h"
#include "libbasexx-0/basexx_version.h"

#include "bxx0_cli_private.h"


/*
 * For every three input bytes four output bytes are generated.
 * Four additonal bytes are created if padding is required.
 *
 * Input buffer size must be a multiple of 3 to get padding only at the end.
 * Input buffer size must be a multiple of 4 too for decode mode.
 */
#define BXX0_I_CLI_INBUF_SIZE  7680U

/*
 * Output buffer size calculation is for encode mode.
 * Data size is always smaller for decode mode.
 */
#define BXX0_I_CLI_OUTBUF_SIZE  (((BXX0_I_CLI_INBUF_SIZE / 3U) * 4U) + 4U)

/* Wrap buffer must be large enough for LF after every character */
#define BXX0_I_CLI_WRAPBUF_SIZE  (BXX0_I_CLI_OUTBUF_SIZE * 2U)


/* Data type for command line options */
typedef struct
{
    const char   *pathname;  /* For "file" */
    unsigned int  wrap;      /* For "-w" option */
    bxx0_i_bool   decode;    /* For "-d" option */
    bxx0_i_bool   ebcdic;    /* For "-c" option */
    bxx0_i_bool   ignore;    /* For "-i" option */
}
bxx0_i_cli_opt;


static bxx0_i_cli_opt bxx0_i_cli_options;

static unsigned char bxx0_i_cli_inbuf[BXX0_I_CLI_INBUF_SIZE];
static unsigned char bxx0_i_cli_outbuf[BXX0_I_CLI_OUTBUF_SIZE];
static unsigned char bxx0_i_cli_wrapbuf[BXX0_I_CLI_WRAPBUF_SIZE];

static size_t              bxx0_i_cli_wrap_pos  = 0;
static const unsigned char bxx0_i_cli_wrap_char = 0x0A;  /* LF */


/* ========================================================================== */
/* Print version of program "name" */
static void bxx0_i_cli_print_version(const char *name)
{
    printf("%s %s\n", name, bxx0_version());
}


/* ========================================================================== */
/* Print help for program "name" */
static void
bxx0_i_cli_print_help(const char *name)
{
    printf("Usage: %s [options] [file]\n", name);
    printf("Options:\n");
    printf("   -b num  Same as \"-w\" for compatibility with FreeBSD\n");
    printf("   -c      Convert from/to EBCDIC (default is ASCII)\n");
    printf("   -d      Decode mode\n");
    printf("   -e      Encode mode (default)\n");
    printf("   -i      Ignore all Non-Alphabet characters in decode mode\n");
    printf("           (nonzero padding bits are ignored too)\n");
    printf("   -w num  Add line breaks after \"num\" characters in "
           "encode mode\n");
    printf("           The value must be of type \"unsigned int\"\n");
    printf("           (default is 76 characters)\n");
    printf("   -h      Print this help message to stdout and exit\n");
    printf("   -v      Print version information to stdout and exit\n");
    printf("   --      POSIX option list terminator\n\n");
    printf("If \"file\" is \"-\" or not specified, stdin is used.\n");
    printf("(See manual page for details)\n");
}


/* ========================================================================== */
/*
 * Parse and store command line options for conversion.
 * Returns zero on success.
 */
static bxx0_i_bool bxx0_i_cli_parse_options(int argc, char **argv)
{
    size_t      opt_num  = argc;
    bxx0_i_bool opt_wrap = 0;
    size_t      i        = 1;

    /* Default value for line wrapping in encode mode */
    bxx0_i_cli_options.wrap = 76;

    for ( ; opt_num > i; ++i)
    {
        if (opt_wrap)
        {
            if (1 != sscanf(argv[i], "%u", &bxx0_i_cli_options.wrap))
            {
                fprintf(stderr, "Invalid value for \"-w\" option\n");
                return 1;
            }
            opt_wrap = 0;
            continue;
        }

        if (!strcmp("-b", argv[i]))
            opt_wrap = 1;
        else if (!strcmp("-c", argv[i]))
            bxx0_i_cli_options.ebcdic = 1;
        else if (!strcmp("-d", argv[i]))
            bxx0_i_cli_options.decode = 1;
        else if (!strcmp("-e", argv[i]))
            bxx0_i_cli_options.decode = 0;
        else if (!strcmp("-i", argv[i]))
            bxx0_i_cli_options.ignore = 1;
        else if (!strcmp("-w", argv[i]))
            opt_wrap = 1;
        else if (!strcmp("--", argv[i]))
        {
            /* POSIX conformant explicit option list termination */
            ++i;
            break;
        }
        else if (('-' == argv[i][0]) && (0 != argv[i][1]))
        {
            fprintf(stderr, "Invalid option\n");
            fprintf(stderr, "Try \"-h\" for help\n");
            return 1;
        }
        else
            break;
    }

    /* First non-option is accepted as "file" operand */
    if (opt_num > i)
    {
        /* "-" must be treated as standard input */
        if (strcmp("-", argv[i]))
            bxx0_i_cli_options.pathname = argv[i];
    }

    return 0;
}


/* ========================================================================== */
/* Get wrap character */
static unsigned char bxx0_i_cli_get_wrap_character(void)
{
    unsigned char wc = bxx0_i_cli_wrap_char;

    if (bxx0_i_cli_options.ebcdic)
        bxx0_ebcdic_to(&wc, 1);

    return wc;
}


/* ========================================================================== */
/*
 * Wrap output data into lines (if requested) and write to stdout.
 * Returns zero on succes.
 */
static bxx0_i_bool bxx0_i_cli_flush(size_t len_out)
{
    if (0U == bxx0_i_cli_options.wrap)
    {
        (void)fwrite(bxx0_i_cli_outbuf, 1U, len_out, stdout);
        if (ferror(stdout))
            return 1;
    }
    else
    {
        static size_t pos    = 0;  /* Position in current line */
        size_t        i_out  = 0;  /* Index in output buffer */
        size_t        i_wrap = 0;  /* Index in line wrap buffer */

        while (len_out > i_out)
        {
            size_t remain = len_out - i_out;

            assert(bxx0_i_cli_options.wrap > pos);

            {
                size_t len = bxx0_i_cli_options.wrap - pos;

                if (remain < len)
                    len = remain;

                assert((BXX0_I_CLI_WRAPBUF_SIZE - i_wrap) >= len);
                (void)memcpy(&bxx0_i_cli_wrapbuf[i_wrap],
                             &bxx0_i_cli_outbuf[i_out], len);
                i_out  += len;
                i_wrap += len;
                pos    += len;
            }

            if (bxx0_i_cli_options.wrap == pos)
            {
                assert((BXX0_I_CLI_WRAPBUF_SIZE - i_wrap) >= 1U);
                bxx0_i_cli_wrapbuf[i_wrap++] = bxx0_i_cli_get_wrap_character();
                pos = 0;
            }
        }
        bxx0_i_cli_wrap_pos = pos;

        (void)fwrite(bxx0_i_cli_wrapbuf, 1U, i_wrap, stdout);
        if (ferror(stdout))
            return 1;
    }

    return 0;
}


/* ========================================================================== */
/*
 * Read intput data and convert it in chunks of BXX0_I_CLI_INBUF_SIZE.
 * Returns exit status for main().
 */
static int bxx0_i_cli_encode(FILE *instream)
{
    bxx0_i_bool data = 0;

    while (!feof(instream))
    {
        size_t len_out = BXX0_I_CLI_OUTBUF_SIZE;
        size_t len_in  = fread(bxx0_i_cli_inbuf, 1U, BXX0_I_CLI_INBUF_SIZE,
                               instream);

        if (ferror(instream))
        {
            fprintf(stderr, "Error while reading data\n");
            return 1;
        }

        if (0U != len_in)
        {
            signed char rv = bxx0_base64_encode(bxx0_i_cli_outbuf, &len_out,
                                                bxx0_i_cli_inbuf,  &len_in, 0);
            const size_t n = BXX0_I_CLI_OUTBUF_SIZE - len_out;

            if (0 > rv)
            {
                fprintf(stderr, "Error while processing data\n");
                switch (rv)
                {
                    case BXX0_BASE64_ENCODE_ERROR_SIZE:
                        fprintf(stderr, "Output buffer too small\n");
                        break;
                    default:
                        fprintf(stderr, "Unknown reason\n");
                }
                return 1;
            }

            if (bxx0_i_cli_options.ebcdic)
                bxx0_ebcdic_to(bxx0_i_cli_outbuf, n);

            if (0 != bxx0_i_cli_flush(n))
            {
                fprintf(stderr, "Error while writing data\n");
                return 1;
            }
            else
                data = 1;
        }
    }

    if (data && (0U != bxx0_i_cli_options.wrap) && (0U != bxx0_i_cli_wrap_pos))
    {
        if (EOF == putc(bxx0_i_cli_get_wrap_character(), stdout))
        {
            fprintf(stderr, "Error while writing data\n");
            return 1;
        }
    }

    return 0;
}


/* ========================================================================== */
/*
 * Read intput data and convert it in chunks of BXX0_I_CLI_INBUF_SIZE.
 * Returns exit status for main().
 */
static int bxx0_i_cli_decode(FILE *instream)
{
    unsigned int flags   = 0;
    size_t       len_req = BXX0_I_CLI_INBUF_SIZE;
    size_t       i_in    = 0;  /* Index in input buffer */

    /* Ignore Non-Alphabet characters and nonzero padding bits on request */
    if (bxx0_i_cli_options.ignore)
        flags = BXX0_BASE64_DECODE_FLAG_IGNORE  |
                BXX0_BASE64_DECODE_FLAG_INVTAIL |
                BXX0_BASE64_DECODE_FLAG_CONCAT;

    while (!feof(instream))
    {
        size_t len_feed = fread(&bxx0_i_cli_inbuf[i_in], 1U, len_req, instream);

        if (ferror(instream))
        {
            fprintf(stderr, "Error while reading data\n");
            return 1;
        }

        if (bxx0_i_cli_options.ebcdic)
            bxx0_ebcdic_from(&bxx0_i_cli_inbuf[i_in], len_feed);

        if (0U != len_feed)
        {
            size_t len_in  = i_in + len_feed;
            size_t len_out = BXX0_I_CLI_OUTBUF_SIZE;
            signed char rv = 0;

            assert(BXX0_I_CLI_INBUF_SIZE >= len_in);
            rv = bxx0_base64_decode(bxx0_i_cli_outbuf, &len_out,
                                    bxx0_i_cli_inbuf,  &len_in, flags);
            if (0 > rv)
            {
                fprintf(stderr, "Error while processing data\n");
                switch (rv)
                {
                    case BXX0_BASE64_DECODE_ERROR_SIZE:
                        fprintf(stderr, "Output buffer too small\n");
                        break;
                    case BXX0_BASE64_DECODE_ERROR_NAC:
                        fprintf(stderr, "Non-Alphabet character\n");
                        break;
                    case BXX0_BASE64_DECODE_ERROR_TAIL:
                        fprintf(stderr, "Invalid tail before padding\n");
                        break;
                    case BXX0_BASE64_DECODE_ERROR_PAD:
                        fprintf(stderr, "Invalid padding\n");
                        break;
                    case BXX0_BASE64_DECODE_ERROR_DAP:
                        fprintf(stderr, "Data after padding\n");
                        break;
                    default:
                        fprintf(stderr, "Unknown reason\n");
                }
                return 1;
            }

            (void)fwrite(bxx0_i_cli_outbuf,
                         1U, BXX0_I_CLI_OUTBUF_SIZE - len_out, stdout);
            if (ferror(stdout))
            {
                fprintf(stderr, "Error while writing data\n");
                return 1;
            }

            /* Prepare next feed */
            len_req = BXX0_I_CLI_INBUF_SIZE;
            i_in    = 0;
            if (0 != len_in)
            {
                /* Copy unconsumed data to beginning of input buffer */
                assert(len_feed >= len_in);
                (void)memmove(bxx0_i_cli_inbuf,
                              &bxx0_i_cli_inbuf[len_feed - len_in], len_in);
                len_req -= len_in;
                i_in     = len_in;
            }
        }
    }

    return 0;
}


/* ========================================================================== */
int main(int argc, char **argv)
{
    FILE       *instream = stdin;
    const char *name     = strrchr(argv[0], '/');

    /* Use argv[0] as program name, if there is no '/' was found */
    if (NULL == name)
        name = argv[0];
    else
        name = &name[1];

    /* Print version */
    if (2 == argc && !strcmp("-v", argv[1]))
    {
        bxx0_i_cli_print_version(name);
        return 0;
    }

    /* Print help */
    if (2 == argc && !strcmp("-h", argv[1]))
    {
        bxx0_i_cli_print_help(name);
        return 0;
    }

    /* Parse and store options for conversion */
    bxx0_i_cli_options.wrap = 76;
    if (bxx0_i_cli_parse_options(argc, argv))
        return 1;

    /* Prepare input file stream */
    if (NULL != bxx0_i_cli_options.pathname)
    {
        instream = fopen(bxx0_i_cli_options.pathname, "rb");
        if (NULL == instream)
        {
            perror("fopen");
            fprintf(stderr, "Failed to open input file\n");
            return 1;
        }
    }

    /* Execute conversion */
    if (bxx0_i_cli_options.decode)
        return bxx0_i_cli_decode(instream);
    else
        return bxx0_i_cli_encode(instream);
}
