#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/graphics/render_texture.h>
#include <glad/glad.h>
#include <glm/gtc/type_ptr.hpp>

namespace frostbite2D {

namespace {

constexpr float kMinContentScale = 0.01f;

int roundToInt(float value) {
  return static_cast<int>(std::lround(value));
}

RenderStyleLayerSettings resolveRenderStyleLayer(RenderStyleProfileId profile,
                                                 RenderStyleLayerRole role) {
  return RenderStyleSettings::FromProfile(profile).layer(role);
}

} // namespace

Renderer& Renderer::get() {
  static Renderer instance;
  return instance;
}

Renderer::Renderer() {
  activeRenderStyle_ =
      resolveRenderStyleLayer(defaultRenderStyleProfile_, activeRenderStyleRole_);
}

bool Renderer::init() {
  if (initialized_) {
    return true;
  }

  if (!batch_.init()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "Failed to initialize batch system");
    return false;
  }

  if (!shaderManager_.init("assets/shaders")) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "Failed to initialize shader manager");
    return false;
  }

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

  SDL_Log("Renderer initialized");
  initialized_ = true;
  recalculateResolutionState();
  return true;
}

void Renderer::shutdown() {
  renderTargetStack_.clear();
  batch_.shutdown();
  shaderManager_.shutdown();
  initialized_ = false;
  frameActive_ = false;
  cullingEnabled_ = true;
}

void Renderer::beginFrame() {
  applyWorldViewport();
  glClearColor(clearColor_[0] / 255.0f, clearColor_[1] / 255.0f,
               clearColor_[2] / 255.0f, clearColor_[3] / 255.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  updateUniforms();
  batch_.begin();
  frameActive_ = true;
}

void Renderer::endFrame() {
  while (!renderTargetStack_.empty()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Renderer: endFrame() auto-closing unfinished render target scope");
    endRenderToTexture();
  }
  batch_.end();
  frameActive_ = false;
}

void Renderer::flush() {
  batch_.flush();
}

bool Renderer::beginRenderToTexture(RenderTexture& target, Camera* camera,
                                    const Color& clearColor, bool clear) {
  if (!initialized_) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Renderer: beginRenderToTexture called before renderer init");
    return false;
  }
  if (!target.IsValid()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Renderer: beginRenderToTexture called with invalid target");
    return false;
  }

  Camera* activeCamera = camera ? camera : camera_;
  if (!activeCamera) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Renderer: beginRenderToTexture requires an active camera");
    return false;
  }

  RenderTargetState state;
  GLint framebufferBinding = 0;
  GLint viewport[4] = {};
  GLfloat clearColorValue[4] = {};
  glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferBinding);
  glGetIntegerv(GL_VIEWPORT, viewport);
  glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColorValue);
  state.framebuffer = static_cast<uint32>(framebufferBinding);
  state.viewportX = viewport[0];
  state.viewportY = viewport[1];
  state.viewportWidth = std::max(viewport[2], 1);
  state.viewportHeight = std::max(viewport[3], 1);
  state.clearColor[0] = clearColorValue[0];
  state.clearColor[1] = clearColorValue[1];
  state.clearColor[2] = clearColorValue[2];
  state.clearColor[3] = clearColorValue[3];
  state.camera = camera_;
  state.cullingEnabled = cullingEnabled_;

  if (frameActive_ || !renderTargetStack_.empty()) {
    batch_.flush();
  } else {
    batch_.begin();
    state.startedStandaloneBatch = true;
  }

  renderTargetStack_.push_back(state);

  glBindFramebuffer(GL_FRAMEBUFFER, target.framebufferID_);
  glViewport(0, 0, std::max(target.width_, 1), std::max(target.height_, 1));
  if (clear) {
    glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
    glClear(GL_COLOR_BUFFER_BIT);
  }

  camera_ = activeCamera;
  cullingEnabled_ = false;
  updateUniforms();
  return true;
}

bool Renderer::endRenderToTexture() {
  if (renderTargetStack_.empty()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Renderer: endRenderToTexture called without matching begin");
    return false;
  }

  RenderTargetState state = renderTargetStack_.back();
  renderTargetStack_.pop_back();

  if (state.startedStandaloneBatch) {
    batch_.end();
  } else {
    batch_.flush();
  }

  glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer);
  glViewport(state.viewportX, state.viewportY, state.viewportWidth,
             state.viewportHeight);
  glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
               state.clearColor[3]);

  camera_ = state.camera;
  cullingEnabled_ = state.cullingEnabled;
  updateUniforms();
  return true;
}

void Renderer::setViewport(int x, int y, int width, int height) {
  resolutionState_.viewportX = x;
  resolutionState_.viewportY = y;
  resolutionState_.viewportWidth = std::max(width, 1);
  resolutionState_.viewportHeight = std::max(height, 1);

  float safeScaleX = std::max(resolutionState_.contentScaleX, kMinContentScale);
  float safeScaleY = std::max(resolutionState_.contentScaleY, kMinContentScale);

  resolutionState_.windowViewportX = roundToInt(
      static_cast<float>(resolutionState_.viewportX) / safeScaleX);
  resolutionState_.windowViewportY = roundToInt(
      static_cast<float>(resolutionState_.viewportY) / safeScaleY);
  resolutionState_.windowViewportWidth = roundToInt(
      static_cast<float>(resolutionState_.viewportWidth) / safeScaleX);
  resolutionState_.windowViewportHeight = roundToInt(
      static_cast<float>(resolutionState_.viewportHeight) / safeScaleY);

  int logicalWidth = std::max(getVirtualWidth(), 1);
  int logicalHeight = std::max(getVirtualHeight(), 1);
  resolutionState_.scaleX =
      static_cast<float>(resolutionState_.windowViewportWidth) /
      static_cast<float>(logicalWidth);
  resolutionState_.scaleY =
      static_cast<float>(resolutionState_.windowViewportHeight) /
      static_cast<float>(logicalHeight);

  if (!useSeparateUIVirtualResolution_) {
    uiResolutionState_ = resolutionState_;
  }

  syncCameraViewport();
}

void Renderer::setWindowSize(int width, int height, float contentScaleX,
                             float contentScaleY) {
  resolutionState_.windowWidth = std::max(width, 1);
  resolutionState_.windowHeight = std::max(height, 1);
  resolutionState_.contentScaleX = std::max(contentScaleX, kMinContentScale);
  resolutionState_.contentScaleY = std::max(contentScaleY, kMinContentScale);
  resolutionState_.drawableWidth = std::max(
      roundToInt(static_cast<float>(resolutionState_.windowWidth) *
                 resolutionState_.contentScaleX),
      1);
  resolutionState_.drawableHeight = std::max(
      roundToInt(static_cast<float>(resolutionState_.windowHeight) *
                 resolutionState_.contentScaleY),
      1);
  recalculateResolutionState();
}

void Renderer::setVirtualResolutionEnabled(bool enabled) {
  useVirtualResolution_ = enabled;
  recalculateResolutionState();
}

void Renderer::setVirtualResolution(int width, int height) {
  virtualWidth_ = std::max(width, 1);
  virtualHeight_ = std::max(height, 1);
  recalculateResolutionState();
}

void Renderer::setResolutionScaleMode(ResolutionScaleMode mode) {
  resolutionMode_ = mode;
  recalculateResolutionState();
}

void Renderer::setSeparateUIVirtualResolutionEnabled(bool enabled) {
  useSeparateUIVirtualResolution_ = enabled;
  recalculateResolutionState();
}

void Renderer::setUIVirtualResolution(int width, int height) {
  uiVirtualWidth_ = width;
  uiVirtualHeight_ = height;
  recalculateResolutionState();
}

void Renderer::setUIResolutionScaleMode(ResolutionScaleMode mode) {
  uiResolutionMode_ = mode;
  recalculateResolutionState();
}

void Renderer::setClearColor(float r, float g, float b, float a) {
  clearColor_[0] = static_cast<uint8>(r * 255);
  clearColor_[1] = static_cast<uint8>(g * 255);
  clearColor_[2] = static_cast<uint8>(b * 255);
  clearColor_[3] = static_cast<uint8>(a * 255);
}

void Renderer::setClearColor(const Color& color) {
  clearColor_[0] = color.r;
  clearColor_[1] = color.g;
  clearColor_[2] = color.b;
  clearColor_[3] = color.a;
}

void Renderer::clear(uint32_t flags) {
  glClear(flags);
}

void Renderer::setDefaultRenderStyleProfile(RenderStyleProfileId profile) {
  defaultRenderStyleProfile_ = profile;
  setActiveRenderStyleProfile(profile, activeRenderStyleRole_);
}

void Renderer::setActiveRenderStyleProfile(RenderStyleProfileId profile,
                                           RenderStyleLayerRole role) {
  if (initialized_ &&
      (activeRenderStyleProfile_ != profile || activeRenderStyleRole_ != role)) {
    batch_.flush();
  }

  activeRenderStyleProfile_ = profile;
  activeRenderStyleRole_ = role;
  activeRenderStyle_ = resolveRenderStyleLayer(profile, role);
}

void Renderer::applyRenderStyleToCamera(Camera* camera,
                                        RenderStyleProfileId profile,
                                        RenderStyleLayerRole role) const {
  if (!camera) {
    return;
  }

  RenderStyleLayerSettings settings = resolveRenderStyleLayer(profile, role);
  camera->setPixelSnapEnabled(settings.cameraPixelSnap);
}

void Renderer::setCamera(Camera* camera) {
  if (initialized_ && camera_ != camera) {
    batch_.flush();
  }

  camera_ = camera;
  syncCameraViewport();

  if (initialized_) {
    updateUniforms();
  }
}

int Renderer::getVirtualWidth() const {
  if (useVirtualResolution_) {
    return std::max(virtualWidth_, 1);
  }
  return std::max(resolutionState_.windowWidth, 1);
}

int Renderer::getVirtualHeight() const {
  if (useVirtualResolution_) {
    return std::max(virtualHeight_, 1);
  }
  return std::max(resolutionState_.windowHeight, 1);
}

int Renderer::getUIVirtualWidth() const {
  if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
      uiVirtualHeight_ > 0) {
    return std::max(uiVirtualWidth_, 1);
  }
  return getVirtualWidth();
}

int Renderer::getUIVirtualHeight() const {
  if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
      uiVirtualHeight_ > 0) {
    return std::max(uiVirtualHeight_, 1);
  }
  return getVirtualHeight();
}

Rect Renderer::getViewportRect() const {
  return resolutionState_.getWindowViewportRect();
}

Rect Renderer::getUIViewportRect() const {
  return uiResolutionState_.getWindowViewportRect();
}

void Renderer::applyWorldViewport() const {
  glViewport(resolutionState_.viewportX, resolutionState_.viewportY,
             resolutionState_.viewportWidth, resolutionState_.viewportHeight);
}

void Renderer::applyUIViewport() const {
  glViewport(uiResolutionState_.viewportX, uiResolutionState_.viewportY,
             uiResolutionState_.viewportWidth, uiResolutionState_.viewportHeight);
}

Vec2 Renderer::screenToVirtual(const Vec2& screenPos) const {
  Rect viewport = getViewportRect();
  if (viewport.width() <= 0.0f || viewport.height() <= 0.0f) {
    return screenPos;
  }

  float localX = screenPos.x - viewport.left();
  float localY = screenPos.y - viewport.top();
  return Vec2(localX * static_cast<float>(getVirtualWidth()) / viewport.width(),
              localY * static_cast<float>(getVirtualHeight()) /
                  viewport.height());
}

Vec2 Renderer::screenToUIVirtual(const Vec2& screenPos) const {
  Rect viewport = getUIViewportRect();
  if (viewport.width() <= 0.0f || viewport.height() <= 0.0f) {
    return screenPos;
  }

  float localX = screenPos.x - viewport.left();
  float localY = screenPos.y - viewport.top();
  return Vec2(localX * static_cast<float>(getUIVirtualWidth()) / viewport.width(),
              localY * static_cast<float>(getUIVirtualHeight()) /
                  viewport.height());
}

Vec2 Renderer::virtualToScreen(const Vec2& virtualPos) const {
  Rect viewport = getViewportRect();
  float logicalWidth = static_cast<float>(std::max(getVirtualWidth(), 1));
  float logicalHeight = static_cast<float>(std::max(getVirtualHeight(), 1));

  return Vec2(viewport.left() + virtualPos.x * viewport.width() / logicalWidth,
              viewport.top() + virtualPos.y * viewport.height() /
                                   logicalHeight);
}

void Renderer::recalculateSingleResolutionState(RenderResolutionState& state,
                                                bool useVirtualResolution,
                                                int virtualWidth,
                                                int virtualHeight,
                                                ResolutionScaleMode mode) const {
  state.windowWidth = std::max(state.windowWidth, 1);
  state.windowHeight = std::max(state.windowHeight, 1);
  state.contentScaleX = std::max(state.contentScaleX, kMinContentScale);
  state.contentScaleY = std::max(state.contentScaleY, kMinContentScale);
  state.drawableWidth = std::max(
      roundToInt(static_cast<float>(state.windowWidth) * state.contentScaleX), 1);
  state.drawableHeight = std::max(
      roundToInt(static_cast<float>(state.windowHeight) * state.contentScaleY), 1);

  int windowViewportX = 0;
  int windowViewportY = 0;
  int windowViewportWidth = state.windowWidth;
  int windowViewportHeight = state.windowHeight;

  int safeVirtualWidth = std::max(virtualWidth, 1);
  int safeVirtualHeight = std::max(virtualHeight, 1);

  if (useVirtualResolution) {
    float widthScale = static_cast<float>(state.windowWidth) /
                       static_cast<float>(safeVirtualWidth);
    float heightScale = static_cast<float>(state.windowHeight) /
                        static_cast<float>(safeVirtualHeight);

    switch (mode) {
      case ResolutionScaleMode::Disabled:
        state.scaleX = widthScale;
        state.scaleY = heightScale;
        break;
      case ResolutionScaleMode::FitHeight: {
        float uniformScale = heightScale;
        float fittedWidth = static_cast<float>(safeVirtualWidth) * uniformScale;
        if (fittedWidth > static_cast<float>(state.windowWidth)) {
          uniformScale = std::min(widthScale, heightScale);
        }

        windowViewportWidth = std::clamp(
            roundToInt(static_cast<float>(safeVirtualWidth) * uniformScale), 1,
            state.windowWidth);
        windowViewportHeight = std::clamp(
            roundToInt(static_cast<float>(safeVirtualHeight) * uniformScale), 1,
            state.windowHeight);
        windowViewportX = (state.windowWidth - windowViewportWidth) / 2;
        windowViewportY = (state.windowHeight - windowViewportHeight) / 2;
        state.scaleX = static_cast<float>(windowViewportWidth) /
                       static_cast<float>(safeVirtualWidth);
        state.scaleY = static_cast<float>(windowViewportHeight) /
                       static_cast<float>(safeVirtualHeight);
        break;
      }
      case ResolutionScaleMode::Fit:
      default: {
        float uniformScale = std::min(widthScale, heightScale);
        windowViewportWidth = std::clamp(
            roundToInt(static_cast<float>(safeVirtualWidth) * uniformScale), 1,
            state.windowWidth);
        windowViewportHeight = std::clamp(
            roundToInt(static_cast<float>(safeVirtualHeight) * uniformScale), 1,
            state.windowHeight);
        windowViewportX = (state.windowWidth - windowViewportWidth) / 2;
        windowViewportY = (state.windowHeight - windowViewportHeight) / 2;
        state.scaleX = static_cast<float>(windowViewportWidth) /
                       static_cast<float>(safeVirtualWidth);
        state.scaleY = static_cast<float>(windowViewportHeight) /
                       static_cast<float>(safeVirtualHeight);
        break;
      }
    }
  } else {
    state.scaleX = 1.0f;
    state.scaleY = 1.0f;
  }

  state.windowViewportX = windowViewportX;
  state.windowViewportY = windowViewportY;
  state.windowViewportWidth = windowViewportWidth;
  state.windowViewportHeight = windowViewportHeight;
  state.viewportX =
      roundToInt(static_cast<float>(windowViewportX) * state.contentScaleX);
  state.viewportY =
      roundToInt(static_cast<float>(windowViewportY) * state.contentScaleY);
  state.viewportWidth = std::max(
      roundToInt(static_cast<float>(windowViewportWidth) * state.contentScaleX),
      1);
  state.viewportHeight = std::max(
      roundToInt(static_cast<float>(windowViewportHeight) * state.contentScaleY),
      1);
}

void Renderer::recalculateResolutionState() {
  resolutionState_.windowWidth = std::max(resolutionState_.windowWidth, 1);
  resolutionState_.windowHeight = std::max(resolutionState_.windowHeight, 1);
  resolutionState_.contentScaleX =
      std::max(resolutionState_.contentScaleX, kMinContentScale);
  resolutionState_.contentScaleY =
      std::max(resolutionState_.contentScaleY, kMinContentScale);

  recalculateSingleResolutionState(resolutionState_, useVirtualResolution_,
                                   virtualWidth_, virtualHeight_,
                                   resolutionMode_);

  uiResolutionState_ = resolutionState_;
  if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
      uiVirtualHeight_ > 0) {
    recalculateSingleResolutionState(uiResolutionState_, true, uiVirtualWidth_,
                                     uiVirtualHeight_, uiResolutionMode_);
  }

  syncCameraViewport();
  if (initialized_) {
    updateUniforms();
  }
}

void Renderer::syncCameraViewport() {
  if (!camera_) {
    return;
  }

  if (activeRenderStyleRole_ == RenderStyleLayerRole::UI) {
    camera_->setViewport(getUIVirtualWidth(), getUIVirtualHeight());
    return;
  }

  camera_->setViewport(getVirtualWidth(), getVirtualHeight());
}

void Renderer::setupBlendMode(BlendMode mode) {
  switch (mode) {
    case BlendMode::None:
      glDisable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      break;
    case BlendMode::Normal:
      glEnable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                          GL_ONE_MINUS_SRC_ALPHA);
      break;
    case BlendMode::Additive:
      glEnable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
      break;
    case BlendMode::Screen:
      glEnable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE,
                          GL_ONE_MINUS_SRC_ALPHA);
      break;
    case BlendMode::Premultiplied:
      glEnable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                          GL_ONE_MINUS_SRC_ALPHA);
      break;
    case BlendMode::Subtractive:
      glEnable(GL_BLEND);
      glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                          GL_ONE_MINUS_SRC_ALPHA);
      glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
      break;
    case BlendMode::Multiply:
      glEnable(GL_BLEND);
      glBlendEquation(GL_FUNC_ADD);
      glBlendFuncSeparate(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
                          GL_ONE_MINUS_SRC_ALPHA);
      break;
  }
}

void Renderer::updateUniforms() {
  if (!camera_) {
    return;
  }

  for (const auto& [name, shaderPtr] : shaderManager_.getLoadedShaders()) {
    (void)name;
    Shader* shader = shaderPtr.Get();
    if (!shader) {
      continue;
    }

    shader->use();
    shader->setMat4("u_view", camera_->getViewMatrix());
    shader->setMat4("u_projection", camera_->getProjectionMatrix());
  }
}

void Renderer::drawQuad(const Vec2& pos, const Size& size, float cr, float cg,
                        float cb, float ca) {
  Rect rect(pos.x, pos.y, size.width, size.height);
  Quad quad = Quad::create(rect, cr, cg, cb, ca);
  Transform2D transform = Transform2D::identity();

  auto* shader = shaderManager_.getShader("colored_quad");
  if (shader) {
    shader->use();
  }

  batch_.submitQuad(quad, transform, nullptr, shader, BlendMode::Normal);
}

void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture) {
  Rect rect(pos.x, pos.y, size.width, size.height);
  Quad quad = Quad::createTextured(rect, Rect(0, 0, size.width, size.height),
                                   Vec2(size.width, size.height),
                                   1.0f, 1.0f, 1.0f, 1.0f,
                                   shouldShrinkSubTextureUVs());
  Transform2D transform = Transform2D::identity();

  auto* shader = shaderManager_.getShader("sprite");
  if (shader) {
    shader->use();
  }

  batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal);
}

void Renderer::drawQuad(const Rect& rect, const Color& color) {
  drawQuad(Vec2(rect.left(), rect.top()), Size(rect.width(), rect.height()),
           color.r, color.g, color.b, color.a);
}

void Renderer::drawSprite(const Vec2& pos, const Size& size, Ptr<Texture> texture) {
  drawQuad(pos, size, texture);
}

void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect,
                          const Vec2& texSize, Ptr<Texture> texture,
                          const Color& color) {
  Rect destRect(pos.x, pos.y, srcRect.width(), srcRect.height());
  Quad quad = Quad::createTextured(destRect, srcRect, texSize, color.r, color.g,
                                   color.b, color.a,
                                   shouldShrinkSubTextureUVs());
  Transform2D transform = Transform2D::identity();

  auto* shader = shaderManager_.getShader("sprite");
  if (shader) {
    shader->use();
  }

  batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal);
}

} // namespace frostbite2D
