////////////////////////////////////////////////////////////////////////////////
//
#define TITLE "usfv - Universal SFV create/verify utility"
#define COPYR "Copyright (C) 2003,2010 Neill Corlett"
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////////////

#include "common.h"
#include "banner.h"

////////////////////////////////////////////////////////////////////////////////

enum { BIG_FILENAME_SIZE = 30000 };

////////////////////////////////////////////////////////////////////////////////

static int usfv_isspace(int c) {
    return (
        (c == 32) ||
        ((c >= 9) && (c <= 13))
    );
}

////////////////////////////////////////////////////////////////////////////////

static int getfilecrc(
    const char *filename,
    uint32_t *crc_out
) {
    static uint32_t crctable[256];
    static char initialized = 0;
    uint32_t crc = 0;
    FILE *f;
    if(!initialized) {
        uint32_t i, j, k;
        for(i = 0; i < 256; i++) {
            j = i ^ 0xFF;
            for (k = 0; k < 8; k++) {
                j = (j & 1) ? 0xEDB88320 ^ (j >> 1) : j >> 1;
            }
            crctable[i] = j ^ 0xFF000000;
        }
        initialized = 1;
    }
    f = fopen(filename, "rb");
    if(!f) { return -1; }
    if(crc_out) {
        for(;;) {
            int c = fgetc(f);
            if(c == EOF) { break; }
            crc = (crc >> 8) ^ crctable[(crc ^ c) & 0xFF];
        }
        *crc_out = crc;
    }
    fclose(f);
    return 0;
}

////////////////////////////////////////////////////////////////////////////////
//
// Convert a time_t to a struct tm, safely
//
static struct tm my_localtime(time_t t) {
    struct tm *p_tm = localtime(&t);
    if(!p_tm) { t = 0; p_tm = localtime(&t); }
    if(!p_tm) {
        struct tm fallback;
        memset(&fallback, 0, sizeof(fallback));
        return fallback;
    }
    return (*p_tm);
}

////////////////////////////////////////////////////////////////////////////////
//
// Returns the number of errors
//
static off_t sfvcreate(
    char *sfvfilename,
    char **filename,
    size_t n
) {
    off_t nerrors = 0;
    FILE *sfvfile = NULL;

    size_t i = 0;
    off_t nfiles_done = 0;

    sfvfile = fopen(sfvfilename, "rb");
    if(sfvfile) {
        printf("Error: %s already exists; will not overwrite\n", sfvfilename);
        goto error;
    }
    sfvfile = fopen(sfvfilename, "wb");
    if(!sfvfile) { goto error_sfvfile; }
    printf("Creating SFV file %s\n", sfvfilename);
    {   char buf[24];
        size_t r;
        struct tm my_tm = my_localtime(time(NULL));
        memset(buf, 0, sizeof(buf));
        r = strftime(buf, sizeof(buf)-1, "%Y-%m-%d at %H:%M.%S", &my_tm);
        if(r < 1) { strncpy(buf, "9999-99-99 at 99:99.99", sizeof(buf)-1); }
        fprintf(sfvfile,
            "; Generated by WIN-SFV32 v1.1a "
            "(usfv) "
            "on %s", buf
        );
    }
    //
    // Note: SFV newlines have to be in CRLF format, regardless of platform
    //
    fputc('\r', sfvfile);
    fputc('\n', sfvfile);
    fputc(';' , sfvfile);
    fputc('\r', sfvfile);
    fputc('\n', sfvfile);
    //
    // Write comments indicating file lengths/times
    //
    for(i = 0; i < n; i++) {
        struct stat mystat;
        int r;
        memset(&mystat, 0, sizeof(mystat));
        r = stat(filename[i], &mystat);
        if(r >= 0) {
            size_t s;
            struct tm my_tm;
            char buf[24];
            if(S_ISDIR(mystat.st_mode)) {
                // Skip directories
                filename[i] = NULL;
                continue;
            }
            my_tm = my_localtime(mystat.st_mtime);
            memset(buf, 0, sizeof(buf));
            s = strftime(buf, sizeof(buf)-1, "%H:%M.%S %Y-%m-%d", &my_tm);
            if(s < 1) { strncpy(buf, "                   ", sizeof(buf)-1); }
            fprintf(sfvfile, ";%13lu  %s %s", ((unsigned long)(mystat.st_size)), buf, filename[i]);
            fputc('\r', sfvfile);
            fputc('\n', sfvfile);
        }
    }
    //
    // Calculate CRCs
    //
    for(i = 0; i < n; i++) {
        uint32_t crc = 0;
        int r;
        if(!filename[i]) { continue; }
        printf("%-22s ", filename[i]);
        fflush(stdout);
        r = getfilecrc(filename[i], &crc);
        if(r) {
            printf("%s\n", strerror(errno));
            nerrors++;
            continue;
        }
        printf("%02X%02X%02X%02X\n",
            (int)((crc >> 24) & 0xFF),
            (int)((crc >> 16) & 0xFF),
            (int)((crc >>  8) & 0xFF),
            (int)((crc >>  0) & 0xFF)
        );
        fprintf(sfvfile,
            "%s %02X%02X%02X%02X",
            filename[i],
            (int)((crc >> 24) & 0xFF),
            (int)((crc >> 16) & 0xFF),
            (int)((crc >>  8) & 0xFF),
            (int)((crc >>  0) & 0xFF)
        );
        fputc('\r', sfvfile);
        fputc('\n', sfvfile);
        nfiles_done++;
    }
    printf(
        "%lu file%s processed\n"
        "%lu error%s encountered\n",
        (unsigned long)nfiles_done, nfiles_done == 1 ? "" : "s",
        (unsigned long)nerrors    , nerrors     == 1 ? "" : "s"
    );

    goto done;

error_sfvfile:
    printfileerror(sfvfile, sfvfilename);
    goto error;

error:
    nerrors = 1;

done:
    if(sfvfile != NULL) { fclose(sfvfile); }

    return nerrors;
}

////////////////////////////////////////////////////////////////////////////////
//
// Returns the number of errors
//
static off_t sfvverify(
    char *sfvfilename
) {
    off_t returncode = 0;
    FILE *sfvfile  = NULL;
    char *filename = NULL;

    int filename_size = 0;
    size_t nfiles = 0;
    size_t nfiles_ok = 0;
    size_t nfiles_bad = 0;
    size_t nfiles_err = 0;

    sfvfile = fopen(sfvfilename, "rb");
    if(!sfvfile) { goto error_sfvfile; }
    printf("Verifying SFV file %s\n", sfvfilename);
    for(;;) {
        int c, i, filename_len;
        const char *crcstr;
        uint32_t crcnum, filecrcnum;
        do { c = fgetc(sfvfile); } while(c != EOF && usfv_isspace(c));
        if(c == EOF) { break; }
        // Skip comments
        if(c == ';') {
            do { c = fgetc(sfvfile); } while(c != EOF && c != '\n');
            if(c == EOF) { break; }
            continue;
        }
        // Assume this is a filename
        filename_len = 0;
        for(;;) {
            if((filename_len + 1) >= filename_size) {
                if(filename_size > BIG_FILENAME_SIZE) {
                    fflush(stdout);
                    printf("Corrupt SFV file\n");
                    goto error;
                }
                filename_size += 100;
                filename = realloc(filename, filename_size);
                if(!filename) {
                    printf("Out of memory\n");
                    goto error;
                }
            }
            filename[filename_len++] = c;
            if(!c) { break; }
            c = fgetc(sfvfile);
            if(c == EOF || c == '\n') {
                c = 0;
            }
        }
        filename_len--;
        while(filename_len && usfv_isspace(filename[filename_len - 1])) {
            filename_len--;
        }
        filename[filename_len] = 0;
        i = filename_len;
        while(i && !usfv_isspace(filename[i - 1])) { i--; }
        crcstr = filename + i;
        while(i && usfv_isspace(filename[i - 1])) { i--; }
        // No filename? just one thing on a line? ignore it
        if(!i) { continue; }
        filename[i] = 0;
        crcnum = strtoul(crcstr, NULL, 16);
        printf("%-22s ", filename);
        fflush(stdout);
        filecrcnum = 0;
        i = getfilecrc(filename, &filecrcnum);
        if(i) {
            nfiles_err++;
            printf("%s\n", strerror(errno));
            continue;
        }
        nfiles++;
        if(filecrcnum == crcnum) {
            nfiles_ok++;
            printf("ok\n");
        } else {
            nfiles_bad++;
            printf("*** BAD *** (%02X%02X%02X%02X, should be %02X%02X%02X%02X)\n",
                (int)((filecrcnum >> 24) & 0xFF),
                (int)((filecrcnum >> 16) & 0xFF),
                (int)((filecrcnum >>  8) & 0xFF),
                (int)((filecrcnum >>  0) & 0xFF),
                (int)((    crcnum >> 24) & 0xFF),
                (int)((    crcnum >> 16) & 0xFF),
                (int)((    crcnum >>  8) & 0xFF),
                (int)((    crcnum >>  0) & 0xFF)
            );
        }
    }
    printf(
        "%lu file%s checked (%lu ok, %lu bad)\n"
        "%lu error%s encountered\n",
        (unsigned long)nfiles    , nfiles     == 1 ? "" : "s",
        (unsigned long)nfiles_ok ,
        (unsigned long)nfiles_bad,
        (unsigned long)nfiles_err, nfiles_err == 1 ? "" : "s"
    );

    returncode = nfiles_err + nfiles_bad;
    goto done;

error_sfvfile:
    printfileerror(sfvfile, sfvfilename);
    goto error;

error:
    returncode = 1;

done:
    if(sfvfile  != NULL) { fclose(sfvfile); }
    if(filename != NULL) { free(filename); }

    return returncode;
}

////////////////////////////////////////////////////////////////////////////////

int main(
    int argc,
    char **argv
) {
    normalize_argv0(argv[0]);

    tzset();
    if(
        (argc < 3) ||
        (argv[1][0] == 0) ||
        (argv[1][1] != 0) ||
        (argv[1][0] != 'c' && argv[1][0] != 'v') ||
        (argv[1][0] == 'c' && argc == 3) ||
        (argv[1][0] == 'v' && argc > 3)
    ) {
        banner();
        printf(
            "Usage:\n"
            "To create a SFV file: %s c sfvfile filenames\n"
            "To verify a SFV file: %s v sfvfile\n", argv[0], argv[0]
        );
        return 1;
    }
    if(argv[1][0] == 'c') {
        return sfvcreate(argv[2], argv + 3, argc - 3);
    } else {
        return sfvverify(argv[2]);
    }
}

////////////////////////////////////////////////////////////////////////////////
