/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Create a blank disk image, format it, and copy some files onto it.
 */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "../diskimg/DiskImg.h"

using namespace DiskImgLib;

#define nil NULL
#define NELEM(x) (sizeof(x) / sizeof((x)[0]))

/*
 * Globals.
 */
FILE* gLog = nil;
pid_t gPid = getpid();

/*
 * Show usage info.
 */
void
Usage(const char* argv0)
{
    fprintf(stderr,
        "Usage: %s {dos|prodos|pascal} size image-filename.po input-file1 ...\n",
        argv0);

    fprintf(stderr, "\n");
    fprintf(stderr, "Example: makedisk prodos 800k foo.po file1.txt file2.txt\n");
}


/*
 * Create a ProDOS-ordered disk image.
 *
 * Returns a DiskImg pointer on success, or nil on failure.
 */
DiskImg*
CreateDisk(const char* fileName, long blockCount)
{
    DIError dierr;
    DiskImg* pDiskImg = nil;

    pDiskImg = new DiskImg;
    dierr = pDiskImg->CreateImage(
        fileName,
        nil,                            // storageName
        DiskImg::kOuterFormatNone,
        DiskImg::kFileFormatUnadorned,
        DiskImg::kPhysicalFormatSectors,
        nil,                            // pNibbleDescr
        DiskImg::kSectorOrderProDOS,
        DiskImg::kFormatGenericProDOSOrd,
        blockCount,
        true);                          // no need to format the image

    if (dierr != kDIErrNone) {
        fprintf(stderr, "ERROR: CreateImage failed: %s\n",
            DIStrError(dierr));
        delete pDiskImg;
        pDiskImg = nil;
    }

    return pDiskImg;
}

/*
 * Copy files to the disk.
 */
int
CopyFiles(DiskFS* pDiskFS, int argc, char** argv)
{
    DIError dierr;
    DiskFS::CreateParms parms;
    A2File* pNewFile;

    struct CreateParms {
        const char*     pathName;       // full pathname
        char            fssep;
        int             storageType;    // determines normal, subdir, or forked
        long            fileType;
        long            auxType;
        int             access;
        time_t          createWhen;
        time_t          modWhen;
    };


    argv--;
    while (argc--) {
        argv++;

        // Confirm this is a regular file, and not a directory.
        struct stat sb;
        if (stat(*argv, &sb) != 0) {
            fprintf(stderr, "Warning: unable to stat '%s'\n", *argv);
            continue;
        }
        if ((sb.st_mode & S_IFREG) == 0) {
            printf("--- Skipping '%s'\n", *argv);
            continue;
        }

        printf("+++ Adding '%s'\n", *argv);

        /*
         * Use external pathname as internal pathname.  This isn't quite
         * right, since things like "../" will end up getting converted
         * to something we don't want, but it'll do for now.
         */
        parms.pathName = *argv;
        parms.fssep = '/';      // UNIX fssep
        parms.storageType = DiskFS::kStorageSeedling;   // not forked, not dir
        parms.fileType = 0;     // NON
        parms.auxType = 0;      // $0000
        parms.access = DiskFS::kFileAccessUnlocked;
        parms.createWhen = time(nil);
        parms.modWhen = time(nil);

        /*
         * Create a new, empty file.  The "pNewFile" pointer does not belong
         * to us, so we should not delete it later, or try to access it
         * after the underlying file is deleted.
         */
        dierr = pDiskFS->CreateFile(&parms, &pNewFile);
        if (dierr != kDIErrNone) {
            fprintf(stderr, "ERROR: unable to create '%s': %s\n",
                *argv, DIStrError(dierr));
            return -1;
        }

        /*
         * Load the input file into memory.
         */
        FILE* fp;
        char* buf;
        long len;

        fp = fopen(*argv, "r");
        if (fp == nil) {
            fprintf(stderr, "ERROR: unable to open input file '%s': %s\n",
                *argv, strerror(errno));
            return -1;
        }

        if (fseek(fp, 0, SEEK_END) != 0) {
            fprintf(stderr, "ERROR: unable to seek input file '%s': %s\n",
                *argv, strerror(errno));
            fclose(fp);
            return -1;
        }

        len = ftell(fp);
        if (len >= 1L<<31) {    // 2GB
            fprintf(stderr, "Warning: file '%s' too large, skipping\n", *argv);
            fclose(fp);
            continue;
        }
        rewind(fp);

        buf = new char[len];
        if (buf == nil) {
            fprintf(stderr, "ERROR: unable to alloc %ld bytes\n", len);
            fclose(fp);
            return -1;
        }

        if (fread(buf, len, 1, fp) != 1) {
            fprintf(stderr, "ERROR: fread of %ld bytes from '%s' failed: %s\n",
                len, *argv, strerror(errno));
            fclose(fp);
            delete[] buf;
            return -1;
        }
        fclose(fp);

        /*
         * Write the buffer to the disk image.
         *
         * The A2FileDescr object is created by "Open" and deleted by
         * "Close".
         */
        A2FileDescr* pFD;

        dierr = pNewFile->Open(&pFD, true);
        if (dierr != kDIErrNone) {
            fprintf(stderr, "ERROR: unable to open new file '%s': %s\n",
                pNewFile->GetPathName(), DIStrError(dierr));
            delete[] buf;
            return -1;
        }

        dierr = pFD->Write(buf, len);
        if (dierr != kDIErrNone) {
            fprintf(stderr, "ERROR: failed writing to '%s': %s\n",
                pNewFile->GetPathName(), DIStrError(dierr));
            pFD->Close();
            pDiskFS->DeleteFile(pNewFile);
            delete[] buf;
            return -1;
        }
        delete[] buf;

        dierr = pFD->Close();
        if (dierr != kDIErrNone) {
            fprintf(stderr, "ERROR: failed while closing '%s': %s\n",
                pNewFile->GetPathName(), DIStrError(dierr));
            return -1;
        }
    }

    return 0;
}

/*
 * Process the request.
 *
 * Returns 0 on success, -1 on failure.
 */
int
Process(const char* formatName, const char* sizeStr,
    const char* outputFileName, int argc, char** argv)
{
    DiskImg::FSFormat format;
    long blockCount;

    if (strcasecmp(formatName, "dos") == 0)
        format = DiskImg::kFormatDOS33;
    else if (strcasecmp(formatName, "prodos") == 0)
        format = DiskImg::kFormatProDOS;
    else if (strcasecmp(formatName, "pascal") == 0)
        format = DiskImg::kFormatPascal;
    else {
        fprintf(stderr, "ERROR: invalid format '%s'\n", formatName);
        return -1;
    }

    if (strcasecmp(sizeStr, "140k") == 0)
        blockCount = 280;
    else if (strcasecmp(sizeStr, "800k") == 0)
        blockCount = 1600;
    else {
        blockCount = atoi(sizeStr);
        if (blockCount <= 0 || blockCount > 65536) {
            fprintf(stderr, "ERROR: invalid size '%s'\n", sizeStr);
            return -1;
        }
    }

    if (access(outputFileName, F_OK) == 0) {
        fprintf(stderr, "ERROR: output file '%s' already exists\n",
            outputFileName);
        return -1;
    }

    assert(argc >= 1);
    assert(*argv != nil);


    const char* volName;
    DiskImg* pDiskImg;
    DiskFS* pDiskFS;
    DIError dierr;

    /*
     * Prepare the disk image file.
     */
    pDiskImg = CreateDisk(outputFileName, blockCount);
    if (pDiskImg == nil)
        return -1;

    if (format == DiskImg::kFormatDOS33)
        volName = "DOS";        // put DOS 3.3 in tracks 0-2
    else
        volName = "TEST";
    dierr = pDiskImg->FormatImage(format, volName);
    if (dierr != kDIErrNone) {
        fprintf(stderr, "ERROR: unable to format disk: %s\n",
            DIStrError(dierr));
        delete pDiskImg;
        return -1;
    }

    /*
     * Prepare to access the image as a filesystem.
     */
    pDiskFS = pDiskImg->OpenAppropriateDiskFS(false);
    if (pDiskFS == nil) {
        fprintf(stderr, "ERROR: unable to open appropriate DiskFS\n");
        delete pDiskImg;
        return -1;
    }

    dierr = pDiskFS->Initialize(pDiskImg, DiskFS::kInitFull);
    if (dierr != kDIErrNone) {
        fprintf(stderr, "ERROR: unable to initialize DiskFS: %s\n",
            DIStrError(dierr));
        delete pDiskFS;
        delete pDiskImg;
        return -1;
    }

    /*
     * Copy the files over.
     */
    if (CopyFiles(pDiskFS, argc, argv) != 0) {
        delete pDiskFS;
        delete pDiskImg;
        return -1;
    }

    /*
     * Clean up.  Note "CloseImage" isn't strictly necessary, but it gives
     * us an opportunity to detect failures.
     */
    delete pDiskFS;

    if (pDiskImg->CloseImage() != 0) {
        fprintf(stderr, "WARNING: CloseImage failed: %s\n",
            DIStrError(dierr));
    }

    delete pDiskImg;
    return 0;
}


/*
 * Handle a debug message from the DiskImg library.
 */
/*static*/ void
MsgHandler(const char* file, int line, const char* msg)
{
    assert(file != nil);
    assert(msg != nil);

#ifdef _DEBUG
    fprintf(gLog, "%05u %s", gPid, msg);
#endif
}

/*
 * Process args.
 */
int
main(int argc, char** argv)
{
#ifdef _DEBUG
    const char* kLogFile = "makedisk-log.txt";
    gLog = fopen(kLogFile, "w");
    if (gLog == nil) {
        fprintf(stderr, "ERROR: unable to open log file\n");
        exit(1);
    }
#endif

#ifdef _DEBUG
    printf("Log file is '%s'\n", kLogFile);
#endif

    Global::SetDebugMsgHandler(MsgHandler);
    Global::AppInit();

    if (argc < 5) {
        Usage(argv[0]);
        exit(2);
    }

    const char* formatName;
    const char* sizeStr;
    const char* outputFileName;

    argv++;
    formatName = *argv++;
    sizeStr = *argv++;
    outputFileName = *argv++;
    argc -= 4;

    if (Process(formatName, sizeStr, outputFileName, argc, argv) == 0)
        fprintf(stderr, "Success!\n");
    else
        fprintf(stderr, "Failed.\n");

    Global::AppCleanup();
#ifdef _DEBUG
    fclose(gLog);
#endif

    exit(0);
}

