/* **********************************************************************************************************************
   Included Header Files
   **********************************************************************************************************************/

#include "MysticRealms.h"



#define _CRTDBG_MAP_ALLOC
#ifdef _DEBUG
    #define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif  // _DEBUG



/* **********************************************************************************************************************
   Constructor
   **********************************************************************************************************************/

MysticRealms::MysticRealms() {
    gameRunning = TRUE;
    window = NULL;
    graphics = NULL;
    input = NULL;
    world = NULL;
    player = NULL;
    playerSprite = NULL;
    width = 640;
    height = 480;
    highResolutionPerformanceCount = 0;
    highResolutionPerformanceCounterFrequency = 0;
    gameUpdateCount = 0.0f;
    currentTime = 0.0f;
    gameState = VD_GAMEWORLD;
    enteringNewArea = FALSE;
};



/* **********************************************************************************************************************
   Destructor
   **********************************************************************************************************************/

MysticRealms::~MysticRealms() {
    if (player != NULL) {
        delete player;
        player = NULL;
    };
    if (world != NULL) {
        delete world;
        world = NULL;
    };
    if (input != NULL) {
        delete input;
        input = NULL;
    };
    if (graphics != NULL) {
        delete graphics;
        graphics = NULL;
    };
    if (window != NULL) {
        delete window;
        window = NULL;
    };
};



/* **********************************************************************************************************************
   Run
   ---
   Starts running the Mystic Realms game.
   #param  instance  Instance of the application.
   **********************************************************************************************************************/

void MysticRealms::Run(HINSTANCE instance) {
    const double gameUpdateFrequency = 90.0f;
    const double maxSecondsForOneGameUpdate = 1 / gameUpdateFrequency;
    MSG message;
    __int64 newCount;

    // initialize main components of the game
    window = new VDWindow();
    graphics = new VDDirect3D();
    input = new VDInput();
    world = new VDWorld();
    player = new VDPlayer();

    // initialize variables
    srand((unsigned int) time(NULL));  // TODO:  move to INIT game state, when we make it
    player->SetCurrentLocation(2, 2);

    // create the application window
    window->CreateMyWindow(instance, "Mystic Realms", width, height);

    // initialize the graphics API
    graphics->InitializeDirect3D(window->GetWindow());

    // initialize input
    input->InitializeInput(instance);
    input->InitializeKeyboard(window->GetWindow());

    // load NPC's
    LoadNPCs();

    // load world
    world->LoadWorld("Maps/World.vdm");

    // add graphics
    RECT drawingArea;
    (void) SetRect(&drawingArea, 0, 0, 32, 32);
    for (int x = 0; x < world->GetWorldSizeX(); x++) {
        for (int y = 0; y < world->GetWorldSizeY(); y++) {
            graphics->AddSprite(world->GetTileImageFileLocation(x, y),
                               drawingArea,
                               (float) x * 32.0f,
                               (float) y * 32.0f,
                               0xFF000000,
                               FALSE,
                               FALSE);
        };
    };
    for (unsigned int n = 0; n < world->GetNPCsSize(); n++) {
        // figure out which direction the NPC is facing
        switch (world->GetNPCDirection(n)) {
            case 0:
                (void) SetRect(&drawingArea, 0, 0, 32, 32);
                break;
            case 1:
                (void) SetRect(&drawingArea, 0, 0, 32, 32);
                break;
            case 2:
                (void) SetRect(&drawingArea, 96, 0, 128, 32);
                break;
            case 3:
                (void) SetRect(&drawingArea, 192, 0, 224, 32);
                break;
            case 4:
                (void) SetRect(&drawingArea, 288, 0, 320, 32);
                break;
            default:
                break;
        };

        // add the sprite
        graphics->AddSprite(world->GetNPCImageFileLocation(n),
                           drawingArea,
                           (float) world->GetNPCCurrentLocationX(n) * 32.0f,
                           (float) world->GetNPCCurrentLocationY(n) * 32.0f,
                           world->GetObjectColorKey(n),
                           FALSE,
                           TRUE);
    };

    (void) SetRect(&drawingArea, 0, 0, 32, 32);
    playerSprite = graphics->AddPlayerSprite("Images/Test.png", drawingArea, (float) width / 2, (float) height / 2, 0xFF000000, TRUE, FALSE);

    // initialize the high performance timer
    BOOL returnValue = QueryPerformanceFrequency((LARGE_INTEGER *) &highResolutionPerformanceCounterFrequency);
    if (returnValue == FALSE) {
        VDError("Failed to retrieve the frequency of the high-resolution performance counter.", __FILE__, __LINE__);
    };
    returnValue = QueryPerformanceCounter((LARGE_INTEGER *) &highResolutionPerformanceCount);
    if (returnValue == FALSE) {
        VDError("Failed to retrieve the current value of the high-resolution performance counter.", __FILE__, __LINE__);
    };
    double accumulator = 0.0f;

    // game loop
    while (gameRunning) {
        // dispatch incoming sent messages, check the thread message queue, retrieve message
        while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {  // TODO:  PM_NOREMOVE?
            TranslateMessage(&message);
            DispatchMessage(&message);
        };

        // get the current count
        returnValue = QueryPerformanceCounter((LARGE_INTEGER *) &newCount);
        if (returnValue == FALSE) {
            VDError("Failed to retrieve the current value of the high-resolution performance counter.", __FILE__, __LINE__);
        };

        int updatesPerCycle = 0;

        // acquire the time it took to run one cycle
        double deltaTime = (double) (newCount - highResolutionPerformanceCount) / highResolutionPerformanceCounterFrequency;
        highResolutionPerformanceCount = newCount;
        currentTime = (double) highResolutionPerformanceCount / highResolutionPerformanceCounterFrequency;

        // clamp the time in case we lose focus of the thread for a while
        if (deltaTime > 0.25) {
            deltaTime = 0.25;
        };

        // allow the accumulator to process as many updates required to catch up to our provided Hz
        accumulator += deltaTime;
        while (accumulator >= maxSecondsForOneGameUpdate) {
            UpdateGame(currentTime);
            updatesPerCycle++;
            gameUpdateCount += maxSecondsForOneGameUpdate;
            accumulator -= maxSecondsForOneGameUpdate;
        };

        // render
        RECT drawingArea;
        drawingArea.left = 0;
        drawingArea.top = 0;
        drawingArea.right = width;
        drawingArea.bottom = height;
        graphics->Render(&drawingArea, updatesPerCycle, width, height, player->GetCurrentLocationX(), player->GetCurrentLocationY());
        // TODO: render direction too
    };
};



/* **********************************************************************************************************************
   UpdateGame
   ----------
   Updates the logic process.
   #param  currentTime  Current time of the program.
   **********************************************************************************************************************/

void MysticRealms::UpdateGame(double currentTime) {
    // gather input
    input->RetrieveLostInput();
    input->UpdateInput();

    // Game State:  World
    if (gameState == VD_GAMEWORLD) {
        int choice = VD_NONE;

        // see if any keys were pressed
        if (input->GetKeyboardInput()[DIK_ESCAPE] & 0x80) {
            gameRunning = FALSE;
        } else if (input->GetKeyboardInput()[DIK_LEFT] & 0x80) {
            choice = VD_LEFT;
        } else if (input->GetKeyboardInput()[DIK_RIGHT] & 0x80) {
            choice = VD_RIGHT;
        } else if (input->GetKeyboardInput()[DIK_UP] & 0x80) {
            choice = VD_UP;
        } else if (input->GetKeyboardInput()[DIK_DOWN] & 0x80) {
            choice = VD_DOWN;
        } else if (input->GetKeyboardInput()[DIK_SPACE] & 0x80) {
            choice = 5;
        };

        // are we entering a new area?
        if (enteringNewArea == TRUE && playerSprite->IsMoving() == FALSE) {
            char newAreaName[FILE_NAME_MAX_SIZE];

            // get the starting point of the new area
            int x = player->GetCurrentLocationX();
            int y = player->GetCurrentLocationY();
            int newX = world->GetTileNewAreaStartingPointX(x, y);
            int newY = world->GetTileNewAreaStartingPointY(x, y);

            // get the name of the new area
            (void) strcpy_s(newAreaName, world->GetTileNewAreaName(x, y));

            // clear world data
            world->ClearWorld();

            // load the new area
            world->LoadWorld(newAreaName);
            player->SetCurrentLocation(newX, newY);
            enteringNewArea = FALSE;

            // clear graphics
            graphics->ClearScreen();

            // load tile sprites
            RECT sourceRect;
            (void) SetRect(&sourceRect, 0, 0, 32, 32);
            for (int x = 0; x < world->GetWorldSizeX(); x++) {
                for (int y = 0; y < world->GetWorldSizeY(); y++) {
                    graphics->AddSprite(world->GetTileImageFileLocation(x, y),
                                        sourceRect,
                                        (float) x * 32.0f,
                                        (float) y * 32.0f,
                                        0xFF000000,
                                        FALSE,
                                        FALSE);
                };
            };

            // load NPC sprites
            for (unsigned int n = 0; n < world->GetNPCsSize(); n++) {
                // figure out which direction the NPC is facing
                switch (world->GetNPCDirection(n)) {
                    case 0:
                        (void) SetRect(&sourceRect, 0, 0, 32, 32);
                        break;
                    case 1:
                        (void) SetRect(&sourceRect, 0, 0, 32, 32);
                        break;
                    case 2:
                        (void) SetRect(&sourceRect, 96, 0, 128, 32);
                        break;
                    case 3:
                        (void) SetRect(&sourceRect, 192, 0, 224, 32);
                        break;
                    case 4:
                        (void) SetRect(&sourceRect, 288, 0, 320, 32);
                        break;
                    default:
                        break;
                };

                // add the NPC sprite
                graphics->AddSprite(world->GetNPCImageFileLocation(n),
                                    sourceRect,
                                    (float) world->GetNPCCurrentLocationX(n) * 32.0f,
                                    (float) world->GetNPCCurrentLocationY(n) * 32.0f,
                                    world->GetObjectColorKey(n),
                                    FALSE,
                                    TRUE);
            };

            // load player sprite
            (void) SetRect(&sourceRect, 0, 0, 32, 32);
            graphics->AddPlayerSprite("Images/Test.png", sourceRect, (float) width / 2, (float) height / 2, 0xFF000000, TRUE, FALSE);
        };

        // user decided to move
        if (choice >= 1 && choice <= 4 && playerSprite->IsMoving() == FALSE) {
            // make sure they're facing that direction already
            if (choice == player->GetDirection()) {
                // get the location they're trying to move to
                int locationX = player->GetCurrentLocationX();
                int locationY = player->GetCurrentLocationY();
                switch (choice) {
                    case 1:
                        locationY -= 1;
                        break;
                    case 2:
                        locationY += 1;
                        break;
                    case 3:
                        locationX -= 1;
                        break;
                    case 4:
                        locationX += 1;
                        break;
                    default:
                        break;
                };

                // see if the tile is within the world
                if (world->IsTileInBounds(locationX, locationY) == TRUE) {
                    // see if the tile is vacant
                    if (world->IsTileVacant(locationX, locationY) == TRUE) {
                        // see if the tile is open
                        int status = world->GetTileStatus(locationX, locationY);

                        // "New Area" tile
                        if (status == 3) {
                            // let it know we're entering a new area
                            enteringNewArea = TRUE;
                        };

                        // "Open" tile or "New Area" tile
                        if (status == 1 || status == 3) {
                            // let the renderer know we're moving
                            graphics->MovingPlayer(choice);

                            // move the player
                            player->Move(choice);
                        };
                    };
                };
            } else {
                player->SetDirection(choice);
            };
        };

        // TODO: temporary
        // user decided to talk
        if (choice == 5 && playerSprite->IsMoving() == FALSE) {
            /*char temp[FILE_NAME_MAX_SIZE];
            (void) strcpy_s(temp, "");
            for (unsigned int n = 0; n < world->GetNPCsSize(); n++) {
                for (list<VDNPC>::iterator anNPC = npcs.begin(); anNPC != npcs.end(); ++anNPC) {
                    if (strcmp(world->GetNPCName(n), anNPC->GetName()) == 0) {
                        // this is the one (and only, we're not checking location.......)
                        // TODO: check adjacent tiles for an NPC first (ideally we'll be checking in the
                        //       direction we're facing), then get the name of the NPC in the adjacent
                        //       tile, compare it to the list of NPC names, then get the dialogue and
                        //       present it
                        (void) strcpy_s(temp, anNPC->GetDialogueText(0));
                        MessageBox(NULL, anNPC->GetDialogueText(0), NULL, MB_OK);
                    };
                };
            };
            if (world->CheckTileOccupancy(1, player->GetCurrentLocationX(), player->GetCurrentLocationY()) == TRUE) {
                if (strcmp(temp, "") != 0) {
                    (void) graphics->AddText(temp, 10, 10, 100, 46, D3DCOLOR_ARGB(255, 255, 255, 255));
                };
            };
            if (world->CheckTileOccupancy(2, player->GetCurrentLocationX(), player->GetCurrentLocationY()) == TRUE) {
                if (strcmp(temp, "") != 0) {
                    (void) graphics->AddText(temp, 10, 10, 100, 46, D3DCOLOR_ARGB(255, 255, 255, 255));
                };
            };
            if (world->CheckTileOccupancy(3, player->GetCurrentLocationX(), player->GetCurrentLocationY()) == TRUE) {
                if (strcmp(temp, "") != 0) {
                    (void) graphics->AddText(temp, 10, 10, 100, 46, D3DCOLOR_ARGB(255, 255, 255, 255));
                };
            };
            if (world->CheckTileOccupancy(4, player->GetCurrentLocationX(), player->GetCurrentLocationY()) == TRUE) {
                if (strcmp(temp, "") != 0) {
                    (void) graphics->AddText(temp, 10, 10, 100, 46, D3DCOLOR_ARGB(255, 255, 255, 255));
                };
            };*/
        };

        // NPC logic
        for (unsigned int n = 0; n < world->GetNPCsSize(); n++) {
            int npcLocX = world->GetNPCCurrentLocationX(n);
            int npcLocY = world->GetNPCCurrentLocationY(n);

            // figure out where the NPC wants to move
            int npcChoice = world->RunNPCLogic(n, currentTime);
            switch (npcChoice) {
                case 1:
                    npcLocY -= 1;
                    break;
                case 2:
                    npcLocY += 1;
                    break;
                case 3:
                    npcLocX -= 1;
                    break;
                case 4:
                    npcLocX += 1;
                    break;
                default:
                    break;
            };

            // make sure the NPC chose to move first, and that he's not already moving
            if (npcChoice != 0 && graphics->IsNPCMoving((float) world->GetNPCCurrentLocationX(n) * 32.0f, (float) world->GetNPCCurrentLocationY(n) * 32.0f) == FALSE) {
                // see if the tile is within the world
                if (world->IsTileInBounds(npcLocX, npcLocY) == TRUE) {
                    // see if the tile is vacant
                    if (world->IsTileVacant(npcLocX, npcLocY) == TRUE && player->GetCurrentLocationX() != npcLocX && player->GetCurrentLocationY() != npcLocY) {
                        // see if the tile is open
                        if (world->GetTileStatus(npcLocX, npcLocY) == 1) {
                            // let the renderer know we're moving the NPC
                            graphics->MovingNPC((float) world->GetNPCCurrentLocationX(n) * 32.0f, (float) world->GetNPCCurrentLocationY(n) * 32.0f, npcChoice);

                            // move the NPC
                            world->MoveNPC(n, npcChoice);
                        };
                    };
                };
            };
        };
    };
};



/* **********************************************************************************************************************
   LoadNPCs
   --------
   Loads the NPC's into the game.
   **********************************************************************************************************************/

void MysticRealms::LoadNPCs() {
    HANDLE file = NULL;
    BOOL returnValue = FALSE;
    DWORD numberOfBytesRead = 0;
    size_t numberOfNPCs = 0;

    // clear the NPC data first
    npcs.clear();

    // open the file
    file = CreateFile("Data/NPCs.vd", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (file == INVALID_HANDLE_VALUE) {
        return;
    };

    // get how many NPC's we're adding
    returnValue = ReadFile(file, &numberOfNPCs, sizeof(numberOfNPCs), &numberOfBytesRead, NULL);
    if (returnValue == FALSE) {
        VDError("Failed to read from the NPCs.vd file.", __FILE__, __LINE__);
    };

    // read all the NPC's
    for (unsigned int n = 0; n < numberOfNPCs; n++) {
        VDNPC newNPC;
        newNPC.LoadNPC(file);
        npcs.push_back(newNPC);
    };

    // close the file
    returnValue = CloseHandle(file);
    if (returnValue == FALSE) {
        VDError("Failed to close the NPCs.vd file.", __FILE__, __LINE__);
    };
};