/////////////////////////////////////////////////////////////////////////
//                                                                     //
// This file contains the implementation of the CRay class.            //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
#include "popassert.hpp"
#include "CConcreteSurface.hpp"
#include "CLight.hpp"
#include "CRay.hpp"
#include "CScene.hpp"



/////////////////////////////////////////////////////////////////////////
//                                                                     //
//  Class    : CRay                                                    //
//                                                                     //
//  Function : colorize()                                              //
//                                                                     //
//  Comments : This function returns the color corresponding to this   //
//             ray, according to the scene passed in parameter.        //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
CColor CRay::colorize(const CScene& scene, real_t *pT /* = NULL */) const
{
  CColor                  hitColor(0,0,0);
  CRay                    LightRay;
  CSurface::hitcontainer  Hits;
  const CConcreteSurface *pSurface = NULL;
  real_t                  t        = infinity();

  for (CScene::surface_container::const_iterator it
         = scene.m_vSurfaces.begin();
       it != scene.m_vSurfaces.end();
       ++it)
    {
      const CConcreteSurface *pTempSurface;
      (*it)->Intersect(*this, Hits, &pTempSurface);

      for (CSurface::hitcontainer::const_iterator it2 = Hits.begin();
           it2 != Hits.end();
           ++it2)
        {
          if (*it2 > scene.m_PropSmallReal && *it2 < t)
            {
              pSurface = pTempSurface;
              t        = (*it2);
              break;
            }
        }
    }

  if (pSurface == NULL)
    {
      hitColor = scene.m_PropBackColor;
    }
  else
    {
      CVector hitNormal;
      CVector hitPoint = m_Origin + (t * m_Direction);
      pSurface->Normal(hitPoint, hitNormal);

      // If the normal is not directed towards the observer (which can
      // occur in with boolean constructive geometry, which surface
      // subtraction), reverse it
      if (hitNormal * m_Direction > 0)
        hitNormal *= -1;

      hitColor = pSurface->m_PropSurfaceColor * pSurface->m_PropAmbientCoef;

      // Light
      real_t fGouraudFactor = 0, fPhongFactor = 0;

      for (CScene::light_container::const_iterator it2
             = scene.m_vLights.begin();
           it2 != scene.m_vLights.end();
           ++it2)
        {
          LightRay.m_Direction = (*it2)->m_PropPosition - hitPoint;

          // No shadow if normal and light are at an angle > 180
          if (LightRay.m_Direction * hitNormal > 0)
            {
              LightRay.m_Origin = hitPoint;

              // Representing the color of a light as seen from the
              // contact point
              CColor tmpColor = (*it2)->m_PropLightColor;

              for (CScene::surface_container::const_iterator it3
                     = scene.m_vSurfaces.begin();
                   it3 != scene.m_vSurfaces.end();
                   ++it3)
                {
                  const CConcreteSurface *pShadowHit;
                  (*it3)->Intersect(LightRay, Hits, &pShadowHit);

                  for (CSurface::hitcontainer::const_iterator it2 =
                         Hits.begin();
                       it2 != Hits.end();
                       ++it2)
                    {
                      if ((*it2) > scene.m_PropSmallReal)
                        {
                          if ((*it2) < 1)
                            {
                              tmpColor *=
                                (// The color is influenced by the
                                 // concrete surface
                                 pShadowHit->m_PropSurfaceColor *
                                 pShadowHit->m_PropRefractionCoef);
                            }
                          else
                            {
                              break;
                            }
                        }
                    }
                }

              // Gouraud shading
              fGouraudFactor =
                (*it2)->m_PropIntensity  * pSurface->m_PropDiffuseCoef *
                (hitNormal*LightRay.m_Direction)/LightRay.m_Direction.norm();

              hitColor +=
                fGouraudFactor * pSurface->m_PropSurfaceColor * tmpColor;

              // Phong shading
              // Reflected ray:

              // Since LightRay is oriented from the hit point towards
              // the light, we get:
              // Reflected = 2*(projection on normal) - LightRay

              // When the vector on which another is projected had
              // unitary lenght:
              CVector Projection = (LightRay.m_Direction*hitNormal)*hitNormal;
              CVector ReflectedRay =
                MAKE_REAL(2.0)*Projection -
                LightRay.m_Direction;

              real_t CosBetweenRays =
                - (ReflectedRay * m_Direction) /
                sqrt((ReflectedRay*ReflectedRay)*(m_Direction*m_Direction));

              if (CosBetweenRays < 0)
                {
                  fPhongFactor = 0;
                }
              else
                {
                  fPhongFactor =
                    (*it2)->m_PropIntensity * pSurface->m_PropSpecularCoef *
                    pow( CosBetweenRays, pSurface->m_PropShininessCoef );
                }

              hitColor += fPhongFactor * tmpColor;
            }
        }


      // Reflection
      if ( (pSurface->m_PropReflexionCoef * m_Energy > scene.m_PropMinEnergy) &&
           (m_Level < scene.m_PropMaxLevel) )
        {
          // Since the ray is going towards the scene object, the
          // reflected ray is:

          // Reflected = 2*(Projection on normal) + incident
          CVector Projection = (m_Direction * hitNormal) * hitNormal;

          CRay reflectedRay;
          reflectedRay.m_Direction = m_Direction - MAKE_REAL(2.0)*Projection;
          reflectedRay.m_Origin    = hitPoint;
          reflectedRay.m_Energy    = pSurface->m_PropReflexionCoef * m_Energy;
          reflectedRay.m_RefrIndex = m_RefrIndex;
          reflectedRay.m_Level     = m_Level + 1;

          hitColor +=
            pSurface->m_PropReflexionCoef *
            reflectedRay.colorize(scene);
        }


      // Refraction
      if ( (pSurface->m_PropRefractionCoef * m_Energy > scene.m_PropMinEnergy) &&
           (m_Level < scene.m_PropMaxLevel) )
        {
          real_t   refrIndex;
          CRay     refractedRay;

          if (m_RefrIndex == pSurface->m_PropRefractionIndex)
            {
              // Inside going out
              refractedRay.m_RefrIndex = scene.m_PropGenRefrIndex;
              refrIndex = pSurface->m_PropRefractionIndex;
            }
          else
            {
              // Outside going in
              refractedRay.m_RefrIndex = pSurface->m_PropRefractionIndex;
              refrIndex                = 1 / pSurface->m_PropRefractionIndex;
            }

          // Cosine of the incidence angle
          real_t incidentCosine = -(m_Direction*hitNormal)/m_Direction.norm();

          // Square of the cosine of refraction angle
          real_t squareRefrCosine =
            1 - refrIndex*refrIndex*(1-incidentCosine*incidentCosine);

          // If the square is negative, total internal reflection
          if (squareRefrCosine < 0)
            {
              CVector Projection       = (m_Direction*hitNormal)*hitNormal;
              refractedRay.m_Direction = m_Direction-MAKE_REAL(2.0)*Projection;
            }
          else
            {
              refractedRay.m_Direction =
                refrIndex * m_Direction / m_Direction.norm() -
                ((real_t)sqrt(squareRefrCosine) - refrIndex*incidentCosine)*hitNormal;
            }
          

          refractedRay.m_Origin    = hitPoint;
          refractedRay.m_Energy    = pSurface->m_PropRefractionCoef * m_Energy;
          refractedRay.m_Level     = m_Level + 1;

          hitColor +=
            pSurface->m_PropRefractionCoef *
            refractedRay.colorize(scene);
        }
    }

  if (pT != NULL)
    {
      *pT = (t - scene.m_PropNearClipPlane) /
        (scene.m_PropFarClipPlane - scene.m_PropNearClipPlane);
    }

  hitColor.clamp();
  return hitColor;
}
