/*
 * OptiPNG: Advanced PNG optimization program.
 * http://optipng.sourceforge.net/
 *
 * Copyright (C) 2001-2017 Cosmin Truta and the Contributing Authors.
 *
 * This software is distributed under the zlib license.
 * Please see the accompanying LICENSE file.
 *
 * PNG optimization is described in detail in the PNG-Tech article
 * "A guide to PNG optimization"
 * http://optipng.sourceforge.net/pngtech/png_optimization.html
 *
 * The idea of running multiple compression trials with different
 * PNG filters and zlib parameters is inspired from the pngcrush
 * program by Glenn Randers-Pehrson.
 * The idea of performing lossless image reductions is inspired
 * from the pngrewrite program by Jason Summers.
 */

#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "optipng.h"
#include "proginfo.h"

#include "bitset.h"
#include "png.h"
#include "pngxutil.h"
#include "zlib.h"


static const char *msg_intro =
    PROGRAM_NAME " version " PROGRAM_VERSION "\n"
    PROGRAM_COPYRIGHT ".\n";

static const char *msg_license =
    "This program is open-source software. See LICENSE for more details.\n"
    "\n"
    "Portions of this software are based in part on the work of:\n"
    "  Jean-loup Gailly and Mark Adler (zlib)\n"
    "  Glenn Randers-Pehrson and the PNG Development Group (libpng)\n"
    "  Miyasaka Masaru (BMP support)\n"
    "  David Koblas (GIF support)\n";

static const char *msg_help_synopsis =
    "Synopsis:\n"
    "    optipng [options] files ...\n"
    "Files:\n"
    "    Image files of type: PNG, BMP, GIF, PNM or TIFF\n";

static const char *msg_help_basic_options =
    "Basic options:\n"
    "    -?, -h, -help\tshow the extended help\n"
    "    -o <level>\t\toptimization level (0-7)\t\t[default: 2]\n"
    "    -v\t\t\trun in verbose mode / show copyright and version info\n";

static const char *msg_help_options =
    "Basic options:\n"
    "    -?, -h, -help\tshow this help\n"
    "    -o <level>\t\toptimization level (0-7)\t\t[default: 2]\n"
    "    -v\t\t\trun in verbose mode / show copyright and version info\n"
    "General options:\n"
    "    -backup, -keep\tkeep a backup of the modified files\n"
    "    -clobber\t\toverwrite existing files\n"
#if 0  /* internal */
    "    -debug\t\tenable debug features\n"
#endif
    "    -fix\t\tenable error recovery\n"
    "    -force\t\tenforce writing of a new output file\n"
    "    -preserve\t\tpreserve file attributes if possible\n"
    "    -quiet, -silent\trun in quiet mode\n"
    "    -simulate\t\trun in simulation mode\n"
    "    -out <file>\t\twrite output file to <file>\n"
    "    -dir <directory>\twrite output file(s) to <directory>\n"
    "    -log <file>\t\tlog messages to <file>\n"
    "    --\t\t\tstop option switch parsing\n"
    "Optimization options:\n"
    "    -f <filters>\tPNG delta filters (0-5)\t\t\t[default: 0,5]\n"
    "    -i <type>\t\tPNG interlace type (0-1)\n"
    "    -zc <levels>\tzlib compression levels (1-9)\t\t[default: 9]\n"
    "    -zm <levels>\tzlib memory levels (1-9)\t\t[default: 8]\n"
    "    -zs <strategies>\tzlib compression strategies (0-3)\t[default: 0-3]\n"
    "    -zw <size>\t\tzlib window size (256,512,1k,2k,4k,8k,16k,32k)\n"
    "    -full\t\tproduce a full report on IDAT (might reduce speed)\n"
    "    -nb\t\t\tno bit depth reduction\n"
    "    -nc\t\t\tno color type reduction\n"
    "    -np\t\t\tno palette reduction\n"
    "    -nx\t\t\tno reductions\n"
    "    -nz\t\t\tno IDAT recoding\n"
    "Editing options:\n"
    "    -snip\t\tcut one image out of multi-image or animation files\n"
    "    -strip <objects>\tstrip metadata objects (e.g. \"all\")\n"
    "Optimization levels:\n"
    "    -o0\t\t<=>\t-o1 -nx -nz\t\t\t\t(0 or 1 trials)\n"
    "    -o1\t\t<=>\t-zc9 -zm8 -zs0 -f0\t\t\t(1 trial)\n"
    "    \t\t(or...)\t-zc9 -zm8 -zs1 -f5\t\t\t(1 trial)\n"
    "    -o2\t\t<=>\t-zc9 -zm8 -zs0-3 -f0,5\t\t\t(8 trials)\n"
    "    -o3\t\t<=>\t-zc9 -zm8-9 -zs0-3 -f0,5\t\t(16 trials)\n"
    "    -o4\t\t<=>\t-zc9 -zm8 -zs0-3 -f0-5\t\t\t(24 trials)\n"
    "    -o5\t\t<=>\t-zc9 -zm8-9 -zs0-3 -f0-5\t\t(48 trials)\n"
    "    -o6\t\t<=>\t-zc1-9 -zm8 -zs0-3 -f0-5\t\t(120 trials)\n"
    "    -o7\t\t<=>\t-zc1-9 -zm8-9 -zs0-3 -f0-5\t\t(240 trials)\n"
    "    -o7 -zm1-9\t<=>\t-zc1-9 -zm1-9 -zs0-3 -f0-5\t\t(1080 trials)\n"
    "Notes:\n"
    "    The combination for -o1 is chosen heuristically.\n"
    "    Exhaustive combinations such as \"-o7 -zm1-9\" are not generally recommended.\n";

static const char *msg_help_examples =
    "Examples:\n"
    "    optipng file.png\t\t\t\t\t\t(default speed)\n"
    "    optipng -o5 file.png\t\t\t\t\t(slow)\n"
    "    optipng -o7 file.png\t\t\t\t\t(very slow)\n";

static const char *msg_help_more =
    "Type \"optipng -h\" for extended help.\n";


static enum
{
    OP_RUN,
    OP_SHOW_HELP,
    OP_SHOW_VERSION
} operation;

static struct
{
    int help;
    int version;
} local_options;

static struct opng_options options;

static FILE *con_file;
static FILE *log_file;

static int start_of_line;


/*
 * Error handling.
 */
static void
error(const char *fmt, ...)
{
    va_list arg_ptr;

    /* Print the error message to stderr and exit. */
    fprintf(stderr, "** Error: ");
    va_start(arg_ptr, fmt);
    vfprintf(stderr, fmt, arg_ptr);
    va_end(arg_ptr);
    fprintf(stderr, "\n");
    exit(EXIT_FAILURE);
}

/*
 * Panic handling.
 */
static void
panic(const char *msg)
{
    /* Print the panic message to stderr and terminate abnormally. */
    fprintf(stderr, "\n** INTERNAL ERROR: %s\n", msg);
    fprintf(stderr, "Please submit a defect report.\n" PROGRAM_URI "\n\n");
    fflush(stderr);
    if (options.debug)
    {
        /* Terminate abnormally, possibly with a stack trace or a core dump. */
        abort();
    }
    else
    {
        /* Terminate abnormally, cleanly. */
        fprintf(stderr,
            "The execution of this program has been terminated abnormally.\n");
        exit(70);  /* EX_SOFTWARE */
    }
}

/*
 * String utility.
 */
static int
opng_strcasecmp(const char *str1, const char *str2)
{
    int ch1, ch2;

    /* Perform a case-insensitive string comparison. */
    for ( ; ; )
    {
        ch1 = tolower(*str1++);
        ch2 = tolower(*str2++);
        if (ch1 != ch2)
            return ch1 - ch2;
        if (ch1 == 0)
            return 0;
    }
    /* FIXME: This function is not MBCS-aware. */
}

/*
 * String utility.
 */
static char *
opng_strltrim(const char *str)
{
    /* Skip the leading whitespace characters. */
    while (isspace(*str))
        ++str;
    return (char *)str;
}

/*
 * String utility.
 */
static char *
opng_strtail(const char *str, size_t num)
{
    size_t len;

    /* Return up to num rightmost characters. */
    len = strlen(str);
    if (len <= num)
        return (char *)str;
    return (char *)str + len - num;
}

/*
 * String utility.
 */
static char *
opng_strpbrk_digit(const char *str)
{
    for ( ; ; )
    {
        if (*str == 0)
            return NULL;
        if (isdigit(*str))
            return (char *)str;
        ++str;
    }
}

/*
 * String conversion utility.
 */
static int
opng_str2ulong(unsigned long *out_val, const char *in_str,
               int allow_multiplier)
{
    const char *begin_ptr;
    char *end_ptr;
    unsigned long multiplier;

    /* Extract the value from the string. */
    /* Do not allow the minus sign, not even for -0. */
    begin_ptr = end_ptr = opng_strltrim(in_str);
    if (*begin_ptr >= '0' && *begin_ptr <= '9')
        *out_val = strtoul(begin_ptr, &end_ptr, 10);
    if (begin_ptr == end_ptr)
    {
        errno = EINVAL;  /* matching failure */
        *out_val = 0;
        return -1;
    }

    if (allow_multiplier)
    {
        /* Check for the following SI suffixes:
         *   'K' or 'k': kibi (1024)
         *   'M':        mebi (1024 * 1024)
         *   'G':        gibi (1024 * 1024 * 1024)
         */
        if (*end_ptr == 'k' || *end_ptr == 'K')
        {
            ++end_ptr;
            multiplier = 1024UL;
        }
        else if (*end_ptr == 'M')
        {
            ++end_ptr;
            multiplier = 1024UL * 1024UL;
        }
        else if (*end_ptr == 'G')
        {
            ++end_ptr;
            multiplier = 1024UL * 1024UL * 1024UL;
        }
        else
            multiplier = 1;
        if (multiplier > 1)
        {
            if (*out_val > ULONG_MAX / multiplier)
            {
                errno = ERANGE;  /* overflow */
                *out_val = ULONG_MAX;
            }
            else
                *out_val *= multiplier;
        }
    }

    /* Check for trailing garbage. */
    if (*opng_strltrim(end_ptr) != 0)
    {
        errno = EINVAL;  /* garbage in input */
        return -1;
    }
    return 0;
}

/*
 * Command line utility.
 */
static void
err_option_arg(const char *opt, const char *opt_arg)
{
    /* Issue an error regarding the incorrect value of the option argument. */
    if (opt_arg == NULL || *opng_strltrim(opt_arg) == 0)
        error("Missing argument for option %s", opt);
    else
        error("Invalid argument for option %s: %s", opt, opt_arg);
}

/*
 * Command line utility.
 */
static int
check_num_option(const char *opt, const char *opt_arg,
                 int lowest, int highest)
{
    unsigned long value;

    /* Extract the numeric value from the option argument. */
    if (opng_str2ulong(&value, opt_arg, 0) != 0 ||
        value > INT_MAX || (int)value < lowest || (int)value > highest)
        err_option_arg(opt, opt_arg);
    return (int)value;
}

/*
 * Command line utility.
 */
static int
check_power2_option(const char *opt, const char *opt_arg,
                    int lowest, int highest)
{
    unsigned long value;
    int result;

    /* Extract the exact log2 of the numeric value from the option argument. */
    /* Allow the 'k', 'M', 'G' suffixes. */
    if (opng_str2ulong(&value, opt_arg, 1) == 0)
    {
        if (lowest < 0)
            lowest = 0;
        if (highest > (int)(CHAR_BIT * sizeof(long) - 2))
            highest = (int)(CHAR_BIT * sizeof(long) - 2);
        for (result = lowest; result <= highest; ++result)
        {
            if ((1UL << result) == value)
                return result;
        }
    }
    err_option_arg(opt, opt_arg);
    return -1;
}

/*
 * Command line utility.
 */
static opng_bitset_t
check_rangeset_option(const char *opt, const char *opt_arg,
                      opng_bitset_t result_mask)
{
    opng_bitset_t result;

    /* Extract the rangeset from the option argument.
     * Accept only non-empty rangesets that fit in the given range.
     */
    if (opng_strparse_rangeset_to_bitset(&result, opt_arg, result_mask) != 0)
        result = OPNG_BITSET_EMPTY;
    if ((result & result_mask) != result)
        result = OPNG_BITSET_EMPTY;
    if (result == OPNG_BITSET_EMPTY)
        err_option_arg(opt, opt_arg);
    return result;
}

/*
 * Command line utility.
 */
static void
check_obj_option(const char *opt, const char *opt_arg)
{
    unsigned int i;

    if (strcmp("all", opt_arg) == 0)
        return;

    /* Issue an error about the unrecognized option argument. */
    /* Make it specific on whether this argument is a chunk name. */
    for (i = 0; i < 4; ++i)
    {
        /* Do not use isalpha(), because it is locale-dependent. */
        if (!((opt_arg[i] >= 'A' && opt_arg[i] <= 'Z') ||
              (opt_arg[i] >= 'a' && opt_arg[i] <= 'z')))
            break;
    }
    if (i == 4 && opt_arg[i] == 0)
        error("Manipulation of individual chunks is not implemented");
    else
        err_option_arg(opt, opt_arg);
}

/*
 * Command line parsing.
 */
static int
scan_option(const char *str,
            char opt_buf[], size_t opt_buf_size, const char **opt_arg_ptr)
{
    const char *ptr;
    unsigned int opt_len;

    /* Check if arg is an "-option". */
    if (str[0] != '-' || str[1] == 0)  /* no "-option", or just "-" */
        return 0;

    /* Extract the normalized option, and possibly the option argument. */
    opt_len = 0;
    ptr = str + 1;
    while (*ptr == '-')  /* "--option", "---option", etc. */
        ++ptr;
    if (*ptr == 0)  /* "--" */
        --ptr;
    for ( ; ; )
    {
        if (opt_len < opt_buf_size)  /* truncate "-verylongoption" */
            opt_buf[opt_len] = (char)tolower(*ptr);
        ++opt_len;
        ++ptr;
        if (*ptr == 0 || isspace(*ptr))  /* "-option" or "-option arg" */
        {
            while (isspace(*ptr))
                ++ptr;
            *opt_arg_ptr = (*ptr != 0) ? ptr : NULL;
            break;
        }
        if (*ptr == '=')  /* "-option=arg" */
        {
            ++ptr;
            *opt_arg_ptr = ptr;
            break;
        }
    }

    /* Finalize the normalized option. */
    if (opt_buf_size > 0)
    {
        if (opt_len < opt_buf_size)
            opt_buf[opt_len] = '\0';
        else
            opt_buf[opt_buf_size - 1] = '\0';
    }
    return 1;
}

/*
 * Command line parsing.
 */
static void
parse_args(int argc, char *argv[])
{
    char *arg;
    char opt[16];
    size_t opt_len;
    const char *xopt;
    int simple_opt, stop_switch;
    opng_bitset_t set;
    int val;
    unsigned int file_count;
    int i;

    /* Initialize. */
    memset(&options, 0, sizeof(options));
    options.optim_level = -1;
    options.interlace = -1;
    file_count = 0;

    /* Iterate over args. */
    stop_switch = 0;
    for (i = 1; i < argc; ++i)
    {
        arg = argv[i];
        if (stop_switch || scan_option(arg, opt, sizeof(opt), &xopt) < 1)
        {
            ++file_count;
            continue;  /* leave file names for process_files() */
        }
        opt_len = strlen(opt);

        /* Prevent process_files() from seeing this arg. */
        argv[i] = NULL;

        /* Normalize the options that allow juxtaposed arguments. */
        if ((strchr("fio", opt[0]) != NULL && isdigit(opt[1])) ||
            (opt[0] == 'z' && isalpha(opt[1]) && isdigit(opt[2])))
        {
            /* -f0-5 <=> -f=0-5; -i1 <=> -i=1; -o3 <=> -o=3;
             * -zc3-9 <=> -zc=3-9; etc.
             */
            opt_len = (size_t)(opng_strpbrk_digit(opt) - opt);
            opt[opt_len] = '\0';
            xopt = opng_strpbrk_digit(arg);
        }

        /* Check the simple options (without option arguments). */
        simple_opt = 1;
        if (strcmp("-", opt) == 0)
        {
            /* -- */
            stop_switch = 1;
        }
        else if (strcmp("?", opt) == 0 ||
                 strncmp("help", opt, opt_len) == 0)
        {
            /* -? | -h | ... | -help */
            local_options.help = 1;
        }
        else if ((strncmp("backup", opt, opt_len) == 0) ||
                 (strncmp("keep", opt, opt_len) == 0))
        {
            /* -b | ... | -backup | -k | ... | -keep */
            options.backup = 1;
        }
        else if (strncmp("clobber", opt, opt_len) == 0)
        {
            /* -c | ... | -clobber */
            options.clobber = 1;
        }
        else if (strcmp("debug", opt) == 0)
        {
            /* -debug */
            /* Do not abbreviate this internal option. */
            options.debug = 1;
        }
        else if (strncmp("fix", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -fi | -fix */
            options.fix = 1;
        }
        else if (strncmp("force", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -fo | ... | -force */
            options.force = 1;
        }
        else if (strncmp("full", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -fu | ... | -full */
            options.full = 1;
        }
        else if (strcmp("nb", opt) == 0)
        {
            /* -nb */
            options.nb = 1;
        }
        else if (strcmp("nc", opt) == 0)
        {
            /* -nc */
            options.nc = 1;
        }
        else if (strcmp("np", opt) == 0)
        {
            /* -np */
            options.np = 1;
        }
        else if (strcmp("nx", opt) == 0)
        {
            /* -nx */
            options.nb = options.nc = options.np = 1;
            /* options.nm = 1; */
        }
        else if (strcmp("nz", opt) == 0)
        {
            /* -nz */
            options.nz = 1;
        }
        else if (strncmp("preserve", opt, opt_len) == 0)
        {
            /* -p | ... | -preserve */
            options.preserve = 1;
        }
        else if ((strncmp("quiet", opt, opt_len) == 0) ||
                 (strncmp("silent", opt, opt_len) == 0 && opt_len >= 3))
        {
            /* -q | ... | -quiet | -sil | ... | -silent */
            options.quiet = 1;
        }
        else if (strncmp("simulate", opt, opt_len) == 0 && opt_len >= 3)
        {
            /* -sim | ... | -simulate */
            options.simulate = 1;
        }
        else if (strncmp("snip", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -sn | ... | -snip */
            options.snip = 1;
        }
        else if (strcmp("v", opt) == 0)
        {
            /* -v */
            options.verbose = 1;
            local_options.version = 1;
        }
        else if (strncmp("verbose", opt, opt_len) == 0 && opt_len >= 4)
        {
            /* -verb | ... | -verbose */
            options.verbose = 1;
        }
        else if (strncmp("version", opt, opt_len) == 0 && opt_len >= 4)
        {
            /* -vers | ... | -version */
            local_options.version = 1;
        }
        else  /* possibly an option with an argument */
        {
            simple_opt = 0;
            if (xopt == NULL)
            {
                if (++i < argc)
                {
                    xopt = argv[i];
                    /* Prevent process_files() from seeing this xopt. */
                    argv[i] = NULL;
                }
                else
                {
                    /* Last option in command line; assume an empty xopt. */
                    xopt = "";
                }
            }
        }

        /* Check the options that have option arguments. */
        if (simple_opt)
        {
            if (xopt != NULL)
                error("No argument allowed for option: %s", arg);
        }
        else if (strcmp("o", opt) == 0)
        {
            /* -o NUM */
            val = check_num_option("-o", xopt, 0, INT_MAX);
            if (options.optim_level < 0)
                options.optim_level = val;
            else if (options.optim_level != val)
                error("Multiple optimization levels are not permitted");
        }
        else if (strcmp("i", opt) == 0)
        {
            /* -i NUM */
            val = check_num_option("-i", xopt, 0, 1);
            if (options.interlace < 0)
                options.interlace = val;
            else if (options.interlace != val)
                error("Multiple interlace types are not permitted");
        }
        else if (strcmp("f", opt) == 0)
        {
            /* -f SET */
            set = check_rangeset_option("-f", xopt, OPNG_FILTER_SET_MASK);
            options.filter_set |= set;
        }
        else if (strcmp("zc", opt) == 0)
        {
            /* -zc SET */
            set = check_rangeset_option("-zc", xopt, OPNG_COMPR_LEVEL_SET_MASK);
            options.compr_level_set |= set;
        }
        else if (strcmp("zm", opt) == 0)
        {
            /* -zm SET */
            set = check_rangeset_option("-zm", xopt, OPNG_MEM_LEVEL_SET_MASK);
            options.mem_level_set |= set;
        }
        else if (strcmp("zs", opt) == 0)
        {
            /* -zs SET */
            set = check_rangeset_option("-zs", xopt, OPNG_STRATEGY_SET_MASK);
            options.strategy_set |= set;
        }
        else if (strcmp("zw", opt) == 0)
        {
            /* -zw NUM */
            val = check_power2_option("-zw", xopt, 8, 15);
            if (options.window_bits == 0)
                options.window_bits = val;
            else if (options.window_bits != val)
                error("Multiple window sizes are not permitted");
        }
        else if (strncmp("strip", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -st OBJ | ... | -strip OBJ */
            check_obj_option("-strip", xopt);
            options.strip_all = 1;
        }
        else if (strncmp("out", opt, opt_len) == 0 && opt_len >= 2)
        {
            /* -ou PATH | -out PATH */
            if (options.out_name != NULL)
                error("Multiple output file names are not permitted");
            if (xopt[0] == 0)
                err_option_arg("-out", NULL);
            options.out_name = xopt;
        }
        else if (strncmp("dir", opt, opt_len) == 0)
        {
            /* -d PATH | ... | -dir PATH */
            if (options.dir_name != NULL)
                error("Multiple output dir names are not permitted");
            if (xopt[0] == 0)
                err_option_arg("-dir", NULL);
            options.dir_name = xopt;
        }
        else if (strncmp("log", opt, opt_len) == 0)
        {
            /* -l PATH | ... | -log PATH */
            if (options.log_name != NULL)
                error("Multiple log file names are not permitted");
            if (xopt[0] == 0)
                err_option_arg("-log", NULL);
            options.log_name = xopt;
        }
        else
        {
            error("Unrecognized option: %s", arg);
        }
    }

    /* Finalize. */
    if (options.out_name != NULL)
    {
        if (file_count > 1)
            error("The option -out requires one input file");
        if (options.dir_name != NULL)
            error("The options -out and -dir are mutually exclusive");
    }
    if (options.log_name != NULL)
    {
        if (opng_strcasecmp(".log", opng_strtail(options.log_name, 4)) != 0)
            error("To prevent accidental data corruption, "
                  "the log file name must end with \".log\"");
    }
    if (local_options.help)
        operation = OP_SHOW_HELP;
    else if (file_count != 0)
        operation = OP_RUN;
    else if (local_options.version)
        operation = OP_SHOW_VERSION;
    else
        operation = OP_SHOW_HELP;
}

/*
 * Application-defined printf callback.
 */
static void
app_printf(const char *fmt, ...)
{
    va_list arg_ptr;

    if (fmt[0] == 0)
        return;
    start_of_line = (fmt[strlen(fmt) - 1] == '\n') ? 1 : 0;

    if (con_file != NULL)
    {
        va_start(arg_ptr, fmt);
        vfprintf(con_file, fmt, arg_ptr);
        va_end(arg_ptr);
    }
    if (log_file != NULL)
    {
        va_start(arg_ptr, fmt);
        vfprintf(log_file, fmt, arg_ptr);
        va_end(arg_ptr);
    }
}

/*
 * Application-defined control print callback.
 */
static void
app_print_cntrl(int cntrl_code)
{
    const char *con_str, *log_str;
    int i;

    if (cntrl_code == '\r')
    {
        /* CR: reset line in console, new line in log file. */
        con_str = "\r";
        log_str = "\n";
        start_of_line = 1;
    }
    else if (cntrl_code == '\v')
    {
        /* VT: new line if current line is not empty, nothing otherwise. */
        if (!start_of_line)
        {
            con_str = log_str = "\n";
            start_of_line = 1;
        }
        else
            con_str = log_str = "";
    }
    else if (cntrl_code < 0 && cntrl_code > -80 && start_of_line)
    {
        /* Minus N: erase first N characters from line, in console only. */
        if (con_file != NULL)
        {
            for (i = 0; i > cntrl_code; --i)
                fputc(' ', con_file);
        }
        con_str = "\r";
        log_str = "";
    }
    else
    {
        /* Unhandled control code (due to internal error): show err marker. */
        con_str = log_str = "<?>";
    }

    if (con_file != NULL)
        fputs(con_str, con_file);
    if (log_file != NULL)
        fputs(log_str, log_file);
}

/*
 * Application-defined progress update callback.
 */
static void
app_progress(unsigned long current_step, unsigned long total_steps)
{
    /* There will be a potentially long wait, so flush the console output. */
    if (con_file != NULL)
        fflush(con_file);
    /* An eager flush of the line-buffered log file is not very important. */

    /* A GUI application would normally update a progress bar. */
    /* Here we ignore the progress info. */
    if (current_step && total_steps)
        return;
}

/*
 * Application initialization.
 */
static void
app_init(void)
{
    start_of_line = 1;

    if (operation == OP_SHOW_HELP || operation == OP_SHOW_VERSION)
        con_file = stdout;
    else if (!options.quiet)
        con_file = stderr;
    else
        con_file = NULL;

    if (options.log_name != NULL)
    {
        /* Open the log file, line-buffered. */
        if ((log_file = fopen(options.log_name, "a")) == NULL)
            error("Can't open log file: %s\n", options.log_name);
        setvbuf(log_file, NULL, _IOLBF, BUFSIZ);
        app_printf("** Warning: %s\n\n",
                   "The option -log is deprecated; use shell redirection");
    }
}

/*
 * Application finalization.
 */
static void
app_finish(void)
{
    if (log_file != NULL)
    {
        /* Close the log file. */
        fclose(log_file);
    }
}

/*
 * File list processing.
 */
static int
process_files(int argc, char *argv[])
{
    int result;
    struct opng_ui ui;
    int i;

    /* Initialize the optimization engine. */
    ui.printf_fn = app_printf;
    ui.print_cntrl_fn = app_print_cntrl;
    ui.progress_fn = app_progress;
    ui.panic_fn = panic;
    if (opng_initialize(&options, &ui) != 0)
        panic("Can't initialize optimization engine");

    /* Iterate over file names. */
    result = EXIT_SUCCESS;
    for (i = 1; i < argc; ++i)
    {
        if (argv[i] == NULL || argv[i][0] == 0)
            continue;  /* this was an "-option" */
        if (opng_optimize(argv[i]) != 0)
            result = EXIT_FAILURE;
    }

    /* Finalize the optimization engine. */
    if (opng_finalize() != 0)
        panic("Can't finalize optimization engine");

    return result;
}

/*
 * The main function.
 */
int
main(int argc, char *argv[])
{
    int result;

    /* Parse the user options and initialize the application. */
    parse_args(argc, argv);
    app_init();
    result = EXIT_SUCCESS;

    if (local_options.version)
    {
        /* Print the copyright and version info. */
        app_printf("%s\n", msg_intro);
    }

    switch (operation)
    {
    case OP_RUN:
        /* Run the application. */
        result = process_files(argc, argv);
        break;
    case OP_SHOW_HELP:
        if (local_options.help)
        {
            /* Print the extended help text. */
            app_printf("%s%s%s",
                       msg_help_synopsis,
                       msg_help_options,
                       msg_help_examples);
        }
        else
        {
            /* Print the basic help text. */
            app_printf("%s%s%s%s",
                       msg_help_synopsis,
                       msg_help_basic_options,
                       msg_help_examples,
                       msg_help_more);
        }
        break;
    case OP_SHOW_VERSION:
        /* Print the licensing terms and the extended version info. */
        app_printf("%s\n", msg_license);
        app_printf("Using libpng version %s and zlib version %s\n",
                   png_get_libpng_ver(NULL), zlibVersion());
        break;
    default:
        result = -1;
    }

    /* Finalize the application. */
    app_finish();
    return result;
}
