/*
 * Copyright (c) 2002 - 2021 Magnus Lind.
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from
 * the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *   1. The origin of this software must not be misrepresented; you must not
 *   claim that you wrote the original software. If you use this software in a
 *   product, an acknowledgment in the product documentation would be
 *   appreciated but is not required.
 *
 *   2. Altered source versions must be plainly marked as such, and must not
 *   be misrepresented as being the original software.
 *
 *   3. This notice may not be removed or altered from any distribution.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include "log.h"
#include "search.h"
#include "optimal.h"
#include "output.h"
#include "getflag.h"
#include "buf_io.h"
#include "exo_helper.h"
#include "exo_util.h"
#include "parse.h"
#include "named_buffer.h"
#include "desfx.h"
#include "perf.h"

extern struct buf sfxdecr;

#define STR2(X) #X
#define STR(X) STR2(X)

#define DEFAULT_OUTFILE "a.out"

static void load_plain_file(const char *name, struct buf *mb)
{
    int file_len;
    int read_len;
    int offset = 0;
    int len = 0;
    FILE *in;

    in = fopen(name, "rb");
    if(in == NULL)
    {
        char *p = strrchr(name, ',');
        if(p == NULL)
        {
            /* not found and no comma  */
            LOG(LOG_ERROR, ("Error: file not found.\n"));
            exit(1);
        }
        *p = '\0';
        if(str_to_int(p + 1, &offset, NULL))
        {
            LOG(LOG_ERROR, ("Error: invalid value for plain file offset.\n"));
            exit(1);
        }
        in = fopen(name, "rb");
        if(in == NULL)
        {
            p = strrchr(name, ',');
            len = offset;
            if(len == 0)
            {
                LOG(LOG_ERROR, ("Error, value for plain file "
                                "len must not be zero.\n"));
                exit(1);
            }
            *p = '\0';
            if(str_to_int(p + 1, &offset, NULL))
            {
                LOG(LOG_ERROR,
                    ("Error: invalid value for plain file offset.\n"));
                exit(1);
            }
            in = fopen(name, "rb");
            if(in == NULL)
            {
                /* really not found */
                LOG(LOG_ERROR, ("Error: file not found.\n"));
                exit(1);
            }
        }
    }
    /* get the real length of the file and validate the offset*/
    if(fseek(in, 0, SEEK_END))
    {
        LOG(LOG_ERROR, ("Error: can't seek to EOF.\n"));
        fclose(in);
        exit(1);
    }
    file_len = ftell(in);
    if(offset < 0)
    {
        offset += file_len;
    }
    if(fseek(in, offset, SEEK_SET))
    {
        LOG(LOG_ERROR, ("Error: can't seek to offset %d.\n", offset));
        fclose(in);
        exit(1);
    }
    if(len <= 0)
    {
        len += file_len - offset;
    }
    if(len < 0 || offset + len > file_len)
    {
        LOG(LOG_ERROR, ("Error: can't read %d bytes from offset %d.\n",
                        len, offset));
        fclose(in);
        exit(1);
    }
    LOG(LOG_VERBOSE, ("Reading %d bytes from offset %d.\n", len, offset));
    do
    {
        char buf[1024];
        int r = 1024 < len? 1024: len;
        read_len = fread(buf, 1, r, in);
        if(read_len < r)
        {
            LOG(LOG_ERROR, ("Error: tried to read %d bytes but got %d.\n",
                            r, read_len));
            fclose(in);
            exit(1);
        }
        buf_append(mb, buf, r);
        len -= r;
    }
    while(len > 0);
    fclose(in);
}

static
int
do_load(const char *file_name, struct buf *mem)
{
    struct load_info info;
    unsigned char *p;

    buf_clear(mem);
    buf_append(mem, NULL, 65536);
    p = buf_data(mem);

    info.basic_txt_start = -1;

    load_located(file_name, p, &info);

    /* move memory to beginning of buffer */
    buf_remove(mem, info.end, -1);
    buf_remove(mem, 0, info.start);

    LOG(LOG_NORMAL, (" Crunching from $%04X to $%04X.\n",
                     info.start, info.end));
    return info.start;
}

struct target_info
{
    int id;
    int sys_token;
    int basic_txt_start;
    int end_of_ram;
    const char *model;
    const char *outformat;
};

static const struct target_info sfx_targets[] =
{
    {1,   0xbf, 0x0501, 0x10000, "Oric", "tap"},
    {20,  0x9e, 0x1001, 0x2000,  "Vic20", "prg"},
    {23,  0x9e, 0x0401, 0x2000,  "Vic20+3kB", "prg"},
    {52,  0x9e, 0x1201, 0x8000,  "Vic20+32kB", "prg"},
    {55,  0x9e, 0x1201, 0x8000,  "Vic20+3kB+32kB", "prg"},
    {16,  0x9e, 0x1001, 0x4000,  "C16", "prg"},
    {4,   0x9e, 0x1001, 0xfd00,  "plus4", "prg"},
    {64,  0x9e, 0x0801, 0x10000, "C64", "prg"},
    {65,  0x9e, 0x2001, 0x10000, "C65", "prg"},
    {128, 0x9e, 0x1c01, 0xff00,  "C128", "prg"},
    {162, 0x8c, 0x0801, 0xc000,  "Apple ][+ and //e", "AppleSingle"},
    {168, -1,   0x2000, 0xd000,  "Atari 400/800 XL/XE", "xex"},
    {4032, 0x9e, 0x0401, 0x8000,  "CBM PET 4032", "prg"},
    {0xbbcb, -1, 0x1902, 0x8000,  "BBC Micro B", "BBCIm/BBCXfer inf"},
    {0,   -1,   -1,     -1,  NULL, NULL}
};

static
int
do_loads(int filec, char *filev[], struct buf *mem,
         int basic_txt_start, int sys_token, int trim_sys,
         int *basic_var_startp, int *runp, enum file_type *typep)
{
    int run = -1;
    int min_start = 65537;
    int max_end = -1;
    int basic_code = 0;
    int i;
    unsigned char *p;
    struct load_info info;
    enum file_type type = RAW;


    buf_clear(mem);
    buf_append(mem, NULL, 65536);
    p = buf_data(mem);

    for (i = 0; i < filec; ++i)
    {
        info.basic_txt_start = basic_txt_start;
        load_located(filev[i], p, &info);
        run = info.run;
        if(run != -1 && runp != NULL)
        {
            LOG(LOG_DEBUG, ("Propagating found run address $%04X.\n",
                            info.run));
            *runp = info.run;
        }
        if (type == RAW)
        {
            type = info.type;
        }
        else if (type != info.type)
        {
            /* mixed types */
            type = UNKNOWN;
        }

        /* do we expect any basic file? */
        if(basic_txt_start >= 0)
        {
            if(info.basic_var_start >= 0)
            {
                basic_code = 1;
                if(basic_var_startp != NULL)
                {
                    *basic_var_startp = info.basic_var_start;
                }
                if(runp != NULL && run == -1)
                {
                    /* only if we didn't get run address from load_located
                     * (run is not -1 if we did) */
                    int stub_len;
                    run = find_sys(p + basic_txt_start, sys_token, &stub_len);
                    *runp = run;
                    if (trim_sys &&
                        basic_txt_start == info.start &&
                        min_start >= info.start)
                    {
                        if (run >= info.start &&
                            run < info.start + stub_len)
                        {
                            /* the run address points into the sys stub,
                               trim up to it but no further */
                            info.start = run;
                        }
                        else
                        {
                            /* trim the sys stub*/
                            info.start += stub_len;
                        }
                    }
                }
            }
        }

        if (info.start < min_start)
        {
            min_start = info.start;
        }
        if (info.end > max_end)
        {
            max_end = info.end;
        }
    }

    if(basic_txt_start >= 0 && !basic_code && run == -1)
    {
        /* no program loaded to the basic start */
        LOG(LOG_ERROR, ("\nError: nothing loaded at the start of basic "
                        "text address ($%04X).\n",
                        basic_txt_start));
        exit(1);
    }

    /* if we have a basic code loaded and we are doing a proper basic start
     * (the caller don't expect a sys address so runp is NULL */
    if(basic_code && runp == NULL)
    {
        int valuepos = basic_txt_start - 1;
        /* the byte immediatley preceeding the basic start must be 0
         * for basic to function properly. */
        if(min_start > valuepos)
        {
            /* It not covered by the files to crunch. Since the
             * default fill value is 0 we don't need to set it but we
             * need to include that location in the crunch as well. */
            min_start = valuepos;
        }
        else
        {
            int value = p[valuepos];
            /* it has been covered by at least one file. Let's check
             * if it is zero. */
            if(value != 0)
            {
                /* Hm, its not, danger Will Robinson! */
                LOG(LOG_WARNING,
                    ("Warning, basic will probably not work since the value of"
                     " the location \npreceeding the basic start ($%04X)"
                     " is not 0 but %d.\n", valuepos, value));
            }
        }
    }

    if (typep != NULL)
    {
        *typep = type;
    }

    /* move memory to beginning of buffer */
    buf_remove(mem, max_end, -1);
    buf_remove(mem, 0, min_start);

    return min_start;
}

static
void print_command_usage(const char *appl, enum log_level level)
{
    /* done */
    LOG(level,
        ("usage: %s level|mem|sfx|raw|desfx [option]... infile[,<address>]...\n", appl));
    print_base_flags(level, DEFAULT_OUTFILE);
    LOG(level,
        ("  To show command specific options combine the command with -?:\n"
         "  %s level -?\n"
         "  %s mem -?\n"
         "  %s sfx -?\n"
         "  %s raw -?\n"
         "  %s desfx -?\n",
         appl, appl, appl, appl, appl));
}

static
void print_level_usage(const char *appl, enum log_level level,
                       const char *default_outfile)
{
    /* done */
    LOG(level,
        ("usage: %s level [option]... infile[,<address>]...\n"
         "  The level command generates outfiles that are intended to be decrunched on\n"
         "  the fly while being read.\n", appl));
    LOG(level,
        ("  -f            crunch forward\n"));
    print_crunch_flags(level, default_outfile);
    LOG(level,
        (" All infiles are crunched separately and concatenated in the outfile in the\n"
         " order they are given on the command-line.\n"));
}

static
void print_mem_usage(const char *appl, enum log_level level,
                     const char *default_outfile)
{
    /* done */
    LOG(level,
        ("usage: %s mem [option]... infile[,<address>]...\n"
         "  The mem command generates outfiles that are intended to be decrunched from\n"
         "  memory after being loaded or assembled there.\n", appl));
    LOG(level,
        ("  -l <address>  adds load address to the outfile, using \"none\" as <address>\n"
         "                will skip the load address, defaults to \"auto\".\n"));
    LOG(level,
        ("  -f            crunch forward\n"));
    print_crunch_flags(level, default_outfile);
    LOG(level,
        (" All infiles are merged into the outfile. They are loaded in the order\n"
         " they are given on the command-line, from left to right.\n"));
}

static
void print_raw_usage(const char *appl, enum log_level level,
                     const char *default_out_name)
{
    LOG(level, ("usage: %s [option]... infile\n", appl));
    LOG(level,
        ("  -b            crunch/decrunch backwards instead of forward\n"
         "  -r            write outfile in reverse order\n"
         "  -d            decrunch (instead of crunch)\n"));
    print_crunch_flags(level, default_out_name);
}

static
void print_sfx_usage(const char *appl, enum log_level level,
                     const char *default_outfile)
{
    const struct target_info *targetp;
    /* done */
    LOG(level,
        ("usage: %s sfx basic[,<start>[,<end>[,<high>]]]|sys[trim][,<start>]|bin|<jmpaddress> [option]... infile[,<address>]...\n"
         "  The sfx command generates outfiles that are intended to decrunch themselves.\n"
         "  The basic start argument will start a basic program.\n"
         "  The sys start argument will auto detect the start address by searching the\n"
         "  basic start for a sys command.\n"
         "  The systrim start argument works like the sys start argument but it will\n"
         "  also trim the sys line from the loaded infile.\n"
         , appl));
    LOG(level,
        ("  the <jmpaddress> start argument will jmp to the given address.\n"
         "  -t<target>    sets the decruncher target, default is 64, must be one of:\n"));
    for(targetp = sfx_targets; targetp->id != 0; ++targetp)
    {
         LOG(level, ("                %i - %s\n", targetp->id, targetp->model));
    }
    LOG(level,
        ("  -X<custom slow effect assembler fragment>\n"
         "  -x[1-3]|<custom fast effect assembler fragment>\n"
         "                decrunch effect, assembler fragment (don't change X-reg, Y-reg\n"
         "                or carry) or 1 - 3 for different fast border flash effects\n"));
    LOG(level,
        ("  -n            no effect, can't be combined with -X or -x\n"
         "  -D<symbol>=<value>\n"
         "                predefines symbols for the sfx assembler\n"
         "  -s<custom enter assembler fragment>\n"
         "                assembler fragment to execute when the decruncher starts.\n"
         "                (don't change Y-reg)\n"
         "  -f<custom exit assembler fragment>\n"
         "                assembler fragment o execute when the decruncher has\n"
         "                finished\n"));
    LOG(level,
        ("  -y<custom sys epilogue assembler fragment>\n"
         "                assembler fragment for bytes that are listed at the end of\n"
         "                the basic line. Can be use to add a very short text.\n"));
    print_crunch_flags(level, default_outfile);
    LOG(level,
        (" All infiles are merged into the outfile. They are loaded in the order\n"
         " they are given on the command-line, from left to right.\n"));
}

static
void print_desfx_usage(const char *appl, enum log_level level,
                       const char *default_outfile)
{
    /* done */
    LOG(level,
        ("usage: %s desfx [option]... infile\n"
         "  The desfx command decrunches files that previously been crunched using the\n"
         "  sfx command.\n", appl));
    LOG(level,
        ("  -e <address>  overrides the automatic entry point detection, using \"load\" as\n"
         "                <address> sets it to the load address of the infile\n"         "  -S            enables performance statistics output and allows multiple input\n"
         "                files"));

    print_base_flags(level, default_outfile);
}

struct io_bufs_located
{
    struct io_bufs io;
    int write_location;
};

static
void generic(const char *appl,
             struct common_flags *flags,
             print_usage_f *print_usage,
             struct buf *noread_in,
             int decrunch_mode,
             int located_mode,
             int infilec, char *infilev[])
{
    struct buf name_buf = STATIC_BUF_INIT;
    struct buf enc_buf = STATIC_BUF_INIT;

    struct vec entries = STATIC_VEC_INIT(sizeof(struct io_bufs_located));
    struct crunch_options *options = flags->options;
    int c;

    if (options->output_header == 0)
    {
        if (decrunch_mode)
        {
            LOG(LOG_ERROR, ("Error: Can't combine -E and -d.\n"));
            print_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }
        if (infilec == 0)
        {
            LOG(LOG_ERROR, ("Error: no input files to process.\n"));
            print_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }
    }
    else if (infilec != 1)
    {
        LOG(LOG_ERROR, ("Error: exactly one input file must be given.\n"));
        print_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }
    else if (located_mode && decrunch_mode)
    {
        LOG(LOG_ERROR, ("Error: Can't combine located mode and -d.\n"));
        print_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }

    for (c = 0; c < infilec; ++c)
    {
        struct io_bufs_located *io = vec_push(&entries, NULL);
        struct buf *inbuf = &io->io.in;
        struct buf *outbuf = &io->io.out;
        buf_init(inbuf);
        buf_init(outbuf);
        io->write_location = -1;

        if (located_mode)
        {
            io->io.in_off = do_load(infilev[c], inbuf);
            io->write_location = io->io.in_off;
            if (options->direction_forward == 0)
            {
                io->write_location += buf_size(inbuf);
            }
        }
        else
        {
            io->io.in_off = 0;
            load_plain_file(infilev[c], inbuf);
            LOG(LOG_BRIEF, (" Reading %d bytes from \"%s\".\n",
                            buf_size(inbuf), infilev[c]));
        }

        if(decrunch_mode && options->write_reverse)
        {
            reverse_buffer(buf_data(inbuf), buf_size(inbuf));
        }
    }

    if(decrunch_mode)
    {
        int inlen;
        int outlen;
        struct decrunch_options dopts;
        struct io_bufs_located *io = vec_get(&entries, 0);
        struct buf *inbuf = &io->io.in;
        int in_off = io->io.in_off;
        struct buf *outbuf = &io->io.out;

        dopts.direction_forward = options->direction_forward;
        dopts.write_reverse = options->write_reverse;
        dopts.flags_proto = options->flags_proto;
        dopts.imported_encoding = options->imported_encoding;

        inlen = buf_size(inbuf);
        decrunch(LOG_NORMAL, inbuf, in_off, outbuf, &dopts);

        outlen = buf_size(outbuf);
        LOG(LOG_BRIEF, (" Decrunched data expanded %d bytes (%0.2f%%)\n",
                        outlen - inlen, 100.0 * (outlen - inlen) / inlen));
    }
    else
    {
        struct crunch_info info;
        crunch_multi(&entries, noread_in, &enc_buf, options, &info);

        print_crunch_info(LOG_NORMAL, &info);
    }
    for (c = 0; c < infilec; ++c)
    {
        struct io_bufs_located *io = vec_get(&entries, c);
        struct buf *inbuf = &io->io.in;
        struct buf *outbuf = &io->io.out;
        const char *p;

        if (io->write_location != -1)
        {
            if (options->direction_forward == 0)
            {
                /* append the write location of decrunching */
                unsigned char *p = buf_insert(outbuf, -1, NULL, 2);
                p[0] = io->write_location & 255;
                p[1] = io->write_location >> 8;
            }
            else
            {
                /* prepend the write location of decrunching */
                unsigned char *p = buf_insert(outbuf, 0, NULL, 2);
                p[0] = io->write_location >> 8;
                p[1] = io->write_location & 255;
            }
        }

        if(!decrunch_mode && options->write_reverse)
        {
            reverse_buffer(buf_data(outbuf), buf_size(outbuf));
        }

        p = flags->outfile;
        if (options->output_header == 0)
        {
            buf_clear(&name_buf);
            buf_printf(&name_buf, "%s.%02d", flags->outfile, c);
            p = buf_data(&name_buf);
        }
        LOG(LOG_BRIEF, (" Writing %d bytes to \"%s\".\n",
                        buf_size(outbuf), p));
        write_file(p, outbuf);

        buf_free(outbuf);
        buf_free(inbuf);
    }

    if (options->output_header == 0)
    {
        if(options->write_reverse)
        {
            reverse_buffer(buf_data(&enc_buf), buf_size(&enc_buf));
        }
        LOG(LOG_BRIEF, (" Writing encoding to \"%s\".\n", flags->outfile));
        write_file(flags->outfile, &enc_buf);
    }
    buf_free(&enc_buf);
    buf_free(&name_buf);
    vec_free(&entries, NULL);
}

static
void level(const char *appl, int argc, char *argv[])
{
    char flags_arr[64];
    int c;
    int infilec;
    char **infilev;
    struct buf noread = STATIC_BUF_INIT, *noreadp = NULL;

    struct crunch_options options = CRUNCH_OPTIONS_DEFAULT;
    struct common_flags flags = {NULL, DEFAULT_OUTFILE};

    options.flags_notrait = TFLAG_LEN0123_SEQ_MIRRORS;
    flags.options = &options;
    options.write_reverse = 1;

    LOG(LOG_DUMP, ("flagind %d\n", flagind));
    sprintf(flags_arr, "f%s", CRUNCH_FLAGS);
    while ((c = getflag(argc, argv, flags_arr)) != -1)
    {
        LOG(LOG_DUMP, (" flagind %d flagopt '%c'\n", flagind, c));
        switch (c)
        {
        case 'f':
            options.direction_forward = 1;
            options.write_reverse = 0;
            break;
        default:
            handle_crunch_flags(c, flagarg, print_level_usage, appl, &flags);
        }
    }


    if (options.noread_filename != NULL)
    {
        struct load_info info;
        unsigned char *p =
            memset(buf_append(&noread, NULL, 65536), 0, 65536);
        load_located(options.noread_filename, p, &info);
        noreadp = &noread;
    }


    infilev = argv + flagind;
    infilec = argc - flagind;

    if (options.output_header == 0)
    {
        generic(appl,
                &flags,
                print_level_usage,
                noreadp,
                0,
                1,
                infilec, infilev);
    }
    else
    {
        struct crunch_info total = STATIC_CRUNCH_INFO_INIT;
        struct buf in;
        struct buf out;

        if (infilec == 0)
        {
            LOG(LOG_ERROR, ("Error: no input files to process.\n"));
            print_level_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }

        buf_init(&in);
        buf_init(&out);

        /* append the files instead of merging them */
        for(c = 0; c < infilec; ++c)
        {
            struct crunch_info info;
            int in_load;
            int in_len;
            int out_pos;
            out_pos = buf_size(&out);

            in_load = do_load(infilev[c], &in);
            in_len = buf_size(&in);

            if(options.direction_forward)
            {
                /* append the starting address of decrunching */
                buf_append_char(&out, in_load >> 8);
                buf_append_char(&out, in_load & 255);
            }

            crunch(&in, in_load, noreadp, &out, &options, &info);

            if(!options.direction_forward)
            {
                /* append the starting address of decrunching */
                buf_append_char(&out, (in_load + in_len) & 255);
                buf_append_char(&out, (in_load + in_len) >> 8);

                /* reverse the just appended segment of the out buffer */
                reverse_buffer((char*)buf_data(&out) + out_pos,
                               buf_size(&out) - out_pos);
            }

            total.traits_used |= info.traits_used;
            if (info.max_len > total.max_len)
            {
                total.max_len = info.max_len;
            }
            if(info.needed_safety_offset > total.needed_safety_offset)
            {
                total.needed_safety_offset = info.needed_safety_offset;
            }
        }

        print_crunch_info(LOG_NORMAL, &total);

        LOG(LOG_BRIEF, (" Writing %d bytes to \"%s\".\n",
                        buf_size(&out), flags.outfile));
        write_file(flags.outfile, &out);

        buf_free(&out);
        buf_free(&in);
    }

    buf_free(&noread);
}

static
void mem(const char *appl, int argc, char *argv[])
{
    char flags_arr[64];
    int load_addr = -1;
    int load_addr_given = 0;
    int prepend_load_addr = 1;
    int c;
    int infilec;
    char **infilev;
    struct buf noread = STATIC_BUF_INIT, *noreadp = NULL;

    struct crunch_options options = CRUNCH_OPTIONS_DEFAULT;
    struct common_flags flags = {NULL, DEFAULT_OUTFILE};

    options.flags_notrait = TFLAG_LEN0123_SEQ_MIRRORS;
    flags.options = &options;

    LOG(LOG_DUMP, ("flagind %d\n", flagind));
    sprintf(flags_arr, "fl:%s", CRUNCH_FLAGS);
    while ((c = getflag(argc, argv, flags_arr)) != -1)
    {
        LOG(LOG_DUMP, (" flagind %d flagopt '%c'\n", flagind, c));
        switch(c)
        {
        case 'f':
            options.direction_forward = 1;
            break;
        case 'l':
            load_addr_given = 1;
            if(strcmp(flagarg, "none") == 0)
            {
                prepend_load_addr = 0;
            }
            else if(strcmp(flagarg, "auto") == 0)
            {
                load_addr = -1;
            }
            else if(str_to_int(flagarg, &load_addr, NULL) != 0 ||
                    load_addr < 0 || load_addr >= 65536)
            {
                LOG(LOG_ERROR,
                    ("Error: invalid address for -l option, "
                     "must be in the range of [0 - 0xffff]\n"));
                print_mem_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            break;
        default:
            handle_crunch_flags(c, flagarg, print_mem_usage, appl, &flags);
        }
    }

    if (options.noread_filename != NULL)
    {
        struct load_info info;
        unsigned char *p =
            memset(buf_append(&noread, NULL, 65536), 0, 65536);
        load_located(options.noread_filename, p, &info);
        noreadp = &noread;
    }

    infilev = argv + flagind;
    infilec = argc - flagind;

    if (options.output_header == 0)
    {
        if (load_addr_given && prepend_load_addr)
        {
            LOG(LOG_ERROR, ("Error: -E implies -lnone and can't be "
                            "combined with a load address.\n"));
            print_mem_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }

        generic(appl,
                &flags,
                print_mem_usage,
                noreadp,
                0,
                1,
                infilec, infilev);
    }
    else
    {
        struct buf in;
        struct buf out;
        struct crunch_info info;
        int in_load;
        int in_len;
        int safety;

        if (infilec == 0)
        {
            LOG(LOG_ERROR, ("Error: no input files to process.\n"));
            print_mem_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }

        buf_init(&in);
        buf_init(&out);

        in_load = do_loads(infilec, infilev, &in, -1, -1, 0, NULL, NULL, NULL);
        in_len = buf_size(&in);

        LOG(LOG_NORMAL, (" Crunching from $%04X to $%04X.\n",
                         in_load, in_load + in_len));

        /* make room for load addr */
        if(prepend_load_addr)
        {
            buf_append(&out, NULL, 2);
        }

        if(options.direction_forward)
        {
            /* append the in_loading address of decrunching */
            buf_append_char(&out, in_load >> 8);
            buf_append_char(&out, in_load & 255);
        }

        crunch(&in, in_load, noreadp, &out, &options, &info);
        safety = info.needed_safety_offset;

        if(!options.direction_forward)
        {
            /* append the in_loading address of decrunching */
            buf_append_char(&out, (in_load + in_len) & 255);
            buf_append_char(&out, (in_load + in_len) >> 8);
        }

        /* prepend load addr */
        if(prepend_load_addr)
        {
            char *p;
            if(load_addr < 0)
            {
                /* auto load addr specified */
                load_addr = in_load;
                if(options.direction_forward)
                {
                    load_addr += in_len + safety - buf_size(&out) + 2;
                }
                else
                {
                    load_addr -= safety;
                }
            }
            p = buf_data(&out);
            p[0] = load_addr & 255;
            p[1] = load_addr >> 8;
            LOG(LOG_NORMAL, (" The load address is $%04X - $%04X.\n",
                             load_addr, load_addr + buf_size(&out) - 2));
        }
        else
        {
            LOG(LOG_NORMAL, (" No load address, data length is $%04X.\n",
                             buf_size(&out)));
        }

        print_crunch_info(LOG_NORMAL, &info);

        if (prepend_load_addr)
        {
            LOG(LOG_BRIEF,
                (" Writing \"%s\" as prg, saving from $%04X to $%04X.\n",
                 flags.outfile, load_addr,
                 load_addr + buf_size(&out) - 2));
        }
        else
        {
            LOG(LOG_BRIEF, (" Writing %d bytes to \"%s\".\n",
                            buf_size(&out), flags.outfile));
        }
        write_file(flags.outfile, &out);

        buf_free(&out);
        buf_free(&in);
    }
    buf_free(&noread);
}

static
const struct target_info *
get_target_info(int target)
{
    const struct target_info *targetp;
    for(targetp = sfx_targets; targetp->id != 0; ++targetp)
    {
        if(target == targetp->id)
        {
            break;
        }
    }
    if(targetp->id == 0)
    {
        targetp = NULL;
    }
    return targetp;
}

static void do_effect(const char *appl, int no_effect, const char *fast,
                      const char *slow)
{
    struct buf *fx = NULL;

    if(no_effect + (fast != NULL) + (slow != NULL) > 1)
    {
        LOG(LOG_ERROR,
            ("Error: can't combine any of the -n, -x or -X flags.\n"));
        print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }
    if(no_effect)
    {
        set_initial_symbol("i_effect", -1);
    }
    else if(fast != NULL)
    {
        int value;
        if(str_to_int(fast, &value, NULL) == 0)
        {
            if(value == 1) set_initial_symbol("i_effect", 1);
            else if(value == 2) set_initial_symbol("i_effect", 2);
            else if(value == 3) set_initial_symbol("i_effect", 3);
            else
            {
                LOG(LOG_ERROR,
                    ("Error: invalid range for effect shorthand, "
                     "must be in the range of [1 - 3]\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
        }
        else
        {
            set_initial_symbol("i_effect_custom", 1);
            fx = new_initial_named_buffer("effect_custom");
            buf_append(fx, fast, strlen(fast));
        }
        set_initial_symbol("i_effect_speed", 1);
    }
    else if(slow != NULL)
    {
        int value;
        if(str_to_int(slow, &value, NULL) == 0)
        {
            LOG(LOG_ERROR, ("Error: Can't use shorthand for -X flag.\n"));
            print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }
        else
        {
            set_initial_symbol("i_effect_custom", 1);
            fx = new_initial_named_buffer("effect_custom");
            buf_append(fx, slow, strlen(slow));
            set_initial_symbol("i_effect_speed", 0);
        }
    }
    else
    {
        set_initial_symbol("i_effect", 0);
        set_initial_symbol("i_effect_speed", 0);
    }
}

static
void sfx(const char *appl, int argc, char *argv[])
{
    int in_load;
    int in_len;
    int basic_txt_start = -1;
    int basic_var_start = -1;
    int basic_highest_addr = -1;
    int decr_target = 64;
    int decr_target_set = 0;
    int entry_addr = -1;
    int trim_sys = 0;
    int no_effect = 0;
    const char *fast = NULL;
    const char *slow = NULL;
    const char *enter_custom = NULL;
    const char *exit_custom = NULL;
    const char *sys_epilogue = NULL;
    char flags_arr[64];
    int c;
    int infilec;
    char **infilev;

    struct crunch_info info;

    struct crunch_options options = CRUNCH_OPTIONS_DEFAULT;
    struct common_flags flags = {NULL, DEFAULT_OUTFILE};
    const struct target_info *targetp;

    struct buf buf1;

    struct buf *in;
    struct buf *out;

    options.flags_proto = PFLAG_BITS_ORDER_BE | PFLAG_BITS_COPY_GT_7 |
        PFLAG_REUSE_OFFSET;
    flags.options = &options;

    if(argc <= 1)
    {
        LOG(LOG_ERROR, ("Error: no start argument given.\n"));
        print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }

    parse_init();

    /* required argument: how to start the crunched program */
    do
    {
        char *p = strtok(argv[1], ",");
        if (strcmp(p, "sys") == 0 || strcmp(p, "systrim") == 0)
        {
            if (strcmp(p, "systrim") == 0)
            {
                trim_sys = 1;
            }
            /* we should look for a basic sys command. */
            entry_addr = -1;
            p = strtok(NULL, ",");
            /* look for an optional basic start address */
            if(p == NULL) break;
            if(str_to_int(p, &basic_txt_start, NULL) != 0)
            {
                LOG(LOG_ERROR,
                    ("Error: invalid value for the start of basic text "
                     "address.\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
        }
        else if(strcmp(p, "basic") == 0)
        {
            /* we should start a basic program. */
            entry_addr = -2;
            p = strtok(NULL, ",");
            /* look for an optional basic start address */
            if(p == NULL) break;
            if(str_to_int(p, &basic_txt_start, NULL) != 0)
            {
                LOG(LOG_ERROR,
                    ("Error: invalid value for the start of basic text "
                     "address.\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            p = strtok(NULL, ",");
            /* look for an optional basic end address */
            if(p == NULL) break;
            if(str_to_int(p, &basic_var_start, NULL) != 0)
            {
                LOG(LOG_ERROR,
                    ("Error: invalid value for the start of basic "
                     "variables address.\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            p = strtok(NULL, ",");
            /* look for an optional highest address used by basic */
            if(p == NULL) break;
            if(str_to_int(p, &basic_highest_addr, NULL) != 0)
            {
                LOG(LOG_ERROR,
                    ("Error: invalid value for the highest address used "
                     "by basic address.\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
        }
        else if (strcmp(p, "bin") == 0)
        {
            entry_addr = -3;
        }
        else if(str_to_int(p, &entry_addr, NULL) != 0 ||
                entry_addr < 0 || entry_addr >= 65536)
        {
            /* we got an address we should jmp to. */
            LOG(LOG_ERROR,
                ("Error: invalid address for <start>, "
                 "must be in the range of [0 - 0xffff]\n"));
            print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
            exit(1);
        }
    }
    while(0);

    LOG(LOG_DUMP, ("flagind %d\n", flagind));
    sprintf(flags_arr, "nD:t:x:X:s:f:y:%s", CRUNCH_FLAGS);
    while ((c = getflag(argc, argv, flags_arr)) != -1)
    {
        char *p;
        LOG(LOG_DUMP, (" flagind %d flagopt '%c'\n", flagind, c));
        switch(c)
        {
        case 't':
            if (str_to_int(flagarg, &decr_target, NULL) != 0 ||
                get_target_info(decr_target) == NULL)
            {
                struct buf errmsg = STATIC_BUF_INIT;
                const struct target_info *targetp;
                const char *glue = "";
                buf_printf(&errmsg, "error: invalid value (%d) for -t option, must be one of: ", decr_target);
                for(targetp = sfx_targets; targetp->id != 0; ++targetp)
                {
                    buf_printf(&errmsg, "%s", glue);
                    buf_printf(&errmsg, "%i", targetp->id);
                    glue = ", ";
                }
                buf_printf(&errmsg, ".");
                LOG(LOG_ERROR, ("%s\n", (char*)buf_data(&errmsg)));
                buf_free(&errmsg);
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            decr_target_set = 1;
            break;
        case 'n':
            no_effect = 1;
            break;
        case 'x':
            fast = flagarg;
            break;
        case 'X':
            slow = flagarg;
            break;
        case 's':
            enter_custom = flagarg;
            break;
        case 'f':
            exit_custom = flagarg;
            break;
        case 'y':
            sys_epilogue = flagarg;
            break;
        case 'D':
            p = strrchr(flagarg, '=');
            if(p != NULL)
            {
                int value;
                if(str_to_int(p + 1, &value, NULL) != 0)
                {
                    LOG(LOG_ERROR, ("Error: invalid value for -D "
                                    "<symbol>[=<value>] option.\n"));
                    print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                    exit(1);
                }
                /* This is ugly, we really should allocate our own
                 * copy of the symbol string. */
                *p = '\0';
                set_initial_symbol(flagarg, value);
            }
            else
            {
                LOG(LOG_ERROR, ("Error: invalid value for -D "
                                "<symbol>=<value> option.\n"));
                print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            break;
        default:
            handle_crunch_flags(c, flagarg, print_sfx_usage, appl, &flags);
        }
    }

    {
        int required = PFLAG_BITS_ORDER_BE | PFLAG_BITS_COPY_GT_7;
        int unsupported = PFLAG_BITS_ALIGN_START | PFLAG_IMPL_1LITERAL;

        if ((options.flags_proto & required) != required)
        {
            LOG(LOG_ERROR,
                ("Warning: -P bits " STR(PBIT_BITS_ORDER_BE)
                 " and " STR(PBIT_BITS_COPY_GT_7)
                 " are required by sfx, setting them.\n"));
            options.flags_proto |= required;
        }
        if ((options.flags_proto & unsupported) != 0)
        {
            LOG(LOG_ERROR,
                ("Warning: -P bits " STR(PBIT_BITS_ALIGN_START)
                 " and " STR(PBIT_IMPL_1LITERAL)
                 " are not supported by sfx, clearing them.\n"));
            options.flags_proto &= ~unsupported;
        }
        options.flags_notrait |= TFLAG_LEN0123_SEQ_MIRRORS;
    }

    if (options.flags_proto & PFLAG_4_OFFSET_TABLES)
    {
        set_initial_symbol("i_fourth_offset_table", 1);
    }
    if (options.flags_proto & PFLAG_REUSE_OFFSET)
    {
        set_initial_symbol("i_reuse_offset", 1);
    }

    do_effect(appl, no_effect, fast, slow);
    if(enter_custom != NULL)
    {
        set_initial_symbol("i_enter_custom", 1);
        buf_append(new_initial_named_buffer("enter_custom"),
                   enter_custom, strlen(enter_custom));
    }
    if(exit_custom != NULL)
    {
        set_initial_symbol("i_exit_custom", 1);
        buf_append(new_initial_named_buffer("exit_custom"),
                   exit_custom, strlen(exit_custom));
    }
    if(sys_epilogue != NULL)
    {
        set_initial_symbol("i_sys_epilogue", 1);
        buf_append(new_initial_named_buffer("sys_epilogue"),
                   sys_epilogue, strlen(sys_epilogue));
    }

    buf_init(&buf1);
    in = &buf1;
    out = new_initial_named_buffer("crunched_data");

    infilev = argv + flagind + 1;
    infilec = argc - flagind - 1;

    if (infilec == 0)
    {
        LOG(LOG_ERROR, ("Error: no input files to process.\n"));
        print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }

    targetp = get_target_info(decr_target);
    if(entry_addr == -2 && (targetp->id == 0xa8 || targetp->id == 0xbbcb))
    {
        /* basic start not implemented for Atari or BBC targets */
        LOG(LOG_ERROR, ("Start address \"basic\" is not supported for "
                        "the %s target.\n", targetp->model));
        print_sfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }

    if(basic_txt_start == -1)
    {
        basic_txt_start = targetp->basic_txt_start;
    }

    {
        int safety;
        int *basic_var_startp;
        int *entry_addrp;
        int basic_start;
        int mode_bin = 0;
        enum file_type type;
        struct buf noread = STATIC_BUF_INIT, *noreadp = NULL;

        if (options.noread_filename != NULL)
        {
            struct load_info info;
            unsigned char *p =
                memset(buf_append(&noread, NULL, 65536), 0, 65536);
            load_located(options.noread_filename, p, &info);
            noreadp = &noread;
        }

        entry_addrp = NULL;
        basic_var_startp = NULL;
        if(entry_addr == -2 && basic_var_start == -1)
        {
            basic_var_startp = &basic_var_start;
        }
        basic_start = -1;
        if (entry_addr == -1)
        {
            /* mode sys */
            basic_start = basic_txt_start;
            entry_addrp = &entry_addr;
        }
        else if (entry_addr == -2)
        {
            /* mode basic */
            basic_start = basic_txt_start;
        }
        else if (entry_addr == -3)
        {
            /* mode bin */
            entry_addrp = &entry_addr;
            mode_bin = 1;
        }

        in_load = do_loads(infilec, infilev, in,
                           basic_start, targetp->sys_token, trim_sys,
                           basic_var_startp, entry_addrp, &type);

        /* if "sfx bin or explicit run address given */
        if (!decr_target_set && (mode_bin || entry_addr >= 0))
        {
            /* target is not set explicitly, auto detect from file type */
            switch (type)
            {
            case ATARI_XEX:
                decr_target = 0xa8;
                break;
            case ORIC_TAP:
                decr_target = 0x1;
                break;
            case APPLESINGLE_SYS:
            case APPLESINGLE:
                decr_target = 0xa2;
                break;
            case BBC_INF:
                decr_target = 0xbbcb;
                break;
            default:
                break;
            }
            /* refetch target info */
            targetp = get_target_info(decr_target);
        }
        if (mode_bin)
        {
            set_initial_symbol_soft("i_load_addr", in_load);
            if (decr_target == 0xa2 && type == APPLESINGLE_SYS)
            {
                /* magic for indicating Apple ][ PRODOS system file */
                set_initial_symbol_soft("i_a2_file_type", 0xff);
            }
        }
        in_len = buf_size(in);

        if(in_load + in_len > targetp->end_of_ram)
        {
            LOG(LOG_ERROR, ("Error:\n The memory of the %s target ends at "
                            "$%04X and can't hold the\n uncrunched data "
                            "that covers $%04X to $%04X.\n",
                            targetp->model, targetp->end_of_ram,
                            in_load, in_load + in_len));
            exit(1);
        }

        LOG(LOG_NORMAL, (" Crunching from $%04X to $%04X.\n",
                         in_load, in_load + in_len));

        if(decr_target == 20 || decr_target == 52)
        {
            /* these are vic20 targets with a memory hole from
             * $0400-$1000. Each page is filled with the value of the
             * high-byte of its address. */
            if(in_load >= 0x0400 && in_load + in_len <= 0x1000)
            {
                /* all the loaded data is in the memory hole.*/
                LOG(LOG_ERROR,
                    ("Error: all data loaded to the memory hole.\n"));
                exit(1);
            }
            else if(in_load >= 0x0400 && in_load < 0x1000 &&
                    in_load + in_len > 0x1000)
            {
                /* The data starts in the memory hole and ends in
                 * RAM. We need to adjust the start. */
                int diff = 0x1000 - in_load;
                in_load += diff;
                in_len -= diff;
                buf_remove(in, 0, diff);
                LOG(LOG_WARNING,
                    ("Warning, trimming address interval to $%04X-$%04X.\n",
                     in_load, in_load + in_len));
            }
            else if(in_load < 0x0400 &&
                    in_load + in_len >= 0x0400 && in_load + in_len < 0x1000)
            {
                /* The data starts in RAM and ends in the memory
                 * hole. We need to adjust the end. */
                int diff = in_load + in_len - 0x0400;
                in_len -= diff;
                buf_remove(in, in_len, -1);
                LOG(LOG_WARNING,
                    ("Warning, trimming address interval to $%04X-$%04X.\n",
                     in_load, in_load + in_len));
            }
            else if(in_load < 0x0400 && in_load + in_len > 0x1000)
            {
                /* The data starts in RAM covers the memory hole and
                 * ends in RAM. */
                int hi, lo;
                char *p;
                p = buf_data(in);
                for(hi = 0x04; hi < 0x10; hi += 0x01)
                {
                    for(lo = 0; lo < 256; ++lo)
                    {
                        int addr = (hi << 8) | lo;
                        p[addr - in_load] = hi;
                    }
                }
                LOG(LOG_NORMAL, ("Memory hole at interval $0400-$1000 "
                                 "included in crunch..\n"));
            }
        }

        /* make room for load addr */
        buf_append(out, NULL, 2);

        crunch(in, in_load, noreadp, out, &options, &info);

        print_crunch_info(LOG_NORMAL, &info);

        safety = info.needed_safety_offset;

        /* append the in_loading address of decrunching */
        buf_append_char(out, (in_load + in_len) & 255);
        buf_append_char(out, (in_load + in_len) >> 8);

        /* prepend load addr */
        {
            char *p;
            p = buf_data(out);
            p[0] = (in_load - safety) & 255;
            p[1] = (in_load - safety) >> 8;
        }

        buf_free(&noread);
    }

    LOG(LOG_NORMAL, (" Target is self-decrunching %s executable",
                     targetp->model));
    if(entry_addr == -1)
    {
        LOG(LOG_ERROR, ("\nError: can't find sys address (token $%02X)"
                        " at basic text start ($%04X).\n",
                        targetp->sys_token, basic_txt_start));
        exit(1);
    }
    if(entry_addr != -2)
    {
        LOG(LOG_NORMAL, (",\n jmp address $%04X.\n", entry_addr));
    }
    else
    {
        LOG(LOG_NORMAL, (",\n basic start ($%04X-$%04X).\n",
                         basic_txt_start, basic_var_start));
    }

    {
        /* add decruncher */
        struct decrunch_options dopts = DECRUNCH_OPTIONS_DEFAULT;
        struct buf source;

        buf_init(&source);
        decrunch(LOG_DEBUG, &sfxdecr, 0, &source, &dopts);

        in = out;
        out = &buf1;
        buf_clear(out);

        set_initial_symbol("r_start_addr", entry_addr);
        /*initial_symbol_dump( LOG_NORMAL, "r_start_addr");*/
        set_initial_symbol("r_target", decr_target);
        /*initial_symbol_dump( LOG_NORMAL, "r_target");*/
        set_initial_symbol("r_in_load", in_load);
        /*initial_symbol_dump( LOG_NORMAL, "r_in_addr");*/
        set_initial_symbol("r_in_len", in_len);
        /*initial_symbol_dump( LOG_NORMAL, "r_in_len");*/

        if(entry_addr == -2)
        {
            /* only set this if its changed from the default. */
            if(basic_txt_start != targetp->basic_txt_start)
            {
                set_initial_symbol("i_basic_txt_start", basic_txt_start);
                initial_symbol_dump( LOG_DEBUG, "i_basic_txt_start");
            }
            /* only set this if we've been given a value for it. */
            if(basic_var_start != -1)
            {
                set_initial_symbol("i_basic_var_start", basic_var_start);
                initial_symbol_dump(LOG_DEBUG, "i_basic_var_start");
            }
            /* only set this if we've been given a value for it. */
            if(basic_highest_addr != -1)
            {
                set_initial_symbol("i_basic_highest_addr", basic_highest_addr);
                initial_symbol_dump(LOG_DEBUG, "i_basic_highest_addr");
            }
        }

        if(info.traits_used & TFLAG_LIT_SEQ)
        {
            set_initial_symbol("i_literal_sequences_used", 1);
            initial_symbol_dump(LOG_DEBUG, "i_literal_sequences_used");
        }
        if(info.max_len <= 256)
        {
            set_initial_symbol("i_max_sequence_length_256", 1);
            initial_symbol_dump(LOG_DEBUG, "i_max_sequence_length_256");
        }

        if(assemble(&source, out) != 0)
        {
            LOG(LOG_ERROR, ("Parse failure.\n"));
            exit(1);
        }
        else
        {
            int table_size = (16+4+16+16)*3;
            i32 lowest_addr;
            i32 max_transfer_len;
            i32 lowest_addr_out;
            i32 highest_addr_out;
            i32 transfer_len;
            i32 i_table_addr;
            i32 stage3end, zp_len_lo, zp_len_hi, zp_src_lo, zp_bits_hi;
            i32 zp_ro_state, i_effect;
            i32 i_ram_enter, i_ram_during, i_ram_exit;
            i32 i_irq_enter, i_irq_during, i_irq_exit;
            i32 i_nmi_enter, i_nmi_during, i_nmi_exit;
            i32 c_effect_color, c_page0location;

            if (options.flags_proto & PFLAG_4_OFFSET_TABLES)
            {
                table_size = (16+4+16+16+16)*3;
            }

            resolve_symbol("lowest_addr", NULL, &lowest_addr);
            resolve_symbol("max_transfer_len", NULL, &max_transfer_len);
            resolve_symbol("lowest_addr_out", NULL, &lowest_addr_out);
            resolve_symbol("highest_addr_out", NULL, &highest_addr_out);
            resolve_symbol("transfer_len", NULL, &transfer_len);
            resolve_symbol("i_table_addr", NULL, &i_table_addr);
            resolve_symbol("stage3end", NULL, &stage3end);
            resolve_symbol("zp_len_lo", NULL, &zp_len_lo);
            resolve_symbol("zp_len_hi", NULL, &zp_len_hi);
            resolve_symbol("zp_src_lo", NULL, &zp_src_lo);
            resolve_symbol("zp_bits_hi", NULL, &zp_bits_hi);
            if (options.flags_proto & PFLAG_REUSE_OFFSET)
            {
                resolve_symbol("zp_ro_state", NULL, &zp_ro_state);
            }
            resolve_symbol("i_effect2", NULL, &i_effect);
            resolve_symbol("i_irq_enter", NULL, &i_irq_enter);
            resolve_symbol("i_irq_during", NULL, &i_irq_during);
            resolve_symbol("i_irq_exit", NULL, &i_irq_exit);

            resolve_symbol("c_page0location", NULL, &c_page0location);
            if (c_page0location == 0)
            {
                if (stage3end > 0x1f2)
                {
                    LOG(LOG_ERROR,
                        ("ERROR: The generated decruncher is too b"
                         "ig. It will be overwritten by its own\n"
                         "stack usage. Please use options or disab"
                         "le features to make it smaller.\n."));
                    exit(1);
                }
            }
            else
            {
                if (stage3end > c_page0location + 0x1ff)
                {
                    LOG(LOG_ERROR,
                        ("ERROR: The generated decruncher is too b"
                         "ig. It is larger than 255 bytes.\n"
                         "Please use options or disable features t"
                         "o make it smaller.\n."));
                    exit(1);
                }
            }
            LOG(LOG_BRIEF, (" Writing \"%s\" as %s, saving from "
                            "$%04X to $%04X.\n", flags.outfile,
                            targetp->outformat, lowest_addr_out,
                            highest_addr_out));

            LOG(LOG_NORMAL, ("Memory layout:   |Start |End   |\n"));
            if (transfer_len > 0)
            {
                LOG(LOG_NORMAL, (" Setup copy from | $%04X| $%04X| to $%04X\n",
                                 highest_addr_out - transfer_len,
                                 highest_addr_out, lowest_addr));
            }
            LOG(LOG_NORMAL, (" Crunched data   | $%04X| $%04X|\n",
                             lowest_addr, lowest_addr + max_transfer_len));
            LOG(LOG_NORMAL, (" Decrunched data | $%04X| $%04X|\n",
                 in_load, in_load + in_len));
            LOG(LOG_NORMAL, (" Decrunch table  | $%04X| $%04X|\n",
                             i_table_addr, i_table_addr + table_size));
            LOG(LOG_NORMAL, (" Decruncher      | $%04X| $%04X| and ",
                             c_page0location + 0xfd, stage3end));
            if (options.flags_proto & PFLAG_REUSE_OFFSET)
            {
                LOG(LOG_NORMAL,
                    ("$%02X,$%02X,$%02X,$%02X,$%02X,$%02X\n", zp_bits_hi,
                     zp_len_lo, zp_len_hi, zp_src_lo, zp_src_lo + 1,
                     zp_ro_state));
            }
            else
            {
                LOG(LOG_NORMAL,
                    ("$%02X,$%02X,$%02X,$%02X,$%02X\n", zp_bits_hi,
                     zp_len_lo, zp_len_hi, zp_src_lo, zp_src_lo + 1));
            }
            if(i_effect == 0 && !resolve_symbol("i_effect_custom", NULL, NULL))
            {
                resolve_symbol("c_effect_color", NULL, &c_effect_color);
                LOG(LOG_NORMAL, (" Decrunch effect writes to $%04X.\n",
                                 c_effect_color));
            }
            LOG(LOG_NORMAL, ("Decruncher:  |Enter |During|Exit  |\n"));
            if (decr_target == 1 || decr_target == 64 || decr_target == 128 ||
                decr_target == 4 || decr_target == 16 || decr_target == 168 ||
                decr_target == 65)
            {
                resolve_symbol("i_ram_enter", NULL, &i_ram_enter);
                resolve_symbol("i_ram_during", NULL, &i_ram_during);
                resolve_symbol("i_ram_exit", NULL, &i_ram_exit);
                LOG(LOG_NORMAL, (" RAM config  |   $%02X|   $%02X|   $%02X|\n",
                     i_ram_enter, i_ram_during, i_ram_exit));
            }
            LOG(LOG_NORMAL, (" IRQ enabled |   %3d|   %3d|   %3d|\n",
                 i_irq_enter, i_irq_during, i_irq_exit));
            if (decr_target == 168)
            {
                resolve_symbol("i_nmi_enter", NULL, &i_nmi_enter);
                resolve_symbol("i_nmi_during", NULL, &i_nmi_during);
                resolve_symbol("i_nmi_exit", NULL, &i_nmi_exit);
                LOG(LOG_NORMAL, (" NMI enabled |   $%02X|   $%02X|   $%02X|\n",
                     i_nmi_enter, i_nmi_during, i_nmi_exit));
            }
            else if (decr_target == 0xbbcb)
            {
                FILE *inf;
                char *p;
                int i;
                /* write shadowing .inf file */
                struct buf name_buf = STATIC_BUF_INIT;
                buf_printf(&name_buf, "%s.inf", flags.outfile);
                p = buf_data(&name_buf);
                inf = fopen(p, "wb");
                p[buf_size(&name_buf) - 4] = '\0';
                for (i = 0; p[i] != '\0' && (i < 7 || (p[i] = '\0')); ++i)
                {
                    p[i] = toupper(p[i]);
                }

                fprintf(inf, "$.%s %06X %06X %X", p,
                        lowest_addr_out | 0xff0000,
                        lowest_addr_out | 0xff0000,
                        buf_size(out));
                fclose(inf);
            }
        }

        buf_free(&source);
    }

    write_file(flags.outfile, out);
    buf_free(&buf1);

    parse_free();
}

static
void raw(const char *appl, int argc, char *argv[])
{
    char flags_arr[64];
    int decrunch_mode = 0;
    int c, infilec;
    char **infilev;
    struct buf noread = STATIC_BUF_INIT, *noreadp = NULL;

    struct crunch_options options = CRUNCH_OPTIONS_DEFAULT;
    struct common_flags flags = {NULL, DEFAULT_OUTFILE};

    flags.options = &options;
    options.direction_forward = 1;

    LOG(LOG_DUMP, ("flagind %d\n", flagind));
    sprintf(flags_arr, "brd%s", CRUNCH_FLAGS);
    while ((c = getflag(argc, argv, flags_arr)) != -1)
    {
        LOG(LOG_DUMP, (" flagind %d flagopt '%c'\n", flagind, c));
        switch (c)
        {
        case 'b':
            options.direction_forward = 0;
            break;
        case 'r':
            options.write_reverse = 1;
            break;
        case 'd':
            decrunch_mode = 1;
            break;
        default:
            handle_crunch_flags(c, flagarg, print_raw_usage, appl, &flags);
        }
    }

    if (options.noread_filename != NULL)
    {
        load_plain_file(options.noread_filename, &noread);
        noreadp = &noread;
    }

    infilev = argv + flagind;
    infilec = argc - flagind;

    generic(appl,
            &flags,
            print_raw_usage,
            noreadp,
            decrunch_mode,
            0,
            infilec, infilev);

    buf_free(&noread);
}

static
void desfx(const char *appl, int argc, char *argv[])
{
    struct perf_ctx perf = STATIC_PERF_INIT;
    struct buf buf = STATIC_BUF_INIT;
    char flags_arr[64];
    int i, c, infilec;
    char **infilev;
    int entry = -1;
    int stat = 0;

    const char *outfile = DEFAULT_OUTFILE;

    LOG(LOG_DUMP, ("flagind %d\n", flagind));
    sprintf(flags_arr, "e:S%s", BASE_FLAGS);
    while ((c = getflag(argc, argv, flags_arr)) != -1)
    {
        LOG(LOG_DUMP, (" flagind %d flagopt '%c'\n", flagind, c));
        switch (c)
        {
        case 'e':
            if(strcmp(flagarg,"load") == 0)
            {
                entry = -2;
            }
            else if(str_to_int(flagarg, &entry, NULL) != 0 ||
                    entry < 0 || entry >= 65536)
            {
                LOG(LOG_ERROR,("Error: invalid address for -e option, "
                               "must be in the range of [0 - 0xffff]\n"));
                print_desfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
                exit(1);
            }
            break;
        case 'S':
            stat = 1;
            break;
        default:
            handle_base_flags(c, flagarg, print_desfx_usage, appl, &outfile);
        }
    }

    infilev = argv + flagind;
    infilec = argc - flagind;

    if (stat == 0 && infilec != 1)
    {
        LOG(LOG_ERROR, ("Error: only one input file must be given.\n"));
        print_desfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }
    else if (infilec < 1)
    {
        LOG(LOG_ERROR, ("Error: at least one input file must be given.\n"));
        print_desfx_usage(appl, LOG_NORMAL, DEFAULT_OUTFILE);
        exit(1);
    }

    for (i = 0; i < infilec; ++i)
    {
        int entry1 = entry;
        struct load_info info;
        struct buf mem;
        u8 *p;
        int start;
        int end;
        u32 cycles;
        int cookedend;
        const char *n;

        buf_init(&mem);
        buf_append(&mem, NULL, 65536);

        p = buf_data(&mem);

        /* load file, don't care about tracking basic */
        info.basic_txt_start = -1;
        load_located(infilev[i], p, &info);

        if(entry1 == -1)
        {
            /* use detected address */
            entry1 = info.run;
        }
        else if(entry1 == -2)
        {
            /* use load address */
            entry1 = info.start;
        }

        /* no start address from load */
        if(entry1 == -1)
        {
            /* look for sys line */
            entry1 = find_sys(p + info.start, -1, NULL);
        }
        if(entry1 == -1)
        {
            LOG(LOG_ERROR, ("Error, can't find entry1 point.\n"));
            exit(1);
        }

        entry1 = decrunch_sfx(p, entry1, &start, &end, &cycles);

        /* change 0x0 into 0x10000 */
        cookedend = ((end - 1) & 0xffff) + 1;

        if (stat != 0)
        {
            int inlen = info.end - info.start;
            int outlen = end - start;
            perf_add(&perf, infilev[i], inlen, outlen, cycles);
        }
        else
        {
            LOG(LOG_NORMAL,
                (" Decrunch took %0.6f Mcycles, "
                 "speed was %0.2f kB/Mc (%0.2f c/B).\n",
                 (double)cycles / 1000000,
                 1000 * (end - start) / (float)cycles,
                 (float)cycles / (end - start)));
        }
        buf_remove(&mem, cookedend, -1);
        buf_remove(&mem, 0, start);
        buf_insert(&mem, 0, NULL, 2);

        p = buf_data(&mem);
        p[0] = start;
        p[1] = start >> 8;

        n = outfile;
        if (stat != 0)
        {
            buf_clear(&buf);
            buf_printf(&buf, "%s.%02d", outfile, i);
            n = buf_data(&buf);
        }

        LOG(LOG_BRIEF, (" Writing \"%s\" as prg, saving from $%04X to $%04X, "
                        "entry at $%04X.\n", n, start, cookedend, entry1));
        write_file(n, &mem);

        buf_free(&mem);
    }

    if (stat != 0)
    {
        buf_clear(&buf);
        perf_buf_print(&perf, &buf);
        LOG(LOG_NORMAL, ("All files:\n%s", (char*)buf_data(&buf)));

        buf_clear(&buf);
        perf_sort_size_cycles(&perf, 1);
        perf_buf_print(&perf, &buf);
        LOG(LOG_NORMAL, ("\nSorted on size:\n%s", (char*)buf_data(&buf)));

        buf_clear(&buf);
        perf_sort_cycles_size(&perf, 1);
        perf_buf_print(&perf, &buf);
        LOG(LOG_NORMAL, ("\nSorted on cycles:\n%s", (char*)buf_data(&buf)));
    }

    buf_free(&buf);
    perf_free(&perf);
}

int
main(int argc, char *argv[])
{
    const char *appl;

    /* init logging */
    LOG_INIT_CONSOLE(LOG_NORMAL);

    appl = fixup_appl(argv[0]);
    if(argc < 2)
    {
        /* missing required command */
        LOG(LOG_ERROR, ("Error: required command is missing.\n"));
        print_command_usage(appl, LOG_ERROR);
        exit(1);
    }
    ++argv;
    --argc;
    if(strcmp(argv[0], "level") == 0)
    {
        level(appl, argc, argv);
    }
    else if(strcmp(argv[0], "mem") == 0)
    {
        mem(appl, argc, argv);
    }
    else if(strcmp(argv[0], "sfx") == 0)
    {
        sfx(appl, argc, argv);
    }
    else if(strcmp(argv[0], "raw") == 0)
    {
        raw(appl, argc, argv);
    }
    else if(strcmp(argv[0], "desfx") == 0)
    {
        desfx(appl, argc, argv);
    }
    else if(strcmp(argv[0], "-v") == 0)
    {
        print_license();
    }
    else if(strcmp(argv[0], "-?") == 0)
    {
        print_command_usage(appl, LOG_NORMAL);
    }
    else
    {
        /* unknown command */
        LOG(LOG_ERROR,
            ("Error: unrecognised command \"%s\".\n", argv[0]));
        print_command_usage(appl, LOG_ERROR);
        exit(1);
    }

    LOG_FREE;

    return 0;
}
