/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ScreenshotGrabber.h"

#include "mozilla/ProfilerMarkers.h"
#include "mozilla/RefPtr.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"

#include "mozilla/layers/Compositor.h"
#include "mozilla/layers/ProfilerScreenshots.h"
#include "mozilla/layers/TextureHost.h"
#include "mozilla/gfx/Point.h"
#include "nsTArray.h"

namespace mozilla {

using namespace gfx;

namespace layers {
namespace profiler_screenshots {

/**
 * The actual implementation of screenshot grabbing.
 * The ScreenshotGrabberImpl object is destroyed if the profiler is
 * disabled and MaybeGrabScreenshot notices it.
 */
class ScreenshotGrabberImpl final {
 public:
  explicit ScreenshotGrabberImpl(const IntSize& aBufferSize);
  ~ScreenshotGrabberImpl();

  void GrabScreenshot(Window& aWindow, const IntSize& aWindowSize);
  void ProcessQueue();

 private:
  struct QueueItem final {
    mozilla::TimeStamp mTimeStamp;
    RefPtr<AsyncReadbackBuffer> mScreenshotBuffer;
    IntSize mScreenshotSize;
    IntSize mWindowSize;
  };

  RefPtr<RenderSource> ScaleDownWindowRenderSourceToSize(
      Window& aWindow, const IntSize& aDestSize,
      RenderSource* aWindowRenderSource, size_t aLevel);

  already_AddRefed<AsyncReadbackBuffer> TakeNextBuffer(Window& aWindow);
  void ReturnBuffer(AsyncReadbackBuffer* aBuffer);

  nsTArray<RefPtr<DownscaleTarget>> mCachedLevels;
  nsTArray<RefPtr<AsyncReadbackBuffer>> mAvailableBuffers;
  Maybe<QueueItem> mCurrentFrameQueueItem;
  nsTArray<QueueItem> mQueue;
  RefPtr<ProfilerScreenshots> mProfilerScreenshots;
  const IntSize mBufferSize;
};

}  // namespace profiler_screenshots

ScreenshotGrabber::ScreenshotGrabber() = default;

ScreenshotGrabber::~ScreenshotGrabber() = default;

void ScreenshotGrabber::MaybeGrabScreenshot(
    profiler_screenshots::Window& aWindow, const IntSize& aWindowSize) {
  if (ProfilerScreenshots::IsEnabled()) {
    if (!mImpl) {
      mImpl = MakeUnique<profiler_screenshots::ScreenshotGrabberImpl>(
          ProfilerScreenshots::ScreenshotSize());
    }
    mImpl->GrabScreenshot(aWindow, aWindowSize);
  } else if (mImpl) {
    Destroy();
  }
}

void ScreenshotGrabber::MaybeProcessQueue() {
  if (ProfilerScreenshots::IsEnabled()) {
    if (!mImpl) {
      mImpl = MakeUnique<profiler_screenshots::ScreenshotGrabberImpl>(
          ProfilerScreenshots::ScreenshotSize());
    }
    mImpl->ProcessQueue();
  } else if (mImpl) {
    Destroy();
  }
}

void ScreenshotGrabber::NotifyEmptyFrame() {
  PROFILER_MARKER_UNTYPED("NoCompositorScreenshot because nothing changed",
                          GRAPHICS);
}

void ScreenshotGrabber::Destroy() { mImpl = nullptr; }

namespace profiler_screenshots {

ScreenshotGrabberImpl::ScreenshotGrabberImpl(const IntSize& aBufferSize)
    : mBufferSize(aBufferSize) {}

ScreenshotGrabberImpl::~ScreenshotGrabberImpl() {
  // Any queue items in mQueue or mCurrentFrameQueueItem will be lost.
  // That's ok: Either the profiler has stopped and we don't care about these
  // screenshots, or the window is closing and we don't really need the last
  // few frames from the window.
}

// Scale down aWindowRenderSource into a RenderSource of size
// mBufferSize * (1 << aLevel) and return that RenderSource.
// Don't scale down by more than a factor of 2 with a single scaling operation,
// because it'll look bad. If higher scales are needed, use another
// intermediate target by calling this function recursively with aLevel + 1.
RefPtr<RenderSource> ScreenshotGrabberImpl::ScaleDownWindowRenderSourceToSize(
    Window& aWindow, const IntSize& aDestSize,
    RenderSource* aWindowRenderSource, size_t aLevel) {
  if (aLevel == mCachedLevels.Length()) {
    mCachedLevels.AppendElement(
        aWindow.CreateDownscaleTarget(mBufferSize * (1 << aLevel)));
  }
  MOZ_RELEASE_ASSERT(aLevel < mCachedLevels.Length());

  RefPtr<RenderSource> renderSource = aWindowRenderSource;
  IntSize sourceSize = aWindowRenderSource->Size();
  if (sourceSize.width > aDestSize.width * 2) {
    sourceSize = aDestSize * 2;
    renderSource = ScaleDownWindowRenderSourceToSize(
        aWindow, sourceSize, aWindowRenderSource, aLevel + 1);
  }

  if (renderSource) {
    if (mCachedLevels[aLevel]->DownscaleFrom(
            renderSource, IntRect({}, sourceSize), IntRect({}, aDestSize))) {
      return mCachedLevels[aLevel]->AsRenderSource();
    }
  }
  return nullptr;
}

void ScreenshotGrabberImpl::GrabScreenshot(Window& aWindow,
                                           const IntSize& aWindowSize) {
  RefPtr<RenderSource> windowRenderSource =
      aWindow.GetWindowContents(aWindowSize);

  if (!windowRenderSource) {
    PROFILER_MARKER_UNTYPED(
        "NoCompositorScreenshot because of unsupported compositor "
        "configuration",
        GRAPHICS);
    return;
  }

  Size windowSize(aWindowSize);
  float scale = std::min(mBufferSize.width / windowSize.width,
                         mBufferSize.height / windowSize.height);
  IntSize scaledSize = IntSize::Round(windowSize * scale);
  RefPtr<RenderSource> scaledTarget = ScaleDownWindowRenderSourceToSize(
      aWindow, scaledSize, windowRenderSource, 0);

  if (!scaledTarget) {
    PROFILER_MARKER_UNTYPED(
        "NoCompositorScreenshot because ScaleDownWindowRenderSourceToSize "
        "failed",
        GRAPHICS);
    return;
  }

  RefPtr<AsyncReadbackBuffer> buffer = TakeNextBuffer(aWindow);
  if (!buffer) {
    PROFILER_MARKER_UNTYPED(
        "NoCompositorScreenshot because AsyncReadbackBuffer creation failed",
        GRAPHICS);
    return;
  }

  buffer->CopyFrom(scaledTarget);

  // This QueueItem will be added to the queue at the end of the next call to
  // ProcessQueue(). This ensures that the buffer isn't mapped into main memory
  // until the next frame. If we did it in this frame, we'd block on the GPU.
  mCurrentFrameQueueItem =
      Some(QueueItem{TimeStamp::Now(), std::move(buffer), scaledSize,
                     windowRenderSource->Size()});
}

already_AddRefed<AsyncReadbackBuffer> ScreenshotGrabberImpl::TakeNextBuffer(
    Window& aWindow) {
  if (!mAvailableBuffers.IsEmpty()) {
    RefPtr<AsyncReadbackBuffer> buffer = mAvailableBuffers[0];
    mAvailableBuffers.RemoveElementAt(0);
    return buffer.forget();
  }
  return aWindow.CreateAsyncReadbackBuffer(mBufferSize);
}

void ScreenshotGrabberImpl::ReturnBuffer(AsyncReadbackBuffer* aBuffer) {
  mAvailableBuffers.AppendElement(aBuffer);
}

void ScreenshotGrabberImpl::ProcessQueue() {
  if (!mQueue.IsEmpty()) {
    if (!mProfilerScreenshots) {
      mProfilerScreenshots = new ProfilerScreenshots();
    }
    for (const auto& item : mQueue) {
      mProfilerScreenshots->SubmitScreenshot(
          item.mWindowSize, item.mScreenshotSize, item.mTimeStamp,
          [&item](DataSourceSurface* aTargetSurface) {
            return item.mScreenshotBuffer->MapAndCopyInto(aTargetSurface,
                                                          item.mScreenshotSize);
          });
      ReturnBuffer(item.mScreenshotBuffer);
    }
  }
  mQueue.Clear();

  if (mCurrentFrameQueueItem) {
    mQueue.AppendElement(std::move(*mCurrentFrameQueueItem));
    mCurrentFrameQueueItem = Nothing();
  }
}

}  // namespace profiler_screenshots
}  // namespace layers
}  // namespace mozilla
