// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_FLOW_SKIA_GPU_OBJECT_H_
#define FLUTTER_FLOW_SKIA_GPU_OBJECT_H_

#include <mutex>
#include <queue>

#include "flutter/fml/memory/ref_counted.h"
#include "flutter/fml/memory/weak_ptr.h"
#include "flutter/fml/task_runner.h"
#include "flutter/fml/trace_event.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/gpu/GrBackendSurface.h"
#include "third_party/skia/include/gpu/GrDirectContext.h"

namespace flutter {

// A queue that holds Skia objects that must be destructed on the given task
// runner.
template <class T>
class UnrefQueue : public fml::RefCountedThreadSafe<UnrefQueue<T>> {
 public:
  using ResourceContext = T;

  void Unref(SkRefCnt* object) {
    if (drain_immediate_) {
      object->unref();
      return;
    }
    std::scoped_lock lock(mutex_);
    objects_.push_back(object);
    if (!drain_pending_) {
      drain_pending_ = true;
      task_runner_->PostDelayedTask(
          [strong = fml::Ref(this)]() { strong->Drain(); }, drain_delay_);
    }
  }

  void DeleteTexture(GrBackendTexture texture) {
    // drain_immediate_ should only be used on Impeller.
    FML_DCHECK(!drain_immediate_);
    std::scoped_lock lock(mutex_);
    textures_.push_back(texture);
    if (!drain_pending_) {
      drain_pending_ = true;
      task_runner_->PostDelayedTask(
          [strong = fml::Ref(this)]() { strong->Drain(); }, drain_delay_);
    }
  }

  // Usually, the drain is called automatically. However, during IO manager
  // shutdown (when the platform side reference to the OpenGL context is about
  // to go away), we may need to pre-emptively drain the unref queue. It is the
  // responsibility of the caller to ensure that no further unrefs are queued
  // after this call.
  void Drain() {
    TRACE_EVENT0("flutter", "SkiaUnrefQueue::Drain");
    std::deque<SkRefCnt*> skia_objects;
    std::deque<GrBackendTexture> textures;
    {
      std::scoped_lock lock(mutex_);
      objects_.swap(skia_objects);
      textures_.swap(textures);
      drain_pending_ = false;
    }
    DoDrain(skia_objects, textures, context_);
  }

  void UpdateResourceContext(sk_sp<ResourceContext> context) {
    context_ = context;
  }

 private:
  const fml::RefPtr<fml::TaskRunner> task_runner_;
  const fml::TimeDelta drain_delay_;
  std::mutex mutex_;
  std::deque<SkRefCnt*> objects_;
  std::deque<GrBackendTexture> textures_;
  bool drain_pending_;
  sk_sp<ResourceContext> context_;
  // Enabled when there is an impeller context, which removes the usage of
  // the queue altogether.
  bool drain_immediate_;

  // The `GrDirectContext* context` is only used for signaling Skia to
  // performDeferredCleanup. It can be nullptr when such signaling is not needed
  // (e.g., in unit tests).
  UnrefQueue(fml::RefPtr<fml::TaskRunner> task_runner,
             fml::TimeDelta delay,
             sk_sp<ResourceContext> context = nullptr,
             bool drain_immediate = false)
      : task_runner_(std::move(task_runner)),
        drain_delay_(delay),
        drain_pending_(false),
        context_(context),
        drain_immediate_(drain_immediate) {}

  ~UnrefQueue() {
    // The ResourceContext must be deleted on the task runner thread.
    // Transfer ownership of the UnrefQueue's ResourceContext reference
    // into a task queued to that thread.
    ResourceContext* raw_context = context_.release();
    fml::TaskRunner::RunNowOrPostTask(
        task_runner_, [objects = std::move(objects_),
                       textures = std::move(textures_), raw_context]() mutable {
          sk_sp<ResourceContext> context(raw_context);
          DoDrain(objects, textures, context);
          context.reset();
        });
  }

  // static
  static void DoDrain(const std::deque<SkRefCnt*>& skia_objects,
                      const std::deque<GrBackendTexture>& textures,
                      sk_sp<ResourceContext> context) {
    for (SkRefCnt* skia_object : skia_objects) {
      skia_object->unref();
    }

    if (context) {
      for (GrBackendTexture texture : textures) {
        context->deleteBackendTexture(texture);
      }

      if (!skia_objects.empty()) {
        context->performDeferredCleanup(std::chrono::milliseconds(0));
      }
    }
  }

  FML_FRIEND_REF_COUNTED_THREAD_SAFE(UnrefQueue);
  FML_FRIEND_MAKE_REF_COUNTED(UnrefQueue);
  FML_DISALLOW_COPY_AND_ASSIGN(UnrefQueue);
};

using SkiaUnrefQueue = UnrefQueue<GrDirectContext>;

/// An object whose deallocation needs to be performed on an specific unref
/// queue. The template argument U need to have a call operator that returns
/// that unref queue.
template <class T>
class SkiaGPUObject {
 public:
  using SkiaObjectType = T;

  SkiaGPUObject() = default;
  SkiaGPUObject(sk_sp<SkiaObjectType> object, fml::RefPtr<SkiaUnrefQueue> queue)
      : object_(std::move(object)), queue_(std::move(queue)) {
    FML_DCHECK(object_);
  }
  SkiaGPUObject(SkiaGPUObject&&) = default;
  ~SkiaGPUObject() { reset(); }

  SkiaGPUObject& operator=(SkiaGPUObject&&) = default;

  sk_sp<SkiaObjectType> skia_object() const { return object_; }

  void reset() {
    if (object_ && queue_) {
      queue_->Unref(object_.release());
    }
    queue_ = nullptr;
    FML_DCHECK(object_ == nullptr);
  }

 private:
  sk_sp<SkiaObjectType> object_;
  fml::RefPtr<SkiaUnrefQueue> queue_;

  FML_DISALLOW_COPY_AND_ASSIGN(SkiaGPUObject);
};

}  // namespace flutter

#endif  // FLUTTER_FLOW_SKIA_GPU_OBJECT_H_
