//
// Created by m on 12/3/21.
//

#include "engine.hpp"
#include "rendering/sdl/sdlrenderer.hpp"
#include "rlengineexception.hpp"
#include "rendering/pipeline_steps/gamerenderingstep.hpp"
#include "rendering/pipeline_steps/fpscounterrenderingstep.hpp"
#include <chrono>
#include <thread>
#include <exception>

using namespace std::chrono_literals;
void Engine::start_loop() {
    const auto one_second_milli = std::chrono::milliseconds(1s);
    if(game == nullptr) {
        throw MissingGameException();
    }
    if(renderer == nullptr) {
        throw MissingRendererException();
    }
    renderer->flush();
    auto game_params = GameInitArgs{this->config.target_fps, this->config.window_width, this->config.window_height};
    game->initialize(game_params);
    Logger::log(LogLevel::DEBUG, "Engine init finished");
    std::vector<std::unique_ptr<RenderingPipelineStep>> rendering_steps = build_render_pipline();
    auto duration_per_frame = one_second_milli / this->config.target_fps;
    while(true) {
        auto starttime = std::chrono::high_resolution_clock::now();

        // Input and update
        auto input = this->input_processor->process_input();
        bool end = game->update(input);
        if(end) {
            break;
        }

        // Rendering
        std::for_each(rendering_steps.begin(), rendering_steps.end(), [this](std::unique_ptr<RenderingPipelineStep>& step) {
            step->render_step(this->renderer.get());
        });
        renderer->flush();

        // timing and sleep
        auto endtime = std::chrono::high_resolution_clock::now();
        auto update_duration = std::chrono::duration_cast<std::chrono::milliseconds>(endtime - starttime);
        std::this_thread::sleep_for(duration_per_frame - update_duration);
    }
}

Engine::Engine(std::unique_ptr<Game> game,
               std::unique_ptr<InputProcessor> input_processor) {
    this->game = std::move(game);
    this->input_processor = std::move(input_processor);
    this->parse_config(ENGINE_CONFIG_FILE);
    Logger::set_minimum_loglevel(this->config.minimum_loglevel);
    this->setup_renderer(this->config.selected_renderer);
}

void Engine::load_config(const std::string &path, const std::function<void (toml::table)>& f) {
    std::string full_path = fmt::format("{}/{}", CONFIG_DIR, path);
    auto table = toml::parse_file(full_path);
    f(table);
}

void Engine::parse_config(const std::string& config_file_path) {
    EngineConfig new_config = EngineConfig();
    load_config("engine.toml", [&new_config](const toml::table& table) {
        new_config.show_fps = table["overlay"]["show_fps"].value_or<bool>(DEFAULT_SHOW_FPS);
        new_config.window_width = table["display"]["window_width"].value_or(DEFAULT_WINDOW_WIDTH);
        new_config.window_height = table["display"]["window_height"].value_or(DEFAULT_WINDOW_HEIGHT);
        new_config.target_fps = table["display"]["target_fps"].value_or(DEFAULT_TARGET_FPS);
        new_config.window_title = table["display"]["window_title"].value_or(DEFAULT_WINDOW_TITLE);
        auto default_font_path = fmt::format("fonts/{}.ttf", table["font"]["default_font_name"].value_or(DEFAULT_FONT_NAME));
        auto default_font_size = table["font"]["default_font_size"].value_or(DEFAULT_FONT_SIZE);
        new_config.default_font = TextRenderDetails{default_font_path, default_font_size, COLOR_WHITE};
        new_config.selected_renderer = static_cast<RendererTypes>(table["display"]["rendering_engine"].value_or((int)RendererTypes::SDL_HARDWARE));
        new_config.minimum_loglevel = static_cast<LogLevel>(table["logging"]["minimum_level"].value_or((int)LogLevel::DEBUG));
    });
    this->config = new_config;
}

void Engine::setup_renderer(RendererTypes rendering_engine) {
    std::unique_ptr<Renderer> rendering_engine_ptr;
    auto render_params = RendererParams{this->config.window_title, this->config.window_width, this->config.window_height, this->config.default_font};
    switch(rendering_engine) {
        case SDL_HARDWARE: rendering_engine_ptr = std::make_unique<SdlRenderer>(render_params); break;
        default: throw RLEngineException("Invalid renderer specified");
    }
    this->renderer = std::move(rendering_engine_ptr);
}

std::vector<std::unique_ptr<RenderingPipelineStep>> Engine::build_render_pipline() {
    std::vector<std::unique_ptr<RenderingPipelineStep>> result = std::vector<std::unique_ptr<RenderingPipelineStep>>();
    result.push_back(std::make_unique<GameRenderingStep>(this->game.get()));
    if(this->config.show_fps) {
        result.push_back(std::make_unique<FpsCounterRenderingStep>());
    }
    return result;
}


Engine::~Engine() = default;

const char *MissingGameException::what() const noexcept {
    return MISSING_GAME_ERROR;
}

const char *MissingRendererException::what() const noexcept {
    return MISSING_RENDERER_ERROR;
}
