// 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.

#include "flutter/lib/ui/painting/multi_frame_codec.h"

#include <utility>

#include "flutter/fml/make_copyable.h"
#include "flutter/lib/ui/painting/display_list_image_gpu.h"
#include "flutter/lib/ui/painting/image.h"
#if IMPELLER_SUPPORTS_RENDERING
#include "flutter/lib/ui/painting/image_decoder_impeller.h"
#endif  // IMPELLER_SUPPORTS_RENDERING
#include "third_party/dart/runtime/include/dart_api.h"
#include "third_party/skia/include/codec/SkCodecAnimation.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "third_party/skia/include/gpu/ganesh/SkImageGanesh.h"
#include "third_party/tonic/logging/dart_invoke.h"

namespace flutter {

MultiFrameCodec::MultiFrameCodec(std::shared_ptr<ImageGenerator> generator)
    : state_(new State(std::move(generator))) {}

MultiFrameCodec::~MultiFrameCodec() = default;

MultiFrameCodec::State::State(std::shared_ptr<ImageGenerator> generator)
    : generator_(std::move(generator)),
      frameCount_(generator_->GetFrameCount()),
      repetitionCount_(generator_->GetPlayCount() ==
                               ImageGenerator::kInfinitePlayCount
                           ? -1
                           : generator_->GetPlayCount() - 1),
      is_impeller_enabled_(UIDartState::Current()->IsImpellerEnabled()),
      nextFrameIndex_(0) {}

static void InvokeNextFrameCallback(
    const fml::RefPtr<CanvasImage>& image,
    int duration,
    const std::string& decode_error,
    std::unique_ptr<DartPersistentValue> callback,
    size_t trace_id) {
  std::shared_ptr<tonic::DartState> dart_state = callback->dart_state().lock();
  if (!dart_state) {
    FML_DLOG(ERROR) << "Could not acquire Dart state while attempting to fire "
                       "next frame callback.";
    return;
  }
  tonic::DartState::Scope scope(dart_state);
  tonic::DartInvoke(callback->value(),
                    {tonic::ToDart(image), tonic::ToDart(duration),
                     tonic::ToDart(decode_error)});
}

std::pair<sk_sp<DlImage>, std::string>
MultiFrameCodec::State::GetNextFrameImage(
    fml::WeakPtr<GrDirectContext> resourceContext,
    const std::shared_ptr<const fml::SyncSwitch>& gpu_disable_sync_switch,
    const std::shared_ptr<impeller::Context>& impeller_context,
    fml::RefPtr<flutter::SkiaUnrefQueue> unref_queue) {
  SkBitmap bitmap = SkBitmap();
  SkImageInfo info = generator_->GetInfo().makeColorType(kN32_SkColorType);
  if (info.alphaType() == kUnpremul_SkAlphaType) {
    SkImageInfo updated = info.makeAlphaType(kPremul_SkAlphaType);
    info = updated;
  }
  if (!bitmap.tryAllocPixels(info)) {
    std::ostringstream ostr;
    ostr << "Failed to allocate memory for bitmap of size "
         << info.computeMinByteSize() << "B";
    std::string decode_error = ostr.str();
    FML_LOG(ERROR) << decode_error;
    return std::make_pair(nullptr, decode_error);
  }

  ImageGenerator::FrameInfo frameInfo =
      generator_->GetFrameInfo(nextFrameIndex_);

  const int requiredFrameIndex =
      frameInfo.required_frame.value_or(SkCodec::kNoFrame);

  if (requiredFrameIndex != SkCodec::kNoFrame) {
    // We are here when the frame said |disposal_method| is
    // `DisposalMethod::kKeep` or `DisposalMethod::kRestorePrevious` and
    // |requiredFrameIndex| is set to ex-frame or ex-ex-frame.
    if (!lastRequiredFrame_.has_value()) {
      FML_DLOG(INFO)
          << "Frame " << nextFrameIndex_ << " depends on frame "
          << requiredFrameIndex
          << " and no required frames are cached. Using blank slate instead.";
    } else {
      // Copy the previous frame's output buffer into the current frame as the
      // starting point.
      bitmap.writePixels(lastRequiredFrame_->pixmap());
      if (restoreBGColorRect_.has_value()) {
        bitmap.erase(SK_ColorTRANSPARENT, restoreBGColorRect_.value());
      }
    }
  }

  // Write the new frame to the output buffer. The bitmap pixels as supplied
  // are already set in accordance with the previous frame's disposal policy.
  if (!generator_->GetPixels(info, bitmap.getPixels(), bitmap.rowBytes(),
                             nextFrameIndex_, requiredFrameIndex)) {
    std::ostringstream ostr;
    ostr << "Could not getPixels for frame " << nextFrameIndex_;
    std::string decode_error = ostr.str();
    FML_LOG(ERROR) << decode_error;
    return std::make_pair(nullptr, decode_error);
  }

  const bool keep_current_frame =
      frameInfo.disposal_method == SkCodecAnimation::DisposalMethod::kKeep;
  const bool restore_previous_frame =
      frameInfo.disposal_method ==
      SkCodecAnimation::DisposalMethod::kRestorePrevious;
  const bool previous_frame_available = lastRequiredFrame_.has_value();

  // Store the current frame in `lastRequiredFrame_` if the frame's disposal
  // method indicates we should do so.
  // * When the disposal method is "Keep", the stored frame should always be
  //   overwritten with the new frame we just crafted.
  // * When the disposal method is "RestorePrevious", the previously stored
  //   frame should be retained and used as the backdrop for the next frame
  //   again. If there isn't already a stored frame, that means we haven't
  //   rendered any frames yet! When this happens, we just fall back to "Keep"
  //   behavior and store the current frame as the backdrop of the next frame.

  if (keep_current_frame ||
      (previous_frame_available && !restore_previous_frame)) {
    // Replace the stored frame. The `lastRequiredFrame_` will get used as the
    // starting backdrop for the next frame.
    lastRequiredFrame_ = bitmap;
    lastRequiredFrameIndex_ = nextFrameIndex_;
  }

  if (frameInfo.disposal_method ==
      SkCodecAnimation::DisposalMethod::kRestoreBGColor) {
    restoreBGColorRect_ = frameInfo.disposal_rect;
  } else {
    restoreBGColorRect_.reset();
  }

#if IMPELLER_SUPPORTS_RENDERING
  if (is_impeller_enabled_) {
    // This is safe regardless of whether the GPU is available or not because
    // without mipmap creation there is no command buffer encoding done.
    return ImageDecoderImpeller::UploadTextureToStorage(
        impeller_context, std::make_shared<SkBitmap>(bitmap),
        std::make_shared<fml::SyncSwitch>(),
        impeller::StorageMode::kHostVisible,
        /*create_mips=*/false);
  }
#endif  // IMPELLER_SUPPORTS_RENDERING

  sk_sp<SkImage> skImage;
  gpu_disable_sync_switch->Execute(
      fml::SyncSwitch::Handlers()
          .SetIfTrue([&skImage, &bitmap] {
            // Defer decoding until time of draw later on the raster thread.
            // Can happen when GL operations are currently forbidden such as
            // in the background on iOS.
            skImage = SkImages::RasterFromBitmap(bitmap);
          })
          .SetIfFalse([&skImage, &resourceContext, &bitmap] {
            if (resourceContext) {
              SkPixmap pixmap(bitmap.info(), bitmap.pixelRef()->pixels(),
                              bitmap.pixelRef()->rowBytes());
              skImage = SkImages::CrossContextTextureFromPixmap(
                  resourceContext.get(), pixmap, true);
            } else {
              // Defer decoding until time of draw later on the raster thread.
              // Can happen when GL operations are currently forbidden such as
              // in the background on iOS.
              skImage = SkImages::RasterFromBitmap(bitmap);
            }
          }));

  return std::make_pair(DlImageGPU::Make({skImage, std::move(unref_queue)}),
                        std::string());
}

void MultiFrameCodec::State::GetNextFrameAndInvokeCallback(
    std::unique_ptr<DartPersistentValue> callback,
    const fml::RefPtr<fml::TaskRunner>& ui_task_runner,
    fml::WeakPtr<GrDirectContext> resourceContext,
    fml::RefPtr<flutter::SkiaUnrefQueue> unref_queue,
    const std::shared_ptr<const fml::SyncSwitch>& gpu_disable_sync_switch,
    size_t trace_id,
    const std::shared_ptr<impeller::Context>& impeller_context) {
  fml::RefPtr<CanvasImage> image = nullptr;
  int duration = 0;
  sk_sp<DlImage> dlImage;
  std::string decode_error;
  std::tie(dlImage, decode_error) =
      GetNextFrameImage(std::move(resourceContext), gpu_disable_sync_switch,
                        impeller_context, std::move(unref_queue));
  if (dlImage) {
    image = CanvasImage::Create();
    image->set_image(dlImage);
    ImageGenerator::FrameInfo frameInfo =
        generator_->GetFrameInfo(nextFrameIndex_);
    duration = frameInfo.duration;
  }
  nextFrameIndex_ = (nextFrameIndex_ + 1) % frameCount_;

  // The static leak checker gets confused by the use of fml::MakeCopyable.
  // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
  ui_task_runner->PostTask(fml::MakeCopyable(
      [callback = std::move(callback), image = std::move(image),
       decode_error = std::move(decode_error), duration, trace_id]() mutable {
        InvokeNextFrameCallback(image, duration, decode_error,
                                std::move(callback), trace_id);
      }));
}

Dart_Handle MultiFrameCodec::getNextFrame(Dart_Handle callback_handle) {
  static size_t trace_counter = 1;
  const size_t trace_id = trace_counter++;

  if (!Dart_IsClosure(callback_handle)) {
    return tonic::ToDart("Callback must be a function");
  }

  auto* dart_state = UIDartState::Current();

  const auto& task_runners = dart_state->GetTaskRunners();

  if (state_->frameCount_ == 0) {
    std::string decode_error("Could not provide any frame.");
    FML_LOG(ERROR) << decode_error;
    task_runners.GetUITaskRunner()->PostTask(fml::MakeCopyable(
        [trace_id, decode_error = std::move(decode_error),
         callback = std::make_unique<DartPersistentValue>(
             tonic::DartState::Current(), callback_handle)]() mutable {
          InvokeNextFrameCallback(nullptr, 0, decode_error, std::move(callback),
                                  trace_id);
        }));
    return Dart_Null();
  }

  task_runners.GetIOTaskRunner()->PostTask(fml::MakeCopyable(
      [callback = std::make_unique<DartPersistentValue>(
           tonic::DartState::Current(), callback_handle),
       weak_state = std::weak_ptr<MultiFrameCodec::State>(state_), trace_id,
       ui_task_runner = task_runners.GetUITaskRunner(),
       io_manager = dart_state->GetIOManager()]() mutable {
        auto state = weak_state.lock();
        if (!state) {
          ui_task_runner->PostTask(fml::MakeCopyable(
              [callback = std::move(callback)]() { callback->Clear(); }));
          return;
        }
        state->GetNextFrameAndInvokeCallback(
            std::move(callback), ui_task_runner,
            io_manager->GetResourceContext(), io_manager->GetSkiaUnrefQueue(),
            io_manager->GetIsGpuDisabledSyncSwitch(), trace_id,
            io_manager->GetImpellerContext());
      }));

  return Dart_Null();
  // The static leak checker gets confused by the control flow, unique
  // pointers and closures in this function.
  // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
}

int MultiFrameCodec::frameCount() const {
  return state_->frameCount_;
}

int MultiFrameCodec::repetitionCount() const {
  return state_->repetitionCount_;
}

}  // namespace flutter
