#include "graphics/renderer.h"
#include "graphics/shader.h"
#include "utility/events.h"
#include "utility/logger.h"
#include "utility/resourcemanager.h"

#include <algorithm>
#include <glm/gtc/type_ptr.hpp>
#include <graphics/postprocess.h>
#include <memory>

Renderer::Renderer(const std::shared_ptr<ResourceManager> &r,
                   const std::shared_ptr<GLWindow> &w)
    : resourceManager(r), glWindow(w) {
  initFrameBuffers();
  initUniformBuffers();
  screenQuad = std::make_unique<ScreenQuad>();
  postProcessor = std::make_unique<Postprocessor>(r);
}

void Renderer::hookEventManager(std::weak_ptr<EventManager> eventManager) {
  if (auto e = eventManager.lock()) {
    e->subscribe<ScreenShakeEvent>([this](const ScreenShakeEvent &event) {
      postProcessor->ApplyEffect(Postprocessor::SHAKE, event.intensity,
                                 event.duration);
    });
    e->subscribe<ScreenBlurEvent>([this](const ScreenBlurEvent &event) {
      postProcessor->ApplyEffect(Postprocessor::BLUR, event.intensity,
                                 event.duration);
    });
    e->subscribe<WindowResizeEvent>(
        [this](const WindowResizeEvent &event) { resizeFrameBuffers(); });
  }
}

// Initialize the framebuffers used by the renderer to allow for post-processing
// effects
void Renderer::initFrameBuffers() {
  glGenFramebuffers(1, &worldBuffer.frame);
  glGenFramebuffers(1, &hudBuffer.frame);
  glBindFramebuffer(GL_FRAMEBUFFER, worldBuffer.frame);

  // World buffer creation
  glGenTextures(1, &worldBuffer.texture);
  glBindTexture(GL_TEXTURE_2D, worldBuffer.texture);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, glWindow->Width(), glWindow->Height(),
               0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);

  // Attaching empty texture as color buffer for framebuffer
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                         worldBuffer.texture, 0);

  if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    LOG(ERROR, "Failed to complete world framebuffer: {}",
        glCheckFramebufferStatus(GL_FRAMEBUFFER));
    assert(1 == 0); // force crash
  }

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  // same thing is done for the hud texture
  glBindFramebuffer(GL_FRAMEBUFFER, hudBuffer.frame);

  glGenTextures(1, &hudBuffer.texture);
  glBindTexture(GL_TEXTURE_2D, hudBuffer.texture);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, glWindow->Width(), glWindow->Height(),
               0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);

  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                         hudBuffer.texture, 0);

  if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    LOG(ERROR, "Failed to complete hud framebuffer: {}",
        glCheckFramebufferStatus(GL_FRAMEBUFFER));
    assert(1 == 0); // force crash
  }

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void Renderer::resizeFrameBuffers() {
  glDeleteFramebuffers(1, &worldBuffer.frame);
  glDeleteFramebuffers(1, &hudBuffer.frame);
  glDeleteTextures(1, &worldBuffer.texture);
  glDeleteTextures(1, &hudBuffer.texture);

  initFrameBuffers();
}

void Renderer::initUniformBuffers() {
  glGenBuffers(1, &uboMatrices);

  glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
  glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
  glBindBuffer(GL_UNIFORM_BUFFER, 0);

  glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboMatrices);
  // glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 *
  // sizeof(glm::mat4));
}

void Renderer::setProjAndViewMatrix(const glm::mat4 &proj,
                                    const glm::mat4 &view) {
  glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
  glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4),
                  glm::value_ptr(proj));
  glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4),
                  glm::value_ptr(view));
  glBindBuffer(GL_UNIFORM_BUFFER, 0);
}

void Renderer::addDrawable(RenderLayer renderLayer, Drawable *drawable) {
  if (renderLayer == RenderLayer::HUD || renderLayer == RenderLayer::Menu)
    HUDLayerPool[renderLayer].push_back(drawable);
  else
    worldLayerPool[renderLayer].push_back(drawable);
}

void Renderer::removeDrawable(RenderLayer renderLayer, Drawable *drawable) {
  auto erase = [&](auto &pool) {
    pool.erase(std::remove(pool.begin(), pool.end(), drawable), pool.end());
  };
  if (renderLayer == RenderLayer::HUD || renderLayer == RenderLayer::Menu) {
    erase(HUDLayerPool[renderLayer]);
  } else
    erase(worldLayerPool[renderLayer]);
}

void Renderer::render() {
  // Bind the world frame buffer
  glBindFramebuffer(GL_FRAMEBUFFER, worldBuffer.frame);
  // clear color and depth buffer
  glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);
  renderPool(worldLayerPool);

  glBindFramebuffer(GL_FRAMEBUFFER, hudBuffer.frame);
  glClearColor(0.0f, 1.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);
  renderPool(HUDLayerPool);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, worldBuffer.texture);
  postProcessor->applyPostProcess(0);
  screenQuad->draw();

  glBindTexture(GL_TEXTURE_2D, hudBuffer.texture);
  // postProcessor->applyPostProcess(1);
  screenQuad->draw();
}

void Renderer::renderPool(auto &layerPool) {
  sortLayerPool(layerPool);
  Shader *curShader = nullptr;
  for (const auto &layer : renderingOrder) {
    unsigned curShaderID = static_cast<unsigned>(-1);
    for (const auto &item : layerPool[layer]) {
      if (item->getShaderID() != curShaderID) {
        curShaderID = item->getShaderID();
        curShader = resourceManager->getShaderByID(curShaderID);
        curShader->use();
      }
      if (!item->getOneShotUniforms().empty()) {
        uploadUniforms(curShaderID, item->getOneShotUniforms());
        item->clearOneShot();
      }
      uploadUniforms(curShaderID, item->getUniforms());
      item->clearUniforms();

      item->draw();
    }
  }
}

void Renderer::clear() {
  worldLayerPool.clear();
  HUDLayerPool.clear();
}

void Renderer::uploadUniforms(const unsigned shaderID,
                              const std::vector<Uniform> &uniforms) {
  Shader *shader = resourceManager->getShaderByID(shaderID);
  if (shader == nullptr) {
    LOG(ERROR, "No shader found with id {} !", shaderID);
    return;
  }
  for (const auto &uniform : uniforms) {
    std::visit(
        [&](auto &&arg) {
          using T = std::decay_t<decltype(arg)>;
          if constexpr (std::is_same_v<T, bool>) {
            shader->setBool(uniform.name, arg);
          } else if constexpr (std::is_same_v<T, int>) {
            shader->setInt(uniform.name, arg);
          } else if constexpr (std::is_same_v<T, float>) {
            shader->setFloat(uniform.name, arg);
          } else if constexpr (std::is_same_v<T, glm::vec2>) {
            shader->setVec2(uniform.name, glm::value_ptr(arg));
          } else if constexpr (std::is_same_v<T, glm::mat4>) {
            shader->setMatrix4f(uniform.name, glm::value_ptr(arg));
          }
        },
        uniform.value);
  }
}
void Renderer::sortLayerPool(auto &layerPool) {
  // Sort by shader id, this works to batch shaders together to avoid shader
  // switching too much
  for (auto &[_, pool] : layerPool) {
    std::sort(pool.begin(), pool.end(),
              [](const Drawable *a, const Drawable *b) {
                return a->getShaderID() < b->getShaderID();
              });
  }
}
