/*
 * NuLib2
 * Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved.
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the BSD License, see the file COPYING.
 *
 * Main entry point and shell command argument processing.
 */
#include "NuLib2.h"
#include <ctype.h>


/*
 * Globals and constants.
 */
const char* gProgName = "NuLib2";


/*
 * Which modifiers are valid with which commands?
 */
typedef struct ValidCombo {
    Command     cmd;
    Boolean     okayForPipe;
    Boolean     filespecRequired;
    const char* modifiers;
} ValidCombo;

static const ValidCombo gValidCombos[] = {
    { kCommandAdd,              false,  true,   "ekcz0jrfu" },
    { kCommandDelete,           false,  true,   "r" },
    { kCommandExtract,          true,   false,  "beslcjrfu" },
    { kCommandExtractToPipe,    true,   false,  "blr" },
    { kCommandListShort,        true,   false,  "br" },
    { kCommandListVerbose,      true,   false,  "br" },
    { kCommandListDebug,        true,   false,  "b" },
    { kCommandTest,             true,   false,  "br" },
    { kCommandHelp,             false,  false,  "" },
};


/*
 * Find an entry in the gValidCombos table matching the specified command.
 *
 * Returns NULL if not found.
 */
static const ValidCombo* FindValidComboEntry(Command cmd)
{
    int i;

    for (i = 0; i < NELEM(gValidCombos); i++) {
        if (gValidCombos[i].cmd == cmd)
            return &gValidCombos[i];
    }

    return NULL;
}

/*
 * Determine whether the specified modifier is valid when used with the
 * current command.
 */
static Boolean IsValidModifier(Command cmd, char modifier)
{
    const ValidCombo* pvc;

    pvc = FindValidComboEntry(cmd);
    if (pvc != NULL) {
        if (strchr(pvc->modifiers, modifier) == NULL)
            return false;
        else
            return true;
    } else
        return false;
}

/*
 * Determine whether the specified command can be used with stdin as input.
 */
static Boolean IsValidOnPipe(Command cmd)
{
    const ValidCombo* pvc;

    pvc = FindValidComboEntry(cmd);
    if (pvc != NULL) {
        return pvc->okayForPipe;
    } else
        return false;
}

/*
 * Determine whether the specified command can be used with stdin as input.
 */
static Boolean IsFilespecRequired(Command cmd)
{
    const ValidCombo* pvc;

    pvc = FindValidComboEntry(cmd);
    if (pvc != NULL) {
        return pvc->filespecRequired;
    } else {
        /* command not found?  warn about it here... */
        fprintf(stderr, "%s: Command %d not found in gValidCombos table\n",
            gProgName, cmd);
        return false;
    }
}


/*
 * Separate the program name out of argv[0].
 */
static const char* GetProgName(const NulibState* pState, const char* argv0)
{
    const char* result;
    char sep;

    /* use the appropriate system pathname separator */
    sep = NState_GetSystemPathSeparator(pState);

    result = strrchr(argv0, sep);
    if (result == NULL)
        result = argv0;
    else
        result++;   /* advance past the separator */
    
    return result;
}


/*
 * Print program usage.
 */
static void Usage(const NulibState* pState)
{
    int32_t majorVersion, minorVersion, bugVersion;
    const char* nufxLibDate;
    const char* nufxLibFlags;

    (void) NuGetVersion(&majorVersion, &minorVersion, &bugVersion,
            &nufxLibDate, &nufxLibFlags);

    printf("\nNuLib2 v%s, linked with NufxLib v%d.%d.%d [%s]\n",
        NState_GetProgramVersion(pState),
        majorVersion, minorVersion, bugVersion, nufxLibFlags);
    printf("Copyright (C) 2000-2017, Andy McFadden.  All Rights Reserved.\n");
    printf("This software is distributed under terms of the BSD License.\n");
    printf("Visit http://www.nulib.com/ for source code and documentation.\n\n");
    printf("Usage: %s -command[modifiers] archive [filename-list]\n\n",
        gProgName);
    printf(
        "  -a  add files, create arc if needed   -x  extract files\n"
        "  -t  list files (short)                -v  list files (verbose)\n"
        "  -p  extract files to pipe, no msgs    -i  test archive integrity\n"
        "  -d  delete files from archive         -h  extended help message\n"
        "\n"
        " modifiers:\n"
        "  -u  update files (add + keep newest)  -f  freshen (update, no add)\n"
        "  -r  recurse into subdirs              -j  junk (don't record) directory names\n"
        "  -0  don't use compression             -c  add one-line comments\n");
    if (NuTestFeature(kNuFeatureCompressDeflate) == kNuErrNone)
        printf("  -z  use zlib 'deflate' compression    ");
    else
        printf("  -z  use zlib [not included]           ");
    if (NuTestFeature(kNuFeatureCompressBzip2) == kNuErrNone)
        printf("-zz use bzip2 BWT compression\n");
    else
        printf("-zz use bzip2 [not included]\n");
    printf(
        "  -l  auto-convert text files           -ll convert CR/LF on ALL files\n"
        "  -s  stomp existing files w/o asking   -k  store files as disk images\n"
        "  -e  preserve ProDOS file types        -ee preserve types and extend names\n"
        "  -b  force Binary II mode\n"
        );
}


/*
 * Handle the "-h" command.
 */
NuError DoHelp(const NulibState* pState)
{
    static const struct {
        Command cmd;
        char letter;
        const char* shortDescr;
        const char* longDescr;
    } help[] = {
        { kCommandListVerbose, 'v', "verbose listing of archive contents",
"  List archive contents in a multi-column format with summary totals.\n"
"  The output is similar to what ShrinkIt produces.  You can specify the\n"
"  names of the files to list.\n"
"\n"
"  The '-b' modifier will force NuLib2 to open the file as Binary II.\n"
"  This can be useful when working with encapsulated archives (.BXY and\n"
"  .BSE) and when the archive is presented on standard input (i.e.\n"
"  \"nulib2 -vb - < archive.bny\").\n",
        },
        { kCommandListShort, 't', "quick dump of table of contents",
"  List the filenames, one per line, without any truncation or embellishment.\n"
"  Useful for feeding into another program.\n",
        },
        { kCommandAdd, 'a', "add files to an archive",
"  Add files to an archive, possibly creating it first.  Files in\n"
"  subdirectories will not be added unless the '-r' flag is provided.\n"
"\n"
"  You can specify the '-z' or '-zz' flag to use \"deflate\" or \"bzip2\"\n"
"  compression, respectively.  These work much better than ShrinkIt's LZW,\n"
"  but the files can't be unpacked on an Apple II.  The flags will only be\n"
"  enabled if NuLib2 was built with the necessary libraries.\n",
        },
        { kCommandExtract, 'x', "extract files from an archive",
"  Extract the specified items from the archive.  If nothing is specified,\n"
"  everything is extracted.  The '-r' modifier tells NuLib2 to do a prefix\n"
"  match, so \"nulib2 -xr archive.shk doc\" will extract everything in the\n"
"  \"doc\" directory.  It will also extract a file called \"document\".  If\n"
"  you just want what's in a directory, tack on the filename separator\n"
"  character, e.g. \"nulib2 -xr archive.shk doc:\".\n"
"\n"
"  When working with Binary II archives, the following suboptions aren't\n"
"  allowed: -u -f -c -l -ll.\n",
        },
        { kCommandExtractToPipe, 'p', "extract files to pipe",
"  Works just like '-x', but all files are written to stdout.  Useful for\n"
"  viewing a file, e.g. \"nulib2 -pl archive.shk document.txt | less\".\n"
"  The progress indicators and interactive prompts are disabled.\n",
        },
        { kCommandTest, 'i', "test archive integrity",
"  Verify the contents of an archive by extracting all files to memory and\n"
"  verifying all CRCs.  Note that uncompressed files in archives created by\n"
"  P8 ShrinkIt and un-SQueezed files in Binary II archives do not have any\n"
"  sort of checksum.\n",
        },
        { kCommandDelete, 'd', "delete files from archive",
"  Delete the named files from the archive.  If you delete all of the files,\n"
"  the archive itself will be removed.\n",
        },
        { kCommandHelp, 'h', "show extended help",
"  This is the extended help text.  Go to www.nulib.com for a full manual.\n",
        },
    };

    int i;


    printf("%s",
"\n"
"Copyright (C) 2000-2007 by Andy McFadden.  All Rights Reserved.\n\n"
"NuLib2 uses NufxLib, a complete library of functions for accessing NuFX\n"
"(ShrinkIt) archives.  Both NufxLib and NuLib2 are free software, distributed\n"
"under terms of the BSD License.  Source code is available from\n"
"http://www.nulib.com/, and copies of the licenses are included.\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
"README file for more details.\n\n"
    );

    printf("Usage: %s -command[modifiers] archive [filename-list]\n\n",
        gProgName);
    printf("If \"archive\" is \"-\", the archive will be read from stdin.\n");

    for (i = 0; i < NELEM(help); i++) {
        const ValidCombo* pvc;
        int j;

        pvc = FindValidComboEntry(help[i].cmd);
        if (pvc == NULL) {
            fprintf(stderr, "%s: internal error: couldn't find vc for %d\n",
                gProgName, help[i].cmd);
            continue;
        }

        printf("\nCommand \"-%c\": %s\n", help[i].letter, help[i].shortDescr);
        printf("  Valid modifiers:");
        for (j = strlen(pvc->modifiers) -1; j >= 0; j--) {
            char ch = pvc->modifiers[j];
            /* print flags, special-casing options that can be doubled */
            if (ch == 'l' || ch == 'e' || ch == 'z')
                printf(" -%c -%c%c", ch, ch, ch);
            else
                printf(" -%c", ch);
        }
        putchar('\n');

        printf("\n%s", help[i].longDescr);
    }
    putchar('\n');

    printf("Compression algorithms supported:\n");
    printf("  SQueeze (Huffman+RLE) ...... %s\n",
        NuTestFeature(kNuFeatureCompressSQ) == kNuErrNone? "yes" : "no");
    printf("  LZW/1 and LZW/2 ............ %s\n",
        NuTestFeature(kNuFeatureCompressLZW) == kNuErrNone ? "yes" : "no");
    printf("  12- and 16-bit LZC ......... %s\n",
        NuTestFeature(kNuFeatureCompressLZC) == kNuErrNone ? "yes" : "no");
    printf("  Deflate .................... %s\n",
        NuTestFeature(kNuFeatureCompressDeflate) == kNuErrNone ? "yes" : "no");
    printf("  Bzip2 ...................... %s\n",
        NuTestFeature(kNuFeatureCompressBzip2) == kNuErrNone ? "yes" : "no");


    return kNuErrNone;
}


/*
 * Process the command-line options.  The results are placed into "pState".
 */
static int ProcessOptions(NulibState* pState, int argc, char* const* argv)
{
    const char* cp;
    int idx;

    /*
     * Must have at least a command letter and an archive filename, unless
     * the command letter is 'h'.  Special-case a solitary "-h" here.
     */
    if (argc == 2 && (tolower(argv[1][0]) == 'h' ||
                     (argv[1][0] == '-' && tolower(argv[1][1] == 'h')) ) )
    {
        DoHelp(NULL);
        return -1;
    }

    if (argc < 3) {
        Usage(pState);
        return -1;
    }

    /*
     * Argv[1] and any subsequent entries that have a leading hyphen
     * are options.  Anything after that is a filename.  Parse until we
     * think we've hit the filename.
     *
     * By UNIX convention, however, stdin is specified as a file called "-".
     */
    for (idx = 1; idx < argc; idx++) {
        cp = argv[idx];

        if (idx > 1 && *cp != '-')
            break;

        if (*cp == '-')
            cp++;
        if (*cp == '\0') {
            if (idx == 1) {
                fprintf(stderr,
                    "%s: You must specify a command after the '-'\n",
                    gProgName);
                goto fail;
            } else {
                /* they're using '-' for the filename */
                break;
            }
        }

        if (idx == 1) {
            switch (tolower(*cp)) {
            case 'a': NState_SetCommand(pState, kCommandAdd);           break;
            case 'x': NState_SetCommand(pState, kCommandExtract);       break;
            case 'p': NState_SetCommand(pState, kCommandExtractToPipe); break;
            case 't': NState_SetCommand(pState, kCommandListShort);     break;
            case 'v': NState_SetCommand(pState, kCommandListVerbose);   break;
            case 'g': NState_SetCommand(pState, kCommandListDebug);     break;
            case 'i': NState_SetCommand(pState, kCommandTest);          break;
            case 'd': NState_SetCommand(pState, kCommandDelete);        break;
            case 'h': NState_SetCommand(pState, kCommandHelp);          break;
            default:
                fprintf(stderr, "%s: Unknown command '%c'\n", gProgName, *cp);
                goto fail;
            }

            cp++;
        }

        while (*cp != '\0') {
            switch (tolower(*cp)) {
            case 'u': NState_SetModUpdate(pState, true);                break;
            case 'f': NState_SetModFreshen(pState, true);               break;
            case 'r': NState_SetModRecurse(pState, true);               break;
            case 'j': NState_SetModJunkPaths(pState, true);             break;
            case '0': NState_SetModNoCompression(pState, true);         break;
            case 's': NState_SetModOverwriteExisting(pState, true);     break;
            case 'k': NState_SetModAddAsDisk(pState, true);             break;
            case 'c': NState_SetModComments(pState, true);              break;
            case 'b': NState_SetModBinaryII(pState, true);              break;
            case 'z':
                if (*(cp+1) == 'z') {
                    if (NuTestFeature(kNuFeatureCompressBzip2) == kNuErrNone)
                        NState_SetModCompressBzip2(pState, true);
                    else {
                        fprintf(stderr,
                            "%s: ERROR: libbz2 support not compiled in\n",
                            gProgName);
                        goto fail;
                    }
                    cp++;
                } else {
                    if (NuTestFeature(kNuFeatureCompressDeflate) == kNuErrNone)
                        NState_SetModCompressDeflate(pState, true);
                    else {
                        fprintf(stderr,
                            "%s: ERROR: zlib support not compiled in\n",
                            gProgName);
                        goto fail;
                    }
                }
                break;
            case 'e':
                if (*(cp-1) == 'e')     /* should never point at invalid */
                    NState_SetModPreserveTypeExtended(pState, true);
                else
                    NState_SetModPreserveType(pState, true);
                break;
            case 'l':
                if (*(cp-1) == 'l')     /* should never point at invalid */
                    NState_SetModConvertAll(pState, true);
                else
                    NState_SetModConvertText(pState, true);
                break;
            default:
                fprintf(stderr, "%s: Unknown modifier '%c'\n", gProgName, *cp);
                goto fail;
            }

            if (!IsValidModifier(NState_GetCommand(pState), (char)tolower(*cp)))
            {
                fprintf(stderr,
                    "%s: The '%c' modifier doesn't make sense here\n",
                    gProgName, tolower(*cp));
                goto fail;
            }

            cp++;
        }
    }

    /*
     * Can't have tea and no tea at the same time.
     */
    if (NState_GetModNoCompression(pState) &&
        NState_GetModCompressDeflate(pState))
    {
        fprintf(stderr, "%s: Can't specify both -0 and -z\n",
            gProgName);
        goto fail;
    }

    /*
     * See if we have an archive name.  If it's "-", see if we allow that.
     */
    Assert(idx < argc);
    NState_SetArchiveFilename(pState, argv[idx]);
    if (IsFilenameStdin(argv[idx])) {
        if (!IsValidOnPipe(NState_GetCommand(pState))) {
            fprintf(stderr, "%s: You can't do that with a pipe\n",
                gProgName);
            goto fail;
        }
    }
    idx++;

    /*
     * See if we have a file specification.  Some of the commands require
     * a filespec; others just perform the requested operation on all of
     * the records in the archive if none is provided.
     */
    if (idx < argc) {
        /* got one or more */
        NState_SetFilespecPointer(pState, &argv[idx]);
        NState_SetFilespecCount(pState, argc - idx);
    } else {
        Assert(idx == argc);
        if (IsFilespecRequired(NState_GetCommand(pState))) {
            fprintf(stderr, "%s: This command requires a list of files\n",
                gProgName);
            goto fail;
        }
        NState_SetFilespecPointer(pState, NULL);
        NState_SetFilespecCount(pState, 0);
    }


#ifdef DEBUG_VERBOSE
    NState_DebugDump(pState);
#endif

    return 0;

fail:
    fprintf(stderr,
        "%s: (invoke without arguments to see usage information)\n",
        gProgName);
    return -1;
}


/*
 * We have all of the parsed command line options in "pState".  Now we just
 * have to do something useful with it.
 *
 * Returns 0 on success, 1 on error.
 */
int DoWork(NulibState* pState)
{
    NuError err;

    switch (NState_GetCommand(pState)) {
    case kCommandAdd:
        err = DoAdd(pState);
        break;
    case kCommandExtract:
        err = DoExtract(pState);
        break;
    case kCommandExtractToPipe:
        err = DoExtractToPipe(pState);
        break;
    case kCommandTest:
        err = DoTest(pState);
        break;
    case kCommandListShort:
        err = DoListShort(pState);
        break;
    case kCommandListVerbose:
        err = DoListVerbose(pState);
        break;
    case kCommandListDebug:
        err = DoListDebug(pState);
        break;
    case kCommandDelete:
        err = DoDelete(pState);
        break;
    case kCommandHelp:
        err = DoHelp(pState);
        break;
    default:
        fprintf(stderr, "ERROR: unexpected command %d\n",
            NState_GetCommand(pState));
        err = kNuErrInternal;
        Assert(0);
        break;
    }

    return (err != kNuErrNone);
}

/*
 * Entry point.
 */
int main(int argc, char** argv)
{
    NulibState* pState = NULL;
    int32_t majorVersion, minorVersion, bugVersion;
    int result = 0;

    (void) NuGetVersion(&majorVersion, &minorVersion, &bugVersion, NULL, NULL);
    if (majorVersion != kNuVersionMajor || minorVersion < kNuVersionMinor) {
        fprintf(stderr, "ERROR: wrong version of NufxLib --"
                        " wanted %d.%d.x, got %d.%d.%d.\n",
            kNuVersionMajor, kNuVersionMinor,
            majorVersion, minorVersion, bugVersion);
        goto bail;
    }

    #if 0
    extern NuResult ErrorMessageHandler(NuArchive* pArchive,
        void* vErrorMessage);
    NuSetGlobalErrorMessageHandler(ErrorMessageHandler);
    #endif

    if (NState_Init(&pState) != kNuErrNone) {
        fprintf(stderr, "ERROR: unable to initialize globals\n");
        exit(1);
    }

    gProgName = GetProgName(pState, argv[0]);

    if (ProcessOptions(pState, argc, argv) < 0) {
        result = 2;
        goto bail;
    }

    if (NState_ExtraInit(pState) != kNuErrNone) {
        fprintf(stderr, "ERROR: additional initialization failed\n");
        exit(1);
    }

    result = DoWork(pState);
    if (result)
        printf("Failed.\n");

bail:
    NState_Free(pState);
    exit(result);
}

