/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "CPostProcessor.h"

#include "CD3D12Device.h"
#include "CDebug.h"
#include "effect/CColorTransformEffect.h"
#include "effect/CDownsampleEffect.h"
#include "effect/CHDR16to10Effect.h"
#include "effect/CRGB24Effect.h"

#include <cstring>
#include <limits>
#include <utility>

namespace
{
  bool NearlyEqual(float a, float b, float tolerance)
  {
    const float delta = a - b;
    return delta >= -tolerance && delta <= tolerance;
  }
}

bool IsIdentityColorTransform(const D12ColorTransform& transform)
{
  static const float matrixTolerance = 1.0f / 1048576.0f;
  static const float lutTolerance    = 1.0f / 65535.0f;

  if (transform.matrixEnabled)
  {
    for (unsigned row = 0; row < 3; ++row)
      for (unsigned column = 0; column < 4; ++column)
      {
        const float expected = row == column ? 1.0f : 0.0f;
        const float effective =
          transform.matrix[row][column] * transform.scalar;
        if (!NearlyEqual(effective, expected, matrixTolerance))
          return false;
      }
  }

  if (transform.lutEnabled)
    for (unsigned i = 0; i < 4096; ++i)
    {
      const float expected = (float)i / 4095.0f;
      if (!NearlyEqual(transform.lut[i][0], expected, lutTolerance) ||
          !NearlyEqual(transform.lut[i][1], expected, lutTolerance) ||
          !NearlyEqual(transform.lut[i][2], expected, lutTolerance))
        return false;
    }

  return true;
}

static void CopyHDRMetadata(D12FrameFormat& dst, const D12FrameFormat& src)
{
  dst.hdrMetadata   = src.hdrMetadata;
  dst.sdrWhiteLevel = src.sdrWhiteLevel;
  std::memcpy(dst.displayPrimary, src.displayPrimary, sizeof(dst.displayPrimary));
  std::memcpy(dst.whitePoint, src.whitePoint, sizeof(dst.whitePoint));
  dst.maxDisplayLuminance       = src.maxDisplayLuminance;
  dst.minDisplayLuminance       = src.minDisplayLuminance;
  dst.maxContentLightLevel      = src.maxContentLightLevel;
  dst.maxFrameAverageLightLevel = src.maxFrameAverageLightLevel;
}

bool CPostProcessor::Init(std::shared_ptr<CD3D12Device> dx12Device,
  bool enableEffects)
{
  m_dx12Device = dx12Device;
  m_device      = dx12Device->GetDevice();
  m_effects.clear();

  if (!enableEffects)
    return true;

  std::unique_ptr<CColorTransformEffect> colorTransform(new CColorTransformEffect());
  if (colorTransform->Init(m_device))
  {
    DEBUG_INFO("Created post-processing effect: %s", colorTransform->GetName());
    m_effects.push_back(std::move(colorTransform));
  }
  else
    return false;

  std::unique_ptr<CDownsampleEffect> downsample(new CDownsampleEffect());
  if (downsample->Init(m_device))
  {
    DEBUG_INFO("Created post-processing effect: %s", downsample->GetName());
    m_effects.push_back(std::move(downsample));
  }

  std::unique_ptr<CHDR16to10Effect> hdr16to10(new CHDR16to10Effect());
  if (hdr16to10->Init(m_device))
  {
    DEBUG_INFO("Created post-processing effect: %s", hdr16to10->GetName());
    m_effects.push_back(std::move(hdr16to10));
  }
  else
    return false;

  std::unique_ptr<CRGB24Effect> rgb24(new CRGB24Effect());
  if (rgb24->Init(m_device))
  {
    DEBUG_INFO("Created post-processing effect: %s", rgb24->GetName());
    m_effects.push_back(std::move(rgb24));
  }

  return true;
}

void CPostProcessor::Reset()
{
  m_effects.clear();
  m_dx12Device.reset();
  m_device.Reset();
  m_srcFormat     = {};
  m_dstFormat     = {};
  m_copyLayout    = {};
  m_copyEffect    = nullptr;
  m_frameSize     = 0;
  m_pitch         = 0;
  m_effectsActive = false;
  m_configured    = false;
}

bool CPostProcessor::HasSameEffectChain(const CPostProcessor& other) const
{
  if (m_effects.size() != other.m_effects.size())
    return false;

  for (size_t i = 0; i < m_effects.size(); ++i)
    if (std::strcmp(m_effects[i]->GetName(),
                    other.m_effects[i]->GetName()) != 0)
      return false;

  return true;
}

bool CPostProcessor::ShareEffectState(const CPostProcessor& other)
{
  if (!HasSameEffectChain(other))
    return false;

  for (size_t i = 0; i < m_effects.size(); ++i)
    m_effects[i]->ShareState(*other.m_effects[i]);

  return true;
}

void CPostProcessor::Update(const D12FrameFormat& srcFormat)
{
  for (const auto& effect : m_effects)
    effect->Update(srcFormat);
}

bool CPostProcessor::NeedsReconfigure(const D12FrameFormat& srcFormat) const
{
  if (!m_configured ||
    srcFormat.desc.Width     != m_srcFormat.desc.Width  ||
    srcFormat.desc.Height    != m_srcFormat.desc.Height ||
    srcFormat.desc.Format    != m_srcFormat.desc.Format ||
    srcFormat.format         != m_srcFormat.format      ||
    srcFormat.width          != m_srcFormat.width       ||
    srcFormat.height         != m_srcFormat.height      ||
    srcFormat.hdr            != m_srcFormat.hdr         ||
    srcFormat.hdrPQ          != m_srcFormat.hdrPQ       ||
    srcFormat.colorTransform != m_srcFormat.colorTransform)
    return true;

  for (const auto& effect : m_effects)
    if (effect->NeedsReconfigure())
      return true;

  return false;
}

bool CPostProcessor::RequiresFullDamage() const
{
  for (const auto& effect : m_effects)
    if (effect->RequiresFullDamage())
      return true;

  return false;
}

bool CPostProcessor::Configure(const D12FrameFormat& srcFormat,
  bool * formatChanged)
{
  if (formatChanged)
    *formatChanged = false;

  if (!NeedsReconfigure(srcFormat))
  {
    // Static HDR metadata may change independently of the resource format.
    // Propagate it without recreating resources or post-processing state.
    CopyHDRMetadata(m_srcFormat, srcFormat);
    CopyHDRMetadata(m_dstFormat, srcFormat);
    return true;
  }

  D12FrameFormat       oldDst        = m_dstFormat;
  D12FrameFormat       cur           = srcFormat;
  CPostProcessEffect * outputEffect  = nullptr;
  bool                 effectsActive = false;

  for (const auto& effect : m_effects)
  {
    D12FrameFormat dst = cur;
    switch (effect->SetFormat(m_device, cur, dst))
    {
    case PostProcessStatus::SUCCESS:
      effect->Enabled = true;
      effectsActive   = true;
      cur             = dst;
      outputEffect    = effect.get();
      DEBUG_INFO("Post-processing effect active: %s", effect->GetName());
      break;

    case PostProcessStatus::BYPASS_EFFECT:
      effect->Enabled = false;
      break;

    case PostProcessStatus::FAILED:
      DEBUG_ERROR("Failed to configure post-processing effect: %s", effect->GetName());
      return false;
    }
  }

  D3D12_PLACED_SUBRESOURCE_FOOTPRINT copyLayout = {};
  CPostProcessEffect * copyEffect               = nullptr;
  unsigned             pitch                    = 0;
  unsigned             dataHeight               = 0;
  if (outputEffect && outputEffect->GetCopyLayout(&pitch, &dataHeight))
    copyEffect = outputEffect;
  else
  {
    if (cur.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D)
    {
      DEBUG_ERROR("Post-processing output has no copy implementation");
      return false;
    }

    m_device->GetCopyableFootprints(
      &cur.desc,
      0,
      1,
      0,
      &copyLayout,
      nullptr,
      nullptr,
      nullptr);
    pitch      = copyLayout.Footprint.RowPitch;
    dataHeight = cur.desc.Height;
  }

  if (!pitch || !dataHeight ||
      pitch > (std::numeric_limits<size_t>::max)() / dataHeight)
  {
    DEBUG_ERROR("Invalid post-processing output layout");
    return false;
  }

  const size_t frameSize = (size_t)pitch * dataHeight;
  if (copyEffect && cur.desc.Width < frameSize)
  {
    DEBUG_ERROR("Post-processing output buffer is too small");
    return false;
  }

  m_srcFormat     = srcFormat;
  m_dstFormat     = cur;
  m_copyLayout    = copyLayout;
  m_copyEffect    = copyEffect;
  m_frameSize     = frameSize;
  m_pitch         = pitch;
  m_effectsActive = effectsActive;
  m_configured    = true;
  if (formatChanged)
    *formatChanged =
      oldDst.desc.Width     != m_dstFormat.desc.Width      ||
      oldDst.desc.Height    != m_dstFormat.desc.Height     ||
      oldDst.desc.Format    != m_dstFormat.desc.Format     ||
      oldDst.dataWidth      != m_dstFormat.dataWidth       ||
      oldDst.dataHeight     != m_dstFormat.dataHeight      ||
      oldDst.pitch          != m_dstFormat.pitch           ||
      oldDst.format         != m_dstFormat.format          ||
      oldDst.width          != m_dstFormat.width           ||
      oldDst.height         != m_dstFormat.height          ||
      oldDst.hdr            != m_dstFormat.hdr             ||
      oldDst.hdrPQ          != m_dstFormat.hdrPQ           ||
      oldDst.sdrWhiteLevel  != m_dstFormat.sdrWhiteLevel   ||
      oldDst.colorTransform != m_dstFormat.colorTransform;
  return true;
}

void CPostProcessor::GetTimingToken(
  unsigned * effectIndex, uint64_t * token) const
{
  if (effectIndex)
    *effectIndex = 0;
  if (token)
    *token = 0;

  for (size_t i = 0; i < m_effects.size(); ++i)
  {
    const uint64_t value = m_effects[i]->GetTimingToken();
    if (!value)
      continue;

    if (effectIndex)
      *effectIndex = (unsigned)i;
    if (token)
      *token = value;
    return;
  }
}

void CPostProcessor::RecordTiming(
  unsigned effectIndex, uint64_t token, bool fullCopy, uint64_t totalTime)
{
  if (!token || effectIndex >= m_effects.size())
    return;

  m_effects[effectIndex]->RecordTiming(token, fullCopy, totalTime);
}

bool CPostProcessor::ShouldCopyFully(
  const RECT dirtyRects[], unsigned nbDirtyRects) const
{
  return m_copyEffect &&
    m_copyEffect->ShouldCopyFully(dirtyRects, nbDirtyRects);
}

void CPostProcessor::CopyToCandidate(
  const ComPtr<ID3D12GraphicsCommandList>& commandList,
  ID3D12Resource * dst, ID3D12Resource * src) const
{
  if (m_copyEffect)
  {
    m_copyEffect->CopyFrame(
      commandList, dst, src, nullptr, 0, true);
    return;
  }

  D3D12_TEXTURE_COPY_LOCATION srcLoc = {};
  srcLoc.pResource        = src;
  srcLoc.Type             = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
  srcLoc.SubresourceIndex = 0;

  D3D12_TEXTURE_COPY_LOCATION dstLoc = {};
  dstLoc.pResource       = dst;
  dstLoc.Type            = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
  dstLoc.PlacedFootprint = m_copyLayout;

  commandList->CopyTextureRegion(
    &dstLoc, 0, 0, 0, &srcLoc, nullptr);
}

void CPostProcessor::CopyFromCandidate(
  const ComPtr<ID3D12GraphicsCommandList>& commandList,
  ID3D12Resource * dst, ID3D12Resource * src,
  const RECT dirtyRects[], unsigned nbDirtyRects, bool fullCopy) const
{
  if (m_copyEffect)
  {
    m_copyEffect->CopyFrame(
      commandList, dst, src, dirtyRects, nbDirtyRects, fullCopy);
    return;
  }

  D3D12_TEXTURE_COPY_LOCATION srcLoc = {};
  srcLoc.pResource       = src;
  srcLoc.Type            = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
  srcLoc.PlacedFootprint = m_copyLayout;

  D3D12_TEXTURE_COPY_LOCATION dstLoc = {};
  dstLoc.pResource       = dst;
  dstLoc.Type            = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
  dstLoc.PlacedFootprint = m_copyLayout;

  if (fullCopy)
  {
    commandList->CopyBufferRegion(
      dst, 0, src, 0, m_frameSize);
    return;
  }

  for (const RECT * rect = dirtyRects;
       rect < dirtyRects + nbDirtyRects; ++rect)
  {
    D3D12_BOX box = {};
    box.left   = rect->left;
    box.top    = rect->top;
    box.front  = 0;
    box.right  = rect->right;
    box.bottom = rect->bottom;
    box.back   = 1;

    commandList->CopyTextureRegion(
      &dstLoc, box.left, box.top, 0, &srcLoc, &box);
  }
}

void CPostProcessor::AdjustFrameDamage(RECT dirtyRects[], unsigned * nbDirtyRects)
{
  for (const auto& effect : m_effects)
    if (effect->Enabled)
      effect->AdjustDamage(dirtyRects, nbDirtyRects);
}

ComPtr<ID3D12Resource> CPostProcessor::Run(
  const ComPtr<ID3D12GraphicsCommandList>& commandList,
  const ComPtr<ID3D12Resource>& src, RECT dirtyRects[],
  unsigned * nbDirtyRects)
{
  ComPtr<ID3D12Resource> next = src;
  for (const auto& effect : m_effects)
  {
    if (!effect->Enabled)
      continue;

    //DEBUG_TRACE("Run post-processing effect: %s", effect->GetName());
    effect->AdjustDamage(dirtyRects, nbDirtyRects);
    next = effect->Run(m_device, commandList, next, dirtyRects, nbDirtyRects);
  }

  return next;
}
