// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause

#include "vtkMultiSliceContextItem.h"

#include "vtkAxis.h"
#include "vtkBrush.h"
#include "vtkCommand.h"
#include "vtkContext2D.h"
#include "vtkContextMouseEvent.h"
#include "vtkContextScene.h"
#include "vtkMath.h"
#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkPen.h"
#include "vtkRect.h"
#include "vtkSmartPointer.h"
#include "vtkVector.h"

#include <vector>

// ******************************* Internal class *****************************
struct vtkMultiSliceContextItem::vtkInternal
{
  int ActiveSize;
  int EdgeMargin;
  vtkNew<vtkAxis> Axis;
  vtkRectf ActiveArea;
  int ActiveSliceIndex;
  std::vector<double> Slices;
  std::vector<bool> SlicesVisibility;
  double LengthForRatio;
  // We assume that we won't have more than 255 slices by axis
  double VisibleSliceValues[255];

  // ----

  vtkInternal(vtkMultiSliceContextItem* parent)
  {
    this->ActiveSize = 10;
    this->EdgeMargin = 15;
    this->ActiveArea.Set(0, 0, 0, 0);
    this->ActiveSliceIndex = -1; // Nothing is selected for now
    parent->AddItem(this->Axis.GetPointer());
  }

  // ----

  bool IsInActiveArea(float x, float y)
  {
    return x >= this->ActiveArea.GetX() && y >= this->ActiveArea.GetY() &&
      x <= (this->ActiveArea.GetX() + this->ActiveArea.GetWidth()) &&
      y <= (this->ActiveArea.GetY() + this->ActiveArea.GetHeight());
  }

  // Return -1 if not found
  int FindSliceIndex(double value, double epsilon)
  {
    double min = value - epsilon;
    double max = value + epsilon;
    size_t size = this->Slices.size();
    for (size_t index = 0; index < size; index++)
    {
      if (this->Slices[index] >= min && this->Slices[index] <= max)
      {
        return static_cast<int>(index);
      }
    }
    return -1;
  }

  // ----

  double* UpdateVisibleValues(int& size)
  {
    std::vector<bool>::iterator visIter = this->SlicesVisibility.begin();
    std::vector<double>::iterator valueIter = this->Slices.begin();
    for (size = 0; valueIter != this->Slices.end() && size < 255; valueIter++, visIter++)
    {
      if (*visIter)
      {
        this->VisibleSliceValues[size] = *valueIter;
        size++;
      }
    }
    return this->VisibleSliceValues;
  }
};

//-----------------------------------------------------------------------------
vtkStandardNewMacro(vtkMultiSliceContextItem);
//-----------------------------------------------------------------------------
vtkMultiSliceContextItem::vtkMultiSliceContextItem()
{
  this->Internal = new vtkInternal(this);
}

//-----------------------------------------------------------------------------
vtkMultiSliceContextItem::~vtkMultiSliceContextItem()
{
  delete this->Internal;
  this->Internal = nullptr;
}

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

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::Paint(vtkContext2D* painter)
{
  // short var name for internal vars
  vtkRectf* activeArea = &this->Internal->ActiveArea;
  vtkAxis* axis = this->Internal->Axis.GetPointer();
  int activeSize = this->Internal->ActiveSize;
  int edgeMargin = this->Internal->EdgeMargin;
  int sliceHandleMargin = 2;

  // Local vars
  int width = this->GetScene()->GetViewWidth();
  int height = this->GetScene()->GetViewHeight();
  int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
  float slicerX1 = 0.f, slicerY1 = 0.f, slicerX2 = 0.f, slicerY2 = 0.f;
  slicerX2 = slicerY2 = activeSize;

  // Reset active area
  activeArea->Set(0, 0, width, height);

  switch (axis->GetPosition())
  {
    case vtkAxis::LEFT:
      activeArea->SetX(width - activeSize);
      slicerX2 = width - sliceHandleMargin;
      slicerX1 = x1 = x2 = (width - activeSize);
      y1 = edgeMargin;
      y2 = height - edgeMargin;
      activeArea->SetWidth(activeSize);
      this->Internal->LengthForRatio = height - edgeMargin - edgeMargin;
      break;
    case vtkAxis::RIGHT:
      slicerX2 = sliceHandleMargin;
      slicerX1 = x1 = x2 = activeSize;
      y1 = edgeMargin;
      y2 = height - edgeMargin;
      activeArea->SetWidth(activeSize);
      this->Internal->LengthForRatio = height - edgeMargin - edgeMargin;
      break;
    case vtkAxis::BOTTOM:
      activeArea->SetY(height - activeSize);
      slicerY2 = height - sliceHandleMargin;
      slicerY1 = y1 = y2 = (height - activeSize);
      x1 = edgeMargin;
      x2 = width - edgeMargin;
      activeArea->SetHeight(activeSize);
      this->Internal->LengthForRatio = width - edgeMargin - edgeMargin;
      break;
    case vtkAxis::TOP:
      slicerY2 = sliceHandleMargin;
      slicerY1 = y1 = y2 = activeSize;
      x1 = edgeMargin;
      x2 = width - edgeMargin;
      activeArea->SetHeight(activeSize);
      this->Internal->LengthForRatio = width - edgeMargin - edgeMargin;
      break;
  }

  // Update the axis
  axis->SetPoint1(x1, y1);
  axis->SetPoint2(x2, y2);
  axis->GetBoundingRect(painter);
  axis->Update();

  // Draw the slices positions
  if (!this->Internal->Slices.empty())
  {
    // Fill the triangles
    vtkNew<vtkBrush> brush;

    // Draw the triangles
    double range[2];
    float polyX[4], polyY[4];
    axis->GetRange(range);
    double rangeLength = range[1] - range[0];
    std::vector<double>::iterator pos = this->Internal->Slices.begin();
    int index = 0;
    for (; pos != this->Internal->Slices.end(); pos++, index++)
    {
      if (this->Internal->ActiveSliceIndex == index)
      {
        brush->SetColor(255, 255, 255);
        painter->ApplyBrush(brush.GetPointer());
      }
      else if (this->Internal->SlicesVisibility[index])
      {
        brush->SetColor(0, 0, 0);
        painter->ApplyBrush(brush.GetPointer());
      }
      else
      {
        brush->SetColor(200, 200, 200);
        painter->ApplyBrush(brush.GetPointer());
      }
      float screen =
        edgeMargin + (this->Internal->LengthForRatio * (*pos - range[0]) / rangeLength);
      switch (axis->GetPosition())
      {
        case vtkAxis::LEFT:
        case vtkAxis::RIGHT:
          polyX[0] = polyX[3] = slicerX1;
          polyY[0] = polyY[3] = screen;
          polyX[1] = slicerX2;
          polyY[1] = screen - 4;
          polyX[2] = slicerX2;
          polyY[2] = screen + 4;
          break;
        case vtkAxis::BOTTOM:
        case vtkAxis::TOP:
          polyX[0] = polyX[3] = screen;
          polyY[0] = polyY[3] = slicerY1;
          polyX[1] = screen - 4;
          polyY[1] = slicerY2;
          polyX[2] = screen + 4;
          polyY[2] = slicerY2;
          break;
      }
      painter->DrawPolygon(polyX, polyY, 4);
    }
  }

  return this->vtkContextItem::Paint(painter);
}

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::Hit(const vtkContextMouseEvent& mouse)
{
  vtkVector2f position = mouse.GetPos();
  return this->Internal->IsInActiveArea(position.GetX(), position.GetY());
}

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::MouseButtonPressEvent(const vtkContextMouseEvent& mouse)
{
  vtkVector2f position = mouse.GetPos();
  double value = 0;
  switch (this->Internal->Axis->GetPosition())
  {
    case vtkAxis::LEFT:
    case vtkAxis::RIGHT:
      value = this->ScreenToRange(position.GetY() - this->Internal->EdgeMargin);
      break;
    case vtkAxis::BOTTOM:
    case vtkAxis::TOP:
      value = this->ScreenToRange(position.GetX() - this->Internal->EdgeMargin);
      break;
  }
  // find corresponding slice index
  this->Internal->ActiveSliceIndex = this->Internal->FindSliceIndex(value, this->ComputeEpsilon());

  return true;
}

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::MouseButtonReleaseEvent(const vtkContextMouseEvent& mouse)
{
  // Toggle visibility
  if (this->Internal->ActiveSliceIndex != -1 &&
    mouse.GetButton() == vtkContextMouseEvent::RIGHT_BUTTON &&
    mouse.GetModifiers() == vtkContextMouseEvent::NO_MODIFIER)
  {
    this->Internal->SlicesVisibility[Internal->ActiveSliceIndex] =
      !this->Internal->SlicesVisibility[Internal->ActiveSliceIndex];
    this->InvokeEvent(vtkMultiSliceContextItem::ModifySliceEvent);
  }

  // Notify any release action to the user
  if (this->Internal->ActiveSliceIndex != -1)
  {
    int data[3] = { mouse.GetButton(), mouse.GetModifiers(), this->Internal->ActiveSliceIndex };
    this->InvokeEvent(vtkCommand::EndInteractionEvent, data);
  }

  // Deselect active slice
  this->Internal->ActiveSliceIndex = -1;
  this->forceRender();
  return true;
}

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::MouseDoubleClickEvent(const vtkContextMouseEvent& mouse)
{
  double value = 0;
  int index = -1;
  switch (this->Internal->Axis->GetPosition())
  {
    case vtkAxis::LEFT:
    case vtkAxis::RIGHT:
      value = this->ScreenToRange(mouse.GetPos().GetY() - this->Internal->EdgeMargin);
      break;
    case vtkAxis::BOTTOM:
    case vtkAxis::TOP:
      value = this->ScreenToRange(mouse.GetPos().GetX() - this->Internal->EdgeMargin);
      break;
  }

  if ((index = this->Internal->FindSliceIndex(value, this->ComputeEpsilon())) == -1)
  {
    // Create
    index = static_cast<int>(this->Internal->Slices.size());
    this->Internal->Slices.push_back(value);
    this->Internal->SlicesVisibility.push_back(true);

    this->Internal->ActiveSliceIndex = index;
    this->InvokeEvent(vtkMultiSliceContextItem::AddSliceEvent);
    this->Internal->ActiveSliceIndex = -1;
  }
  else
  {
    // Delete
    std::vector<double>::iterator pos = this->Internal->Slices.begin();
    std::vector<bool>::iterator posV = this->Internal->SlicesVisibility.begin();
    pos += index;
    posV += index;
    this->Internal->Slices.erase(pos);
    this->Internal->SlicesVisibility.erase(posV);

    this->Internal->ActiveSliceIndex = index;
    this->InvokeEvent(vtkMultiSliceContextItem::RemoveSliceEvent);
    this->Internal->ActiveSliceIndex = -1;
  }

  // Make sure we paint that new slice
  this->forceRender();
  return true;
}

//-----------------------------------------------------------------------------
bool vtkMultiSliceContextItem::MouseMoveEvent(const vtkContextMouseEvent& mouse)
{
  if (Internal->ActiveSliceIndex > -1)
  {
    double value = 0;
    switch (this->Internal->Axis->GetPosition())
    {
      case vtkAxis::LEFT:
      case vtkAxis::RIGHT:
        value = this->ScreenToRange(mouse.GetPos().GetY() - this->Internal->EdgeMargin);
        break;
      case vtkAxis::BOTTOM:
      case vtkAxis::TOP:
        value = this->ScreenToRange(mouse.GetPos().GetX() - this->Internal->EdgeMargin);
        break;
    }
    this->Internal->Slices[Internal->ActiveSliceIndex] = value;
    this->forceRender();
    this->InvokeEvent(vtkMultiSliceContextItem::ModifySliceEvent);
  }

  return true;
}

//-----------------------------------------------------------------------------
double vtkMultiSliceContextItem::ScreenToRange(float position)
{
  double range[2];
  this->Internal->Axis->GetRange(range);
  double rangeLength = range[1] - range[0];
  double value = range[0] + rangeLength * (position / this->Internal->LengthForRatio);

  // Cap the value to the range
  if (value > range[1])
  {
    value = range[1];
  }
  else if (value < range[0])
  {
    value = range[0];
  }

  return value;
}

//-----------------------------------------------------------------------------
double vtkMultiSliceContextItem::ComputeEpsilon(int numberOfPixel)
{
  double range[2];
  this->Internal->Axis->GetRange(range);
  double rangeLength = range[1] - range[0];
  return rangeLength * (numberOfPixel / this->Internal->LengthForRatio);
}

//-----------------------------------------------------------------------------
void vtkMultiSliceContextItem::forceRender()
{
  // Mark the scene as dirty
  this->GetScene()->SetDirty(true);
}

//-----------------------------------------------------------------------------
const double* vtkMultiSliceContextItem::GetVisibleSlices(int& nbSlices) const
{
  return this->Internal->UpdateVisibleValues(nbSlices);
}

//-----------------------------------------------------------------------------
const double* vtkMultiSliceContextItem::GetSlices(int& nbSlices) const
{
  nbSlices = static_cast<int>(this->Internal->Slices.size());
  return nbSlices > 0 ? this->Internal->Slices.data() : nullptr;
}

//-----------------------------------------------------------------------------
void vtkMultiSliceContextItem::SetSlices(double* values, bool* visibility, int numberOfSlices)
{
  this->Internal->Slices.clear();
  this->Internal->SlicesVisibility.clear();
  for (int i = 0; i < numberOfSlices; ++i)
  {
    this->Internal->Slices.push_back(values[i]);
    this->Internal->SlicesVisibility.push_back(visibility[i]);
  }
}

//-----------------------------------------------------------------------------
vtkAxis* vtkMultiSliceContextItem::GetAxis()
{
  return this->Internal->Axis.GetPointer();
}

//-----------------------------------------------------------------------------
void vtkMultiSliceContextItem::SetActiveSize(int size)
{
  this->Internal->ActiveSize = size;
}

//-----------------------------------------------------------------------------
void vtkMultiSliceContextItem::SetEdgeMargin(int size)
{
  this->Internal->EdgeMargin = size;
}
//-----------------------------------------------------------------------------
double vtkMultiSliceContextItem::GetSliceValue(int sliceIndex)
{
  return this->Internal->Slices[sliceIndex];
}

//-----------------------------------------------------------------------------
int vtkMultiSliceContextItem::GetNumberOfSlices()
{
  return static_cast<int>(this->Internal->Slices.size());
}

//-----------------------------------------------------------------------------
int vtkMultiSliceContextItem::GetActiveSliceIndex()
{
  return this->Internal->ActiveSliceIndex;
}
