/******************************************************************************
*	File		-	core.cpp
*	Author		-	Joey Pollack
*	Date		-	2021/08/30 (y/m/d)
*	Mod Date	-	2021/08/30 (y/m/d)
*	Description	-	The Core Engine Class. Manages the engine components.
******************************************************************************/

#include "core.h"
#include "version.h"

#include "core_console.h"

#include <dearimgui/imgui.h>
#include <LunariumConfig.h>

#include <platform/terminal.h>

// Run modes
#include <run_modes/testbed/testbed.h>
#include <run_modes/editor/editor.h>

// Sub Systems
#include <platform/window.h>
#include <input/input_manager.h>
#include <graphics/opengl/glGraphics.h>
#include <gui/gui.h>
// #include <gui/logGui.h>
// #include <gui/luaConsole.h>
#include <scripting/script_manager.h>
#include <scripting/coreAPI.h>

namespace lunarium
{
    Core* Core::mpInstance = nullptr;
    Core::Core()
        : mbIsInit(false), mpArgs(nullptr), mpWindow(nullptr), mpGraphics(nullptr), mpInput(nullptr),
        mGUI(GUI::GetInstance()), mbMidRender(false), mbMidTextureRender(false), mpRunMode(nullptr), mbShowGuiDemo(false)
    {
    
    }

    Core& Core::GetInstance()
    {
        if (!mpInstance)
        {
            mpInstance = new Core;
        }

        return *mpInstance;
    }

    ////////////////////////////////////////////////////////////
    //				SHUTDOWN
    ////////////////////////////////////////////////////////////
    void Core::Shutdown()
    {
        if (!mpInstance)
            return;
        
        Logger::Info(LogCategory::CORE, "Lunarium is shutting down!");

        int x, y;
        mpInstance->MainWindow().GetPosition(&x, &y);
        mpInstance->mState.Display.WindowStartPosition.X = x;
        mpInstance->mState.Display.WindowStartPosition.Y = y;
        

        mpInstance->mpWindow->GetFramebufferSize(&mpInstance->mState.Display.WindowedSize.Width, 
                                                 &mpInstance->mState.Display.WindowedSize.Height);
        
        mpInstance->mpWindow->GetPosition(&mpInstance->mState.Display.WindowStartPosition.X,
                                          &mpInstance->mState.Display.WindowStartPosition.Y);

        mpInstance->mState.SaveToFile();
  
        // Run Mode shuts down first
        mpInstance->mpRunMode->Shutdown();
        delete mpInstance->mpRunMode;
        mpInstance->mpRunMode = nullptr;

        // Shutdown subsystems
        CoreAPI::FreeInstance();
        ScriptManager::FreeInstance();

        //LuaConsole::FreeInstance();
        //LogGui::FreeInstance();
        GUI::GetInstance().Shutdown();
        GUI::FreeInstance();

        mpInstance->mpInput->Shutdown();
        delete mpInstance->mpInput;
        mpInstance->mpInput = nullptr;

        mpInstance->mpGraphics->Shutdown();
        delete mpInstance->mpGraphics;
        mpInstance->mpGraphics = nullptr;

        mpInstance->mpWindow->Shutdown();
        delete mpInstance->mpWindow;
        mpInstance->mpWindow = nullptr;

        delete mpInstance->mpArgs;
        mpInstance->mpArgs = nullptr;

        mpInstance->mbIsInit = false;
        delete mpInstance;
        mpInstance = nullptr;

    }

    ////////////////////////////////////////////////////////////
    //				INITIALIZATION
    ////////////////////////////////////////////////////////////
    void Core::Initialize(int argc, char** argv)
    {
        if (mbIsInit)
        {
            return;
        }

        // Setup the log system
		mMasterLogFile.open("Lunarium_Master.log", std::ios_base::app);
		mMasterLogFile << "\n\n";


		mErrorLogFile.open("Lunarium_Errors.log", std::ios_base::app);

        mGraphicsLogFile.open("Lunarium_Graphics.log", std::ios_base::app);
		mErrorLogFile << "\n\n";

		if (mMasterLogFile.is_open())
			Logger::GetInstance()->AddListener(new FileListener(mMasterLogFile));

		if (mErrorLogFile.is_open())
			Logger::GetInstance()->AddListener(new FileListener(mErrorLogFile, LogLevel::ERROR | LogLevel::FATAL_ERROR));
        
        if (mGraphicsLogFile.is_open())
			Logger::GetInstance()->AddListener(new FileListener(mGraphicsLogFile, LogLevel::GRAPHICS_INTERNAL_DEBUG | LogLevel::GRAPHICS_INTERNAL_ERROR));

        Logger::GetInstance()->SetAllowRepeats(true);
        
        // Init the Debug log window
        OpRes result;
        mPanelIDs.CoreConsole = AddPanel(new CoreConsole);
        
        Logger::Info(LogCategory::CORE, "Running Lunarium version %s", Version::GetVersion().ToString().c_str());

        // Attempt to load the engine state file. This file should be placed in the same directory as the lunarium program.
        if (Failed(State::CreateFromFile("engine_state.json", mState)))
        {
            Logger::Warn(LogCategory::CORE, "Unable to load state file: engine_state.json. Loading default state.");
            mState = State::CreateDefault();
        }
        else
        {
            Logger::Info(LogCategory::CORE, "Loaded state file: engine_state.json");
        }

        // Parse command line args -- None right now
        std::vector<Args::SwitchDesc> sd;
        mpArgs = new Args(argc, argv, '-', sd);

        // Init Graphics/Window system
        mpWindow = new Window;
        result = mpWindow->Initialize(mState);
        if (Failed(result))
        {
            Logger::Fatal(LogCategory::CORE,
                        "Could not initialize the Window system: %s", result.Description.c_str());
            return;
        }

        if (RenderSystem::OPENGL == mState.Display.Renderer)
        {
            mpGraphics = new OglGraphics;
        }
        else if (RenderSystem::VULKAN == mState.Display.Renderer)
        {
            Logger::Fatal(LogCategory::CORE,
                        "Can not create Vulkan graphics system because it is not yet implemented. Must use OpenGL instead.");
            return;
        }
        else
        {
            Logger::Fatal(LogCategory::CORE,
                        "Could not create graphics system: Unknown render framework specified.");
            return;
        }

        // TODO: This should probably be based on a state setting instead
#ifdef _DEBUG
        result = mpGraphics->Initialize(mpWindow);
#else   
        result = mpGraphics->Initialize(mpWindow, false);
#endif

        if (Failed(result))
        {
            Logger::Fatal(LogCategory::CORE,
                    "Could not initialized the graphics system: %s", result.Description);
            return;
        }

        mpGraphics->SetClearColor(Color(0.5f, 0.5f, 0.75f, 1.0f));

        // INPUT
        mpInput = new InputManager;
        mpInput->Initialize(mpWindow);

        // GUI
        result = mGUI.Initialize(mpWindow->GetWindow());
        if (Failed(result))
        {
            Logger::Warn(LogCategory::CORE,
                    "Could not initialized the main GUI system: %s", result.Description);
        }

        // SCRIPTING
        ScriptManager& scriptMan = ScriptManager::GetInstance();
        result = scriptMan.Initialize();
        if (Failed(result))
        {
            Logger::Warn(LogCategory::CORE,
                    "Could not initialized the LUA script manager: %s", result.Description);
        }

        CoreAPI& capi = CoreAPI::GetInstance();
        result = capi.Initialize(scriptMan);
        if (Failed(result))
        {
            Logger::Warn(LogCategory::CORE,
                    "Could not initialized the LUA Core API: %s", result.Description);
        }

// RUN MODE
        const char* types[] = { "game", "editor", "test" };
        Logger::Info(LogCategory::CORE, "Running in mode: %s", types[mState.Mode]);
        if (RunMode::MODE_TEST == mState.Mode)
        {
            mpRunMode = new TestBed;
        }
        
        else if (RunMode::MODE_EDITOR == mState.Mode)
        {
#if BUILD_NO_EDITOR
        Logger::Fatal(LogCategory::CORE, "The Editor is not available with this build");
        return;
#else
            mpRunMode = new editor::Editor;
            FreePanel(mPanelIDs.CoreConsole);   // Editor uses it's own console
#endif
        }

        // Initialize the Run Mode
        if (Failed(mpRunMode->Initialize()))
        {
            Logger::Fatal(LogCategory::CORE,
                        "Could not initialize the Run Mode: %s", result.Description.c_str());
            return;
        }
        mbIsInit = true;
    }

    bool Core::IsInit() const
    {
        return mbIsInit;
    }

    const State& Core::GetState() const
    {
        return mState;
    }

     void Core::SignalShutdown()
     {
        mpWindow->SetShouldCloseFlag(true);
     }

    ////////////////////////////////////////////////////////////
    //				GAME LOOP
    ////////////////////////////////////////////////////////////
    void Core::RunGameLoop()
    {
        mFrameCounter.Reset();

        // TODO: Init frame counter
        while (!mpWindow->ShouldWindowClose())
        {
            mFrameCounter.NewFrame();

            // Display FPS in window title for now
            std::string title = "Lunarium - FPS: ";
            title += std::to_string(mFrameCounter.GetFrameData().CurrentFPS);
            glfwSetWindowTitle(mpWindow->GetWindow(), title.c_str());

            // Get pointers to gui panels
            CoreConsole* con = (CoreConsole*)mPanels[mPanelIDs.CoreConsole];

            // Poll input
            Window::PollEvents();
            auto keyEvents = mpInput->PollKeys();

            if (!ImGui::GetIO().WantCaptureKeyboard)
            {
                // Send key events
                for (int i = 0; i < keyEvents.KeysPressed.size(); i++)
                {
                    mpRunMode->OnKeyPress(keyEvents.KeysPressed[i]);
                }

                for (int i = 0; i < keyEvents.KeysReleased.size(); i++)
                {
                    mpRunMode->OnKeyRelease(keyEvents.KeysReleased[i]);
                }
            }
            else
            {
                // Check if there is a new LUA command
                std::string command;
                if (con && con->GetNewCommand(command))
                {
                    // Logger::Debug(LogCategory::SCRIPTING, "New LUA command: %s", command.c_str());
                    OpRes result = ScriptManager::RunScript(command.c_str());
                    if (Failed(result))
                    {
                        Logger::Error(LogCategory::SCRIPTING,  result.Description.c_str());
                    }
                }
            }            


            // UPDATE game state
            mpRunMode->OnTick(mFrameCounter.GetFrameData().LastFrameTime);

            // DEBUG PANELS
            if (Core::Input().IsKeyPressed(KeyCode::F2, true) && con)
            {
                con->SetOpen(!con->IsOpen());
            }

            if (Core::Input().IsKeyPressed(KeyCode::F3, true))
            {
                mbShowGuiDemo = !mbShowGuiDemo;
            }


            // RENDER
            if (mbMidTextureRender)
            {
                Logger::Warn(LogCategory::CORE, "Render to texture was not ended!");
                EndRenderToTexture();
            }

            mGUI.NewFrame();
            mpGraphics->BeginDraw();
            mbMidRender = true;

            // Gui windows
            if (con)
            {
                con->DoFrame();
            }

            if (mbShowGuiDemo)
            {
                mGUI.ShowDemoWindow(mbShowGuiDemo);
            }

            // Run mode
            mpRunMode->OnRender(mpGraphics);

            // END RENDER
            mGUI.EndFrame();
            mpGraphics->EndDraw();
            mbMidRender = false;
        }
    }

    OpRes Core::BeginRenderToTexture(int id)
    {
        if (mbMidRender)
        {
            return OpRes::Fail("Can not switch render targets in the middle of rendering");
        }

        mbMidTextureRender = true;
        mpGraphics->BeginDraw(id);
        return OpRes::OK();
    }

    Image* Core::EndRenderToTexture()
    {
        if (!mbMidTextureRender)
        {
            return nullptr;
        }

        mbMidTextureRender = false;
        return mpGraphics->EndDraw();
    }

    ////////////////////////////////////////////////////////////
    //				STATIC INTERFACE
    ////////////////////////////////////////////////////////////
    
    Window& Core::MainWindow()
    {
        return *mpInstance->mpWindow;
    }

    IGraphics& Core::Graphics()
    {
        return *mpInstance->mpGraphics;
    }
    
    InputManager& Core::Input()
    {
        return *mpInstance->mpInput;
    }
    
    ////////////////////////////////////////////////////////////
    //				HELPERS
    ////////////////////////////////////////////////////////////
    
    uint32_t Core::AddPanel(gui::Panel* p)
    {
        mPanels.push_back(p);
        return mPanels.size() - 1;
    }

    
    void Core::FreePanel(uint32_t id)
    {
        delete mPanels[id];
        mPanels[id] = nullptr;
    }

    	CoreLogListener::CoreLogListener(uint32_t acceptedLogLevels, uint32_t acceptedLogCategories, const char* myName)
		: LogListener(acceptedLogLevels, acceptedLogCategories, myName)
	{
	}

	bool CoreLogListener::Log(LogMessage& message)
	{
		if (!LevelIsSet(message.LogLevel) ||
			!CategoryIsSet(message.LogCategory))
			return false;

        std::cout << std::endl;

        switch (message.LogLevel)
        {
            case LogLevel::GRAPHICS_INTERNAL_ERROR:
            case LogLevel::FATAL_ERROR:
            case LogLevel::ERROR:
                std::cout << Terminal::Color(TermColor::TC_RED); break;
            case LogLevel::WARNING: std::cout << Terminal::Color(TermColor::TC_YELLOW); break;
            case LogLevel::INFO: std::cout << Terminal::Color(TermColor::TC_GREEN); break;
            case LogLevel::DEBUG: std::cout << Terminal::Color(TermColor::TC_BLUE); break;
            case LogLevel::TRACE: std::cout << Terminal::Color(TermColor::TC_CYAN); break;
            case LogLevel::GRAPHICS_INTERNAL_DEBUG: break;

        }

		 std::cout << Logger::TimeStamp() << Logger::GetCategoryName(message.LogCategory)
					<< Logger::GetLevelName(message.LogLevel) << message.Message << Terminal::Color(TermColor::TC_DEFAULT) << std::flush;
		return true;
	}
}