#include "shader.h"

#include <fstream>
#include <iostream>
#include <string>

#include "renderer.h"

using namespace std;

Shader::Shader(const string &vertexShader, const string &fragmentShader,
               const string &geometryShader) {
  const auto vertexShaderId = CompileShader(vertexShader, GL_VERTEX_SHADER);
  const auto fragmentShaderId =
      CompileShader(fragmentShader, GL_FRAGMENT_SHADER);
  GLuint geometryShaderId{0};

  id_ = glCreateProgram();

  glAttachShader(id_, vertexShaderId);
  CheckGLError("Error in attaching the vertex shader");
  glAttachShader(id_, fragmentShaderId);
  CheckGLError("Error in attaching the fragment shader");

  if (!geometryShader.empty()) {
    geometryShaderId = CompileShader(geometryShader, GL_GEOMETRY_SHADER);
    glAttachShader(id_, geometryShaderId);
    CheckGLError("Error in attaching the geometry shader");
  }

  glLinkProgram(id_);
  CheckProgramivError(id_, GL_LINK_STATUS,
                      "Error when creating shader program");

  glDeleteShader(vertexShaderId);
  glDeleteShader(fragmentShaderId);
  if (geometryShaderId != 0) {
    glDeleteShader(geometryShaderId);
  }

  glUseProgram(id_);
  PrintStatus();
}

Shader::~Shader() { glDeleteProgram(id_); }

bool Shader::CopyDataToUniform(const glm::mat4 &data,
                               const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }

  glUniformMatrix4fv(location, 1, GL_FALSE, &data[0][0]);
  return true;
}

bool Shader::CopyDataToUniform(const int count, const glm::mat4 *data,
                               const std::string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }

  glUniformMatrix4fv(location, count, GL_FALSE, &(*data)[0][0]);
  return true;
}

bool Shader::CopyDataToUniform(const glm::vec4 &data,
                               const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }
  glUniform4fv(location, 1, &data[0]);
  return true;
}

bool Shader::CopyDataToUniform(const glm::vec3 &data,
                               const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }
  glUniform3fv(location, 1, &data[0]);
  return true;
}

bool Shader::CopyDataToUniform(const float data, const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }
  glUniform1fv(location, 1, &data);
  return true;
}

bool Shader::CopyDataToUniform(const int data, const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }
  glUniform1iv(location, 1, &data);
  return true;
}

bool Shader::CopyDataToUniform(const bool data, const string &name) const {
  const auto location = glGetUniformLocation(id_, name.c_str());
  if (location == -1) {
    return false;
  }
  const int actualData = data;
  glUniform1iv(location, 1, &actualData);
  return true;
}

GLuint Shader::CompileShader(const std::string &shaderSource,
                             const GLenum shaderType) {
  const GLuint id = glCreateShader(shaderType);
  const auto shaderC_str = shaderSource.c_str();
  glShaderSource(id, 1, &shaderC_str, nullptr);
  glCompileShader(id);
  CheckShaderivError(id, GL_COMPILE_STATUS,
                     "Error when compiling shader file:\n" + shaderSource);
  return id;
}

void Shader::PrintStatus() const {
  GLint rc;
  GLsizei length;

  glValidateProgram(id_);
  glGetProgramiv(id_, GL_VALIDATE_STATUS, &rc);
  if (rc == GL_TRUE) {
    cout << "Shader is valid" << endl;
  } else {
    cerr << "Shader is not valid, status: " << rc << endl;
  }

  glGetProgramiv(id_, GL_INFO_LOG_LENGTH, &rc);
  if (rc == 0) {
    cout << "No shader info log";
  } else {
    auto *log = new GLchar[rc];
    glGetProgramInfoLog(id_, rc, &length, log);
    cout << "Shader info log: " << log;
    delete[] log;
  }
  cout << endl;

  glGetProgramiv(id_, GL_ATTACHED_SHADERS, &rc);
  cout << "Number of attached shaders: " << rc << "\n";

  glGetProgramiv(id_, GL_ACTIVE_ATTRIBUTES, &rc);
  cout << "Number of active attributes: " << rc << "\n";

  GLsizei bufSize;
  glGetProgramiv(id_, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &bufSize);
  GLint size;
  GLenum type;
  const auto attributeName = new GLchar[bufSize];
  for (int i = 0; i < rc; ++i) {
    glGetActiveAttrib(id_, i, bufSize, &length, &size, &type, attributeName);
    cout << "\t" << attributeName << ": " << type << "\n";
  }
  delete[] attributeName;
  cout << flush;

  glGetProgramiv(id_, GL_ACTIVE_UNIFORMS, &rc);
  cout << "Number of active uniforms: " << rc << "\n";

  glGetProgramiv(id_, GL_ACTIVE_UNIFORM_MAX_LENGTH, &bufSize);
  auto *uniformName = new GLchar[bufSize];
  for (int i = 0; i < rc; ++i) {
    glGetActiveUniform(id_, i, bufSize, &length, &size, &type, uniformName);
    cout << "\t" << uniformName << ": " << type << "\n";
  }
  delete[] uniformName;
  cout << flush;
  Renderer::CheckGLError();
}

void Shader::CheckGLError(const string &errorMessage) {
  const auto rc = glGetError();
  if (rc != GL_NO_ERROR) {
    cerr << errorMessage << endl;
    throw runtime_error(reinterpret_cast<const char *>(gluErrorString(rc)));
  }
}

void Shader::CheckProgramivError(const GLuint program, const GLenum pname,
                                 const string &errorMessage) {
  GLint rc;
  glGetProgramiv(program, pname, &rc);
  if (rc != GL_TRUE) {
    cerr << errorMessage << endl;
    GLsizei length, bufSize;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufSize);
    const auto error = new GLchar[bufSize];
    glGetProgramInfoLog(program, bufSize, &length, error);
    throw runtime_error(error);
  }
}

void Shader::CheckShaderivError(const GLuint shader, const GLenum pname,
                                const string &errorMessage) {
  GLint rc;
  glGetShaderiv(shader, pname, &rc);
  if (rc != GL_TRUE) {
    cerr << errorMessage << endl;
    GLsizei length, bufSize;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &bufSize);
    const auto error = new GLchar[bufSize];
    glGetShaderInfoLog(shader, bufSize, &length, error);
    throw runtime_error(error);
  }
}
