#version 130

%DEFINE_REFLECTION%
%DEFINE_SHADOW%
%DEFINE_LAMP%
#define MAXRAYDEPTH %MAXRAYDEPTH%
#define REFL_THRESHOLD %REFL_THRESHOLD%
#define FAR_Z %FAR_Z%

//#pragma optimize(off)

uniform sampler1D vertices;
uniform sampler1D normals;
uniform usampler1D faces;
uniform sampler1D objBoxes;
uniform isampler1D objRanges;
uniform sampler1D materials;
uniform int objCount;
uniform vec3 lightPos;

in vec3 eye;
in vec3 ray;
out vec4 colour;

/*
  Sugarat megadó struktúra.
  A kiinduló sugárhoz is ezt a struktúrát használjuk, de ott csak a kiindulási pont és az irány fontos.
  Metszéspont keresésnél is egy ilyen struktúrát kapunk vissza, ilyenkor azonban találat esetén már minden elem hordoz információt.
  Talált metszéspont esetén a visszatérési struktúrában a pozíció a metszéspont helyét adja vissza.
*/
//---------
struct SRay
{
  int plane;    // A sugarat metsző lap indexe (-1, ha nincs ilyen).
  vec3 pos;     // A sugár kiindulási pontja.
  vec3 ray;     // A sugár iránya.
  float tMax;   // Maximálisan megengedett eltávolodás.
  vec3 normal;  // Az adott pontban vett normálvektor.
  int mat;      // Anyag indexe.
};

//------------------------------------------------------------------------------
vec3 RayTriangle(SRay ray, vec3 a, vec3 ab, vec3 ac)
//------------------------------------------------------------------------------
{
  // Sugár-háromszög metszet.
  /*
    A sugár:
      R(t) = O + t*D
      itt: ray.pos + tuv[0] * ray.ray

    A háromszög:
      T(u, v) = (1 - u - v)*V[0] + u*V[1] + v*V[2]
      itt: (1 - tuv[1] - tuv[2])*A + tuv[1]*B + tuv[2]*C
  */
  vec3 T = ray.pos - a;         // O - V[0]
  vec3 P = cross(ray.ray, ac);  // D × E2
  vec3 Q = cross(T, ab);        // T × E1
  return vec3(dot(Q, ac), dot(P, T), dot(Q, ray.ray)) / dot(P, ab);
}

//------------------------------------------------------------------------------
bool RayBox(SRay ray, vec3 boxMin, vec3 boxMax)
//------------------------------------------------------------------------------
{
  float tMin1, tMax1, tMin2, tMax2;

  if (ray.ray[0] > 0.0)
  {
    tMin1 = (boxMin[0] - ray.pos[0]) / ray.ray[0];
    tMax1 = (boxMax[0] - ray.pos[0]) / ray.ray[0];
  } else
  {
    tMin1 = (boxMax[0] - ray.pos[0]) / ray.ray[0];
    tMax1 = (boxMin[0] - ray.pos[0]) / ray.ray[0];
  }

  if (ray.ray[1] > 0.0)
  {
    tMin2 = (boxMin[1] - ray.pos[1]) / ray.ray[1];
    tMax2 = (boxMax[1] - ray.pos[1]) / ray.ray[1];
  } else
  {
    tMin2 = (boxMax[1] - ray.pos[1]) / ray.ray[1];
    tMax2 = (boxMin[1] - ray.pos[1]) / ray.ray[1];
  }

  tMin1 = max(tMin1, tMin2);
  tMax1 = min(tMax1, tMax2);

  if (tMin1 > tMax1)
    return false;

  if (ray.ray[2] > 0.0)
  {
    tMin2 = (boxMin[2] - ray.pos[2]) / ray.ray[2];
    tMax2 = (boxMax[2] - ray.pos[2]) / ray.ray[2];
  } else
  {
    tMin2 = (boxMax[2] - ray.pos[2]) / ray.ray[2];
    tMax2 = (boxMin[2] - ray.pos[2]) / ray.ray[2];
  }

  tMin1 = max(tMin1, tMin2);
  tMax1 = min(tMax1, tMax2);

  if (tMin1 > tMax1)
    return false;

  return tMax1 >= 0.0;
}

//------------------------------------------------------------------------------
SRay Emit(SRay start)
//------------------------------------------------------------------------------
{
  SRay result;

  // --- Sugárkövetés ---
  uvec3 faceVert;  // A lap három csúcspontjának indexe.
  uvec3 faceNorm;  // A lap három csúcsnormáljának indexe.
  vec3 vert[3];  // A háromszög A, B, C csúcspontja.
  vec3 norm[3];  // A háromszög három normálvektora.
  vec3 edge[2];  // Az A->B és az A->C vektorok.
  bool isSmooth;  // Phong.
  vec3 pos;
  vec3 tuv;
  vec3 flatNorm;
  float lastT;
  int lastID;
  vec3 objMin, objMax;
  ivec3 objRange;

  result.plane = -1;

  // --- Az összes objektummal nézünk befoglaló metszést ---
  for (int j = 0; j < objCount; j++)
  {
    objMin = texelFetch(objBoxes, j << 1, 0).rgb;
    objMax = texelFetch(objBoxes, j << 1 | 1, 0).rgb;

    // Azokat a befoglalókat, amelyek már biztos az előző találat mögött vannak, elhagyjuk.
    if (result.plane > -1)
    {
      if ((start.ray.x > 0.0) && (objMin.x > result.pos.x)) continue;
      if ((start.ray.x < 0.0) && (objMax.x < result.pos.x)) continue;
      if ((start.ray.y > 0.0) && (objMin.y > result.pos.y)) continue;
      if ((start.ray.y < 0.0) && (objMax.y < result.pos.y)) continue;
      if ((start.ray.z > 0.0) && (objMin.z > result.pos.z)) continue;
      if ((start.ray.z < 0.0) && (objMax.z < result.pos.z)) continue;
    }

    if (!RayBox(start, objMin, objMax))
      continue;

    objRange = texelFetch(objRanges, j, 0).rgb;

    // --- Az objektum összes lapjával nézünk metszést ---
    for (int i = objRange[0]; i <= objRange[1]; i++)
    {
      if (i == start.plane)
        continue;

      // Kiválasztjuk a három csúcs helyének és normáljának indexét.
      faceVert = texelFetch(faces, i << 1, 0).rgb;
      faceNorm = texelFetch(faces, i << 1 | 1, 0).rgb;
      isSmooth = (faceNorm[0] != faceNorm[1]) || (faceNorm[1] != faceNorm[2]);

      // Kiválasztjuk a három csúcs koordinátáját.
      vert[0] = texelFetch(vertices, int(faceVert[0]), 0).rgb;
      vert[1] = texelFetch(vertices, int(faceVert[1]), 0).rgb;
      vert[2] = texelFetch(vertices, int(faceVert[2]), 0).rgb;

      // Ugyan ez normálra.
      norm[0] = texelFetch(normals, int(faceNorm[0]), 0).rgb;
      if (isSmooth)
      {
        norm[1] = texelFetch(normals, int(faceNorm[1]), 0).rgb;
        norm[2] = texelFetch(normals, int(faceNorm[2]), 0).rgb;
      }

      // Az A->B és A->C élek.
      edge[0] = vert[1] - vert[0];
      edge[1] = vert[2] - vert[0];

      // A sík normálvektora.
      if (isSmooth)
        flatNorm = normalize(cross(edge[0], edge[1]));
      else
        flatNorm = norm[0];

      // Hátsó lap eldobás.
      if (dot(flatNorm, start.ray) >= 0.0)
        continue;

      // A metszéspont t, u, v paraméterének kiszámolása.
      tuv = RayTriangle(start, vert[0], edge[0], edge[1]);

      // Ha a távolság negatív (a metszéspont az indulási pont mögött van), vagy
      // ha a pont a háromszögön kívűlre esik, vagy
      // ha a pont túlságosan távol van, akkor kihagyjuk.
      // Ha ez az első találat, vagy már volt találatunk, de ez közelebb esik, akkor feljegyezzük.
      if ((tuv[0] > 0.0) && (tuv[1] >= 0.0) && (tuv[2] >= 0.0) && (tuv[1] + tuv[2] <= 1.0) && (tuv[0] <= start.tMax) && ((result.plane == -1) || (tuv[0] < lastT)))
      {
        lastT = tuv[0];
        lastID = j;

        result.plane = i;
        result.pos = start.pos + start.ray * tuv[0];  // Az új pozíció a metszéspont.
        result.ray = start.ray;
        if (isSmooth)
          result.normal = normalize(tuv[1]*norm[1] + tuv[2]*norm[2] + (1.0 - tuv[1] - tuv[2])*norm[0]);
        else
          result.normal = flatNorm;
        result.mat = objRange[2];
      }
    }
  }

  return result;
}

//------------------------------------------------------------------------------
void main(void)
//------------------------------------------------------------------------------
{
  SRay actRay;
  #ifdef SHADOW
  SRay shadowRay;
  #endif
  actRay.pos = eye;
  actRay.ray = normalize(ray);
  actRay.tMax = FAR_Z;
  actRay.plane = -1;

  float refl = 1.0;
  vec3 col = vec3(0.0, 0.0, 0.0);
  vec3 tempCol;
  vec2 material;

  vec3 l;
  #ifndef LAMP
  l = normalize(lightPos);
  #endif

  #ifdef REFLECTION
  for (int i = 1; i <= MAXRAYDEPTH; ++i)
  {
    if (i > 1)
    {
      actRay.ray = reflect(actRay.ray, actRay.normal);
      actRay.tMax = FAR_Z;
    }
  #endif
    actRay = Emit(actRay);

    if (actRay.plane == -1)
    {
      col += refl * vec3(0.3, 0.5, 0.9);
      #ifdef REFLECTION
      break;
      #else
      colour = vec4(col, 1.0);
      return;
      #endif
    }

    #ifdef LAMP
    l = normalize(lightPos - actRay.pos);
    #endif

    float lv = max(dot(l, actRay.normal), 0.0);
    tempCol = texelFetch(materials, actRay.mat * 3, 0).rgb * lv;
    material = texelFetch(materials, actRay.mat * 3 + 2, 0).rg;
    if (lv > 0.0)
    {
      tempCol += texelFetch(materials, actRay.mat * 3 + 1, 0).rgb * max(pow(dot(actRay.normal, normalize(l - actRay.ray)), material[0]), 0.0);

      #ifdef SHADOW
      shadowRay.plane = actRay.plane;
      shadowRay.pos = actRay.pos;
      shadowRay.ray = l;
      #ifdef LAMP
      shadowRay.tMax = (lightPos.x - actRay.pos.x) / shadowRay.ray.x;
      #else
      shadowRay.tMax = FAR_Z;
      #endif
      shadowRay = Emit(shadowRay);
      if (shadowRay.plane > -1)
        tempCol = vec3(0.0, 0.0, 0.0);
      #endif
    }

    #ifdef REFLECTION
    if ((i == MAXRAYDEPTH) || (refl < REFL_THRESHOLD))
    {
      i = MAXRAYDEPTH;
    #endif
      col += refl * tempCol;
  #ifdef REFLECTION
    } else
      col += refl * (1.0 - material[1]) * tempCol;
    refl *= material[1];
  }
  #endif

  colour = vec4(col, 1.0);
}
