#include "shader.h"
#include "file.h"


Shader::~Shader() {
  clear_shader(this);
}

bool create_shader(Shader *shader, const char *vertex_shader_path, const char *fragment_shader_path) {
  string vertex_shader_code = read_entire_file(vertex_shader_path);
  string fragment_shader_code = read_entire_file(fragment_shader_path);
  

  shader->id = glCreateProgram();

  if (!shader) {
    printf("Error creating shader shader!\n");
    return false;
  }

  {
    bool success = add_shader(shader, vertex_shader_code.c_str(), VERTEX);

    if (!success) {
      printf("Error adding vertex shader");
      return false;
    }
  }

  {
    bool success = add_shader(shader, fragment_shader_code.c_str(), FRAGMENT);

    if (!success) {
      printf("Error adding fragment shader");
      return false;
    }
  }

  s32 result = 0;
  s8 errors[1024] = {0};

  glLinkProgram(shader->id);
  glGetProgramiv(shader->id, GL_LINK_STATUS, &result);

  if (!result) {
    glGetProgramInfoLog(shader->id, sizeof(errors), nullptr, errors);
    printf("Error linking shader: '%s'\n", errors);
    return false;
  }

  glValidateProgram(shader->id);
  glGetProgramiv(shader->id, GL_VALIDATE_STATUS, &result);

  if (!result) {
    glGetProgramInfoLog(shader->id, sizeof(errors), nullptr, errors);
    printf("Error validating shader: '%s'\n", errors);
    return false;
  }

  shader->projection = glGetUniformLocation(shader->id, "projection");
  shader->model = glGetUniformLocation(shader->id, "model");
  shader->view = glGetUniformLocation(shader->id, "view");
  shader->eye_position = glGetUniformLocation(shader->id, "eye_position");

  shader->directional_light.color = glGetUniformLocation(shader->id, "sun.light.color");
  shader->directional_light.ambient_intensity = glGetUniformLocation(shader->id, "sun.light.ambient_intensity");
  shader->directional_light.diffuse_intensity = glGetUniformLocation(shader->id, "sun.light.diffuse_intensity");
  shader->directional_light.direction = glGetUniformLocation(shader->id, "sun.direction");

  shader->shininess = glGetUniformLocation(shader->id, "material.shininess");
  shader->specular_intensity = glGetUniformLocation(shader->id, "material.specular_intensity");

  shader->point_light_count = glGetUniformLocation(shader->id, "point_light_count");
  shader->spot_light_count = glGetUniformLocation(shader->id, "spot_light_count");

  // Point lights
  for (size_t i=0; i<MAX_POINT_LIGHTS; i++) {
    char location_buffer[100] = {"\0"};

    // Common
    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].light.color", i);
    shader->point_lights[i].color = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].light.ambient_intensity", i);
    shader->point_lights[i].ambient_intensity = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].light.diffuse_intensity", i);
    shader->point_lights[i].diffuse_intensity = glGetUniformLocation(shader->id, location_buffer);

    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].position", i);
    shader->point_lights[i].position = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].constant", i);
    shader->point_lights[i].constant = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "point_lights[%zu].constant", i);
    shader->point_lights[i].constant = glGetUniformLocation(shader->id, location_buffer);
  }

  // Spot lights
  for (size_t i=0; i<MAX_SPOT_LIGHTS; i++) {
    char location_buffer[100] = {"\0"};

    // Common
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.light.color", i);
    shader->spot_lights[i].color = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.light.ambient_intensity", i);
    shader->spot_lights[i].ambient_intensity = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.light.diffuse_intensity", i);
    shader->spot_lights[i].diffuse_intensity = glGetUniformLocation(shader->id, location_buffer);

    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.position", i);
    shader->spot_lights[i].position = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.constant", i);
    shader->spot_lights[i].constant = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].point_light.constant", i);
    shader->spot_lights[i].constant = glGetUniformLocation(shader->id, location_buffer);

    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].direction", i);
    shader->spot_lights[i].direction = glGetUniformLocation(shader->id, location_buffer);
    snprintf(location_buffer, sizeof(location_buffer), "spot_lights[%zu].edge", i);
    shader->spot_lights[i].edge = glGetUniformLocation(shader->id, location_buffer);
  }

  return true;
}

bool add_shader(Shader *shader, const char *shader_code, Shader_Type shader_type) {
  u32 _shader = glCreateShader(shader_type);

  const char *code[1];
  code[0] = shader_code;

  int code_length[1];
  code_length[0] = strlen(shader_code);

  glShaderSource(_shader, 1, code, code_length);
  glCompileShader(_shader);

  int result = 0;
  char errors[1024] = {0};

  glGetShaderiv(_shader, GL_COMPILE_STATUS, &result);
  if (!result) {
    glGetShaderInfoLog(_shader, sizeof(errors), nullptr, errors);
    printf("Error compiling the %d shader->id: '%s'\n", shader_type, errors);
    return false;
  }

  glAttachShader(shader->id, _shader);

  return true;
}

void clear_shader(Shader *shader) {
  if (shader->id != 0) {
    glDeleteProgram(shader->id);
    shader->id = 0;
  }

  shader->model = 0;
  shader->projection = 0;
}

void use_light(Shader *shader, Directional_Light *light) {
  glUniform3f(shader->directional_light.color, light->color.x, light->color.y, light->color.z);
  glUniform1f(shader->directional_light.ambient_intensity, light->ambient_intensity);

  glUniform3f(shader->directional_light.direction, light->direction.x, light->direction.y, light->direction.z);
  glUniform1f(shader->directional_light.diffuse_intensity, light->diffuse_intensity);
}

void use_light(Shader *shader, Point_Light *point_lights, u32 point_light_count) {
  if (point_light_count > MAX_POINT_LIGHTS) point_light_count = MAX_POINT_LIGHTS;

  glUniform1i(shader->point_light_count, point_light_count);

  for (size_t i = 0; i < point_light_count; i++) {
    glUniform3f(shader->point_lights[i].color, point_lights[i].color.x, point_lights[i].color.y, point_lights[i].color.z);
    glUniform1f(shader->point_lights[i].ambient_intensity, point_lights[i].ambient_intensity);
    glUniform1f(shader->point_lights[i].diffuse_intensity, point_lights[i].diffuse_intensity);

    glUniform3f(shader->point_lights[i].position, point_lights[i].position.x, point_lights[i].position.y, point_lights[i].position.z);
    glUniform1f(shader->point_lights[i].constant, point_lights[i].constant);
    glUniform1f(shader->point_lights[i].linear, point_lights[i].linear);
    glUniform1f(shader->point_lights[i].exponent, point_lights[i].exponent);
  }
}

void use_light(Shader *shader, Spot_Light *spot_lights, u32 spot_light_count) {
  if (spot_light_count > MAX_SPOT_LIGHTS) spot_light_count = MAX_SPOT_LIGHTS;

  glUniform1i(shader->spot_light_count, spot_light_count);

  for (size_t i = 0; i < spot_light_count; i++) {
    glUniform3f(shader->spot_lights[i].color, spot_lights[i].color.x, spot_lights[i].color.y, spot_lights[i].color.z);
    glUniform1f(shader->spot_lights[i].ambient_intensity, spot_lights[i].ambient_intensity);
    glUniform1f(shader->spot_lights[i].diffuse_intensity, spot_lights[i].diffuse_intensity);

    glUniform3f(shader->spot_lights[i].position, spot_lights[i].position.x, spot_lights[i].position.y, spot_lights[i].position.z);
    glUniform1f(shader->spot_lights[i].constant, spot_lights[i].constant);
    glUniform1f(shader->spot_lights[i].linear, spot_lights[i].linear);
    glUniform1f(shader->spot_lights[i].exponent, spot_lights[i].exponent);

    glUniform3f(shader->spot_lights[i].direction, spot_lights[i].direction.x, spot_lights[i].direction.y, spot_lights[i].direction.z);
    glUniform1f(shader->spot_lights[i].edge, spot_lights[i].processed_edge);
  }
}

