/** \defgroup Rendering Rendering classes */

/** @addtogroup Rendering
    @{
*/

/** @file CRenderObj.cpp
    \brief This file contains the implementation of the CRenderObj class.

    It is more like a structure since all its members are public; in
    fact, it is its purpose: holding data concerning the image that
    will be rendered.
*/

#include <cmath>
#include <sstream>
#include <string>
#include <vector>

#include "popassert.h"
#include "CColor.h"
#include "CVector3f.h"
#include "CVectBase.h"
#include "compute.h"
#include "CMathExpression.h"
#include "CRenderObj.h"




/**
   It sets all the settings to their default value.
*/
CRenderObj::CRenderObj()
  : m_FormulaString("z=10*x*y*exp(-x^2-y^2)"),
    m_MinVector(-2, -2, -2), m_MaxVector(2, 2, 2),
    m_ViewerPos(4, 0, 4),
    m_LightDirection(-1, -2, -3),
    m_SurfaceAmbientColor(0.15f, 0.15f, 0.3f),
    m_SurfaceDiffuseColor(0.35f, 0.35f, 0.7f),
    m_SurfaceSpecularColor(0.15f, 0.15f, 0.0f),
    m_BoxAmbientColor(0.15f, 0.15f, 0.15f),
    m_BoxDiffuseColor(0.35f, 0.35f, 0.35f),
    m_BoxSpecularColor(0, 0, 0),
    m_BackgroundColor(0.2f, 0.2f, 0.2f),
    m_CenterPos(0, 0, 0)
{
  m_SignIncrement = 0.01f;
  m_NoSignIncrement = 0.001f;
  m_NoSignPrecision = 0.02f;
  m_Zoom = 0.0f;

  m_SurfaceSpecularExponent = 1.0f;
  m_BoxSpecularExponent = 1.0f;

  m_Options = OPTION_PERSPECTIVE | OPTION_BOUNDINGBOX | OPTION_SIGN;
  m_Width = 300;
  m_Height = 300;

  m_pFormula   = NULL;
  m_pFormulaDx = NULL;
  m_pFormulaDy = NULL;
  m_pFormulaDz = NULL;
}



/**
   This function is used in the copy constructor and in the
   assignment operator.
*/
void CRenderObj::Copy(const CRenderObj& obj)
{
  m_FormulaString           = obj.m_FormulaString;
  m_MinVector               = obj.m_MinVector;
  m_MaxVector               = obj.m_MaxVector;
  m_ViewerPos               = obj.m_ViewerPos;
  m_LightDirection          = obj.m_LightDirection;
  m_SurfaceAmbientColor     = obj.m_SurfaceAmbientColor;
  m_SurfaceDiffuseColor     = obj.m_SurfaceDiffuseColor;
  m_SurfaceSpecularColor    = obj.m_SurfaceSpecularColor;
  m_SurfaceSpecularExponent = obj.m_SurfaceSpecularExponent;
  m_BoxAmbientColor         = obj.m_BoxAmbientColor;
  m_BoxDiffuseColor         = obj.m_BoxDiffuseColor;
  m_BoxSpecularColor        = obj.m_BoxSpecularColor;
  m_BoxSpecularExponent     = obj.m_BoxSpecularExponent;
  m_BackgroundColor         = obj.m_BackgroundColor;
  m_CenterPos               = obj.m_CenterPos;
  m_SignIncrement           = obj.m_SignIncrement;
  m_NoSignIncrement         = obj.m_NoSignIncrement;
  m_NoSignPrecision         = obj.m_NoSignPrecision;
  m_Zoom                    = obj.m_Zoom;
  m_Options                 = obj.m_Options;
  m_Width                   = obj.m_Width;
  m_Height                  = obj.m_Height;

  Update();
}


/**
   It builds the expression tree for the formula itself and for its
   derivatives according to x, y and z.
*/
void CRenderObj::Update(void)
{
  m_CenterPos = (m_MaxVector + m_MinVector) / 2;
  m_VectorBase.setRadius( (m_MaxVector - m_MinVector).Norm() / 2);
  m_VectorBase.setViewerPos(m_ViewerPos, false);
  m_VectorBase.setBoxPos(m_CenterPos);

  // Equation of the surface
  if (m_pFormula)
    {
      delete m_pFormula;
      m_pFormula = NULL;
    }

  if (CMathExpression::Validate(m_FormulaString))
    m_pFormula = new CMathExpression(m_FormulaString.c_str());


  // Derivative according to x
  if (m_pFormulaDx)
    {
      delete m_pFormulaDx;
      m_pFormulaDx = 0;
    }

  if (m_pFormula)
    m_pFormulaDx = m_pFormula->GetDerivative(CValue::TYPE_X);
      

  // Derivative according to y
  if (m_pFormulaDy)
    {
      delete m_pFormulaDy;
      m_pFormulaDy = 0;
    }

  if (m_pFormula)
    m_pFormulaDy = m_pFormula->GetDerivative(CValue::TYPE_Y);


  // Derivative according to z
  if (m_pFormulaDz)
    {
      delete m_pFormulaDz;
      m_pFormulaDz = 0;
    }

  if (m_pFormula)
    m_pFormulaDz = m_pFormula->GetDerivative(CValue::TYPE_Z);
}



/**
   It returns a string containing the error message to display, or ""
   if all parameters are valid.
*/
std::string CRenderObj::Validate() const
{
  // Check equation
  if (!CMathExpression::Validate(m_FormulaString))
    return ("The equation of the formula is not valid.");


  // Check floating point values
  if (m_SignIncrement <= 0)
    return ("Invalid SignIncrement");
  if (m_NoSignIncrement <= 0)
    return ("Invalid NoSignIncrement");
  if (m_NoSignPrecision <= 0)
    return ("Invalid NoSignPrecision");
  if (m_SurfaceSpecularExponent <= 0)
    return ("Invalid SurfaceSpecularExponent");
  if (m_BoxSpecularExponent <= 0)
    return ("Invalid BoxSpecularExponent");


  // Check for bounding box range
  if (m_MinVector.x >= m_MaxVector.x)
    return ("Error in the X range of the viewing volume.");
  if (m_MinVector.y >= m_MaxVector.y)
    return ("Error in the Y range of the viewing volume.");
  if (m_MinVector.z >= m_MaxVector.z)
    return ("Error in the Z range of the viewing volume.");


  // Check surface color
  // Ambient color
  if (m_SurfaceAmbientColor[0] < 0 || m_SurfaceAmbientColor[0] > 1)
    return ("The red component of the surface ambient "
            "color must be between 0 and 1");
  if (m_SurfaceAmbientColor[1] < 0 || m_SurfaceAmbientColor[1] > 1)
    return ("The green component of the surface ambient "
            "color must be between 0 and 1");
  if (m_SurfaceAmbientColor[2] < 0 || m_SurfaceAmbientColor[2] > 1)
    return ("The blue component of the surface ambient "
            "color must be between 0 and 1");
  if (m_SurfaceAmbientColor[3] < 0 || m_SurfaceAmbientColor[3] > 1)
    return ("The alpha component of the surface ambient "
            "color must be between 0 and 1");

  // Diffuse color
  if (m_SurfaceDiffuseColor[0] < 0 || m_SurfaceDiffuseColor[0] > 1)
    return ("The red component of the surface diffuse "
            "color must be between 0 and 1");
  if (m_SurfaceDiffuseColor[1] < 0 || m_SurfaceDiffuseColor[1] > 1)
    return ("The green component of the surface diffuse "
            "color must be between 0 and 1");
  if (m_SurfaceDiffuseColor[2] < 0 || m_SurfaceDiffuseColor[2] > 1)
    return ("The blue component of the surface diffuse "
            "color must be between 0 and 1");
  if (m_SurfaceDiffuseColor[3] < 0 || m_SurfaceDiffuseColor[3] > 1)
    return ("The alpha component of the surface diffuse "
            "color must be between 0 and 1");

  // Specular color
  if (m_SurfaceSpecularColor[0] < 0 || m_SurfaceSpecularColor[0] > 1)
    return ("The red component of the surface Specular "
            "color must be between 0 and 1");
  if (m_SurfaceSpecularColor[1] < 0 || m_SurfaceSpecularColor[1] > 1)
    return ("The green component of the surface Specular "
            "color must be between 0 and 1");
  if (m_SurfaceSpecularColor[2] < 0 || m_SurfaceSpecularColor[2] > 1)
    return ("The blue component of the surface Specular "
            "color must be between 0 and 1");
  if (m_SurfaceSpecularColor[3] < 0 || m_SurfaceSpecularColor[3] > 1)
    return ("The alpha component of the surface Specular "
            "color must be between 0 and 1");


  // Check box color
  // Ambient color
  if (m_BoxAmbientColor[0] < 0 || m_BoxAmbientColor[0] > 1)
    return ("The red component of the box ambient "
            "color must be between 0 and 1");
  if (m_BoxAmbientColor[1] < 0 || m_BoxAmbientColor[1] > 1)
    return ("The green component of the box ambient "
            "color must be between 0 and 1");
  if (m_BoxAmbientColor[2] < 0 || m_BoxAmbientColor[2] > 1)
    return ("The blue component of the box ambient "
            "color must be between 0 and 1");
  if (m_BoxAmbientColor[3] < 0 || m_BoxAmbientColor[3] > 1)
    return ("The alpha component of the box ambient "
            "color must be between 0 and 1");

  // Diffuse color
  if (m_BoxDiffuseColor[0] < 0 || m_BoxDiffuseColor[0] > 1)
    return ("The red component of the box diffuse "
            "color must be between 0 and 1");
  if (m_BoxDiffuseColor[1] < 0 || m_BoxDiffuseColor[1] > 1)
    return ("The green component of the box diffuse "
            "color must be between 0 and 1");
  if (m_BoxDiffuseColor[2] < 0 || m_BoxDiffuseColor[2] > 1)
    return ("The blue component of the box diffuse "
            "color must be between 0 and 1");
  if (m_BoxDiffuseColor[3] < 0 || m_BoxDiffuseColor[3] > 1)
    return ("The alpha component of the box diffuse "
            "color must be between 0 and 1");

  // Specular color
  if (m_BoxSpecularColor[0] < 0 || m_BoxSpecularColor[0] > 1)
    return ("The red component of the box specular "
            "color must be between 0 and 1");
  if (m_BoxSpecularColor[1] < 0 || m_BoxSpecularColor[1] > 1)
    return ("The green component of the box specular "
            "color must be between 0 and 1");
  if (m_BoxSpecularColor[2] < 0 || m_BoxSpecularColor[2] > 1)
    return ("The blue component of the box specular "
            "color must be between 0 and 1");
  if (m_BoxSpecularColor[3] < 0 || m_BoxSpecularColor[3] > 1)
    return ("The alpha component of the box specular "
            "color must be between 0 and 1");


  // Check background color
  if (m_BackgroundColor[0] < 0 || m_BackgroundColor[0] > 1)
    return ("The red component of the background "
            "color must be between 0 and 1");
  if (m_BackgroundColor[1] < 0 || m_BackgroundColor[1] > 1)
    return ("The green component of the background "
            "color must be between 0 and 1");
  if (m_BackgroundColor[2] < 0 || m_BackgroundColor[2] > 1)
    return ("The blue component of the background "
            "color must be between 0 and 1");
  if (m_BackgroundColor[3] < 0 || m_BackgroundColor[3] > 1)
    return ("The alpha component of the background "
            "color must be between 0 and 1");


  // Check for output image dimensions
  if (m_Width < 1)
    return ("Invalid witdh.");
  if (m_Height < 1)
    return ("Invalid height.");

  return ("");
}



/**
   It calculates (if perspective is on) the vector going from the
   viewer to the (x,y) point on a plane described by the second and
   third vector of the base, which are perpendicular to the first
   vector.  This plane contains the center of the bounding box and is
   a square that contains the sphere that contains the bounding box.
   If there is no perspective, the direction is simply the first
   vector of the base.
*/
CVector3f CRenderObj::GetDirection(int x, int y) const
{
  if (m_Options & OPTION_PERSPECTIVE)
    {
      return (m_VectorBase.getB1()+
              2*m_VectorBase.getRadius()*
              (m_VectorBase.getB2()*(((float) x / (float) m_Width) - 0.5f) +
               m_VectorBase.getB3()*(0.5f - ((float) y / (float) m_Height))
               ) );
    }
  else
    {
      return m_VectorBase.getB1();
    }
}


/**
   It calculates (if perspective is off) the position of the starting
   point of the ray, which is the position in the picture that is
   taken.  If there is perspective, then the starting point is always
   the same: the position of the viewer, but it can be put back a
   little bit to see all the surface, according to the zoom.  If the
   zoom is set to 0, the viewer will be left it its position; if the
   zoom is set to 1, the viewer will be placed so that all the surface
   is seen.
*/
CVector3f CRenderObj::GetStartingPoint(int x, int y) const
{
  if (m_Options & OPTION_PERSPECTIVE)
    {
      return(m_ViewerPos - m_VectorBase.getB1() *
             (m_VectorBase.getRadius() / m_VectorBase.getB1().Norm() 
              * m_Zoom));
    }
  else
    {
      return (m_ViewerPos +
              2*m_VectorBase.getRadius()*
              (m_VectorBase.getB2()*(((float) x / (float) m_Width) - 0.5f) +
               m_VectorBase.getB3()*(0.5f - ((float) y / (float) m_Height))
               ) );

    }
}



/**
   It does (a maximum of) three iterations of finding the closest plan
   to the direction, until it gets on the bounding box.
*/
void CRenderObj::GetCloseToBoundingBox(CVector3f& StartingPoint,
                                       const CVector3f& Direction) const
{
  // Spacing when the ray is placed near the cube
  float Spacing = 2 *( (m_Options & OPTION_SIGN) ? 
                       m_SignIncrement : m_NoSignIncrement);

  // The ray is placed near the bounding box.  The nearest planed is
  // calculated three times and the ray is put at the nearest plane
  // (or doesn't move if it is already a face of the bounding box).
  if ( (StartingPoint.x > m_MaxVector.x) ||
       (StartingPoint.x < m_MinVector.x) ||
       (StartingPoint.y > m_MaxVector.y) ||
       (StartingPoint.y < m_MinVector.y) ||
       (StartingPoint.z > m_MaxVector.z) ||
       (StartingPoint.z < m_MinVector.z) )
    {
      for (int i = 0; i < 3; ++i)
        {
          float TempDistance;
          float StartingDistance = (m_CenterPos - StartingPoint).Norm();

          // plane: x = m_MinVector.x
          TempDistance = (m_MinVector.x - StartingPoint.x) / Direction.x;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          // plane: x = m_MaxVector.x
          TempDistance = (m_MaxVector.x - StartingPoint.x) / Direction.x;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          // plane: y = m_MinVector.y
          TempDistance = (m_MinVector.y - StartingPoint.y) / Direction.y;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          // plane: y = m_MaxVector.y
          TempDistance = (m_MaxVector.y - StartingPoint.y) / Direction.y;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          // plane: z = m_MinVector.z
          TempDistance = (m_MinVector.z - StartingPoint.z) / Direction.z;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          // plane: z = m_MaxVector.z
          TempDistance = (m_MaxVector.z - StartingPoint.z) / Direction.z;
          if ((TempDistance > 0) && (TempDistance < StartingDistance))
            StartingDistance = TempDistance;

          StartingPoint += StartingDistance * Direction;

          // If the end of the cube is reach, stop
          if ((StartingPoint.x < (m_MaxVector.x + Spacing)) &&
              (StartingPoint.x > (m_MinVector.x - Spacing)) &&
              (StartingPoint.y < (m_MaxVector.y + Spacing)) &&
              (StartingPoint.y > (m_MinVector.y - Spacing)) &&
              (StartingPoint.z < (m_MaxVector.z + Spacing)) &&
              (StartingPoint.z > (m_MinVector.z - Spacing)))
            {
              break;
            }
        }
    }
}



/**
   It uses raytracing to get the color.
*/
CColor CRenderObj::GetPixelColor(int x, int y) const
{
  POP_ASSERT(m_pFormula != NULL, "Using a NULL pointer.");
  int StartingSign = 0;
  CVector3f Direction = GetDirection(x, y);
  Direction.Normalize();
  CVector3f StartingPoint = GetStartingPoint(x, y);

  bool Over = !(InBoundingBox(StartingPoint, Direction));
  bool Hit = false;

  // Step of the direction vector
  float VectInc;

  if (!Over)
    {
      GetCloseToBoundingBox(StartingPoint, Direction);
      Over = !(InBoundingBox(StartingPoint, Direction));

      if (m_Options & OPTION_SIGN)
        {
          if (m_pFormula->GetValue(StartingPoint) > 0)
            StartingSign = 1;
          else
            StartingSign = -1;

          VectInc = m_SignIncrement;
        }
      else
        {
          VectInc = m_NoSignIncrement;
        }
    }

  while (!Over)
    {
      // Check if the point is on the surface
      if (m_Options & OPTION_SIGN)
        Hit = (StartingSign * m_pFormula->GetValue(StartingPoint) < 0);
      else
        Hit = (fabs(m_pFormula->GetValue(StartingPoint)) < m_NoSignPrecision);

      if (Hit)
        {
          // If the point is on the surface, but no in the bounding
          // box, don't stop, but switch StartingSign.
          if ( !( (StartingPoint.x > m_MinVector.x) &&
                  (StartingPoint.x < m_MaxVector.x) &&
                  (StartingPoint.y > m_MinVector.y) &&
                  (StartingPoint.y < m_MaxVector.y) &&
                  (StartingPoint.z > m_MinVector.z) &&
                  (StartingPoint.z < m_MaxVector.z) ) )
            {
              StartingSign *= -1;
              Hit = false;
            }
          else
            {
              CVector3f Normal(m_pFormulaDx->GetValue(StartingPoint),
                               m_pFormulaDy->GetValue(StartingPoint),
                               m_pFormulaDz->GetValue(StartingPoint));
              if (Normal * Direction > 0)
                Normal *= -1;
              Normal.Normalize();

              // Ambient color
              CColor ReturnColor = m_SurfaceAmbientColor;

              // Diffuse color
              float DiffuseFactor = - (m_LightDirection * Normal);
              if (DiffuseFactor > 0)
                {
                  ReturnColor +=
                    m_SurfaceDiffuseColor * ( DiffuseFactor / 
                                              ( m_LightDirection.Norm()) ) ;
                }

              // Specular color
              if (m_Options & OPTION_OPENGLSPECULAR)
                {
                  if (m_LightDirection * Normal < 0)
                    {
                      CVector3f NewVect =
                        - m_LightDirection / m_LightDirection.Norm() -
                        Direction / Direction.Norm();
                      NewVect.Normalize();

                      float Fact = NewVect * Normal / Normal.Norm();

                      if (Fact > 0)
                        {
                          ReturnColor +=
                            m_SurfaceSpecularColor * 
                            pow(Fact, m_SurfaceSpecularExponent);
                        }
                    }
                }
              else
                {
                  CVector3f ReflectedRay =
                    2*(m_LightDirection * Normal) * Normal - m_LightDirection;

                  float SpecularFactor = ReflectedRay * Direction;
                  if (SpecularFactor > 0)
                    {
                      ReturnColor +=
                        m_SurfaceSpecularColor * pow( SpecularFactor /
                                                      ( ReflectedRay.Norm() *
                                                        Direction.Norm() ),
                                                      m_SurfaceSpecularExponent);
                    }
                }
              return (ReturnColor);
            }
        }

      // Check to see if the point is close to a plane AND that it
      // is located between the minimum and maximum value of the
      // two other dimensions.
      if (m_Options & OPTION_BOUNDINGBOX)
        {
          CVector3f PlaneNormal;
          bool PlaneHit = false;

          if ( (fabs(StartingPoint.x - m_MinVector.x) < VectInc) &&
               (Direction.x < 0) &&
               (StartingPoint.y > m_MinVector.y) &&
               (StartingPoint.y < m_MaxVector.y) &&
               (StartingPoint.z > m_MinVector.z) &&
               (StartingPoint.z < m_MaxVector.z) )
            {
              PlaneNormal = CVector3f(-1, 0, 0);
              PlaneHit = true;
            }
          else if ( (fabs(StartingPoint.x - m_MaxVector.x) < VectInc) &&
                    (Direction.x > 0) &&
                    (StartingPoint.y > m_MinVector.y) &&
                    (StartingPoint.y < m_MaxVector.y) &&
                    (StartingPoint.z > m_MinVector.z) &&
                    (StartingPoint.z < m_MaxVector.z) )
            {
              PlaneNormal = CVector3f(1, 0, 0);
              PlaneHit = true;
            }
          else if ( (fabs(StartingPoint.y - m_MinVector.y) < VectInc) &&
                    (Direction.y < 0) &&
                    (StartingPoint.x > m_MinVector.x) &&
                    (StartingPoint.x < m_MaxVector.x) &&
                    (StartingPoint.z > m_MinVector.z) &&
                    (StartingPoint.z < m_MaxVector.z) )
            {
              PlaneNormal = CVector3f(0, -1, 0);
              PlaneHit = true;
            }
          else if ( (fabs(StartingPoint.y - m_MaxVector.y) < VectInc) &&
                    (Direction.y > 0) &&
                    (StartingPoint.x > m_MinVector.x) &&
                    (StartingPoint.x < m_MaxVector.x) &&
                    (StartingPoint.z > m_MinVector.z) &&
                    (StartingPoint.z < m_MaxVector.z) )
            {
              PlaneNormal = CVector3f(0, 1, 0);
              PlaneHit = true;
            }
          else if ( (fabs(StartingPoint.z - m_MinVector.z) < VectInc) &&
                    (Direction.z < 0) &&
                    (StartingPoint.x > m_MinVector.x) &&
                    (StartingPoint.x < m_MaxVector.x) &&
                    (StartingPoint.y > m_MinVector.y) &&
                    (StartingPoint.y < m_MaxVector.y) )
            {
              PlaneNormal = CVector3f(0, 0, -1);
              PlaneHit = true;
            }
          else if ( (fabs(StartingPoint.z - m_MaxVector.z) < VectInc) &&
                    (Direction.z > 0) &&
                    (StartingPoint.x > m_MinVector.x) &&
                    (StartingPoint.x < m_MaxVector.x) &&
                    (StartingPoint.y > m_MinVector.y) &&
                    (StartingPoint.y < m_MaxVector.y) )
            {
              PlaneNormal = CVector3f(0, 0, 1);
              PlaneHit = true;
            }

          if (PlaneHit)
            {
              // Ambient color
              CColor ReturnColor = m_BoxAmbientColor;

              // Diffuse color
              float PlaneDiffuseFactor = m_LightDirection * PlaneNormal;
              if (PlaneDiffuseFactor > 0)
                {
                  ReturnColor +=
                    m_BoxDiffuseColor * ( PlaneDiffuseFactor /
                                          ( m_LightDirection.Norm()) ) ;
                }

              // Specular color
              if (m_Options & OPTION_OPENGLSPECULAR)
                {
                  if (m_LightDirection * PlaneNormal < 0)
                    {
                      CVector3f NewVect =
                        - m_LightDirection / m_LightDirection.Norm() -
                        Direction / Direction.Norm();
                      NewVect.Normalize();

                      float Fact = NewVect * PlaneNormal / PlaneNormal.Norm();

                      if (Fact > 0)
                        {
                          ReturnColor +=
                            m_BoxSpecularColor *
                            pow(Fact, m_BoxSpecularExponent);
                        }
                    }
                }
              else
                {
                  CVector3f ReflectedRay =
                    2*(m_LightDirection * PlaneNormal) * PlaneNormal -
                    m_LightDirection;

                  float SpecularFactor = ReflectedRay * Direction;
                  if (SpecularFactor > 0)
                    {
                      ReturnColor +=
                        m_BoxSpecularColor * pow( SpecularFactor /
                                                  ( ReflectedRay.Norm() *
                                                    Direction.Norm() ),
                                                  m_BoxSpecularExponent);
                    }
                }

              return (ReturnColor);
            }
        }  // End of checking for the drawing of the bounding box.

      StartingPoint += Direction * VectInc;

      Over = !(InBoundingBox(StartingPoint, Direction));
    }

  return (m_BackgroundColor);
}

/** @} */
