text: #include "application.hpp"
  #include <bp/texture.hpp>
  #include <bp/opengl45_gpu_device.hpp>
  #include <bp/scene_tree.hpp>
  #include <bp/resource_registry.hpp>
  #include <bp/resource_factory_registry.hpp>
  #include <bp/resource_factory.hpp>
  #include <bp/environment_paths.hpp>
  #include <iostream>
  #include <thread>
  #include <glad/glad.h>
  
  namespace bp
  {
  
  static void APIENTRY on_gl_debug_message(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userparam)
  {
      // ignore non-significant error/warning codes
      if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return;
  
      std::cout << "---------------" << std::endl;
      std::cout << "Debug message (" << id << "): " <<  message << std::endl;
  
      switch (source)
      {
          case GL_DEBUG_SOURCE_API:             std::cout << "Source: API"; break;
          case GL_DEBUG_SOURCE_WINDOW_SYSTEM:   std::cout << "Source: Window System"; break;
          case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break;
          case GL_DEBUG_SOURCE_THIRD_PARTY:     std::cout << "Source: Third Party"; break;
          case GL_DEBUG_SOURCE_APPLICATION:     std::cout << "Source: Application"; break;
          case GL_DEBUG_SOURCE_OTHER:           std::cout << "Source: Other"; break;
      } std::cout << std::endl;
  
      switch (type)
      {
          case GL_DEBUG_TYPE_ERROR:               std::cout << "Type: Error"; break;
          case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break;
          case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:  std::cout << "Type: Undefined Behaviour"; break;
          case GL_DEBUG_TYPE_PORTABILITY:         std::cout << "Type: Portability"; break;
          case GL_DEBUG_TYPE_PERFORMANCE:         std::cout << "Type: Performance"; break;
          case GL_DEBUG_TYPE_MARKER:              std::cout << "Type: Marker"; break;
          case GL_DEBUG_TYPE_PUSH_GROUP:          std::cout << "Type: Push Group"; break;
          case GL_DEBUG_TYPE_POP_GROUP:           std::cout << "Type: Pop Group"; break;
          case GL_DEBUG_TYPE_OTHER:               std::cout << "Type: Other"; break;
      } std::cout << std::endl;
  
      switch (severity)
      {
          case GL_DEBUG_SEVERITY_HIGH:         std::cout << "Severity: high"; break;
          case GL_DEBUG_SEVERITY_MEDIUM:       std::cout << "Severity: medium"; break;
          case GL_DEBUG_SEVERITY_LOW:          std::cout << "Severity: low"; break;
          case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break;
      } std::cout << std::endl;
      std::cout << std::endl;
  }
  
  void game_instance::starting(application& app)
  {
  
  }
  
  void game_instance::started(application& app)
  {
  
  }
  
  void game_instance::stopping(application& app)
  {
  
  }
  
  void game_instance::stopped(application& app)
  {
  
  }
  
  void game_instance::rendering(application& app)
  {
  
  }
  
  void game_instance::rendered(application& app)
  {
  
  }
  
  application::application(std::unique_ptr<game_instance> game)
  : game_{std::move(game)}
  , running_{false}
  {
      if(game_)
      {
          game_->starting(*this);
      }
  }
  
  application::~application()
  {
      if(game_)
      {
          game_->stopped(*this);
      }
  }
  
  gpu_device& application::gpu() noexcept
  {
      return *gpu_;
  }
  
  const gpu_device& application::gpu() const noexcept
  {
      return *gpu_;
  }
  
  resource_registry& application::resources() noexcept
  {
      return *resources_;
  }
  
  const resource_registry& application::resources() const noexcept
  {
      return *resources_;
  }
  
  const frame_duration& application::dt() const noexcept
  {
      return dt_;
  }
  
  void application::override_with_cli(int argc, char* argv[])
  {
      // Initialize the application
  }
  
  void application::start()
  {
      SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
      SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
      SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
  
      main_window_ = SDL_CreateWindow("Bored Platypus", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
      if(!main_window_)
      {
          std::cerr << "failed to create main window";
          throw std::runtime_error{""};
      }
  
      gl_context_ = SDL_GL_CreateContext(main_window_);
      if(!gl_context_)
      {
          std::cerr << "failed to create OpenGL 4.5 core context" << std::endl;
          throw std::runtime_error{""};
      }
  
      if(!gladLoadGLLoader(SDL_GL_GetProcAddress))
      {
          std::cerr << "failed to load OpenGL 4.5 extensions" << std::endl;
          throw std::runtime_error{""};
      }
  
      // Debug output
      glEnable(GL_DEBUG_OUTPUT);
      glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
      glDebugMessageCallback(on_gl_debug_message, nullptr);
      glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
  
  
      // Output identifications about the OpenGL context used by the application
      std::cout << "OpenGL Vendor: " << glGetString(GL_VENDOR) << '\n'
                << "OpenGL Renderer: " << glGetString(GL_RENDERER) << '\n'
                << "OpenGL Version: " << glGetString(GL_VERSION) << '\n'
                << std::endl;
  
      std::cout << "Environment paths: \n"
                << "Executable: " << current_executable_path() << '\n'
                << "Project: " << current_project_root_path() << '\n'
                << "Configurations: " << current_user_config_path("test") << '\n'
                << "Cache: " << current_user_cache_path("test") << '\n'
                << "Saves: " << current_user_save_path("test") << '\n'
                << std::endl;
  
      gpu_ = std::make_unique<opengl45_gpu_device>();
  
      // Setup the resource registry
      resources_ = std::make_unique<resource_registry>();
      register_default_resource_types();
      std::cout << "resources registry ready" << std::endl;
  
      running_ = true;
  
      main_window_node_.set_window(main_window_, gl_context_);
  
      // Set the node scene
      scene_ = std::make_unique<scene_tree>();
      scene_->change_root(&main_window_node_);
  
      std::cout << "main scene ready" << std::endl;
  
      game_->started(*this);
  }
  
  void application::register_default_resource_types()
  {
      resources_->factories().register_resource(texture::static_resource_type(),
                                                make_resource_factory([this]() -> std::unique_ptr<resource>
                                                        {
                                                          return std::make_unique<texture>(*gpu_);
                                                        }));
  }
  
  int application::wait()
  {
      const frame_duration TARGET_FRAME_DURATION{1.0 / 60.0};
  
      std::chrono::steady_clock::time_point start_of_frame = std::chrono::steady_clock::now();
      dt_ = TARGET_FRAME_DURATION;
  
      while(running_)
      {
          poll_events();
          tick();
          render();
  
          std::chrono::steady_clock::time_point end_of_frame = std::chrono::steady_clock::now();
          auto previous_frame_duration = end_of_frame - start_of_frame;
          start_of_frame = end_of_frame;
  
          if(previous_frame_duration < TARGET_FRAME_DURATION)
          {
              std::this_thread::sleep_for(TARGET_FRAME_DURATION - previous_frame_duration);
          }
  
          dt_ = TARGET_FRAME_DURATION;
      }
  
      game_->stopping(*this);
  
      SDL_GL_DeleteContext(gl_context_);
      SDL_DestroyWindow(main_window_);
  
      return 0;
  }
  
  void application::poll_events()
  {
      SDL_Event event;
      while(SDL_PollEvent(&event))
      {
          switch(event.type)
          {
              case SDL_QUIT:
                  running_ = false;
                  break;
              case SDL_WINDOWEVENT:
                  handle_window_event(event.window);
                  break;
              case SDL_KEYDOWN:
              case SDL_KEYUP:
                  //on_key_event(event.key);
                  break;
              case SDL_TEXTINPUT:
                  //on_text_input(event.text);
                  break;
              case SDL_TEXTEDITING:
                  //on_text_editing(event.edit);
                  break;
              case SDL_MOUSEBUTTONDOWN:
              case SDL_MOUSEBUTTONUP:
                  //on_mouse_button_event(event.button);
                  break;
          }
      }
  }
  
  void application::handle_window_event(const SDL_WindowEvent& window_event)
  {
      SDL_Window* window = SDL_GetWindowFromID(window_event.windowID);
      window_control_node* window_node = window_control_node::find_node_by_window(window);
  
      std::unique_ptr<window_node_event> node_event_to_send;
      switch(window_event.event)
      {
          case SDL_WINDOWEVENT_SIZE_CHANGED:
          case SDL_WINDOWEVENT_RESIZED:
              node_event_to_send = std::make_unique<window_resized_node_event>(window_node, window_event.data1, window_event.data2);
              break;
          case SDL_WINDOWEVENT_FOCUS_LOST:
              // Keyboard focus lost
              break;
          case SDL_WINDOWEVENT_FOCUS_GAINED:
              // Keyboard focus gained
              break;
          case SDL_WINDOWEVENT_ENTER:
              // Mouse focus gained
              break;
          case SDL_WINDOWEVENT_LEAVE:
              // Mouse focus lost
              break;
          case SDL_WINDOWEVENT_CLOSE:
              // Window requested to close
              break;
      }
  
      // Send the event to the scene
      if(node_event_to_send)
      {
          scene_->send_event_to(*node_event_to_send, window_node);
      }
  }
  
  void application::tick()
  {
      // Tick the current scene
      scene_->tick(dt_);
  }
  
  void application::render()
  {
      game_->rendering(*this);
      scene_->render(*gpu_);
      game_->rendered(*this);
  
      SDL_GL_SwapWindow(main_window_);
  }
  
  int application::launch_impl(int argc, char* argv[], std::unique_ptr<game_instance> game)
  {
      if(SDL_Init(SDL_INIT_VIDEO) != 0)
      {
          std::cerr << "failed to initialize SDL" << std::endl;
          return 1;
      }
  
      // Initializes the application
      application app(std::move(game));
      app.override_with_cli(argc, argv);
  
      // Starts the game loop
      app.start();
  
      // Wait for the game loop to finish
      const int result = app.wait();
  
      SDL_Quit();
  
      return result;
  }
  
  }