#include "material.h"
#include "pattern.h"
#include "yamlloader.h"
#include <assert.h>
#include <logger.h>
#include <math.h>
#include <memory.h>

MATERIAL_Material *MATERIAL_new(void) {
  MATERIAL_Material *m = malloc(sizeof(MATERIAL_Material));
  TUPLES_init_color(&m->color, 1, 1, 1);
  m->ambient = 0.1;
  m->diffuse = 0.9;
  m->specular = 0.9;
  m->shininess = 200.0;
  m->reflective = 0.0;
  m->transparency = 0.0;
  m->refractive_index = 1.0;
  m->pattern = NULL;
  m->shadow_calc = true;
  return m;
}
void MATERIAL_delete(MATERIAL_Material *m) {
  assert(m);
  TUPLES_destroy(&m->color);
  if (m->pattern) {
    PATTERN_delete(m->pattern);
  }
  free(m);
}

bool MATERIAL_is_equal(const MATERIAL_Material *m1, const MATERIAL_Material *m2) {
  return TUPLES_is_equal(&m1->color, &m2->color) && double_equal(m1->diffuse, m2->diffuse) && double_equal(m1->shininess, m2->shininess) &&
         double_equal(m1->ambient, m2->ambient) && double_equal(m1->specular, m2->specular);
}

MATERIAL_Material *MATERIAL_new_copy(const MATERIAL_Material *src) {
  assert(src);
  MATERIAL_Material *dest = MATERIAL_new();
  *dest = *src;
  if (src->pattern) {
    dest->pattern = PATTERN_new_copy(src->pattern);
  }
  return dest;
}

struct calculate_lighting_context {
  TUPLES_Color sum;
  double intensity;
  const MATERIAL_Material *material;
  const TUPLES_Point *position;
  const TUPLES_Vector *normal_vector;
  const TUPLES_Vector *eye_vector;
  const TUPLES_Color *effective_color;
  const TUPLES_Color *light_color;
};

static void calculate_lighting(TUPLES_Point *light_point, void *context) {
  struct calculate_lighting_context *c = (struct calculate_lighting_context *)context;

  TUPLES_Color diffuse;
  TUPLES_Color specular;

  TUPLES_Vector lightv;
  TUPLES_subtract(&lightv, light_point, c->position);
  TUPLES_normalize(&lightv);

  double light_dot_normal = TUPLES_dot(&lightv, c->normal_vector);
  if (light_dot_normal < 0) {
    // negative means light is on other side of surface
    TUPLES_init_color(&diffuse, 0, 0, 0);
    TUPLES_init_color(&specular, 0, 0, 0);
  } else {
    TUPLES_multiply(&diffuse, c->effective_color, c->material->diffuse * light_dot_normal);

    TUPLES_negate(&lightv);
    TUPLES_Vector reflectv;
    TUPLES_reflect(&reflectv, &lightv, c->normal_vector);
    double reflect_dot_eye = TUPLES_dot(&reflectv, c->eye_vector);

    if (reflect_dot_eye < 0 || double_equal(0.0, reflect_dot_eye)) {
      // light is reflecting away
      TUPLES_init_color(&specular, 0, 0, 0);
    } else {
      double factor = pow(reflect_dot_eye, c->material->shininess);
      TUPLES_multiply(&specular, c->light_color, c->material->specular * factor);
    }
  }

  TUPLES_add(&c->sum, &c->sum, &diffuse);
  TUPLES_add(&c->sum, &c->sum, &specular);
}

void MATERIAL_lighting(TUPLES_Color *dest, const SHAPE_Shape *shape, const LIGHTS_Light *light, const TUPLES_Point *position, const TUPLES_Vector *eye_vector,
                       const TUPLES_Vector *normal_vector, double intensity) {
  assert(dest);
  assert(shape);
  assert(light);
  assert(position);
  assert(eye_vector);
  assert(normal_vector);
  assert(shape->material);

  TUPLES_Color color_or_pattern;
  const MATERIAL_Material *material = shape->material;
  if (material->pattern) {
    PATTERN_color_at_shape(&color_or_pattern, material->pattern, shape, position);
  } else {
    TUPLES_copy(&color_or_pattern, &shape->material->color);
  }

  TUPLES_Color effective_color;
  TUPLES_multiply_colors(&effective_color, &color_or_pattern, &light->intensity);

  /* calculate ambient contribution */
  TUPLES_multiply(dest, &effective_color, material->ambient);

  struct calculate_lighting_context context = (struct calculate_lighting_context){
      .intensity = intensity,
      .material = material,
      .position = position,
      .normal_vector = normal_vector,
      .eye_vector = eye_vector,
      .effective_color = &effective_color,
      .light_color = &light->intensity,
  };
  TUPLES_init_color(&context.sum, 0, 0, 0);

  LIGHTS_iterate_points_on_light(light, &calculate_lighting, &context);

  TUPLES_divide(&context.sum, &context.sum, light->samples);
  TUPLES_multiply(&context.sum, &context.sum, intensity);
  TUPLES_add(dest, dest, &context.sum);
}

void MATERIAL_set_pattern(MATERIAL_Material *material, const PATTERN_Pattern *pattern) {
  assert(material);
  assert(pattern);
  if (material->pattern) {
    PATTERN_delete(material->pattern);
  }
  material->pattern = PATTERN_new_copy(pattern);
}

bool MATERIAL_casts_shadow(const MATERIAL_Material *m) {
  assert(m);
  return m->shadow_calc;
}

void parse_material_map_entry(char *key, char *value, void *context) {
  assert(key);
  assert(value);
  assert(context);
  MATERIAL_Material *mat = (MATERIAL_Material *)context;

  if (strcmp("color", key) == 0) {
    YAMLLOADER_parse_color(value, &mat->color);
  } else if (strcmp("ambient", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->ambient);
  } else if (strcmp("diffuse", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->diffuse);
  } else if (strcmp("specular", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->specular);
  } else if (strcmp("shininess", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->shininess);
  } else if (strcmp("reflective", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->reflective);
  } else if (strcmp("transparency", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->transparency);
  } else if (strcmp("refractive-index", key) == 0) {
    YAMLLOADER_parse_double(value, &mat->refractive_index);
  } else if (strcmp("shadow", key) == 0) {
    YAMLLOADER_parse_bool(value, &mat->shadow_calc);
  } else {
    LOGGER_log(LOGGER_WARN, "Unrecognized map key while parsing material: %s", key);
  }
}

MATERIAL_Material *MATERIAL_parse_material(char *data) {
  assert(data);

  MATERIAL_Material *material = MATERIAL_new();
  //No validation, as there are no required fields...
  YAMLLOADER_parse_map_entries(data, parse_material_map_entry, material);

  return material;
}