/*
 * objlink - Link files (RAW and PRG) into one PRG object
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2002-2012 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include <errno.h>
#include "dmlib.h"
#include "dmargs.h"
#include "dmfile.h"
#include "dmmutex.h"

#define MAX_FILENAMES    (128)
#define MAX_MEMBLOCKS    (128)


/* Typedefs
 */
typedef struct
{
    ssize_t start, end;	// Start and end address
    int type;           // Type
    char *name;         // Name of the block
    int placement;
} DMMemBlock;

typedef struct
{
    char *name;         // Description of memory model
    char *desc;
    ssize_t size;        // Total addressable memory size
    ssize_t nmemBlocks;  // Defined memory areas
    DMMemBlock memBlocks[MAX_MEMBLOCKS];
} DMMemModel;

typedef struct
{
    char *filename;
    int type;
    int placement;
    ssize_t addr;
} DMSourceFile;

// Source file type
enum
{
    STYPE_RAW = 1,
    STYPE_PRG,
    STYPE_PRGA
};

// How to determine block placement / address
enum
{
    PLACE_STATIC = 1,  // Already known
    PLACE_ARGUMENT,   // Commandline argument
    PLACE_FILE,       // From file
};

enum
{
    FMT_GENERIC = 1,
    FMT_PLAIN,
    FMT_DECIMAL
};

enum
{
    MTYPE_NONE = 0,
    MTYPE_ROM,        // Hard ROM
    MTYPE_ROM_WT,     // Write to RAM through ROM
    MTYPE_IO,         // I/O lines
    MTYPE_RES         // RESERVED
};

enum
{
    LA_NONE = -2,
    LA_AUTO = -1
};

/* Memory models
 */
const DMMemModel memoryModels[] = {
    { "C64 unrestricted", "$01 = $34", (64*1024), 0, {
    { 0, 0, 0, NULL, 0 }
    }},

    { "C64 normal (IO+Basic+Kernal)", "$01 = $37", (64*1024), 3, {
    { 0xA000, 0xBFFF,    MTYPE_ROM_WT,    "Basic ROM",  PLACE_STATIC },
    { 0xD000, 0xDFFF,    MTYPE_IO,        "I/O",  PLACE_STATIC },
    { 0xE000, 0xFFFF,    MTYPE_ROM_WT,    "Kernal ROM",  PLACE_STATIC },
    }},

    { "C64 modified (IO+Kernal)", "$01 = $36", (64*1024), 2, {
    { 0xD000, 0xDFFF,    MTYPE_IO,        "I/O",  PLACE_STATIC },
    { 0xE000, 0xFFFF,    MTYPE_ROM_WT,    "Kernal ROM",  PLACE_STATIC },
    }},

    { "C64 modified (IO only)", "$01 = $35", (64*1024), 1, {
    { 0xD000, 0xDFFF,    MTYPE_IO,        "I/O",  PLACE_STATIC },
    }},

    { "C64 modified (Char+Kernal+Basic)", "$01 = $33", (64*1024), 3, {
    { 0xA000, 0xBFFF,    MTYPE_ROM_WT,    "Basic ROM",  PLACE_STATIC },
    { 0xD000, 0xDFFF,    MTYPE_ROM,       "Char ROM", PLACE_STATIC },
    { 0xE000, 0xFFFF,    MTYPE_ROM_WT,    "Kernal ROM", PLACE_STATIC },
    }},

/*
    { "C64 normal", "$01 = $37", (64*1024), 0, {
    { 0x0000, 0x0000,    MTYPE_RAM,    "" },
    }},
*/
};

static const int nmemoryModels = sizeof(memoryModels) / sizeof(memoryModels[0]);


/* Global variables
 */
int           nsrcFiles = 0;              // Number of source files
DMSourceFile  srcFiles[MAX_FILENAMES];    // Source file names

int        nmemBlocks = 0;
DMMemBlock memBlocks[MAX_FILENAMES];

char       *optLinkFileName = NULL;
int        optLinkFileFormat = FMT_GENERIC;

BOOL       optDescribe = FALSE,
           optAllowOverlap = FALSE;

Uint32     optInitValue = 0;
int        optInitValueType = 1;
ssize_t    optCropStart, optCropEnd;
BOOL       optCropOutput = FALSE;

ssize_t    optLoadAddress = LA_AUTO;

int        optMemModel = 0;
const DMMemModel *memModel = NULL;
Uint8      *memory = NULL;

char       *optDestName = NULL;


/* Arguments
 */
static DMOptArg optList[] = {
    {  0, '?', "help",        "Show this help", OPT_NONE },
    {  1, 'r', "input-raw",   "RAW input: -r <file>:<addr>", OPT_ARGREQ },
    {  2, 'p', "input-prg",   "PRG input: -p <file>[:<addr>]", OPT_ARGREQ },
    { 12, 's', "section",     "Reserved section: -s <start>-<end>[,name] or <start>:<len>[,name]", OPT_ARGREQ },
    {  5, 'o', "output",      "Specify output file, -o <file>", OPT_ARGREQ },
    {  6, 'O', "overlap",     "Allow overlapping memory areas", OPT_NONE },
    {  7, 'm', "model",       "Set memory model", OPT_ARGREQ },
    {  8, 'l', "link-file",   "Output addresses and labels into file", OPT_ARGREQ },
    {  9, 'f', "format",      "Format of link-file: (g)eneric, (p)lain, (d)ecimal", OPT_ARGREQ },
    { 10, 'i', "initvalue",   "Initialize memory with: -i <byte/word/dword>:[bwd]", OPT_ARGREQ },
    { 11, 'd', "describe",    "Output ASCII memory map description", OPT_NONE },
    { 13, 'c', "crop",        "Crop output file to: -c <start>-<end> or <start>:<len>", OPT_ARGREQ },
    { 14, 'L', "load-address","Set output file load address (or 'none' for 'raw' output)", OPT_ARGREQ },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowHelp()
{
    int i;

    dmPrintBanner(stdout, dmProgName, "[options]");
    dmArgsPrintHelp(stdout, optList, optListN);

    printf(
    "\n"
    "Each numeric argument can be prefixed with $ or 0x for hexadecimal values.\n"
    "NOTICE! -p filename:<addr> will ignore load address and use <addr> instead!\n"
    "\n"
    "Available memory models:\n");

    for (i = 0; i < nmemoryModels; i++)
    {
        const DMMemModel *m = &memoryModels[i];
        printf("  %d = %-40s [%s] (%d kB)\n",
            i, m->name, m->desc, m->size / 1024);
    }
}


off_t dmGetFileSize(FILE *f)
{
    off_t len, pos = ftell(f);
    fseeko(f, 0, SEEK_END);
    len = ftell(f);
    fseek(f, pos, SEEK_SET);
    return len;
}


/* Memory block handling
 */
void reserveMemBlock(ssize_t startAddr, ssize_t endAddr, const char *blockName, int blockType)
{
    if (startAddr > endAddr)
    {
        dmError("ERROR! Block '%s' has startAddr=$%.4x > endAddr=$%.4x!\n",
            blockName, startAddr, endAddr);
        exit(4);
    }

    if (nmemBlocks < MAX_FILENAMES)
    {
        memBlocks[nmemBlocks].start = startAddr;
        memBlocks[nmemBlocks].end   = endAddr;
        memBlocks[nmemBlocks].name  = dm_strdup(blockName);
        memBlocks[nmemBlocks].type  = blockType;
        nmemBlocks++;
    }
    else
    {
        dmError("Maximum number of memBlock definitions (%d) exceeded!\n",
            MAX_FILENAMES);
        exit(4);
    }
}


int compareMemBlock(const void *cva, const void *cvb)
{
    const DMMemBlock *a = cva, *b = cvb;
    return a->start - b->start;
}


BOOL dmParseSection(const char *arg, ssize_t *sectStart, ssize_t *sectEnd, char **sectName, BOOL canHasName)
{
    char sectMode, *sep, *str, *namesep;
    ssize_t tmpi;

    // Define reserved section
    // Create a copy of the argument
    if ((str = dm_strdup(arg)) == NULL)
    {
        dmError("Could not allocate temporary string!\n");
        exit(128);
    }

    // Get start address
    if ((sep = strchr(str, '-')) == NULL &&
        (sep = strchr(str, ':')) == NULL)
    {
        dmError("Section definition '%s' invalid.\n", arg);
        goto error;
    }
    sectMode = *sep;
    *sep = 0;

    // Get value
    if (!dmGetIntVal(str, sectStart))
    {
        dmError("Section start address '%s' in '%s' invalid.\n", str, arg);
        goto error;
    }
    
    // Check for name
    namesep = strchr(sep + 1, ',');
    if (canHasName && namesep != NULL)
    {
        *namesep = 0;
        namesep++;
        if (*namesep == 0)
        {
            dmError("Section definition '%s' name is empty. Either specify name or leave it out.\n",
                arg);
            goto error;
        }
        *sectName = dm_strdup(namesep);
    }
    else
    if (namesep != NULL)
    {
        dmError("Section definition does not allow a name, syntax error in '%s' at '%s'.\n",
            arg, namesep);
        goto error;
    }

    // Get end address or length
    if (!dmGetIntVal(sep + 1, &tmpi))
    {
        dmError("Section %s '%s' in '%s' invalid.\n",
            sectMode == '-' ? "end address" : "length",
            sep + 1, arg);
        goto error;
    }

    if (sectMode == ':')
    {
        *sectEnd = *sectStart + tmpi - 1;
    }
    else
    {
        if (tmpi < *sectStart)
        {
            dmError("Section start address > end address in '%s'.\n",
                arg);
            goto error;
        }
        *sectEnd = tmpi;
    }

    dmFree(str);
    return TRUE;

error:
    dmFree(str);
    return FALSE;
}


BOOL dmParseInputFile(char *arg, const int type1, const int type2, const char *desc, BOOL requireAddr)
{
    ssize_t tmpi = 0;
    BOOL hasAddr = FALSE;
    char *sep;

    if ((sep = strrchr(arg, ':')) != NULL)
    {
        *sep = 0;
        if (!dmGetIntVal(sep + 1, &tmpi))
        {
            dmError("Invalid %s address '%s' specified for '%s'.\n",
                desc, sep + 1, arg);
            return FALSE;
        }
        hasAddr = TRUE;
    }
    else
    if (requireAddr)
    {
        dmError("No %s loading address specified for '%s'.\n", desc, arg);
        return FALSE;
    }

    srcFiles[nsrcFiles].filename = arg;
    srcFiles[nsrcFiles].type = hasAddr ? type1 : type2;
    srcFiles[nsrcFiles].addr = tmpi;
    nsrcFiles++;
    return TRUE;
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    char *p;
    ssize_t tmpi;

    switch (optN) {
    case 0:
        argShowHelp();
        exit(0);
        break;

    case 1:
        // Add RAW
        if (!dmParseInputFile(optArg, STYPE_RAW, STYPE_RAW, "RAW", TRUE))
            return FALSE;
        break;

    case 2:
        // Add PRG
        if (!dmParseInputFile(optArg, STYPE_PRGA, STYPE_PRG, "PRG", FALSE))
            return FALSE;
        break;

    case 5:
        // Set output file name
        optDestName = optArg;
        break;

    case 6:
        // Allow overlapping segments
        optAllowOverlap = TRUE;
        dmError("Warning, allowing overlapping data.\n");
        break;

    case 7:
        // Set memory model
        optMemModel = atoi(optArg);
        if (optMemModel < 0 || optMemModel >= nmemoryModels)
        {
            dmError("Invalid memory model number %i!\n", optMemModel);
            return FALSE;
        }
        break;

    case 8:
        // Linker file
        optLinkFileName = optArg;
        break;

    case 9:
        // Linker file format
        switch (tolower(optArg[0]))
        {
            case 'g':
                optLinkFileFormat = FMT_GENERIC;
                break;
            case 'p':
                optLinkFileFormat = FMT_PLAIN;
                break;
            case 'd':
                optLinkFileFormat = FMT_DECIMAL;
                break;

            default:
                dmError("Invalid/unknown linker file format '%s'!\n",
                    optArg);
                return FALSE;
        }
        break;

    case 10:
        // Initialization value
        optInitValueType = 1;
        if ((p = strrchr(optArg, ':')) != NULL)
        {
            *p = 0;
            switch (tolower(p[1]))
            {
                case 'b': optInitValueType = 1; break;
                case 'w': optInitValueType = 2; break;
                case 'd': optInitValueType = 4; break;
                default:
                    dmError("Invalid init value type '%c' specified for '%s'.\n",
                        p[1], optArg);
                    return FALSE; 
            }
        }
        if (!dmGetIntVal(optArg, &tmpi))
        {
            dmError("Invalid initvalue '%s'.\n", optArg);
            return FALSE;
        }
        optInitValue = tmpi;
        break;

    case 11:
        // Set describe mode
        optDescribe = TRUE;
        break;

    case 12:
        {
            char *sectName = "Clear";
            ssize_t sectStart, sectEnd, sectLen;
            if (!dmParseSection(optArg, &sectStart, &sectEnd, &sectName, TRUE))
                return FALSE;

            // Allocate memory block
            sectLen = sectEnd - sectStart + 1;
            dmMsg(1, "Reserve $%.4x - $%.4x ($%x, %d bytes) as '%s'\n",
                sectStart, sectEnd, sectLen, sectLen, sectName);

            reserveMemBlock(sectStart, sectEnd, sectName, MTYPE_RES);
        }
        break;

    case 13:
        {
            size_t cropLen;
            if (!dmParseSection(optArg, &optCropStart, &optCropEnd, NULL, FALSE))
                return FALSE;

            cropLen = optCropEnd - optCropEnd + 1;
            dmMsg(1, "Cutting output to $%.4x - $%.4x ($%x, %d bytes)\n",
                optCropStart, optCropEnd, cropLen, cropLen);

            optCropOutput = TRUE;
        }
        break;

    case 14:
        // Set loading address
        if (strcasecmp(optArg, "none") == 0)
            optLoadAddress = LA_NONE;
        else
        {
            if (!dmGetIntVal(optArg, &tmpi))
            {
                dmError("Invalid loading address '%s'.\n", optArg);
                return FALSE;
            }
            if (tmpi < 0 || tmpi >= 64*1024)
            {
                dmError("Invalid or insane loading address %d/$%x!\n",
                    tmpi);
                return FALSE;
            }
            optLoadAddress = tmpi;
        }
        break;

    default:
        dmError("Unknown argument '%s'.\n", currArg);
        return FALSE;
    }

    return TRUE;
}


int dmLoadPRG(const char *filename, BOOL forceAddr, const ssize_t destAddr)
{
    FILE *f;
    ssize_t dataSize, loadAddr, endAddr;
    Uint16 tmpAddr;

    // Open the input file
    if ((f = fopen(filename, "rb")) == NULL)
    {
        dmError("Error opening input file '%s' (%s).\n",
            filename, strerror(errno));
        return 1;
    }

    // Get filesize
    if ((dataSize = dmGetFileSize(f) - 2) < 0)
    {
        dmError("Error getting file size for '%s'.\n", filename);
        return 6;
    }

    // Get loading address
    if (!dm_fread_le16(f, &tmpAddr))
    {
        dmError("Error reading input file '%s' (%s).\n",
            filename, strerror(errno));
        return 2;
    }

    // Show information
    loadAddr = forceAddr ? destAddr : tmpAddr;
    endAddr = loadAddr + dataSize - 1;

    dmPrint(1, "* Loading '%s', %s at $%.4x-$%.4x",
        filename, forceAddr ? "PRGA" : "PRG", loadAddr, endAddr);

    if (endAddr >= memModel->size)
    {
        dmPrint(1, " .. Does not fit into the memory!\n");
        return 5;
    }

    // Load data
    if (fread(&memory[loadAddr], dataSize, 1, f) < 1)
    {
        dmPrint(1, " .. Error: %s.\n",
            strerror(errno));
        return 4;
    }

    dmPrint(1, " .. OK\n");

    // Add to list of blocks
    reserveMemBlock(loadAddr, endAddr, filename, MTYPE_RES);

    return 0;
}


int dmLoadRAW(const char *filename, const ssize_t destAddr)
{
    FILE *f;
    ssize_t dataSize, endAddr;

    // Open the input file
    if ((f = fopen(filename, "rb")) == NULL)
    {
        dmError("Error opening input file '%s' (%s).\n",
            filename, strerror(errno));
        return 1;
    }

    // Get filesize
    if ((dataSize = dmGetFileSize(f)) < 0)
    {
        dmError("Error getting file size for '%s'.\n", filename);
        return 6;
    }

    // Show information
    endAddr = destAddr + dataSize - 1;
    dmPrint(1, "* Loading '%s', RAW at $%.4x-$%.4x",
        filename, destAddr, endAddr);

    if (endAddr >= memModel->size)
    {
        dmPrint(1, " .. Does not fit into the memory!\n");
        return 5;
    }

    // Load data
    if (fread(&memory[destAddr], dataSize, 1, f) < 1)
    {
        dmPrint(1, " .. Error: %s.\n",
            strerror(errno));
        return 4;
    }

    dmPrint(1, " .. OK\n");

    // Add info to list
    reserveMemBlock(destAddr, endAddr, filename, MTYPE_RES);

    return 0;
}


int outputLinkData(FILE *dfile, const char *blockName, const int blockStart, const int blockEnd)
{
    char *tmpStr, *s, *t;
    int blockSize;

    blockSize = (blockEnd - blockStart + 1);

    // Create label name from filename
    tmpStr = dm_strdup(blockName);
    if (tmpStr == NULL)
    {
        dmError("Could not allocate memory for string '%s'!\n",
            blockName);
        return -1;
    }

    if ((t = strrchr(tmpStr, '/')))
        s = (t + 1);
    else if ((t = strrchr(tmpStr, '\\')))
        s = (t + 1);
    else
        s = tmpStr;

    if ((t = strrchr(s, '.')))
        *t = 0;

    for (t = s; *t; t++)
    {
        if (!isalnum(*t))
            *t = '_';
    }

    // Print the label line
    switch (optLinkFileFormat)
    {
        case FMT_PLAIN:
            fprintf(dfile, "%s = $%.4x\n", tmpStr, blockStart);
            break;

        case FMT_DECIMAL:
            fprintf(dfile, "%s = %d\n", tmpStr, blockStart);
            break;

        case FMT_GENERIC:
        default:
            fprintf(dfile, "; %s ($%.4x - $%.4x, %d/$%x bytes)\n",
                blockName, blockStart, blockEnd, blockSize, blockSize);
            fprintf(dfile, "%s = $%.4x\n", s, blockStart);
            break;
    }

    dmFree(tmpStr);
    return 0;
}


/* Print out an ASCII presentation of memory map
 */
void memPrintLine(FILE *f)
{
    fprintf(f, "              +------------------------------------------+\n");
}

void memPrintEmpty(FILE *f, ssize_t n)
{
    ssize_t i;
    for (i = 0; i < n; i++)
    fprintf(f, "              |                                          |\n");
}

void dmDescribeMemory(FILE *f)
{
    int i;
    DMMemBlock *prev = NULL;

    memPrintLine(f);

    for (i = 0; i < nmemBlocks; i++)
    {
        DMMemBlock *curr = &memBlocks[i];
        char desc[512], *s;
        ssize_t siz, kz;

        // Check for empty, unreserved areas
        siz = (curr->start - 1) - (prev->end + 1) + 1;
        if (prev != NULL && siz > 1)
        {
            kz = siz / (1024 * 2);

            if (kz > 1) memPrintEmpty(f, kz);

            snprintf(desc, sizeof(desc), "EMPTY (%d)", siz);
            fprintf(f, "$%.4x - $%.4x | %-40s |\n", prev->end + 1, curr->start - 1, desc);

            if (kz > 1) memPrintEmpty(f, kz);
            memPrintLine(f);
        }
        prev = curr;

        // Print current block
        switch (curr->type)
        {
            case MTYPE_NONE:   s = "N/A (NC)"; break;
            case MTYPE_ROM:    s = "ROM"; break;
            case MTYPE_ROM_WT: s = "ROM/WT"; break;
            case MTYPE_IO:     s = "I/O"; break;
            case MTYPE_RES:    s = "RSVD"; break;
            default:           s = "????"; break;
        }

        siz = curr->end - curr->start + 1;
        kz = siz / (1024 * 2);

        if (kz > 1) memPrintEmpty(f, kz);
        snprintf(desc, sizeof(desc), "%s (%s, %d)", curr->name, s, siz);
        fprintf(f, "$%.4x - $%.4x | %-40s |\n", curr->start, curr->end, desc);
        if (kz > 1) memPrintEmpty(f, kz);
        memPrintLine(f);

    }

    fprintf(f,
    "\n"
    "NC     = Not Connected\n"
    "RSVD   = Reserved\n"
    "ROM/WT = RAM under 'write-through' ROM\n"
    "\n"
    );
}


/*
 * The main program
 */
int main(int argc, char *argv[])
{
    FILE *dfile = NULL;
    BOOL hasOverlaps;
    int i, j;
    ssize_t startAddr, endAddr, dataSize, totalSize;

    dmInitProg("objlink", "Simple file-linker", "0.80", NULL, NULL);
    dmVerbosity = 1;

    // Parse arguments
    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, NULL, TRUE))
        exit(1);

    if (nsrcFiles < 1)
    {
        dmError("Nothing to do. (try --help)\n");
        exit(0);
    }

    // Allocate memory
    memModel = &memoryModels[optMemModel];
    dmMsg(1, "Using memory model #%d '%s', %d bytes.\n",
        optMemModel, memModel->name, memModel->size);

    memory = (Uint8 *) dmMalloc(memModel->size + 32);
    if (memory == NULL)
    {
        dmError("Could not allocate memory.\n");
        exit(2);
    }

    // Initialize memory
    dmMsg(1, "Initializing memory with ");

    if (optInitValueType == 1 || optInitValue <= 0xff)
    {
        dmPrint(1, "BYTE 0x%.2x\n", optInitValue);
        memset(memory, optInitValue, memModel->size);
    }
    else
    if (optInitValueType == 2 || optInitValue <= 0xffff)
    {
        uint16_t *mp = (uint16_t *) memory;
        dmPrint(1, "WORD 0x%.4x\n", optInitValue);
        for (i = memModel->size / sizeof(*mp); i; i--)
        {
            *mp++ = optInitValue;
        }
    }
    else
    {
        Uint32 *mp = (Uint32 *) memory;
        dmPrint(1, "DWORD 0x%.8x\n", optInitValue);
        for (i = memModel->size / sizeof(*mp); i; i--)
        {
            *mp++ = optInitValue;
        }
    }

    // Load the datafiles
    for (i = 0; i < nsrcFiles; i++)
    switch (srcFiles[i].type)
    {
        case STYPE_RAW:
            dmLoadRAW(srcFiles[i].filename, srcFiles[i].addr);
            break;

        case STYPE_PRG:
            dmLoadPRG(srcFiles[i].filename, FALSE, 0);
            break;

        case STYPE_PRGA:
            dmLoadPRG(srcFiles[i].filename, TRUE, srcFiles[i].addr);
            break;
    }

    // Add memory model blocks
    dmMsg(1, "Applying memory model restrictions...\n");
    for (i = 0; i < memModel->nmemBlocks; i++)
    {
        reserveMemBlock(
            memModel->memBlocks[i].start,
            memModel->memBlocks[i].end,
            memModel->memBlocks[i].name,
            memModel->memBlocks[i].type);
    }

    // Sort the blocks
    qsort(memBlocks, nmemBlocks, sizeof(DMMemBlock), compareMemBlock);

    // Check for overlapping conflicts
    hasOverlaps = FALSE;
    for (i = 0; i < nmemBlocks; i++)
    for (j = 0; j < nmemBlocks; j++)
    if (j != i && memBlocks[i].type == MTYPE_RES)
    {
        DMMemBlock *mbi = &memBlocks[i],
                   *mbj = &memBlocks[j];

        // Check for per-file conflicts
        if ((mbj->start >= mbi->start && mbj->start <= mbi->end) ||
            (mbj->end >= mbi->start && mbj->end <= mbi->end))
        {
            dmPrint(1, "* '%s' and '%s' overlap ($%.4x-$%.4x  vs  $%.4x-$%.4x)\n",
                mbi->name, mbj->name, mbi->start,
                mbi->end, mbj->start, mbj->end);
            hasOverlaps = TRUE;
        }
    }

    if (!optAllowOverlap && hasOverlaps)
    {
        dmError("Error occured, overlaps not allowed.\n");
        exit(5);
    }

    // Find out start and end-addresses
    startAddr = memModel->size;
    totalSize = endAddr = 0;
    for (i = 0; i < nmemBlocks; i++)
    {
        DMMemBlock *mbi = &memBlocks[i];
        if (mbi->type == MTYPE_RES)
        {
            if (mbi->start < startAddr)
                startAddr = mbi->start;

            if (mbi->end > endAddr)
                endAddr = mbi->end;

            totalSize += (mbi->end - mbi->start + 1);
        }
    }

    if (startAddr >= memModel->size || endAddr < startAddr)
    {
        dmError("Invalid saveblock addresses (start=$%.4x, end=$%.4x)!\n", startAddr, endAddr);
        exit(8);
    }

    // Output linkfile
    if (optLinkFileName)
    {
        dmMsg(1, "Writing linkfile to '%s'\n", optLinkFileName);
        if ((dfile = fopen(optLinkFileName, "wb")) == NULL)
        {
            dmError("Error creating file '%s' (%s).\n", optLinkFileName, strerror(errno));
            exit(1);
        }

        switch (optLinkFileFormat)
        {
            case FMT_GENERIC:
            default:
                fprintf(dfile, "; Definitions generated by %s v%s\n",
                    dmProgName, dmProgVersion);
                break;
        }

        for (i = 0; i < nmemBlocks; i++)
        {
            DMMemBlock *mbi = &memBlocks[i];
            outputLinkData(dfile, mbi->name, mbi->start, mbi->end);
        }

        fclose(dfile);
    }

    // Show some information
    if (optCropOutput)
    {
        startAddr = optCropStart;
        endAddr = optCropEnd;
    }

    dataSize = endAddr - startAddr + 1;

    if (dataSize - totalSize > 0)
    {
        dmMsg(1, "Total of %d/$%x bytes unused(?) areas.\n",
            dataSize - totalSize, dataSize - totalSize);
    }

    dmMsg(1, "Writing $%.4x - $%.4x (%d/$%x bytes) ",
        startAddr, endAddr, dataSize, dataSize);


    // Open the destination file
    if (optDestName == NULL)
    {
        dfile = stdout;
        dmPrint(1, "...\n");
    }
    else if ((dfile = fopen(optDestName, "wb")) == NULL)
    {
        dmError("Error creating output file '%s' (%s).\n", optDestName, strerror(errno));
        exit(1);
    }
    else
        dmPrint(1, "to '%s'\n", optDestName);

    // Save loading address
    if (optLoadAddress >= 0)
    {
        dmMsg(1, "Using specified loading address $%.4x\n", optLoadAddress);
        dm_fwrite_le16(dfile, optLoadAddress);
    }
    else
    if (optLoadAddress == LA_AUTO)
    {
        dmMsg(1, "Using automatic loading address $%.4x\n", startAddr);
        dm_fwrite_le16(dfile, startAddr);
    }
    else
    {
        dmMsg(1, "Writing raw output, without loading address.\n");
    }

    // Save the data
    if (fwrite(&memory[startAddr], dataSize, 1, dfile) < 1)
    {
        dmError("Error writing to file (%s)\n", strerror(errno));
    }

    fclose(dfile);

    // Describe
    if (optDescribe)
        dmDescribeMemory(stdout);

    exit(0);
    return 0;
}
