#ifndef RENDERER_H
#define RENDERER_H

#include "linmath.h"
#include "rendertarget.h"

typedef struct {
  vec3 *verts;
  int *index;
  vec3 *normals;
  vec3 *color;
  int vert_size;
  int index_size;
  float shininess;
} model;

typedef struct {
  vec4 *points[3];
  float dot00;
  float dot01;
  float dot11;
  vec2 v0;
  vec2 v1;
  vec3 norm;
  float baryFactor;
  float bounding[4]; // min x, max x, min y, max y

  vec3 barycentrics;
} polygon;

// model static inline create_model(vec3 *verts, int *index, int vert_size,
//                                  int index_size) {
//   model m = {verts, 0, 0, index, vert_size, index_size};
//   // printf("%f\n", verts[0][0]);
//   vec3_print(verts[0]);
//   return m;
// };
//
const vec3 lightDir = {0.707, 0.707, 0.0f};

void initPoly(polygon *p) {
  vec4 **points = p->points;
  float *bounding = p->bounding;

  // vec2_sub(p->v0, *points[2], *points[0]);
  // vec2_sub(p->v1, *points[1], *points[0]);

  vec3 v0 = {};
  vec3 v1 = {};
  vec3_sub(v0, *points[2], *points[0]);
  vec3_sub(v1, *points[1], *points[0]);

  vec3_mul_cross(p->norm, v0, v1);

  vec2_dup(p->v0, v0);
  vec2_dup(p->v1, v1);

  p->dot00 = vec2_dot(p->v0, p->v0);
  p->dot01 = vec2_dot(p->v0, p->v1);
  p->dot11 = vec2_dot(p->v1, p->v1);

  p->baryFactor = 1.0f / (p->dot00 * p->dot11 - p->dot01 * p->dot01);

  bounding[0] = (*points[0])[0];
  bounding[1] = (*points[0])[0];
  bounding[2] = (*points[0])[1];
  bounding[3] = (*points[0])[1];

  for (int i = 1; i < 3; i++) {
    if (bounding[0] > (*points[i])[0])
      bounding[0] = (*points[i])[0];
    if (bounding[1] < (*points[i])[0])
      bounding[1] = (*points[i])[0];
    if (bounding[2] > (*points[i])[1])
      bounding[2] = (*points[i])[1];
    if (bounding[3] < (*points[i])[1])
      bounding[3] = (*points[i])[1];
  }
}

void calcBarycentrics(polygon *p, vec2 s, vec3 r) {
  vec2 v2 = {};
  vec2_sub(v2, s, *p->points[0]);

  float dot02 = vec2_dot(p->v0, v2);
  float dot12 = vec2_dot(p->v1, v2);

  r[2] = p->dot11 * dot02 - p->dot01 * dot12;
  r[1] = p->dot00 * dot12 - p->dot01 * dot02;
  vec3_scale(r, r, p->baryFactor);
  r[0] = 1.f - r[1] - r[2];
}

float fApplyBary(const vec3 bary, float a, float b, float c) {
  return bary[0] * a + bary[1] * b + bary[2] * c;
}

void vApplyBary(const vec3 bary, const vec3 a, const vec3 b, const vec3 c,
                vec3 dst) {
  vec3 tmp = {};
  dst[0] = 0;
  dst[1] = 0;
  dst[2] = 0;
  vec3_scale(tmp, a, bary[0]);
  vec3_add(dst, dst, tmp);
  vec3_scale(tmp, b, bary[1]);
  vec3_add(dst, dst, tmp);
  vec3_scale(tmp, c, bary[2]);
  vec3_add(dst, dst, tmp);
}

void render(render_target *target, const model *model, mat4x4 *matrix,
            vec4 *buffer) {

  vec4 tmp = {};

  for (int i = 0; i < model->vert_size; i++) {
    tmp[0] = model->verts[i][0];
    tmp[1] = model->verts[i][1];
    tmp[2] = model->verts[i][2];
    tmp[3] = 1.0f;

    mat4x4_mul_vec4(buffer[i], *matrix, tmp);

    vec4_scale(buffer[i], buffer[i], 1.0 / buffer[i][3]);
    // vec4_print(buffer[i]);
  }

  polygon p = {};

  vec3 *normals[3] = {};
  vec3 *colors[3] = {};

  for (int i = 0; i < model->index_size; i += 6) {
    p.points[0] = buffer + model->index[i];
    p.points[1] = buffer + model->index[i + 2];
    p.points[2] = buffer + model->index[i + 4];

    initPoly(&p);

    if (p.norm[2] > 0)
      continue;

    normals[0] = model->normals + model->index[i + 1];
    normals[1] = model->normals + model->index[i + 3];
    normals[2] = model->normals + model->index[i + 5];

    colors[0] = model->color + model->index[i];
    colors[1] = model->color + model->index[i + 2];
    colors[2] = model->color + model->index[i + 4];

    float texWidth = 2.0f / target->width;
    float texHight = 2.0f / target->width;

    // float startX = MAX(-1.0f, p.bounding[0]);
    // float startY = MAX(-1.0f, p.bounding[2]);
    // float endX = MIN(1.0f, p.bounding[1]);
    // float endY = MIN(1.0f, p.bounding[3]);

    float startX = (MAX(-1.0f, p.bounding[0]) + 1.0f) * 0.5f * target->width;
    float endY = (-MAX(-1.0f, p.bounding[2]) + 1.0f) * 0.5f * target->height;
    float endX = (MIN(1.0f, p.bounding[1]) + 1.0f) * 0.5f * target->width;
    float startY = (-MIN(1.0f, p.bounding[3]) + 1.0f) * 0.5f * target->height;

    vec2 sp = {};
    vec3 bary = {};
    vec4 val = {0};
    vec3 nor = {};
    vec3 color = {};

    vec3 viewDir = {};
    vec3 halfWay = {};

    for (int y = startY; y < endY; y += 1) {
      for (int x = startX; x < endX; x += 1) {
        // for (float y = startY; y < endY; y += texHight) {
        //   for (float x = startX; x < endX; x += texWidth) {
        float fX = ((float)x) * texWidth - 1.0f;
        float fY = 1.0f - ((float)y) * texHight;
        sp[0] = fX;
        sp[1] = fY;
        calcBarycentrics(&p, sp, bary);
        if (bary[0] >= 0.0f && bary[1] >= 0.0f && bary[2] >= 0.0f) {
          uint8_t depth = (uint8_t)(255.0 * fApplyBary(bary, (*p.points[0])[2],
                                                       (*p.points[1])[2],
                                                       (*p.points[2])[2]));
          // if (depth < getDepth(target, x, y)) {
          vApplyBary(bary, (*normals[0]), (*normals[1]), (*normals[2]), nor);
          vApplyBary(bary, (*colors[0]), (*colors[1]), (*colors[2]), color);

          viewDir[0] = fX;
          viewDir[1] = -fY;
          viewDir[2] = 2.144663;

          vec3_norm(viewDir, viewDir);
          vec3_add(halfWay, viewDir, lightDir);
          vec3_norm(halfWay, halfWay);

          vec3_dup(val, color);
          float diffuse = MAX(vec3_dot(nor, lightDir), 0.0f);
          // float l = powf(MAX(vec3_dot(halfWay, nor), 0.0), 2.0);
          float spec = powf(MAX(vec3_dot(halfWay, nor), 0.0), model->shininess);
          spec *= (model->shininess + 8.0) / 25.1327;
          // val[0] = nor[0] * 0.5 + 0.5;
          // val[1] = nor[1] * 0.5 + 0.5;
          // val[2] = nor[2] * 0.5 + 0.5;
          // val[0] = ((float)depth) / 255.0f;
          // val[1] = ((float)depth) / 255.0f;
          // val[2] = ((float)depth) / 255.0f;
          vec3_scale(val, val, diffuse * 0.5 + spec + 0.5);
          // vec3_set(val, l);
          val[3] = ((float)depth) / 255.0f;

          set(target, x, y, val);
          // }
        }
      }
    }
  }
}

#endif
