#include "point_light.h"

#include <array>
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <iostream>

#include "constants.h"
#include "shader.h"

using namespace std;

PointLight::PointLight(const glm::vec3 &pos) : Renderable(false), pos_(pos) {
  // Much of this code retrieved and modified 2024/03/31 from
  // https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
  // By Joey de Vries (https://twitter.com/JoeyDeVriez)
  // Licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/legalcode)

  glGenTextures(1, &depth_);
  glBindTexture(GL_TEXTURE_CUBE_MAP, depth_);

  for (auto i = 0; i < 6; ++i) {
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT,
                 kShadowMapSize, kShadowMapSize, 0, GL_DEPTH_COMPONENT,
                 GL_FLOAT, nullptr);
  }

  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

  glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

  glGenFramebuffers(1, &fbo_);
  glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
  glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth_, 0);
  glDrawBuffer(GL_NONE);
  glReadBuffer(GL_NONE);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

PointLight::~PointLight() {
  CleanUp();

  glDeleteTextures(1, &depth_);
  glDeleteFramebuffers(1, &fbo_);
}

void PointLight::LoadData(const Shader *shader) const {
  if (!shader->CopyDataToUniform(ambient_, "pointLight.ambient")) {
    cerr << "Point light ambient not loaded to shader" << endl;
  }
  if (!shader->CopyDataToUniform(diffuse_, "pointLight.diffuse")) {
    cerr << "Point light diffuse not loaded to shader" << endl;
  }
  if (!shader->CopyDataToUniform(specularPower_, "pointLight.specularPower")) {
    cerr << "Point light specular power not loaded to shader" << endl;
  }
  if (!shader->CopyDataToUniform(specular_, "pointLight.specular")) {
    cerr << "Point light specular not loaded to shader" << endl;
  }
  if (!shader->CopyDataToUniform(pos_, "pointLight.position")) {
    cerr << "Point light position not loaded to shader" << endl;
  }
}

void PointLight::GenerateCubeMaps(
    const vector<Renderable *> &renderables) const {
  // Much of this code retrieved and modified 2024/03/31 from
  // https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
  // By Joey de Vries (https://twitter.com/JoeyDeVriez)
  // CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/legalcode)
  const auto shadowProj =
      glm::perspective(glm::pi<float>() / 2, 1.0f, kNearPlane, kFarPlane);

  auto shadowTransforms = array<glm::mat4, 6>();
  shadowTransforms[0] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0));
  shadowTransforms[1] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(-1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0));
  shadowTransforms[2] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(0.0, 1.0, 0.0), glm::vec3(0.0, 0.0, 1.0));
  shadowTransforms[3] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(0.0, -1.0, 0.0), glm::vec3(0.0, 0.0, -1.0));
  shadowTransforms[4] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(0.0, 0.0, 1.0), glm::vec3(0.0, -1.0, 0.0));
  shadowTransforms[5] =
      shadowProj *
      lookAt(pos_, pos_ + glm::vec3(0.0, 0.0, -1.0), glm::vec3(0.0, -1.0, 0.0));

  glViewport(0, 0, kShadowMapSize, kShadowMapSize);
  glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
  glClear(GL_DEPTH_BUFFER_BIT);
  glUseProgram(shadow_.id());

  if (!shadow_.CopyDataToUniform(6, shadowTransforms.data(),
                                 "shadowMatrices")) {
    cerr << "Shadow matrix not in shader" << endl;
  }
  if (!shadow_.CopyDataToUniform(pos_, "lightPos")) {
    cerr << "Light position not in shader" << endl;
  }
  if (!shadow_.CopyDataToUniform(kFarPlane, "farPlane")) {
    cerr << "Far plane not in shader" << endl;
  }

  for (const auto renderable : renderables) {
    renderable->Render(&shadow_);
  }

  glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void PointLight::SetData() {
  vertexCount_ = 1;
  vertices_ = new Vertex({pos_, glm::vec3(0, 0, 1)});

  indexCount_ = 1;
  indices_ = new unsigned int(0);
}
