// Created on: 2015-01-16
// Created by: Anastasia BORISOVA
// Copyright (c) 2015 OPEN CASCADE SAS
//
// This file is part of Open CASCADE Technology software library.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License version 2.1 as published
// by the Free Software Foundation, with special exception defined in the file
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
// distribution for complete text of the license and disclaimer of any warranty.
//
// Alternatively, this file may be used under the terms of Open CASCADE
// commercial license or contractual agreement.

#include <OpenGl_BackgroundArray.hxx>

#include <Aspect_FillMethod.hxx>
#include <OpenGl_Texture.hxx>
#include <OpenGl_View.hxx>

#ifndef M_SQRT2
  #define M_SQRT2 1.41421356237309504880168872420969808
#endif

//=================================================================================================

OpenGl_BackgroundArray::OpenGl_BackgroundArray(const Graphic3d_TypeOfBackground theType)
    : OpenGl_PrimitiveArray(nullptr, Graphic3d_TOPA_TRIANGLES, nullptr, nullptr, nullptr),
      myType(theType),
      myFillMethod(Aspect_FM_NONE),
      myViewWidth(0),
      myViewHeight(0),
      myToUpdate(false)
{
  myDrawMode   = GL_TRIANGLES;
  myIsFillType = true;

  myGradientParams.color1 = NCollection_Vec4<float>(0.0f, 0.0f, 0.0f, 1.0f);
  myGradientParams.color2 = NCollection_Vec4<float>(0.0f, 0.0f, 0.0f, 1.0f);
  myGradientParams.type   = Aspect_GradientFillMethod_None;
}

//=================================================================================================

void OpenGl_BackgroundArray::SetTextureParameters(const Aspect_FillMethod theFillMethod)
{
  if (myType != Graphic3d_TOB_TEXTURE)
  {
    return;
  }

  myFillMethod = theFillMethod;
  invalidateData();
}

//=================================================================================================

void OpenGl_BackgroundArray::SetTextureFillMethod(const Aspect_FillMethod theFillMethod)
{
  myFillMethod = theFillMethod;
  invalidateData();
}

//=================================================================================================

void OpenGl_BackgroundArray::SetGradientParameters(const Quantity_Color&           theColor1,
                                                   const Quantity_Color&           theColor2,
                                                   const Aspect_GradientFillMethod theType)
{
  if (myType != Graphic3d_TOB_GRADIENT)
  {
    return;
  }

  double anR, aG, aB;
  theColor1.Values(anR, aG, aB, Quantity_TOC_RGB);
  myGradientParams.color1 = NCollection_Vec4<float>((float)anR, (float)aG, (float)aB, 0.0f);

  theColor2.Values(anR, aG, aB, Quantity_TOC_RGB);
  myGradientParams.color2 = NCollection_Vec4<float>((float)anR, (float)aG, (float)aB, 0.0f);

  myGradientParams.type = theType;
  invalidateData();
}

//=================================================================================================

void OpenGl_BackgroundArray::SetGradientFillMethod(const Aspect_GradientFillMethod theType)
{
  if (myType != Graphic3d_TOB_GRADIENT)
  {
    return;
  }

  myGradientParams.type = theType;
  invalidateData();
}

//=================================================================================================

bool OpenGl_BackgroundArray::IsDefined() const
{
  switch (myType)
  {
    case Graphic3d_TOB_GRADIENT:
      return myGradientParams.type != Aspect_GradientFillMethod_None;
    case Graphic3d_TOB_TEXTURE:
      return myFillMethod != Aspect_FM_NONE;
    case Graphic3d_TOB_CUBEMAP:
      return true;
    case Graphic3d_TOB_NONE:
      return false;
  }
  return false;
}

//=================================================================================================

void OpenGl_BackgroundArray::invalidateData()
{
  myToUpdate = true;
}

//=================================================================================================

bool OpenGl_BackgroundArray::init(const occ::handle<OpenGl_Workspace>& theWorkspace) const
{
  const occ::handle<OpenGl_Context>& aCtx = theWorkspace->GetGlContext();

  if (myIndices.IsNull())
  {
    myIndices = new Graphic3d_IndexBuffer(Graphic3d_Buffer::DefaultAllocator());
  }
  if (myAttribs.IsNull())
  {
    myAttribs = new Graphic3d_Buffer(Graphic3d_Buffer::DefaultAllocator());
  }

  switch (myType)
  {
    case Graphic3d_TOB_GRADIENT: {
      if (!createGradientArray(aCtx))
      {
        return false;
      }
      break;
    }
    case Graphic3d_TOB_TEXTURE: {
      if (!createTextureArray(theWorkspace))
      {
        return false;
      }
      break;
    }
    case Graphic3d_TOB_CUBEMAP: {
      if (!createCubeMapArray())
      {
        return false;
      }
      break;
    }
    case Graphic3d_TOB_NONE:
    default: {
      return false;
    }
  }

  // Init VBO
  if (myIsVboInit)
  {
    clearMemoryGL(aCtx);
  }
  buildVBO(aCtx, true);
  myIsVboInit = true;

  // Data is up-to-date
  myToUpdate = false;
  return true;
}

//=================================================================================================

bool OpenGl_BackgroundArray::createGradientArray(const occ::handle<OpenGl_Context>& theCtx) const
{
  // Initialize data for primitive array
  Graphic3d_Attribute aGragientAttribInfo[] = {{Graphic3d_TOA_POS, Graphic3d_TOD_VEC2},
                                               {Graphic3d_TOA_COLOR, Graphic3d_TOD_VEC3}};

  if (!myAttribs->Init(4, aGragientAttribInfo, 2))
  {
    return false;
  }
  if (!myIndices->Init<unsigned short>(6))
  {
    return false;
  }
  const unsigned short THE_FS_QUAD_TRIS[6] = {0, 1, 3, 1, 2, 3};
  for (unsigned int aVertIter = 0; aVertIter < 6; ++aVertIter)
  {
    myIndices->SetIndex(aVertIter, THE_FS_QUAD_TRIS[aVertIter]);
  }

  NCollection_Vec2<float> aVertices[4] = {
    NCollection_Vec2<float>(float(myViewWidth), 0.0f),
    NCollection_Vec2<float>(float(myViewWidth), float(myViewHeight)),
    NCollection_Vec2<float>(0.0f, float(myViewHeight)),
    NCollection_Vec2<float>(0.0f, 0.0f)};

  float* aCorners[4]     = {};
  float  aDiagCorner1[3] = {};
  float  aDiagCorner2[3] = {};

  switch (myGradientParams.type)
  {
    case Aspect_GradientFillMethod_Horizontal: {
      aCorners[0] = myGradientParams.color2.ChangeData();
      aCorners[1] = myGradientParams.color2.ChangeData();
      aCorners[2] = myGradientParams.color1.ChangeData();
      aCorners[3] = myGradientParams.color1.ChangeData();
      break;
    }
    case Aspect_GradientFillMethod_Vertical: {
      aCorners[0] = myGradientParams.color2.ChangeData();
      aCorners[1] = myGradientParams.color1.ChangeData();
      aCorners[2] = myGradientParams.color1.ChangeData();
      aCorners[3] = myGradientParams.color2.ChangeData();
      break;
    }
    case Aspect_GradientFillMethod_Diagonal1: {
      aCorners[0]     = myGradientParams.color2.ChangeData();
      aCorners[2]     = myGradientParams.color1.ChangeData();
      aDiagCorner1[0] = aDiagCorner2[0] = 0.5f * (aCorners[0][0] + aCorners[2][0]);
      aDiagCorner1[1] = aDiagCorner2[1] = 0.5f * (aCorners[0][1] + aCorners[2][1]);
      aDiagCorner1[2] = aDiagCorner2[2] = 0.5f * (aCorners[0][2] + aCorners[2][2]);
      aCorners[1]                       = aDiagCorner1;
      aCorners[3]                       = aDiagCorner2;
      break;
    }
    case Aspect_GradientFillMethod_Diagonal2: {
      aCorners[1]     = myGradientParams.color1.ChangeData();
      aCorners[3]     = myGradientParams.color2.ChangeData();
      aDiagCorner1[0] = aDiagCorner2[0] = 0.5f * (aCorners[1][0] + aCorners[3][0]);
      aDiagCorner1[1] = aDiagCorner2[1] = 0.5f * (aCorners[1][1] + aCorners[3][1]);
      aDiagCorner1[2] = aDiagCorner2[2] = 0.5f * (aCorners[1][2] + aCorners[3][2]);
      aCorners[0]                       = aDiagCorner1;
      aCorners[2]                       = aDiagCorner2;
      break;
    }
    case Aspect_GradientFillMethod_Corner1:
    case Aspect_GradientFillMethod_Corner2:
    case Aspect_GradientFillMethod_Corner3:
    case Aspect_GradientFillMethod_Corner4: {
      Graphic3d_Attribute aCornerAttribInfo[] = {{Graphic3d_TOA_POS, Graphic3d_TOD_VEC2},
                                                 {Graphic3d_TOA_UV, Graphic3d_TOD_VEC2}};

      NCollection_Vec2<float> anUVs[4] = {NCollection_Vec2<float>(1.0f, 0.0f),
                                          NCollection_Vec2<float>(1.0f, 1.0f),
                                          NCollection_Vec2<float>(0.0f, 1.0f),
                                          NCollection_Vec2<float>(0.0f, 0.0f)};

      if (!myAttribs->Init(4, aCornerAttribInfo, 2))
      {
        return false;
      }
      for (int anIt = 0; anIt < 4; ++anIt)
      {
        NCollection_Vec2<float>* aVertData =
          reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(anIt));
        *aVertData = aVertices[anIt];

        NCollection_Vec2<float>* anUvData = reinterpret_cast<NCollection_Vec2<float>*>(
          myAttribs->changeValue(anIt) + myAttribs->AttributeOffset(1));
        // cyclically move highlighted corner depending on myGradientParams.type
        *anUvData = anUVs[(anIt + myGradientParams.type - Aspect_GradientFillMethod_Corner1) % 4];
      }
      return true;
    }
    case Aspect_GradientFillMethod_Elliptical: {
      // construction of a circle circumscribed about a view rectangle
      // using parametric equation (scaled by aspect ratio and centered)
      const int aSubdiv = 64;
      if (!myAttribs->Init(aSubdiv + 2, aGragientAttribInfo, 2))
      {
        return false;
      }

      NCollection_Vec2<float> anEllipVerts[aSubdiv + 2];
      anEllipVerts[0] =
        NCollection_Vec2<float>(float(myViewWidth) / 2.0f, float(myViewHeight) / 2.0f);
      double aTetta = (M_PI * 2.0) / aSubdiv;
      double aParam = 0.0;
      for (int anIt = 1; anIt < aSubdiv + 2; ++anIt)
      {
        anEllipVerts[anIt] = NCollection_Vec2<float>(
          float(std::cos(aParam) * M_SQRT2 * myViewWidth / 2.0 + myViewWidth / 2.0f),
          float(std::sin(aParam) * M_SQRT2 * myViewHeight / 2.0 + myViewHeight / 2.0f));

        aParam += aTetta;
      }
      for (int anIt = 0; anIt < aSubdiv + 2; ++anIt)
      {
        NCollection_Vec2<float>* aVertData =
          reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(anIt));
        *aVertData = anEllipVerts[anIt];

        NCollection_Vec3<float>* aColorData = reinterpret_cast<NCollection_Vec3<float>*>(
          myAttribs->changeValue(anIt) + myAttribs->AttributeOffset(1));
        *aColorData = myGradientParams.color2.rgb();
      }
      // the central vertex is colored in different way
      NCollection_Vec3<float>* aColorData = reinterpret_cast<NCollection_Vec3<float>*>(
        myAttribs->changeValue(0) + myAttribs->AttributeOffset(1));
      *aColorData = myGradientParams.color1.rgb();

      if (!myIndices->Init<unsigned short>(3 * aSubdiv))
      {
        return false;
      }
      for (int aCurTri = 0; aCurTri < aSubdiv; aCurTri++)
      {
        myIndices->SetIndex(aCurTri * 3 + 0, 0);
        myIndices->SetIndex(aCurTri * 3 + 1, aCurTri + 1);
        myIndices->SetIndex(aCurTri * 3 + 2, aCurTri + 2);
      }

      return true;
    }
    case Aspect_GradientFillMethod_None: {
      break;
    }
  }

  for (int anIt = 0; anIt < 4; ++anIt)
  {
    NCollection_Vec2<float>* aVertData =
      reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(anIt));
    *aVertData = aVertices[anIt];

    NCollection_Vec3<float>* aColorData = reinterpret_cast<NCollection_Vec3<float>*>(
      myAttribs->changeValue(anIt) + myAttribs->AttributeOffset(1));
    *aColorData =
      theCtx
        ->Vec4FromQuantityColor(
          NCollection_Vec4<float>(aCorners[anIt][0], aCorners[anIt][1], aCorners[anIt][2], 1.0f))
        .rgb();
  }

  return true;
}

//=================================================================================================

bool OpenGl_BackgroundArray::createTextureArray(
  const occ::handle<OpenGl_Workspace>& theWorkspace) const
{
  Graphic3d_Attribute aTextureAttribInfo[] = {{Graphic3d_TOA_POS, Graphic3d_TOD_VEC2},
                                              {Graphic3d_TOA_UV, Graphic3d_TOD_VEC2}};

  if (!myAttribs->Init(4, aTextureAttribInfo, 2))
  {
    return false;
  }

  GLfloat aTexRangeX = 1.0f; // texture <s> coordinate
  GLfloat aTexRangeY = 1.0f; // texture <t> coordinate

  // Set up for stretching or tiling
  GLfloat anOffsetX = 0.5f * (float)myViewWidth;
  GLfloat anOffsetY = 0.5f * (float)myViewHeight;

  // Setting this coefficient to -1.0f allows to tile textures relatively to the top-left corner of
  // the view (value 1.0f corresponds to the initial behavior - tiling from the bottom-left corner)
  GLfloat aCoef = -1.0f;

  // Get texture parameters
  const occ::handle<OpenGl_Context>& aCtx         = theWorkspace->GetGlContext();
  const OpenGl_Aspects*              anAspectFace = theWorkspace->Aspects();
  GLfloat aTextureWidth  = (GLfloat)anAspectFace->TextureSet(aCtx)->First()->SizeX();
  GLfloat aTextureHeight = (GLfloat)anAspectFace->TextureSet(aCtx)->First()->SizeY();

  if (myFillMethod == Aspect_FM_CENTERED)
  {
    anOffsetX = 0.5f * aTextureWidth;
    anOffsetY = 0.5f * aTextureHeight;
  }
  else if (myFillMethod == Aspect_FM_TILED)
  {
    aTexRangeX = (GLfloat)myViewWidth / aTextureWidth;
    aTexRangeY = (GLfloat)myViewHeight / aTextureHeight;
  }

  // NOTE: texture is mapped using GL_REPEAT wrapping mode so integer part
  // is simply ignored, and negative multiplier is here for convenience only
  // and does not result e.g. in texture mirroring

  NCollection_Vec2<float>* aData =
    reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(0));
  aData[0] = NCollection_Vec2<float>(anOffsetX, -aCoef * anOffsetY);
  aData[1] = NCollection_Vec2<float>(aTexRangeX, 0.0f);

  aData    = reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(1));
  aData[0] = NCollection_Vec2<float>(anOffsetX, aCoef * anOffsetY);
  aData[1] = NCollection_Vec2<float>(aTexRangeX, aCoef * aTexRangeY);

  aData    = reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(2));
  aData[0] = NCollection_Vec2<float>(-anOffsetX, -aCoef * anOffsetY);
  aData[1] = NCollection_Vec2<float>(0.0f, 0.0f);

  aData    = reinterpret_cast<NCollection_Vec2<float>*>(myAttribs->changeValue(3));
  aData[0] = NCollection_Vec2<float>(-anOffsetX, aCoef * anOffsetY);
  aData[1] = NCollection_Vec2<float>(0.0f, aCoef * aTexRangeY);

  if (!myIndices->Init<unsigned short>(6))
  {
    return false;
  }
  const unsigned short THE_FS_QUAD_TRIS[6] = {0, 1, 2, 1, 3, 2};
  for (unsigned int aVertIter = 0; aVertIter < 6; ++aVertIter)
  {
    myIndices->SetIndex(aVertIter, THE_FS_QUAD_TRIS[aVertIter]);
  }

  return true;
}

//=================================================================================================

bool OpenGl_BackgroundArray::createCubeMapArray() const
{
  const Graphic3d_Attribute aCubeMapAttribInfo[] = {{Graphic3d_TOA_POS, Graphic3d_TOD_VEC3}};

  if (myAttribs.IsNull())
  {
    myAttribs = new Graphic3d_Buffer(Graphic3d_Buffer::DefaultAllocator());
    myIndices = new Graphic3d_IndexBuffer(Graphic3d_Buffer::DefaultAllocator());
  }
  if (!myAttribs->Init(8, aCubeMapAttribInfo, 1) || !myIndices->Init<unsigned short>(6 * 3 * 2))
  {
    return false;
  }

  {
    NCollection_Vec3<float>* aData =
      reinterpret_cast<NCollection_Vec3<float>*>(myAttribs->changeValue(0));
    aData[0].SetValues(-1.0, -1.0, 1.0);
    aData[1].SetValues(1.0, -1.0, 1.0);
    aData[2].SetValues(-1.0, 1.0, 1.0);
    aData[3].SetValues(1.0, 1.0, 1.0);
    aData[4].SetValues(-1.0, -1.0, -1.0);
    aData[5].SetValues(1.0, -1.0, -1.0);
    aData[6].SetValues(-1.0, 1.0, -1.0);
    aData[7].SetValues(1.0, 1.0, -1.0);
  }
  {
    const unsigned short THE_BOX_TRIS[] = {
      0, 1, 2, 2, 1, 3, // top face
      1, 5, 7, 1, 7, 3, // right face
      0, 6, 4, 0, 2, 6, // left face
      4, 6, 5, 6, 7, 5, // bottom face
      0, 5, 1, 0, 4, 5, // front face
      2, 7, 6, 2, 3, 7  // back face
    };
    for (unsigned int aVertIter = 0; aVertIter < 6 * 3 * 2; ++aVertIter)
    {
      myIndices->SetIndex(aVertIter, THE_BOX_TRIS[aVertIter]);
    }
  }

  return true;
}

//=================================================================================================

void OpenGl_BackgroundArray::Render(const occ::handle<OpenGl_Workspace>& theWorkspace,
                                    Graphic3d_Camera::Projection         theProjection) const
{
  const occ::handle<OpenGl_Context>& aCtx       = theWorkspace->GetGlContext();
  int                                aViewSizeX = aCtx->Viewport()[2];
  int                                aViewSizeY = aCtx->Viewport()[3];
  NCollection_Vec2<int>              aTileOffset, aTileSize;

  if (aCtx->Camera()->Tile().IsValid())
  {
    aViewSizeX = aCtx->Camera()->Tile().TotalSize.x();
    aViewSizeY = aCtx->Camera()->Tile().TotalSize.y();

    aTileOffset = aCtx->Camera()->Tile().OffsetLowerLeft();
    aTileSize   = aCtx->Camera()->Tile().TileSize;
  }
  if (myToUpdate || myViewWidth != aViewSizeX || myViewHeight != aViewSizeY || myAttribs.IsNull()
      || myVboAttribs.IsNull())
  {
    myViewWidth  = aViewSizeX;
    myViewHeight = aViewSizeY;
    init(theWorkspace);
  }

  NCollection_Mat4<float> aProjection = aCtx->ProjectionState.Current();
  NCollection_Mat4<float> aWorldView  = aCtx->WorldViewState.Current();

  if (myType == Graphic3d_TOB_CUBEMAP)
  {
    Graphic3d_Camera aCamera(aCtx->Camera());
    aCamera.SetZRange(0.01, 1.0); // is needed to avoid perspective camera exception

    // cancel translation
    aCamera.MoveEyeTo(gp_Pnt(0.0, 0.0, 0.0));

    // Handle projection matrix:
    // - Cancel any head-to-eye translation for HMD display;
    // - Ignore stereoscopic projection in case of non-HMD 3D display
    //   (ideally, we would need a stereoscopic cubemap image; adding a parallax makes no sense);
    // - Force perspective projection when orthographic camera is active
    //   (orthographic projection makes no sense for cubemap).
    const bool isCustomProj = aCamera.IsCustomStereoFrustum() || aCamera.IsCustomStereoProjection();
    aCamera.SetProjectionType(theProjection == Graphic3d_Camera::Projection_Orthographic
                                  || !isCustomProj
                                ? Graphic3d_Camera::Projection_Perspective
                                : theProjection);

    aProjection = aCamera.ProjectionMatrixF();
    aWorldView  = aCamera.OrientationMatrixF();
    if (isCustomProj)
    {
      // get projection matrix without pre-multiplied stereoscopic head-to-eye translation
      if (theProjection == Graphic3d_Camera::Projection_MonoLeftEye)
      {
        NCollection_Mat4<float> aMatProjL, aMatHeadToEyeL, aMatProjR, aMatHeadToEyeR;
        aCamera.StereoProjectionF(aMatProjL, aMatHeadToEyeL, aMatProjR, aMatHeadToEyeR);
        aProjection = aMatProjL;
      }
      else if (theProjection == Graphic3d_Camera::Projection_MonoRightEye)
      {
        NCollection_Mat4<float> aMatProjL, aMatHeadToEyeL, aMatProjR, aMatHeadToEyeR;
        aCamera.StereoProjectionF(aMatProjL, aMatHeadToEyeL, aMatProjR, aMatHeadToEyeR);
        aProjection = aMatProjR;
      }
    }
  }
  else
  {
    aProjection.InitIdentity();
    aWorldView.InitIdentity();
    if (aCtx->Camera()->Tile().IsValid())
    {
      aWorldView.SetDiagonal(
        NCollection_Vec4<float>(2.0f / aTileSize.x(), 2.0f / aTileSize.y(), 1.0f, 1.0f));
      if (myType == Graphic3d_TOB_GRADIENT)
      {
        aWorldView.SetColumn(3,
                             NCollection_Vec4<float>(-1.0f - 2.0f * aTileOffset.x() / aTileSize.x(),
                                                     -1.0f - 2.0f * aTileOffset.y() / aTileSize.y(),
                                                     0.0f,
                                                     1.0f));
      }
      else
      {
        aWorldView.SetColumn(
          3,
          NCollection_Vec4<float>(
            -1.0f + (float)aViewSizeX / aTileSize.x() - 2.0f * aTileOffset.x() / aTileSize.x(),
            -1.0f + (float)aViewSizeY / aTileSize.y() - 2.0f * aTileOffset.y() / aTileSize.y(),
            0.0f,
            1.0f));
      }
    }
    else
    {
      aWorldView.SetDiagonal(
        NCollection_Vec4<float>(2.0f / myViewWidth, 2.0f / myViewHeight, 1.0f, 1.0f));
      if (myType == Graphic3d_TOB_GRADIENT)
      {
        aWorldView.SetColumn(3, NCollection_Vec4<float>(-1.0f, -1.0f, 0.0f, 1.0f));
      }
    }
  }

  aCtx->ProjectionState.Push();
  aCtx->WorldViewState.Push();
  aCtx->ProjectionState.SetCurrent(aProjection);
  aCtx->WorldViewState.SetCurrent(aWorldView);
  aCtx->ApplyProjectionMatrix();
  aCtx->ApplyModelViewMatrix();

  OpenGl_PrimitiveArray::Render(theWorkspace);

  aCtx->ProjectionState.Pop();
  aCtx->WorldViewState.Pop();
  aCtx->ApplyProjectionMatrix();
}
