#include "automap.h"

#include <stdio.h>
#include <string.h>

#include <algorithm>

#include "art.h"
#include "color.h"
#include "config.h"
#include "dbox.h"
#include "debug.h"
#include "draw.h"
#include "game.h"
#include "game_mouse.h"
#include "game_sound.h"
#include "graph_lib.h"
#include "input.h"
#include "item.h"
#include "kb.h"
#include "map.h"
#include "memory.h"
#include "object.h"
#include "platform_compat.h"
#include "settings.h"
#include "svga.h"
#include "text_font.h"
#include "window_manager.h"

namespace fallout {

#define AUTOMAP_OFFSET_COUNT (AUTOMAP_MAP_COUNT * ELEVATION_COUNT)

#define AUTOMAP_WINDOW_WIDTH (519)
#define AUTOMAP_WINDOW_HEIGHT (480)

#define AUTOMAP_PIPBOY_VIEW_X (238)
#define AUTOMAP_PIPBOY_VIEW_Y (105)

static void automapRenderInMapWindow(int window, int elevation, unsigned char* backgroundData, int flags);
static int automapSaveEntry(File* stream);
static int automapLoadEntry(int map, int elevation);
static int automapSaveHeader(File* stream);
static int automapLoadHeader(File* stream);
static void _decode_map_data(int elevation);
static int automapCreate();
static int _copy_file_data(File* stream1, File* stream2, int length);

typedef enum AutomapFrm {
    AUTOMAP_FRM_BACKGROUND,
    AUTOMAP_FRM_BUTTON_UP,
    AUTOMAP_FRM_BUTTON_DOWN,
    AUTOMAP_FRM_SWITCH_UP,
    AUTOMAP_FRM_SWITCH_DOWN,
    AUTOMAP_FRM_COUNT,
} AutomapFrm;

typedef struct AutomapEntry {
    int dataSize;
    unsigned char isCompressed;
    unsigned char* compressedData;
    unsigned char* data;
} AutomapEntry;

// 0x41ADE0
static const int _defam[AUTOMAP_MAP_COUNT][ELEVATION_COUNT] = {
    { -1, -1, -1 },
    { -1, -1, -1 },
    { -1, -1, -1 },
};

// 0x41B560
static int _displayMapList[AUTOMAP_MAP_COUNT] = {
    -1,
    -1,
    -1,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    -1,
    -1,
    0,
    0,
    0,
    0,
    0,
    -1,
    -1,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    0,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    0,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    0,
    -1,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    0,
    0,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
    -1,
};

// 0x41B7E0
static const int gAutomapFrmIds[AUTOMAP_FRM_COUNT] = {
    171, // automap.frm - automap window
    8, // lilredup.frm - little red button up
    9, // lilreddn.frm - little red button down
    172, // autoup.frm - switch up
    173, // autodwn.frm - switch down
};

// 0x5108C4
static int gAutomapFlags = 0;

// 0x56CB18
static AutomapHeader gAutomapHeader;

// 0x56D2A0
static AutomapEntry gAutomapEntry;

// automap_init
// 0x41B7F4
int automapInit()
{
    gAutomapFlags = 0;
    automapCreate();
    return 0;
}

// 0x41B808
int automapReset()
{
    gAutomapFlags = 0;
    automapCreate();
    return 0;
}

// 0x41B81C
void automapExit()
{
    char path[COMPAT_MAX_PATH];
    snprintf(path, sizeof(path), "%s\\%s\\%s", settings.system.master_patches_path.c_str(), "MAPS", AUTOMAP_DB);
    compat_remove(path);
}

// 0x41B87C
int automapLoad(File* stream)
{
    return fileReadInt32(stream, &gAutomapFlags);
}

// 0x41B898
int automapSave(File* stream)
{
    return fileWriteInt32(stream, gAutomapFlags);
}

// 0x41B8B4
int _automapDisplayMap(int map)
{
    return _displayMapList[map];
}

// 0x41B8BC
void automapShow(bool isInGame, bool isUsingScanner)
{
    ScopedGameMode gm(GameMode::kAutomap);

    int frmIds[AUTOMAP_FRM_COUNT];
    memcpy(frmIds, gAutomapFrmIds, sizeof(gAutomapFrmIds));

    FrmImage frmImages[AUTOMAP_FRM_COUNT];
    for (int index = 0; index < AUTOMAP_FRM_COUNT; index++) {
        int fid = buildFid(OBJ_TYPE_INTERFACE, frmIds[index], 0, 0, 0);
        if (!frmImages[index].lock(fid)) {
            return;
        }
    }

    int color;
    if (isInGame) {
        color = _colorTable[8456];
        _obj_process_seen();
    } else {
        color = _colorTable[22025];
    }

    int oldFont = fontGetCurrent();
    fontSetCurrent(101);

    int automapWindowX = (screenGetWidth() - AUTOMAP_WINDOW_WIDTH) / 2;
    int automapWindowY = (screenGetHeight() - AUTOMAP_WINDOW_HEIGHT) / 2;
    int window = windowCreate(automapWindowX, automapWindowY, AUTOMAP_WINDOW_WIDTH, AUTOMAP_WINDOW_HEIGHT, color, WINDOW_MODAL | WINDOW_MOVE_ON_TOP);

    int scannerBtn = buttonCreate(window,
        111,
        454,
        15,
        16,
        -1,
        -1,
        -1,
        KEY_LOWERCASE_S,
        frmImages[AUTOMAP_FRM_BUTTON_UP].getData(),
        frmImages[AUTOMAP_FRM_BUTTON_DOWN].getData(),
        NULL,
        BUTTON_FLAG_TRANSPARENT);
    if (scannerBtn != -1) {
        buttonSetCallbacks(scannerBtn, _gsound_red_butt_press, _gsound_red_butt_release);
    }

    int cancelBtn = buttonCreate(window,
        277,
        454,
        15,
        16,
        -1,
        -1,
        -1,
        KEY_ESCAPE,
        frmImages[AUTOMAP_FRM_BUTTON_UP].getData(),
        frmImages[AUTOMAP_FRM_BUTTON_DOWN].getData(),
        NULL,
        BUTTON_FLAG_TRANSPARENT);
    if (cancelBtn != -1) {
        buttonSetCallbacks(cancelBtn, _gsound_red_butt_press, _gsound_red_butt_release);
    }

    int switchBtn = buttonCreate(window,
        457,
        340,
        42,
        74,
        -1,
        -1,
        KEY_LOWERCASE_L,
        KEY_LOWERCASE_H,
        frmImages[AUTOMAP_FRM_SWITCH_UP].getData(),
        frmImages[AUTOMAP_FRM_SWITCH_DOWN].getData(),
        NULL,
        BUTTON_FLAG_TRANSPARENT | BUTTON_FLAG_0x01);
    if (switchBtn != -1) {
        buttonSetCallbacks(switchBtn, _gsound_toggle_butt_press_, _gsound_toggle_butt_press_);
    }

    if ((gAutomapFlags & AUTOMAP_WTH_HIGH_DETAILS) == 0) {
        _win_set_button_rest_state(switchBtn, 1, 0);
    }

    int elevation = gElevation;

    gAutomapFlags &= AUTOMAP_WTH_HIGH_DETAILS;

    if (isInGame) {
        gAutomapFlags |= AUTOMAP_IN_GAME;
    }

    if (isUsingScanner) {
        gAutomapFlags |= AUTOMAP_WITH_SCANNER;
    }

    automapRenderInMapWindow(window, elevation, frmImages[AUTOMAP_FRM_BACKGROUND].getData(), gAutomapFlags);

    bool isoWasEnabled = isoDisable();
    gameMouseSetCursor(MOUSE_CURSOR_ARROW);

    bool done = false;
    while (!done) {
        sharedFpsLimiter.mark();

        bool needsRefresh = false;

        // FIXME: There is minor bug in the interface - pressing H/L to toggle
        // high/low details does not update switch state.
        int keyCode = inputGetInput();
        switch (keyCode) {
        case KEY_TAB:
        case KEY_ESCAPE:
        case KEY_UPPERCASE_A:
        case KEY_LOWERCASE_A:
            done = true;
            break;
        case KEY_UPPERCASE_H:
        case KEY_LOWERCASE_H:
            if ((gAutomapFlags & AUTOMAP_WTH_HIGH_DETAILS) == 0) {
                gAutomapFlags |= AUTOMAP_WTH_HIGH_DETAILS;
                needsRefresh = true;
            }
            break;
        case KEY_UPPERCASE_L:
        case KEY_LOWERCASE_L:
            if ((gAutomapFlags & AUTOMAP_WTH_HIGH_DETAILS) != 0) {
                gAutomapFlags &= ~AUTOMAP_WTH_HIGH_DETAILS;
                needsRefresh = true;
            }
            break;
        case KEY_UPPERCASE_S:
        case KEY_LOWERCASE_S:
            if (elevation != gElevation) {
                elevation = gElevation;
                needsRefresh = true;
            }

            if ((gAutomapFlags & AUTOMAP_WITH_SCANNER) == 0) {
                Object* scanner = NULL;

                Object* item1 = critterGetItem1(gDude);
                if (item1 != NULL && item1->pid == PROTO_ID_MOTION_SENSOR) {
                    scanner = item1;
                } else {
                    Object* item2 = critterGetItem2(gDude);
                    if (item2 != NULL && item2->pid == PROTO_ID_MOTION_SENSOR) {
                        scanner = item2;
                    }
                }

                if (scanner != NULL && miscItemGetCharges(scanner) > 0) {
                    needsRefresh = true;
                    gAutomapFlags |= AUTOMAP_WITH_SCANNER;
                    miscItemConsumeCharge(scanner);
                } else {
                    soundPlayFile("iisxxxx1");

                    MessageListItem messageListItem;
                    // 17 - The motion sensor is not installed.
                    // 18 - The motion sensor has no charges remaining.
                    const char* title = getmsg(&gMiscMessageList, &messageListItem, scanner != NULL ? 18 : 17);
                    showDialogBox(title, NULL, 0, 165, 140, _colorTable[32328], NULL, _colorTable[32328], 0);
                }
            }

            break;
        case KEY_CTRL_Q:
        case KEY_ALT_X:
        case KEY_F10:
            showQuitConfirmationDialog();
            break;
        case KEY_F12:
            takeScreenshot();
            break;
        }

        if (_game_user_wants_to_quit != 0) {
            break;
        }

        if (needsRefresh) {
            automapRenderInMapWindow(window, elevation, frmImages[AUTOMAP_FRM_BACKGROUND].getData(), gAutomapFlags);
            needsRefresh = false;
        }

        renderPresent();
        sharedFpsLimiter.throttle();
    }

    if (isoWasEnabled) {
        isoEnable();
    }

    windowDestroy(window);
    fontSetCurrent(oldFont);
}

// Renders automap in Map window.
//
// 0x41BD1C
static void automapRenderInMapWindow(int window, int elevation, unsigned char* backgroundData, int flags)
{
    int color;
    if ((flags & AUTOMAP_IN_GAME) != 0) {
        color = _colorTable[8456];
    } else {
        color = _colorTable[22025];
    }

    windowFill(window, 0, 0, AUTOMAP_WINDOW_WIDTH, AUTOMAP_WINDOW_HEIGHT, color);
    windowDrawBorder(window);

    unsigned char* windowBuffer = windowGetBuffer(window);
    blitBufferToBuffer(backgroundData, AUTOMAP_WINDOW_WIDTH, AUTOMAP_WINDOW_HEIGHT, AUTOMAP_WINDOW_WIDTH, windowBuffer, AUTOMAP_WINDOW_WIDTH);

    for (Object* object = objectFindFirstAtElevation(elevation); object != NULL; object = objectFindNextAtElevation()) {
        if (object->tile == -1) {
            continue;
        }

        int objectType = FID_TYPE(object->fid);
        unsigned char objectColor;

        if ((flags & AUTOMAP_IN_GAME) != 0) {
            if (objectType == OBJ_TYPE_CRITTER
                && (object->flags & OBJECT_HIDDEN) == 0
                && (flags & AUTOMAP_WITH_SCANNER) != 0
                && (object->data.critter.combat.results & DAM_DEAD) == 0) {
                objectColor = _colorTable[31744];
            } else {
                if ((object->flags & OBJECT_SEEN) == 0) {
                    continue;
                }

                if (object->pid == PROTO_ID_0x2000031) {
                    objectColor = _colorTable[32328];
                } else if (objectType == OBJ_TYPE_WALL) {
                    objectColor = _colorTable[992];
                } else if (objectType == OBJ_TYPE_SCENERY
                    && (flags & AUTOMAP_WTH_HIGH_DETAILS) != 0
                    && object->pid != PROTO_ID_0x2000158) {
                    objectColor = _colorTable[480];
                } else if (object == gDude) {
                    objectColor = _colorTable[31744];
                } else {
                    objectColor = _colorTable[0];
                }
            }
        }

        int v10 = -2 * (object->tile % 200) - 10 + AUTOMAP_WINDOW_WIDTH * (2 * (object->tile / 200) + 9) - 60;
        if ((flags & AUTOMAP_IN_GAME) == 0) {
            switch (objectType) {
            case OBJ_TYPE_ITEM:
                objectColor = _colorTable[6513];
                break;
            case OBJ_TYPE_CRITTER:
                objectColor = _colorTable[28672];
                break;
            case OBJ_TYPE_SCENERY:
                objectColor = _colorTable[448];
                break;
            case OBJ_TYPE_WALL:
                objectColor = _colorTable[12546];
                break;
            case OBJ_TYPE_MISC:
                objectColor = _colorTable[31650];
                break;
            default:
                objectColor = _colorTable[0];
            }
        }

        if (objectColor != _colorTable[0]) {
            unsigned char* v12 = windowBuffer + v10;
            if ((flags & AUTOMAP_IN_GAME) != 0) {
                if (*v12 != _colorTable[992] || objectColor != _colorTable[480]) {
                    v12[0] = objectColor;
                    v12[1] = objectColor;
                }

                if (object == gDude) {
                    v12[-1] = objectColor;
                    v12[-AUTOMAP_WINDOW_WIDTH] = objectColor;
                    v12[AUTOMAP_WINDOW_WIDTH] = objectColor;
                }
            } else {
                v12[0] = objectColor;
                v12[1] = objectColor;
                v12[AUTOMAP_WINDOW_WIDTH] = objectColor;
                v12[AUTOMAP_WINDOW_WIDTH + 1] = objectColor;

                v12[AUTOMAP_WINDOW_WIDTH - 1] = objectColor;
                v12[AUTOMAP_WINDOW_WIDTH + 2] = objectColor;
                v12[AUTOMAP_WINDOW_WIDTH * 2] = objectColor;
                v12[AUTOMAP_WINDOW_WIDTH * 2 + 1] = objectColor;
            }
        }
    }

    int textColor;
    if ((flags & AUTOMAP_IN_GAME) != 0) {
        textColor = _colorTable[992];
    } else {
        textColor = _colorTable[12546];
    }

    if (mapGetCurrentMap() != -1) {
        char* areaName = mapGetCityName(mapGetCurrentMap());
        windowDrawText(window, areaName, 240, 150, 380, textColor | 0x2000000);

        char* mapName = mapGetName(mapGetCurrentMap(), elevation);
        windowDrawText(window, mapName, 240, 150, 396, textColor | 0x2000000);
    }

    windowRefresh(window);
}

// Renders automap in Pipboy window.
//
// 0x41C004
int automapRenderInPipboyWindow(int window, int map, int elevation)
{
    unsigned char* windowBuffer = windowGetBuffer(window) + 640 * AUTOMAP_PIPBOY_VIEW_Y + AUTOMAP_PIPBOY_VIEW_X;

    unsigned char wallColor = _colorTable[992];
    unsigned char sceneryColor = _colorTable[480];

    gAutomapEntry.data = (unsigned char*)internal_malloc(11024);
    if (gAutomapEntry.data == NULL) {
        debugPrint("\nAUTOMAP: Error allocating data buffer!\n");
        return -1;
    }

    if (automapLoadEntry(map, elevation) == -1) {
        internal_free(gAutomapEntry.data);
        return -1;
    }

    int v1 = 0;
    unsigned char v2 = 0;
    unsigned char* ptr = gAutomapEntry.data;

    // FIXME: This loop is implemented incorrectly. Automap requires 400x400 px,
    // but it's top offset is 105, which gives max y 505. It only works because
    // lower portions of automap data contains zeroes. If it doesn't this loop
    // will try to set pixels outside of window buffer, which usually leads to
    // crash.
    for (int y = 0; y < HEX_GRID_HEIGHT; y++) {
        for (int x = 0; x < HEX_GRID_WIDTH; x++) {
            v1 -= 1;
            if (v1 <= 0) {
                v1 = 4;
                v2 = *ptr++;
            }

            switch ((v2 & 0xC0) >> 6) {
            case 1:
                *windowBuffer++ = wallColor;
                *windowBuffer++ = wallColor;
                break;
            case 2:
                *windowBuffer++ = sceneryColor;
                *windowBuffer++ = sceneryColor;
                break;
            default:
                windowBuffer += 2;
                break;
            }

            v2 <<= 2;
        }

        windowBuffer += 640 + 240;
    }

    internal_free(gAutomapEntry.data);

    return 0;
}

// automap_pip_save
// 0x41C0F0
int automapSaveCurrent()
{
    int map = mapGetCurrentMap();
    int elevation = gElevation;

    int entryOffset = gAutomapHeader.offsets[map][elevation];
    if (entryOffset < 0) {
        return 0;
    }

    debugPrint("\nAUTOMAP: Saving AutoMap DB index %d, level %d\n", map, elevation);

    bool dataBuffersAllocated = false;
    gAutomapEntry.data = (unsigned char*)internal_malloc(11024);
    if (gAutomapEntry.data != NULL) {
        gAutomapEntry.compressedData = (unsigned char*)internal_malloc(11024);
        if (gAutomapEntry.compressedData != NULL) {
            dataBuffersAllocated = true;
        }
    }

    if (!dataBuffersAllocated) {
        // FIXME: Leaking gAutomapEntry.data.
        debugPrint("\nAUTOMAP: Error allocating data buffers!\n");
        return -1;
    }

    // NOTE: Not sure about the size.
    char path[256];
    snprintf(path, sizeof(path), "%s\\%s", "MAPS", AUTOMAP_DB);

    File* stream1 = fileOpen(path, "r+b");
    if (stream1 == NULL) {
        debugPrint("\nAUTOMAP: Error opening automap database file!\n");
        debugPrint("Error continued: automap_pip_save: path: %s", path);
        internal_free(gAutomapEntry.data);
        internal_free(gAutomapEntry.compressedData);
        return -1;
    }

    if (automapLoadHeader(stream1) == -1) {
        debugPrint("\nAUTOMAP: Error reading automap database file header!\n");
        internal_free(gAutomapEntry.data);
        internal_free(gAutomapEntry.compressedData);
        fileClose(stream1);
        return -1;
    }

    _decode_map_data(elevation);

    int compressedDataSize = graphCompress(gAutomapEntry.data, gAutomapEntry.compressedData, 10000);
    if (compressedDataSize == -1) {
        gAutomapEntry.dataSize = 10000;
        gAutomapEntry.isCompressed = 0;
    } else {
        gAutomapEntry.dataSize = compressedDataSize;
        gAutomapEntry.isCompressed = 1;
    }

    if (entryOffset != 0) {
        snprintf(path, sizeof(path), "%s\\%s", "MAPS", AUTOMAP_TMP);

        File* stream2 = fileOpen(path, "wb");
        if (stream2 == NULL) {
            debugPrint("\nAUTOMAP: Error creating temp file!\n");
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            fileClose(stream1);
            return -1;
        }

        fileRewind(stream1);

        if (_copy_file_data(stream1, stream2, entryOffset) == -1) {
            debugPrint("\nAUTOMAP: Error copying file data!\n");
            fileClose(stream1);
            fileClose(stream2);
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        if (automapSaveEntry(stream2) == -1) {
            fileClose(stream1);
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        int nextEntryDataSize;
        if (fileReadInt32(stream1, &nextEntryDataSize) == -1) {
            debugPrint("\nAUTOMAP: Error reading database #1!\n");
            fileClose(stream1);
            fileClose(stream2);
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        int automapDataSize = fileGetSize(stream1);
        if (automapDataSize == -1) {
            debugPrint("\nAUTOMAP: Error reading database #2!\n");
            fileClose(stream1);
            fileClose(stream2);
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        int nextEntryOffset = entryOffset + nextEntryDataSize + 5;
        if (automapDataSize != nextEntryOffset) {
            if (fileSeek(stream1, nextEntryOffset, SEEK_SET) == -1) {
                debugPrint("\nAUTOMAP: Error writing temp data!\n");
                fileClose(stream1);
                fileClose(stream2);
                internal_free(gAutomapEntry.data);
                internal_free(gAutomapEntry.compressedData);
                return -1;
            }

            if (_copy_file_data(stream1, stream2, automapDataSize - nextEntryOffset) == -1) {
                debugPrint("\nAUTOMAP: Error copying file data!\n");
                fileClose(stream1);
                fileClose(stream2);
                internal_free(gAutomapEntry.data);
                internal_free(gAutomapEntry.compressedData);
                return -1;
            }
        }

        int diff = gAutomapEntry.dataSize - nextEntryDataSize;
        for (int map = 0; map < AUTOMAP_MAP_COUNT; map++) {
            for (int elevation = 0; elevation < ELEVATION_COUNT; elevation++) {
                if (gAutomapHeader.offsets[map][elevation] > entryOffset) {
                    gAutomapHeader.offsets[map][elevation] += diff;
                }
            }
        }

        gAutomapHeader.dataSize += diff;

        if (automapSaveHeader(stream2) == -1) {
            fileClose(stream1);
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        fileSeek(stream2, 0, SEEK_END);
        fileClose(stream2);
        fileClose(stream1);
        internal_free(gAutomapEntry.data);
        internal_free(gAutomapEntry.compressedData);

        // NOTE: Not sure about the size.
        char automapDbPath[512];
        snprintf(automapDbPath, sizeof(automapDbPath), "%s\\%s\\%s", settings.system.master_patches_path.c_str(), "MAPS", AUTOMAP_DB);
        if (compat_remove(automapDbPath) != 0) {
            debugPrint("\nAUTOMAP: Error removing database!\n");
            return -1;
        }

        // NOTE: Not sure about the size.
        char automapTmpPath[512];
        snprintf(automapTmpPath, sizeof(automapTmpPath), "%s\\%s\\%s", settings.system.master_patches_path.c_str(), "MAPS", AUTOMAP_TMP);
        if (compat_rename(automapTmpPath, automapDbPath) != 0) {
            debugPrint("\nAUTOMAP: Error renaming database!\n");
            return -1;
        }
    } else {
        bool proceed = true;
        if (fileSeek(stream1, 0, SEEK_END) != -1) {
            if (fileTell(stream1) != gAutomapHeader.dataSize) {
                proceed = false;
            }
        } else {
            proceed = false;
        }

        if (!proceed) {
            debugPrint("\nAUTOMAP: Error reading automap database file header!\n");
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            fileClose(stream1);
            return -1;
        }

        if (automapSaveEntry(stream1) == -1) {
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        gAutomapHeader.offsets[map][elevation] = gAutomapHeader.dataSize;
        gAutomapHeader.dataSize += gAutomapEntry.dataSize + 5;

        if (automapSaveHeader(stream1) == -1) {
            internal_free(gAutomapEntry.data);
            internal_free(gAutomapEntry.compressedData);
            return -1;
        }

        fileSeek(stream1, 0, SEEK_END);
        fileClose(stream1);
        internal_free(gAutomapEntry.data);
        internal_free(gAutomapEntry.compressedData);
    }

    return 1;
}

// Saves automap entry into stream.
//
// 0x41C844
static int automapSaveEntry(File* stream)
{
    unsigned char* buffer;
    if (gAutomapEntry.isCompressed == 1) {
        buffer = gAutomapEntry.compressedData;
    } else {
        buffer = gAutomapEntry.data;
    }

    if (_db_fwriteLong(stream, gAutomapEntry.dataSize) == -1) {
        goto err;
    }

    if (fileWriteUInt8(stream, gAutomapEntry.isCompressed) == -1) {
        goto err;
    }

    if (fileWriteUInt8List(stream, buffer, gAutomapEntry.dataSize) == -1) {
        goto err;
    }

    return 0;

err:

    debugPrint("\nAUTOMAP: Error writing automap database entry data!\n");
    fileClose(stream);

    return -1;
}

// 0x41C8CC
static int automapLoadEntry(int map, int elevation)
{
    gAutomapEntry.compressedData = NULL;

    char path[COMPAT_MAX_PATH];
    snprintf(path, sizeof(path), "%s\\%s", "MAPS", AUTOMAP_DB);

    bool success = true;

    File* stream = fileOpen(path, "r+b");
    if (stream == NULL) {
        debugPrint("\nAUTOMAP: Error opening automap database file!\n");
        debugPrint("Error continued: AM_ReadEntry: path: %s", path);
        return -1;
    }

    if (automapLoadHeader(stream) == -1) {
        debugPrint("\nAUTOMAP: Error reading automap database header!\n");
        fileClose(stream);
        return -1;
    }

    if (gAutomapHeader.offsets[map][elevation] <= 0) {
        success = false;
        goto out;
    }

    if (fileSeek(stream, gAutomapHeader.offsets[map][elevation], SEEK_SET) == -1) {
        success = false;
        goto out;
    }

    if (_db_freadInt(stream, &(gAutomapEntry.dataSize)) == -1) {
        success = false;
        goto out;
    }

    if (fileReadUInt8(stream, &(gAutomapEntry.isCompressed)) == -1) {
        success = false;
        goto out;
    }

    if (gAutomapEntry.isCompressed == 1) {
        gAutomapEntry.compressedData = (unsigned char*)internal_malloc(11024);
        if (gAutomapEntry.compressedData == NULL) {
            debugPrint("\nAUTOMAP: Error allocating decompression buffer!\n");
            fileClose(stream);
            return -1;
        }

        if (fileReadUInt8List(stream, gAutomapEntry.compressedData, gAutomapEntry.dataSize) == -1) {
            success = 0;
            goto out;
        }

        if (graphDecompress(gAutomapEntry.compressedData, gAutomapEntry.data, 10000) == -1) {
            debugPrint("\nAUTOMAP: Error decompressing DB entry!\n");
            fileClose(stream);
            return -1;
        }
    } else {
        if (fileReadUInt8List(stream, gAutomapEntry.data, gAutomapEntry.dataSize) == -1) {
            success = false;
            goto out;
        }
    }

out:

    fileClose(stream);

    if (!success) {
        debugPrint("\nAUTOMAP: Error reading automap database entry data!\n");

        return -1;
    }

    if (gAutomapEntry.compressedData != NULL) {
        internal_free(gAutomapEntry.compressedData);
    }

    return 0;
}

// Saves automap.db header.
//
// 0x41CAD8
static int automapSaveHeader(File* stream)
{
    fileRewind(stream);

    if (fileWriteUInt8(stream, gAutomapHeader.version) == -1) {
        goto err;
    }

    if (_db_fwriteLong(stream, gAutomapHeader.dataSize) == -1) {
        goto err;
    }

    if (_db_fwriteLongCount(stream, (int*)gAutomapHeader.offsets, AUTOMAP_OFFSET_COUNT) == -1) {
        goto err;
    }

    return 0;

err:

    debugPrint("\nAUTOMAP: Error writing automap database header!\n");

    fileClose(stream);

    return -1;
}

// Loads automap.db header.
//
// 0x41CB50
static int automapLoadHeader(File* stream)
{

    if (fileReadUInt8(stream, &(gAutomapHeader.version)) == -1) {
        return -1;
    }

    if (_db_freadInt(stream, &(gAutomapHeader.dataSize)) == -1) {
        return -1;
    }

    if (_db_freadIntCount(stream, (int*)gAutomapHeader.offsets, AUTOMAP_OFFSET_COUNT) == -1) {
        return -1;
    }

    if (gAutomapHeader.version != 1) {
        return -1;
    }

    return 0;
}

// 0x41CBA4
static void _decode_map_data(int elevation)
{
    memset(gAutomapEntry.data, 0, SQUARE_GRID_SIZE);

    _obj_process_seen();

    Object* object = objectFindFirstAtElevation(elevation);
    while (object != NULL) {
        if (object->tile != -1 && (object->flags & OBJECT_SEEN) != 0) {
            int contentType;

            int objectType = FID_TYPE(object->fid);
            if (objectType == OBJ_TYPE_SCENERY && object->pid != PROTO_ID_0x2000158) {
                contentType = 2;
            } else if (objectType == OBJ_TYPE_WALL) {
                contentType = 1;
            } else {
                contentType = 0;
            }

            if (contentType != 0) {
                int v1 = 200 - object->tile % 200;
                int v2 = v1 / 4 + 50 * (object->tile / 200);
                int v3 = 2 * (3 - v1 % 4);
                gAutomapEntry.data[v2] &= ~(0x03 << v3);
                gAutomapEntry.data[v2] |= (contentType << v3);
            }
        }
        object = objectFindNextAtElevation();
    }
}

// 0x41CC98
static int automapCreate()
{
    gAutomapHeader.version = 1;
    gAutomapHeader.dataSize = 1925;
    memcpy(gAutomapHeader.offsets, _defam, sizeof(_defam));

    char path[COMPAT_MAX_PATH];
    snprintf(path, sizeof(path), "%s\\%s", "MAPS", AUTOMAP_DB);

    File* stream = fileOpen(path, "wb");
    if (stream == NULL) {
        debugPrint("\nAUTOMAP: Error creating automap database file!\n");
        return -1;
    }

    if (automapSaveHeader(stream) == -1) {
        return -1;
    }

    fileClose(stream);

    return 0;
}

// Copy data from stream1 to stream2.
//
// 0x41CD6C
static int _copy_file_data(File* stream1, File* stream2, int length)
{
    void* buffer = internal_malloc(0xFFFF);
    if (buffer == NULL) {
        return -1;
    }

    // NOTE: Original code is slightly different, but does the same thing.
    while (length != 0) {
        int chunkLength = std::min(length, 0xFFFF);

        if (fileRead(buffer, chunkLength, 1, stream1) != 1) {
            break;
        }

        if (fileWrite(buffer, chunkLength, 1, stream2) != 1) {
            break;
        }

        length -= chunkLength;
    }

    internal_free(buffer);

    if (length != 0) {
        return -1;
    }

    return 0;
}

// 0x41CE74
int automapGetHeader(AutomapHeader** automapHeaderPtr)
{
    char path[COMPAT_MAX_PATH];
    snprintf(path, sizeof(path), "%s\\%s", "MAPS", AUTOMAP_DB);

    File* stream = fileOpen(path, "rb");
    if (stream == NULL) {
        debugPrint("\nAUTOMAP: Error opening database file for reading!\n");
        debugPrint("Error continued: ReadAMList: path: %s", path);
        return -1;
    }

    if (automapLoadHeader(stream) == -1) {
        debugPrint("\nAUTOMAP: Error reading automap database header pt2!\n");
        fileClose(stream);
        return -1;
    }

    fileClose(stream);

    *automapHeaderPtr = &gAutomapHeader;

    return 0;
}

void automapSetDisplayMap(int map, bool available)
{
    if (map >= 0 && map < AUTOMAP_MAP_COUNT) {
        _displayMapList[map] = available ? 0 : -1;
    }
}

} // namespace fallout
