// $Id: saveundo.c,v 1.15 2003/10/20 16:05:06 iain Exp $

#include "git.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>

typedef const git_uint8 * MemoryPage;
typedef MemoryPage * MemoryMap;

typedef struct UndoRecord UndoRecord;

struct UndoRecord
{
    git_uint32   endMem;
    MemoryMap    memoryMap;
    git_sint32   stackSize;
    git_sint32 * stack;
    glui32       heapSize;
    glui32     * heap;
    UndoRecord * prev;
    UndoRecord * next;
};

static UndoRecord * gUndo = NULL;
static git_uint32 gUndoSize = 0;
static git_uint32 gMaxUndoSize = 256 * 1024;

static void reserveSpace (git_uint32);
static void deleteRecord (UndoRecord * u);

void initUndo (git_uint32 size)
{
    gMaxUndoSize = size;
    gUndoSize = 0;
    gUndo = NULL;
}

int saveUndo (git_sint32 * base, git_sint32 * sp)
{
    git_uint32 undoSize = sizeof(UndoRecord);
    git_uint32 mapSize = sizeof(MemoryPage*) * (gEndMem - gRamStart) / 256;
    git_uint32 stackSize = sizeof(git_sint32) * (sp - base);
    git_uint32 totalSize = undoSize + mapSize + stackSize;

    git_uint32 addr = gRamStart; // Address in glulx memory.
    git_uint32 slot = 0;         // Slot in our memory map.
    
    UndoRecord * undo = malloc (undoSize);
    if (undo == NULL)
        fatalError ("Couldn't allocate undo record");
        
    undo->endMem = gEndMem;
    undo->memoryMap = malloc (mapSize);
    undo->stackSize = stackSize;
    undo->stack = malloc (stackSize);
    undo->prev = NULL;
    undo->next = NULL;

    if (undo->memoryMap == NULL || undo->stack == NULL)
        fatalError ("Couldn't allocate memory for undo");

    // Save the stack.
    memcpy (undo->stack, base, undo->stackSize);

    // Are we diffing against the previous undo record,
    // or against the initial gamefile state?
    if (gUndo == NULL)
    {
        // We're diffing against the gamefile.        
        for ( ; addr < gExtStart ; addr += 256, ++slot)
        {
            if (memcmp (gRom + addr, gRam + addr, 256) != 0)
            {
                // We need to save this page.
                git_uint8 * page = malloc(256);
                if (page == NULL)
                    fatalError ("Couldn't allocate memory for undo");
                    
                memcpy (page, gRam + addr, 256);
                undo->memoryMap[slot] = page;
                totalSize += 256;
            }
            else
            {
                // We don't need to save this page.
                // Just make it point into ROM.
                undo->memoryMap[slot] = gRom + addr;
            }
        }

        // If the memory map has been extended, save the exended area
        for (addr = gExtStart ; addr < gEndMem ; addr += 256, ++slot)
        {
            git_uint8 * page = malloc(256);
            if (page == NULL)
                fatalError ("Couldn't allocate memory for undo");
                
            memcpy (page, gRam + addr, 256);
            undo->memoryMap[slot] = page;
            totalSize += 256;
        }
    }
    else
    {
        // We're diffing against the most recent undo record.
        git_uint32 endMem = (gUndo->endMem < gEndMem) ? gUndo->endMem : gEndMem;
        for ( ; addr < endMem ; addr += 256, ++slot)
        {
            if (memcmp (gUndo->memoryMap [slot], gRam + addr, 256) != 0)
            {
                // We need to save this page.
                git_uint8 * page = malloc(256);
                memcpy (page, gRam + addr, 256);
                undo->memoryMap[slot] = page;
                totalSize += 256;
            }
            else
            {
                // We don't need to save this page. Just copy
                // the pointer from the previous undo record.
                undo->memoryMap[slot] = gUndo->memoryMap[slot];
            }
        }

        // If the memory map has been extended, save the exended area
        for (addr = endMem ; addr < gEndMem ; addr += 256, ++slot)
        {
            git_uint8 * page = malloc(256);
            if (page == NULL)
                fatalError ("Couldn't allocate memory for undo");
                
            memcpy (page, gRam + addr, 256);
            undo->memoryMap[slot] = page;
            totalSize += 256;
        }
    }

    // Save the heap.
    if (heap_get_summary (&(undo->heapSize), &(undo->heap)))
        fatalError ("Couldn't get heap summary");
    totalSize += undo->heapSize * 4;

    // Link this record into the undo list.
    
    undo->prev = gUndo;
    if (gUndo)
        gUndo->next = undo;
    
    gUndo = undo;
    gUndoSize += totalSize;

    // Delete old records until we have enough free space.
    reserveSpace (0);

    // And we're done.
    return 0;
}

int restoreUndo (git_sint32* base, git_uint32 protectPos, git_uint32 protectSize)
{
    if (gUndo == NULL)
    {
        // Nothing to undo!
        return 1;
    }
    else
    {
        UndoRecord * undo = gUndo;

        git_uint32 addr = gRamStart;     // Address in glulx memory.
        MemoryMap map = undo->memoryMap; // Glulx memory map.

        // Restore the size of the memory map
        heap_clear ();
        resizeMemory (undo->endMem, 1);

        // Restore the stack.
        memcpy (base, undo->stack, undo->stackSize);
        gStackPointer = base + (undo->stackSize / sizeof(git_sint32));

        // Restore the contents of RAM.

        if (protectSize > 0 && protectPos < gEndMem)
        {
            for ( ; addr < (protectPos & ~0xff) ; addr += 256, ++map)
                memcpy (gRam + addr, *map, 256);
            
            memcpy (gRam + addr, *map, protectPos & 0xff);
            protectSize += protectPos & 0xff;
            
            while (protectSize > 256)
                addr += 256, ++map, protectSize -= 256;

            if (addr < gEndMem)
            {
                memcpy (gRam + addr + protectSize,
                        *map + protectSize,
                        256 - protectSize);
            }
            addr += 256, ++map;
        }

        for ( ; addr < gEndMem ; addr += 256, ++map)
            memcpy (gRam + addr, *map, 256);

        // Restore the heap.
        if (heap_apply_summary (undo->heapSize, undo->heap))
            fatalError ("Couldn't apply heap summary");

        // Delete the undo record.

        gUndo = undo->prev;
        deleteRecord (undo);

        if (gUndo)
            gUndo->next = NULL;
        else
            assert (gUndoSize == 0);

        // And we're done.
        return 0;
    }
}

void resetUndo ()
{
    reserveSpace (gMaxUndoSize);
    assert (gUndo == NULL);
    assert (gUndoSize == 0);
}

void shutdownUndo ()
{
    resetUndo();
}

static void reserveSpace (git_uint32 n)
{
    UndoRecord * u = gUndo;
    if (u == NULL)
        return;

    // Find the oldest undo record.

    while (u->prev)
        u = u->prev;

    // Delete records until we've freed up the required amount of space.

    while (gUndoSize + n > gMaxUndoSize)
    {
        if (u->next)
        {
            assert (u->next->prev == u);
            u = u->next;

            deleteRecord (u->prev);
            u->prev = NULL;
        }
        else
        {
            assert (u == gUndo);
            gUndo = NULL;

            deleteRecord (u);
            assert (gUndoSize == 0);
            break;
        }
    }
}

static void deleteRecord (UndoRecord * u)
{
    git_uint32 addr = gRamStart; // Address in glulx memory.
    git_uint32 slot = 0;         // Slot in our memory map.

    // Zero out all the slots which are duplicates
    // of pages held in older undo records.

    if (u->prev)
    {
        // We're diffing against the previous undo record.
        while (addr < u->endMem && addr < u->prev->endMem)
        {
            if (u->memoryMap [slot] == u->prev->memoryMap [slot])
                u->memoryMap [slot] = NULL;
            addr += 256, ++slot;
        }
    }
    else
    {
        // We're diffing against the gamefile.
        while (addr < u->endMem && addr < gExtStart)
        {
            if (u->memoryMap [slot] == (gRom + addr))
                u->memoryMap [slot] = NULL;
            addr += 256, ++slot;
        }
    }

    // Zero out all the slots which are duplicates
    // of newer undo records.

    if (u->next)
    {
        addr = gRamStart;
        slot = 0;

        while (addr < u->endMem && addr < u->next->endMem)
        {
            if (u->memoryMap [slot] == u->next->memoryMap [slot])
                u->memoryMap [slot] = NULL;
            addr += 256, ++slot;
        }
    }

    // Free all the slots which are owned by this record only.

    addr = gRamStart;
    slot = 0;
    while (addr < u->endMem)
    {
        if (u->memoryMap [slot])
        {
            free ((void*) u->memoryMap [slot]);
            gUndoSize -= 256;
        }
        addr += 256, ++slot;
    }

    // Free the memory map itself.
    free ((void*) u->memoryMap);
    gUndoSize -= sizeof(MemoryPage*) * (u->endMem - gRamStart) / 256;

    // Free the stack.
    free (u->stack);
    gUndoSize -= u->stackSize;

    // Free the heap.
    free (u->heap);
    gUndoSize -= u->heapSize * 4;

    // Finally, free the record.
    free (u);
    gUndoSize -= sizeof(UndoRecord);
}
