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

#include "vtkOpenGLFXAAFilter.h"

#include "vtk_glad.h"

#include "vtkFXAAOptions.h"
#include "vtkObjectFactory.h"
#include "vtkOpenGLBufferObject.h"
#include "vtkOpenGLError.h"
#include "vtkOpenGLQuadHelper.h"
#include "vtkOpenGLRenderTimer.h"
#include "vtkOpenGLRenderUtilities.h"
#include "vtkOpenGLRenderWindow.h"
#include "vtkOpenGLRenderer.h"
#include "vtkOpenGLShaderCache.h"
#include "vtkOpenGLState.h"
#include "vtkOpenGLVertexArrayObject.h"
#include "vtkShaderProgram.h"
#include "vtkTextureObject.h"
#include "vtkTimerLog.h"
#include "vtkTypeTraits.h"

#include <algorithm>
#include <cassert>

// Our fragment shader:
#include "vtkFXAAFilterFS.h"

// Define to perform/dump benchmarking info:
// #define FXAA_BENCHMARK

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkOpenGLFXAAFilter);

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

  os << indent << "RelativeContrastThreshold: " << this->RelativeContrastThreshold << "\n";
  os << indent << "HardContrastThreshold: " << this->HardContrastThreshold << "\n";
  os << indent << "SubpixelBlendLimit: " << this->SubpixelBlendLimit << "\n";
  os << indent << "SubpixelContrastThreshold: " << this->SubpixelContrastThreshold << "\n";
  os << indent << "EndpointSearchIterations: " << this->EndpointSearchIterations << "\n";
  os << indent << "UseHighQualityEndpoints: " << this->UseHighQualityEndpoints << "\n";

  os << indent << "DebugOptionValue: ";
  switch (this->DebugOptionValue)
  {
    default:
    case vtkFXAAOptions::FXAA_NO_DEBUG:
      os << "FXAA_NO_DEBUG\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_SUBPIXEL_ALIASING:
      os << "FXAA_DEBUG_SUBPIXEL_ALIASING\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_EDGE_DIRECTION:
      os << "FXAA_DEBUG_EDGE_DIRECTION\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_EDGE_NUM_STEPS:
      os << "FXAA_DEBUG_EDGE_NUM_STEPS\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_EDGE_DISTANCE:
      os << "FXAA_DEBUG_EDGE_DISTANCE\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_EDGE_SAMPLE_OFFSET:
      os << "FXAA_DEBUG_EDGE_SAMPLE_OFFSET\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_ONLY_SUBPIX_AA:
      os << "FXAA_DEBUG_ONLY_SUBPIX_AA\n";
      break;
    case vtkFXAAOptions::FXAA_DEBUG_ONLY_EDGE_AA:
      os << "FXAA_DEBUG_ONLY_EDGE_AA\n";
      break;
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::Execute(vtkOpenGLRenderer* ren)
{
  assert(ren);
  this->Renderer = ren;

  this->StartTimeQuery(this->PreparationTimer);
  this->Prepare();
  this->LoadInput();
  this->EndTimeQuery(this->PreparationTimer);

  this->StartTimeQuery(this->FXAATimer);
  this->ApplyFilter();
  this->EndTimeQuery(this->FXAATimer);

  this->Finalize();
  this->PrintBenchmark();

  this->Renderer = nullptr;
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::ReleaseGraphicsResources()
{
  this->FreeGLObjects();
  this->PreparationTimer->ReleaseGraphicsResources();
  this->FXAATimer->ReleaseGraphicsResources();
  if (this->QHelper)
  {
    delete this->QHelper;
    this->QHelper = nullptr;
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::UpdateConfiguration(vtkFXAAOptions* opts)
{
  // Use the setters -- some of these options will trigger a shader rebuild
  // when they change, and the setters hold the logic for determining this.
  this->SetRelativeContrastThreshold(opts->GetRelativeContrastThreshold());
  this->SetHardContrastThreshold(opts->GetHardContrastThreshold());
  this->SetSubpixelBlendLimit(opts->GetSubpixelBlendLimit());
  this->SetSubpixelContrastThreshold(opts->GetSubpixelContrastThreshold());
  this->SetEndpointSearchIterations(opts->GetEndpointSearchIterations());
  this->SetUseHighQualityEndpoints(opts->GetUseHighQualityEndpoints());
  this->SetDebugOptionValue(opts->GetDebugOptionValue());
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::SetUseHighQualityEndpoints(bool val)
{
  if (this->UseHighQualityEndpoints != val)
  {
    this->NeedToRebuildShader = true;
    this->Modified();
    this->UseHighQualityEndpoints = val;
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::SetDebugOptionValue(vtkFXAAOptions::DebugOption opt)
{
  if (this->DebugOptionValue != opt)
  {
    this->NeedToRebuildShader = true;
    this->Modified();
    this->DebugOptionValue = opt;
  }
}

//------------------------------------------------------------------------------
vtkOpenGLFXAAFilter::vtkOpenGLFXAAFilter()
  : BlendState(false)
  , DepthTestState(false)
  , PreparationTimer(new vtkOpenGLRenderTimer)
  , FXAATimer(new vtkOpenGLRenderTimer)
  , RelativeContrastThreshold(1.f / 8.f)
  , HardContrastThreshold(1.f / 16.f)
  , SubpixelBlendLimit(3.f / 4.f)
  , SubpixelContrastThreshold(1.f / 4.f)
  , EndpointSearchIterations(12)
  , UseHighQualityEndpoints(true)
  , DebugOptionValue(vtkFXAAOptions::FXAA_NO_DEBUG)
  , NeedToRebuildShader(true)
  , Renderer(nullptr)
  , Input(nullptr)
  , QHelper(nullptr)
{
  std::fill(this->Viewport, this->Viewport + 4, 0);
}

//------------------------------------------------------------------------------
vtkOpenGLFXAAFilter::~vtkOpenGLFXAAFilter()
{
  if (this->QHelper)
  {
    delete this->QHelper;
    this->QHelper = nullptr;
  }
  this->FreeGLObjects();
  delete PreparationTimer;
  delete FXAATimer;
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::Prepare()
{
  this->Renderer->GetTiledSizeAndOrigin(
    &this->Viewport[2], &this->Viewport[3], &this->Viewport[0], &this->Viewport[1]);

  // Check if we need to create a new working texture:
  if (this->Input)
  {
    unsigned int rendererWidth = static_cast<unsigned int>(this->Viewport[2]);
    unsigned int rendererHeight = static_cast<unsigned int>(this->Viewport[3]);
    if (this->Input->GetWidth() != rendererWidth || this->Input->GetHeight() != rendererHeight)
    {
      this->FreeGLObjects();
    }
  }

  if (!this->Input)
  {
    this->CreateGLObjects();
  }

  vtkOpenGLState* ostate = this->Renderer->GetState();
  this->BlendState = ostate->GetEnumState(GL_BLEND);
  this->DepthTestState = ostate->GetEnumState(GL_DEPTH_TEST);

#ifdef __APPLE__
  // Restore viewport to its original size. This is necessary only on
  // MacOS when HiDPI is supported. Enabling HiDPI has the side effect that
  // Cocoa will start overriding any glViewport calls in application code.
  // For reference, see QCocoaWindow::initialize().
  int origin[2];
  int usize, vsize;
  this->Renderer->GetTiledSizeAndOrigin(&usize, &vsize, origin, origin + 1);
  ostate->vtkglViewport(origin[0], origin[1], usize, vsize);
#endif

  ostate->vtkglDisable(GL_BLEND);
  ostate->vtkglDisable(GL_DEPTH_TEST);

  vtkOpenGLCheckErrorMacro("Error after saving GL state.");
}

//------------------------------------------------------------------------------
// Delete the vtkObject subclass pointed at by ptr if it is set.
namespace
{
template <typename T>
void DeleteHelper(T*& ptr)
{
  if (ptr)
  {
    ptr->Delete();
    ptr = nullptr;
  }
}
} // end anon namespace

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::FreeGLObjects()
{
  DeleteHelper(this->Input);
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::CreateGLObjects()
{
  assert(!this->Input);
  this->Input = vtkTextureObject::New();
  vtkOpenGLRenderWindow* renWin =
    static_cast<vtkOpenGLRenderWindow*>(this->Renderer->GetRenderWindow());
  this->Input->SetContext(renWin);
  this->Input->SetFormat(GL_RGBA);

  // we need to get the format of current color buffer in order to allocate the right format
  // for the texture used in FXAA
  int internalFormat = renWin->GetColorBufferInternalFormat(0);

  if (internalFormat != 0)
  {
    this->Input->SetInternalFormat(static_cast<unsigned int>(internalFormat));
  }
  else // the query failed, fallback to classic texture
  {
    // ES doesn't support GL_RGB8, and OpenGL 3 doesn't support GL_RGB.
    // What a world.
#ifdef GL_ES_VERSION_3_0
    this->Input->SetInternalFormat(GL_RGBA);
#else  // OpenGL ES
    this->Input->SetInternalFormat(GL_RGBA8);
#endif // OpenGL ES
  }

  // Required for FXAA, since we interpolate texels for blending.
  this->Input->SetMinificationFilter(vtkTextureObject::Linear);
  this->Input->SetMagnificationFilter(vtkTextureObject::Linear);

  // Clamp to edge, since we'll be sampling off-texture texels:
  this->Input->SetWrapS(vtkTextureObject::ClampToEdge);
  this->Input->SetWrapT(vtkTextureObject::ClampToEdge);
  this->Input->SetWrapR(vtkTextureObject::ClampToEdge);

  this->Input->Allocate2D(
    this->Viewport[2], this->Viewport[3], 4, vtkTypeTraits<vtkTypeUInt8>::VTK_TYPE_ID);
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::LoadInput()
{
  this->Input->CopyFromFrameBuffer(
    this->Viewport[0], this->Viewport[1], 0, 0, this->Viewport[2], this->Viewport[3]);
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::ApplyFilter()
{
  typedef vtkOpenGLRenderUtilities GLUtil;

  vtkOpenGLRenderWindow* renWin =
    static_cast<vtkOpenGLRenderWindow*>(this->Renderer->GetRenderWindow());

  this->Input->Activate();

  if (this->NeedToRebuildShader)
  {
    delete this->QHelper;
    this->QHelper = nullptr;
    this->NeedToRebuildShader = false;
  }

  if (!this->QHelper)
  {
    std::string fragShader = vtkFXAAFilterFS;
    this->SubstituteFragmentShader(fragShader);
    this->QHelper = new vtkOpenGLQuadHelper(renWin, GLUtil::GetFullScreenQuadVertexShader().c_str(),
      fragShader.c_str(), GLUtil::GetFullScreenQuadGeometryShader().c_str());
  }
  else
  {
    renWin->GetShaderCache()->ReadyShaderProgram(this->QHelper->Program);
  }

  vtkShaderProgram* program = this->QHelper->Program;
  if (!program)
  {
    vtkWarningMacro(
      "Unable to retrieve shader program from internal vtkOpenGLQuadHelper instance.");
    return;
  }
  program->SetUniformi("Input", this->Input->GetTextureUnit());
  float invTexSize[2] = { 1.f / static_cast<float>(this->Viewport[2]),
    1.f / static_cast<float>(this->Viewport[3]) };
  program->SetUniform2f("InvTexSize", invTexSize);

  program->SetUniformf("RelativeContrastThreshold", this->RelativeContrastThreshold);
  program->SetUniformf("HardContrastThreshold", this->HardContrastThreshold);
  program->SetUniformf("SubpixelBlendLimit", this->SubpixelBlendLimit);
  program->SetUniformf("SubpixelContrastThreshold", this->SubpixelContrastThreshold);
  program->SetUniformi("EndpointSearchIterations", this->EndpointSearchIterations);

  this->QHelper->Render();

  this->Input->Deactivate();
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::SubstituteFragmentShader(std::string& fragShader)
{
  if (this->UseHighQualityEndpoints)
  {
    vtkShaderProgram::Substitute(
      fragShader, "//VTK::EndpointAlgo::Def", "#define FXAA_USE_HIGH_QUALITY_ENDPOINTS");
  }

#define DEBUG_OPT_CASE(optName)                                                                    \
  case vtkFXAAOptions::optName:                                                                    \
    vtkShaderProgram::Substitute(fragShader, "//VTK::DebugOptions::Def", "#define " #optName);     \
    break

  switch (this->DebugOptionValue)
  {
    default:
    case vtkFXAAOptions::FXAA_NO_DEBUG:
      break;
      DEBUG_OPT_CASE(FXAA_DEBUG_SUBPIXEL_ALIASING);
      DEBUG_OPT_CASE(FXAA_DEBUG_EDGE_DIRECTION);
      DEBUG_OPT_CASE(FXAA_DEBUG_EDGE_NUM_STEPS);
      DEBUG_OPT_CASE(FXAA_DEBUG_EDGE_DISTANCE);
      DEBUG_OPT_CASE(FXAA_DEBUG_EDGE_SAMPLE_OFFSET);
      DEBUG_OPT_CASE(FXAA_DEBUG_ONLY_SUBPIX_AA);
      DEBUG_OPT_CASE(FXAA_DEBUG_ONLY_EDGE_AA);
  }

#undef DEBUG_OPT_CASE
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::Finalize()
{
  vtkOpenGLState* ostate = this->Renderer->GetState();
  if (this->BlendState)
  {
    ostate->vtkglEnable(GL_BLEND);
  }
  if (this->DepthTestState)
  {
    ostate->vtkglEnable(GL_DEPTH_TEST);
  }

  vtkOpenGLCheckErrorMacro("Error after restoring GL state.");
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::StartTimeQuery(vtkOpenGLRenderTimer* timer)
{
  // Since it may take a few frames for the results to become available,
  // check if we've started the timer already.
  if (!timer->Started())
  {
    timer->Start();
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::EndTimeQuery(vtkOpenGLRenderTimer* timer)
{
  // Since it may take a few frames for the results to become available,
  // check if we've stopped the timer already.
  if (!timer->Stopped())
  {
    timer->Stop();
  }
}

//------------------------------------------------------------------------------
void vtkOpenGLFXAAFilter::PrintBenchmark()
{
  if (this->PreparationTimer->Ready() && this->FXAATimer->Ready())
  {

#ifdef FXAA_BENCHMARK
    int numPixels = this->Input->GetWidth() * this->Input->GetHeight();
    float ptime = this->PreparationTimer->GetElapsedMilliseconds();
    float ftime = this->FXAATimer->GetElapsedMilliseconds();
    float ttime = ptime + ftime;

    float ptimePerPixel =
      (this->PreparationTimer->GetElapsedNanoseconds() / static_cast<float>(numPixels));
    float ftimePerPixel =
      (this->FXAATimer->GetElapsedNanoseconds() / static_cast<float>(numPixels));
    float ttimePerPixel = ptimePerPixel + ftimePerPixel;

    std::cerr << "FXAA Info:\n"
              << " - Number of pixels: " << numPixels << "\n"
              << " - Preparation time: " << ptime << "ms (" << ptimePerPixel << "ns per pixel)\n"
              << " - FXAA time: " << ftime << "ms (" << ftimePerPixel << "ns per pixel)\n"
              << " - Total time: " << ttime << "ms (" << ttimePerPixel << "ns per pixel)\n";
#endif // FXAA_BENCHMARK

    this->PreparationTimer->Reset();
    this->FXAATimer->Reset();
  }
}
VTK_ABI_NAMESPACE_END
