// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkVRPanelRepresentation.h"

#include "vtkCamera.h"
#include "vtkEventData.h"
#include "vtkObjectFactory.h"
#include "vtkQuaternion.h"
#include "vtkRenderer.h"
#include "vtkTextActor3D.h"
#include "vtkTextProperty.h"
#include "vtkTransform.h"
#include "vtkVRRenderWindow.h"

// TODO
// - add option to remove crop planes

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkVRPanelRepresentation);

//------------------------------------------------------------------------------
vtkVRPanelRepresentation::vtkVRPanelRepresentation()
{
  this->TextActor = vtkTextActor3D::New();
  this->TextActor->GetTextProperty()->SetFontSize(17);
  this->Text = "This is a Panel Widget";
  this->TextActor->SetInput(this->Text.c_str());

  vtkTextProperty* prop = this->TextActor->GetTextProperty();
  this->TextActor->ForceOpaqueOn();
  this->TextActor->SetUserMatrix(vtkMatrix4x4::New());
  this->TextActor->GetUserMatrix()->Delete();
  prop->SetFontFamilyToTimes();
  prop->SetFrame(1);
  prop->SetFrameWidth(12);
  prop->SetFrameColor(0.0, 0.0, 0.0);
  prop->SetBackgroundOpacity(0.5);
  prop->SetBackgroundColor(0.0, 0.0, 0.0);
  prop->SetFontSize(25);

  this->InteractionState = vtkVRPanelRepresentation::Outside;
  this->CoordinateSystem = World;
  this->AllowAdjustment = true;
}

//------------------------------------------------------------------------------
vtkVRPanelRepresentation::~vtkVRPanelRepresentation()
{
  this->TextActor->Delete();
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::SetCoordinateSystemToWorld()
{
  if (this->CoordinateSystem == World)
  {
    return;
  }
  this->TextActor->GetUserMatrix()->Identity();
  this->CoordinateSystem = World;
  this->Modified();
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::SetCoordinateSystemToHMD()
{
  if (this->CoordinateSystem == HMD)
  {
    return;
  }
  this->CoordinateSystem = HMD;
  this->Modified();
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::SetCoordinateSystemToLeftController()
{
  if (this->CoordinateSystem == LeftController)
  {
    return;
  }
  this->CoordinateSystem = LeftController;
  this->Modified();
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::SetCoordinateSystemToRightController()
{
  if (this->CoordinateSystem == RightController)
  {
    return;
  }
  this->CoordinateSystem = RightController;
  this->Modified();
}

//------------------------------------------------------------------------------
int vtkVRPanelRepresentation::ComputeComplexInteractionState(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void* calldata, int)
{
  if (!this->AllowAdjustment || this->InteractionState == vtkVRPanelRepresentation::Moving)
  {
    return this->InteractionState;
  }

  vtkEventData* edata = static_cast<vtkEventData*>(calldata);
  vtkEventDataDevice3D* edd = edata->GetAsEventDataDevice3D();
  if (edd)
  {
    double pos[4];
    edd->GetWorldPosition(pos);
    pos[3] = 1.0;

    const double* bds = this->TextActor->GetBounds();
    double length = sqrt((bds[1] - bds[0]) * (bds[1] - bds[0]) +
      (bds[3] - bds[2]) * (bds[3] - bds[2]) + (bds[5] - bds[4]) * (bds[5] - bds[4]));
    double tolerance = length * 0.05;
    if (pos[0] > bds[0] - tolerance && pos[0] < bds[1] + tolerance && pos[1] > bds[2] - tolerance &&
      pos[1] < bds[3] + tolerance && pos[2] > bds[4] - tolerance && pos[2] < bds[5] + tolerance)
    {
      this->InteractionState = vtkVRPanelRepresentation::Moving;
    }
    else
    {
      this->InteractionState = vtkVRPanelRepresentation::Outside;
    }
  }

  return this->InteractionState;
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::StartComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void* calldata)
{
  vtkEventData* edata = static_cast<vtkEventData*>(calldata);
  vtkEventDataDevice3D* edd = edata->GetAsEventDataDevice3D();
  if (edd)
  {
    edd->GetWorldPosition(this->StartEventPosition);
    this->LastEventPosition[0] = this->StartEventPosition[0];
    this->LastEventPosition[1] = this->StartEventPosition[1];
    this->LastEventPosition[2] = this->StartEventPosition[2];
    edd->GetWorldOrientation(this->StartEventOrientation);
    std::copy(
      this->StartEventOrientation, this->StartEventOrientation + 4, this->LastEventOrientation);
  }
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::ComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void* calldata)
{
  vtkEventData* edata = static_cast<vtkEventData*>(calldata);
  vtkEventDataDevice3D* edd = edata->GetAsEventDataDevice3D();
  if (edd)
  {
    double eventPos[3];
    edd->GetWorldPosition(eventPos);
    double eventDir[4];
    edd->GetWorldOrientation(eventDir);

    // Process the motion
    if (this->InteractionState == vtkVRPanelRepresentation::Moving)
    {
      // this->TranslateOutline(this->LastEventPosition, eventPos);
      this->UpdatePose(this->LastEventPosition, this->LastEventOrientation, eventPos, eventDir);
    }

    // Book keeping
    this->LastEventPosition[0] = eventPos[0];
    this->LastEventPosition[1] = eventPos[1];
    this->LastEventPosition[2] = eventPos[2];
    std::copy(eventDir, eventDir + 4, this->LastEventOrientation);
    this->Modified();
  }
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::EndComplexInteraction(
  vtkRenderWindowInteractor*, vtkAbstractWidget*, unsigned long, void*)
{
  this->InteractionState = vtkVRPanelRepresentation::Outside;
}

//------------------------------------------------------------------------------
// Loop through all points and translate and rotate them
void vtkVRPanelRepresentation::UpdatePose(double* p1, double* orient1, double* p2, double* orient2)
{
  if (this->CoordinateSystem == World)
  {
    this->UpdatePropPose(this->TextActor, p1, orient1, p2, orient2);
    return;
  }

  if (this->CoordinateSystem == HMD)
  {
    vtkMatrix4x4* mat = this->TextActor->GetUserMatrix();
    mat->Register(this);
    this->TextActor->SetUserMatrix(nullptr);

    this->TempMatrix->DeepCopy(mat);
    this->TempMatrix->Invert();
    double p14[4];
    std::copy(p1, p1 + 3, p14);
    p14[3] = 1.0;
    double p24[4];
    std::copy(p2, p2 + 3, p24);
    p24[3] = 1.0;
    this->TempMatrix->MultiplyPoint(p14, p14);
    this->TempMatrix->MultiplyPoint(p24, p24);

    double trans[3];
    for (int i = 0; i < 3; i++)
    {
      trans[i] = p24[i] - p14[i];
    }

    vtkTransform* newTransform = this->TempTransform;

    // changes in Z adjust the scale
    double ratio = (0.5 + trans[2] / this->LastScale) / 0.5;
    double* scale = this->TextActor->GetScale();
    this->TextActor->SetScale(scale[0] * ratio, scale[1] * ratio, scale[2] * ratio);
    this->TextActor->AddPosition(trans[0], trans[1], 0.0);

    // compute the net rotation
    vtkQuaternion<double> q1;
    q1.SetRotationAngleAndAxis(
      vtkMath::RadiansFromDegrees(orient1[0]), orient1[1], orient1[2], orient1[3]);
    vtkQuaternion<double> q2;
    q2.SetRotationAngleAndAxis(
      vtkMath::RadiansFromDegrees(orient2[0]), orient2[1], orient2[2], orient2[3]);
    q1.Conjugate();
    q2 = q2 * q1;
    double axis[4];
    axis[0] = vtkMath::DegreesFromRadians(q2.GetRotationAngleAndAxis(axis + 1));

    newTransform->Identity();
    newTransform->PostMultiply();
    newTransform->Concatenate(this->TempMatrix);
    newTransform->TransformNormal(axis + 1, axis + 1);

    vtkMatrix4x4* oldMatrix = this->TempMatrix;
    this->TextActor->GetMatrix(oldMatrix);

    newTransform->Identity();
    newTransform->PostMultiply();
    newTransform->Concatenate(oldMatrix);
    newTransform->Translate(-(p14[0]), -(p14[1]), -(p14[2]));
    newTransform->RotateWXYZ(axis[0], axis[1], axis[2], axis[3]);
    newTransform->Translate(p14[0], p14[1], p14[2]);

    this->TextActor->SetPosition(newTransform->GetPosition());
    this->TextActor->SetOrientation(newTransform->GetOrientation());
    this->TextActor->SetUserMatrix(mat);
    mat->UnRegister(this);
  }

  if (this->CoordinateSystem == LeftController || this->CoordinateSystem == RightController)
  {
    vtkMatrix4x4* mat = this->TextActor->GetUserMatrix();
    mat->Register(this);
    this->TextActor->SetUserMatrix(nullptr);

    this->TempMatrix->DeepCopy(mat);
    this->TempMatrix->Invert();
    double p14[4];
    std::copy(p1, p1 + 3, p14);
    p14[3] = 1.0;
    double p24[4];
    std::copy(p2, p2 + 3, p24);
    p24[3] = 1.0;
    this->TempMatrix->MultiplyPoint(p14, p14);
    this->TempMatrix->MultiplyPoint(p24, p24);

    double trans[3];
    for (int i = 0; i < 3; i++)
    {
      trans[i] = p24[i] - p14[i];
    }
    this->TextActor->AddPosition(trans[0], trans[1], trans[2]);

    vtkTransform* newTransform = this->TempTransform;

    // compute the net rotation
    vtkQuaternion<double> q1;
    q1.SetRotationAngleAndAxis(
      vtkMath::RadiansFromDegrees(orient1[0]), orient1[1], orient1[2], orient1[3]);
    vtkQuaternion<double> q2;
    q2.SetRotationAngleAndAxis(
      vtkMath::RadiansFromDegrees(orient2[0]), orient2[1], orient2[2], orient2[3]);
    q1.Conjugate();
    q2 = q2 * q1;
    double axis[4];
    axis[0] = vtkMath::DegreesFromRadians(q2.GetRotationAngleAndAxis(axis + 1));

    newTransform->Identity();
    newTransform->PostMultiply();
    newTransform->Concatenate(this->TempMatrix);
    newTransform->TransformNormal(axis + 1, axis + 1);

    vtkMatrix4x4* oldMatrix = this->TempMatrix;
    this->TextActor->GetMatrix(oldMatrix);

    newTransform->Identity();
    newTransform->PostMultiply();
    newTransform->Concatenate(oldMatrix);
    newTransform->Translate(-(p14[0]), -(p14[1]), -(p14[2]));
    newTransform->RotateWXYZ(axis[0], axis[1], axis[2], axis[3]);
    newTransform->Translate(p14[0], p14[1], p14[2]);

    this->TextActor->SetPosition(newTransform->GetPosition());
    this->TextActor->SetOrientation(newTransform->GetOrientation());
    this->TextActor->SetUserMatrix(mat);
    mat->UnRegister(this);
  }
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::ReleaseGraphicsResources(vtkWindow* w)
{
  this->TextActor->ReleaseGraphicsResources(w);
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::ComputeMatrix(vtkRenderer* ren)
{
  // check whether or not need to rebuild the matrix
  // only rebuild on left eye otherwise we get two different
  // poses for two eyes
  vtkCamera* cam = ren->GetActiveCamera();
  if (this->CoordinateSystem != World && cam->GetLeftEye())
  {
    vtkVRRenderWindow* rw = static_cast<vtkVRRenderWindow*>(ren->GetVTKWindow());

    if (this->CoordinateSystem == HMD)
    {
      vtkTransform* vt = cam->GetViewTransformObject();
      vt->GetInverse(this->TextActor->GetUserMatrix());

      if (rw->GetPhysicalScale() != this->LastScale)
      {
        double ratio = rw->GetPhysicalScale() / this->LastScale;
        double* scale = this->TextActor->GetScale();
        this->TextActor->SetScale(scale[0] * ratio, scale[1] * ratio, scale[2] * ratio);
        double* pos = this->TextActor->GetPosition();
        this->TextActor->SetPosition(pos[0] * ratio, pos[1] * ratio, -0.5 * rw->GetPhysicalScale());
        this->LastScale = rw->GetPhysicalScale();
      }
      else
      {
        double* pos = this->TextActor->GetPosition();
        this->TextActor->SetPosition(pos[0], pos[1], -0.5 * rw->GetPhysicalScale());
      }
    }

    vtkEventDataDevice device = this->CoordinateSystem == LeftController
      ? vtkEventDataDevice::LeftController
      : vtkEventDataDevice::RightController;

    vtkNew<vtkMatrix4x4> deviceToWorldMatrix;
    if (rw->GetDeviceToWorldMatrixForDevice(device, deviceToWorldMatrix))
    {
      // transform the text to the devices location in world coordinates
      this->TextActor->GetUserMatrix()->DeepCopy(deviceToWorldMatrix);
    }
  }
}

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

  // make sure the device has the same matrix
  // if (true /* HMD coords * /)
  {
    vtkRenderer* ren = static_cast<vtkRenderer*>(v);
    this->ComputeMatrix(ren);
  }

  int count = this->TextActor->RenderOpaqueGeometry(v);
  return count;
}

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

  int count = this->TextActor->RenderTranslucentPolygonalGeometry(v);

  return count;
}

//------------------------------------------------------------------------------
vtkTypeBool vtkVRPanelRepresentation::HasTranslucentPolygonalGeometry()
{
  if (!this->GetVisibility())
  {
    return 0;
  }

  int result = 0;

  result |= this->TextActor->HasTranslucentPolygonalGeometry();

  return result;
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::PlaceWidget(double bds[6])
{
  this->TextActor->GetUserMatrix()->Identity();
  if (this->CoordinateSystem == World)
  {
    // center the planel
    this->TextActor->SetPosition(
      0.5 * (bds[0] + bds[1]), 0.5 * (bds[2] + bds[3]), 0.5 * (bds[4] + bds[5]));

    double length = sqrt((bds[1] - bds[0]) * (bds[1] - bds[0]) +
      (bds[3] - bds[2]) * (bds[3] - bds[2]) + (bds[5] - bds[4]) * (bds[5] - bds[4]));
    this->TextActor->SetScale(length / 700.0, length / 700.0, length / 700.0);
    this->LastScale = length;
  }

  if (this->CoordinateSystem != World)
  {
    double scale = sqrt((bds[1] - bds[0]) * (bds[1] - bds[0]) +
      (bds[3] - bds[2]) * (bds[3] - bds[2]) + (bds[5] - bds[4]) * (bds[5] - bds[4]));
    this->TextActor->SetScale(scale / 700.0, scale / 700.0, scale / 700.0);
    this->LastScale = scale;
    this->TextActor->SetPosition(0.0, 0.0, -0.5 * scale);
  }
}

void vtkVRPanelRepresentation::PlaceWidgetExtended(
  const double* bds, const double* normal, const double* upvec, double scale)
{
  this->TextActor->GetUserMatrix()->Identity();
  this->TextActor->SetOrientation(0, 0, 0);
  this->LastScale = scale;

  // grab the bounding box of the text
  // so we can position and scale to that
  int tbounds[4];
  this->TextActor->GetBoundingBox(tbounds);
  int maxdim = tbounds[1] - tbounds[0];
  if (tbounds[3] - tbounds[2] > maxdim)
  {
    maxdim = tbounds[3] - tbounds[2];
  }
  // should always be at least 50 pixels
  // for any reasonable string.
  maxdim = maxdim > 50 ? maxdim : 50.0;

  // make the normal ortho to upvec
  vtkVector3d nup(upvec);
  nup.Normalize();
  vtkVector3d nvpn(normal);
  nvpn.Normalize();
  vtkVector3d nvright = nup.Cross(nvpn);
  nvright.Normalize();
  nvpn = nvright.Cross(nup);

  double basis[16];
  basis[0] = nvright[0];
  basis[4] = nvright[1];
  basis[8] = nvright[2];
  basis[12] = 0;
  basis[1] = nup[0];
  basis[5] = nup[1];
  basis[9] = nup[2];
  basis[13] = 0;
  basis[2] = nvpn[0];
  basis[6] = nvpn[1];
  basis[10] = nvpn[2];
  basis[14] = 0;
  basis[3] = 0;
  basis[7] = 0;
  basis[11] = 0;
  basis[15] = 1;

  vtkNew<vtkTransform> basisT;
  basisT->SetMatrix(basis);
  this->TextActor->SetOrientation(basisT->GetOrientation());

  if (this->CoordinateSystem == World)
  {
    // center the planel and default size to 400cm
    this->TextActor->SetScale(0.4 * scale / maxdim, 0.4 * scale / maxdim, 0.4 * scale / maxdim);
    this->TextActor->SetPosition(
      0.5 * (bds[0] + bds[1]), 0.5 * (bds[2] + bds[3]), 0.5 * (bds[4] + bds[5]));
  }

  if (this->CoordinateSystem == LeftController || this->CoordinateSystem == RightController)
  {
    // position the planel and default size to 400cm
    this->TextActor->SetScale(0.4 / maxdim, 0.4 / maxdim, 0.4 / maxdim);
    this->TextActor->SetPosition(0.5 * (bds[0] + bds[1]) - 0.2 * (tbounds[1] - tbounds[0]) / maxdim,
      0.5 * (bds[2] + bds[3]), 0.5 * (bds[4] + bds[5]));
  }

  if (this->CoordinateSystem == HMD)
  {
    // center the planel and default size to 400cm
    this->TextActor->SetScale(0.4 * scale / maxdim, 0.4 * scale / maxdim, 0.4 * scale / maxdim);
    this->TextActor->SetPosition(-0.2 * (tbounds[1] - tbounds[0]) * scale / maxdim,
      -0.2 * (tbounds[3] - tbounds[2]) * scale / maxdim, -0.5 * scale);
  }
}

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::BuildRepresentation()
{
  // if (this->GetMTime() > this->BuildTime)
  // {
  //   this->TextActor3D->SetPosition(tpos);

  //   // scale should cover 10% of FOV
  //   double fov = ren->GetActiveCamera()->GetViewAngle();
  //   double tsize = 0.1*2.0*atan(fov*0.5); // 10% of fov
  //   tsize /= 200.0;  // about 200 pixel texture map
  //   scale *= tsize;
  //   this->TextActor3D->SetScale(scale, scale, scale);
  //   this->TextActor3D->SetInput(text.c_str());

  //   this->BuildTime.Modified();
  // }
}

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

//------------------------------------------------------------------------------
void vtkVRPanelRepresentation::SetText(const char* text)
{
  if (this->Text == text)
  {
    return;
  }
  this->Text = text;

  this->TextActor->SetInput(this->Text.c_str());
  this->Modified();
}
VTK_ABI_NAMESPACE_END
