/*=========================================================================

Program:   Visualization Toolkit
Module:    vtkVRMenuRepresentation.cxx

Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
All rights reserved.
See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

This software is distributed WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkVRMenuRepresentation.h"

#include "vtkCamera.h"
#include "vtkObjectFactory.h"
#include "vtkOpenGLState.h"
#include "vtkRenderer.h"
#include "vtkTextActor3D.h"
#include "vtkTextProperty.h"
#include "vtkTransform.h"
#include "vtkVRRenderWindow.h"
#include "vtkWidgetEvent.h"

vtkStandardNewMacro(vtkVRMenuRepresentation);

class vtkVRMenuRepresentation::InternalElement
{
public:
  vtkNew<vtkTextActor3D> TextActor;
  vtkCommand* Command;
  std::string Name;

  InternalElement()
  {
    vtkTextProperty* prop = this->TextActor->GetTextProperty();
    this->TextActor->ForceOpaqueOn();

    prop->SetFontFamilyToTimes();
    prop->SetFrame(1);
    prop->SetFrameWidth(12);
    prop->SetFrameColor(1.0, 1.0, 1.0);
    prop->SetFrameColor(0.0, 0.0, 0.0);
    prop->SetBackgroundOpacity(1.0);
    prop->SetBackgroundColor(0.0, 0.0, 0.0);
    prop->SetFontSize(32);
  }
};

//------------------------------------------------------------------------------
vtkVRMenuRepresentation::vtkVRMenuRepresentation()
{
  this->VisibilityOff();
}

//------------------------------------------------------------------------------
vtkVRMenuRepresentation::~vtkVRMenuRepresentation()
{
  this->RemoveAllMenuItems();
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::PushFrontMenuItem(const char* name, const char* text, vtkCommand* cmd)
{
  vtkVRMenuRepresentation::InternalElement* el = new vtkVRMenuRepresentation::InternalElement();
  el->TextActor->SetInput(text);
  el->Command = cmd;
  el->Name = name;
  this->Menus.push_front(el);
  this->Modified();
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::RenameMenuItem(const char* name, const char* text)
{
  for (auto itr : this->Menus)
  {
    if (itr->Name == name)
    {
      itr->TextActor->SetInput(text);
      this->Modified();
    }
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::RemoveMenuItem(const char* name)
{
  for (auto itr = this->Menus.begin(); itr != this->Menus.end(); ++itr)
  {
    if ((*itr)->Name == name)
    {
      delete *itr;
      this->Menus.erase(itr);
      this->Modified();
      return;
    }
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::RemoveAllMenuItems()
{
  while (!this->Menus.empty())
  {
    auto itr = this->Menus.begin();
    delete *itr;
    this->Menus.erase(itr);
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::StartComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void* calldata)
{
  vtkEventData* edata = static_cast<vtkEventData*>(calldata);
  vtkEventDataDevice3D* ed = edata->GetAsEventDataDevice3D();
  if (ed)
  {
    this->CurrentOption = 0;
    this->Modified();
    this->BuildRepresentation();
    this->VisibilityOn();
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::EndComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void*)
{
  this->VisibilityOff();
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::ComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long event, void* calldata)
{
  switch (event)
  {
    case vtkWidgetEvent::Select3D:
    {
      this->VisibilityOff();
      long count = 0;
      for (auto& menu : this->Menus)
      {
        if (count == std::lround(this->CurrentOption))
        {
          menu->Command->Execute(this, vtkWidgetEvent::Select3D,
            static_cast<void*>(const_cast<char*>(menu->Name.c_str())));
        }
        count++;
      }
    }
    break;

    case vtkWidgetEvent::Move3D:
    {
      vtkEventData* edata = static_cast<vtkEventData*>(calldata);
      vtkEventDataDevice3D* ed = edata->GetAsEventDataDevice3D();
      if (!ed)
      {
        return;
      }
      const double* dir = ed->GetWorldDirection();
      // adjust CurrentOption based on controller orientation
      vtkVRRenderWindow* renWin =
        static_cast<vtkVRRenderWindow*>(this->Renderer->GetRenderWindow());
      double* vup = renWin->GetPhysicalViewUp();
      double dot = vtkMath::Dot(dir, vup);
      this->CurrentOption -= dot * 0.12;
      if (this->CurrentOption < 0)
      {
        this->CurrentOption = 0.0;
      }
      else if (this->CurrentOption > this->Menus.size() - 1)
      {
        this->CurrentOption = this->Menus.size() - 1;
      }
      this->BuildRepresentation();
    }
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::ReleaseGraphicsResources(vtkWindow* w)
{
  for (auto& menu : this->Menus)
  {
    menu->TextActor->ReleaseGraphicsResources(w);
  }
}

//------------------------------------------------------------------------------
int vtkVRMenuRepresentation::RenderOverlay(vtkViewport* v)
{
  if (!this->GetVisibility())
  {
    return 0;
  }

  vtkVRRenderWindow* renWin = static_cast<vtkVRRenderWindow*>(this->Renderer->GetRenderWindow());
  vtkOpenGLState* ostate = renWin->GetState();

  // always draw over the rest
  ostate->vtkglDepthFunc(GL_ALWAYS);
  for (auto& menu : this->Menus)
  {
    menu->TextActor->RenderOpaqueGeometry(v);
  }
  ostate->vtkglDepthFunc(GL_LEQUAL);

  return static_cast<int>(this->Menus.size());
}

//------------------------------------------------------------------------------
vtkTypeBool vtkVRMenuRepresentation::HasTranslucentPolygonalGeometry()
{
  return 0;
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::BuildRepresentation()
{
  vtkVRRenderWindow* renWin = static_cast<vtkVRRenderWindow*>(this->Renderer->GetRenderWindow());
  double physicalScale = renWin->GetPhysicalScale();

  if (this->GetMTime() > this->BuildTime)
  {
    // Compute camera position and orientation
    vtkCamera* cam = this->Renderer->GetActiveCamera();
    cam->GetPosition(this->PlacedPos);
    double* dop = cam->GetDirectionOfProjection();
    vtkMath::Normalize(dop);

    renWin->GetPhysicalViewUp(this->PlacedVUP);
    double vupdot = vtkMath::Dot(dop, this->PlacedVUP);
    if (fabs(vupdot) < 0.999)
    {
      this->PlacedDOP[0] = dop[0] - this->PlacedVUP[0] * vupdot;
      this->PlacedDOP[1] = dop[1] - this->PlacedVUP[1] * vupdot;
      this->PlacedDOP[2] = dop[2] - this->PlacedVUP[2] * vupdot;
      vtkMath::Normalize(this->PlacedDOP);
    }
    else
    {
      renWin->GetPhysicalViewDirection(this->PlacedDOP);
    }
    vtkMath::Cross(this->PlacedDOP, this->PlacedVUP, this->PlacedVRight);

    vtkNew<vtkMatrix4x4> rot;
    for (int i = 0; i < 3; ++i)
    {
      rot->SetElement(0, i, this->PlacedVRight[i]);
      rot->SetElement(1, i, this->PlacedVUP[i]);
      rot->SetElement(2, i, -this->PlacedDOP[i]);
    }
    rot->Transpose();
    vtkTransform::GetOrientation(this->PlacedOrientation, rot);

    this->BuildTime.Modified();
  }

  // Set frame distance to camera
  double frameDistance = physicalScale * 1.5; // 1.5 meters

  double fov = this->Renderer->GetActiveCamera()->GetViewAngle();
  double psize = frameDistance * 0.03 * 2.0 * atan(fov * 0.5); // 3% of fov
  double tscale = psize / 55.0;                                // about 55 pixel high texture map

  // Frame position
  double frameCenter[3];

  long count = 0;
  for (auto& menu : this->Menus)
  {
    double shift = count - this->CurrentOption;
    long icurr = std::lround(this->CurrentOption);

    if (count == icurr)
    {
      menu->TextActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0);
    }
    else
    {
      menu->TextActor->GetTextProperty()->SetColor(0.6, 0.6, 0.6);
    }
    double angle = -shift * 2.0 * 3.1415926 / 180.0; // about 2 degree vert sep
    double fdist = frameDistance * (1.0 + 3.0 * (1.0 - cos(angle)));
    double udist = 3.0 * frameDistance * sin(angle);

    frameCenter[0] = this->PlacedPos[0] + fdist * this->PlacedDOP[0] -
      psize * this->PlacedVRight[0] + udist * this->PlacedVUP[0];
    frameCenter[1] = this->PlacedPos[1] + fdist * this->PlacedDOP[1] -
      psize * this->PlacedVRight[1] + udist * this->PlacedVUP[1];
    frameCenter[2] = this->PlacedPos[2] + fdist * this->PlacedDOP[2] -
      psize * this->PlacedVRight[2] + udist * this->PlacedVUP[2];

    menu->TextActor->SetScale(tscale, tscale, tscale);
    menu->TextActor->SetPosition(frameCenter);
    menu->TextActor->SetOrientation(this->PlacedOrientation);
    menu->TextActor->RotateX(-angle * 180.0 / 3.1415926);
    count++;
  }
}

//------------------------------------------------------------------------------
void vtkVRMenuRepresentation::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);

  os << indent << "CurrentOption: " << this->CurrentOption << "\n";

  os << indent << "PlacedPos: (" << this->PlacedPos[0] << ", " << this->PlacedPos[1] << ","
     << this->PlacedPos[2] << ")\n";
  os << indent << "PlacedDOP: (" << this->PlacedDOP[0] << ", " << this->PlacedDOP[1] << ","
     << this->PlacedDOP[2] << ")\n";
  os << indent << "PlacedVUP: (" << this->PlacedVUP[0] << ", " << this->PlacedVUP[1] << ","
     << this->PlacedVUP[2] << ")\n";
  os << indent << "PlacedVRight: (" << this->PlacedVRight[0] << ", " << this->PlacedVRight[1] << ","
     << this->PlacedVRight[2] << ")\n";
  os << indent << "PlacedOrientation: (" << this->PlacedOrientation[0] << ", "
     << this->PlacedOrientation[1] << "," << this->PlacedOrientation[2] << ")\n";
}
