// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/viz/service/frame_sinks/frame_sink_bundle_impl.h"

#include <map>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#include "components/viz/common/display/display_scheduler_draw_result.h"
#include "components/viz/service/frame_sinks/compositor_frame_sink_impl.h"
#include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/viz/public/mojom/compositing/compositor_frame_sink.mojom.h"
#include "services/viz/public/mojom/compositing/frame_sink_bundle.mojom.h"

namespace viz {

// A SinkGroup is responsible for batching messages out to a group of
// bundled CompositorFrameSink clients who all share a common BeginFrameSource.
// FrameSinkBundleImpls may own any number of SinkGroups, and groups are created
// or destroyed as needed when a sink is added to or removed from the bundle.
//
// Note that the BeginFrameSource is only observed by this SinkGroup while there
// are active FrameSinks present who have explicitly indicated a need for
// BeginFrame notifications. This avoids generation and processing of unused
// frame events which might otherwise incur substantial overhead.
class FrameSinkBundleImpl::SinkGroup : public BeginFrameObserver {
 public:
  SinkGroup(FrameSinkManagerImpl& manager,
            FrameSinkBundleImpl& bundle,
            BeginFrameSource& source,
            mojom::FrameSinkBundleClient& client)
      : manager_(manager), bundle_(bundle), source_(source), client_(client) {}

  ~SinkGroup() override {
    if (is_observing_begin_frame_) {
      source_->RemoveObserver(this);
    }
  }

  bool IsEmpty() const { return frame_sinks_.empty(); }

  base::WeakPtr<SinkGroup> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

  void AddFrameSink(uint32_t sink_id) {
    frame_sinks_.insert(sink_id);

    FrameSinkId id(bundle_->id().client_id(), sink_id);
    if (auto* support = manager_->GetFrameSinkForId(id)) {
      if (support->needs_begin_frame()) {
        frame_sinks_needing_begin_frame_.insert(sink_id);
        UpdateBeginFrameObservation();
      }
    }
  }

  void RemoveFrameSink(uint32_t sink_id) {
    frame_sinks_.erase(sink_id);
    unacked_submissions_.erase(sink_id);
    FlushMessages();

    frame_sinks_needing_begin_frame_.erase(sink_id);
    UpdateBeginFrameObservation();
  }

  void SetNeedsBeginFrame(uint32_t sink_id, bool needs_begin_frame) {
    if (needs_begin_frame) {
      frame_sinks_needing_begin_frame_.insert(sink_id);
    } else {
      frame_sinks_needing_begin_frame_.erase(sink_id);
    }

    UpdateBeginFrameObservation();
  }

  void WillSubmitFrame(uint32_t sink_id) {
    unacked_submissions_.insert(sink_id);
  }

  void EnqueueDidReceiveCompositorFrameAck(
      uint32_t sink_id,
      std::vector<ReturnedResource> resources) {
    pending_received_frame_acks_.push_back(
        mojom::BundledReturnedResources::New(sink_id, std::move(resources)));

    unacked_submissions_.erase(sink_id);

    // We expect to be notified about the consumption of all previously
    // submitted frames at approximately the same time. This condition allows us
    // to batch most ack messages without any significant delays. Note that
    // sink IDs are added to `unacked_submissions_` in WillSubmitFrame() for
    // each sink that submits a frame.
    if (unacked_submissions_.empty()) {
      FlushMessages();
    }
  }

  void EnqueueOnBeginFrame(
      uint32_t sink_id,
      const BeginFrameArgs& args,
      const base::flat_map<uint32_t, FrameTimingDetails>& details,
      std::vector<ReturnedResource> resources) {
    pending_on_begin_frames_.push_back(mojom::BeginFrameInfo::New(
        sink_id, args, details, std::move(resources)));
    if (!defer_on_begin_frames_) {
      FlushMessages();
    }
  }

  void EnqueueReclaimResources(uint32_t sink_id,
                               std::vector<ReturnedResource> resources) {
    // We always defer ReclaimResources until the next flush, whether it's done
    // for frame acks or OnBeginFrames.
    pending_reclaimed_resources_.push_back(
        mojom::BundledReturnedResources::New(sink_id, std::move(resources)));
  }

  // BeginFrameObserver implementation:
  void OnBeginFrame(const BeginFrameArgs& args) override {
    last_used_begin_frame_args_ = args;

    // We expect the calls below to result in reentrant calls to our own
    // EnqueueOnBeginFrame(), via the sink's BundleClientProxy. In a sense this
    // means we act both as the sink's BeginFrameSource and its client. The
    // indirection is useful since the sink performs some non-trivial logic in
    // OnBeginFrame() and passes computed data to the client, which we want to
    // be able to forward in our batched OnBeginFrame notifications back to the
    // real remote client.
    defer_on_begin_frames_ = true;
    for (const auto sink_id : frame_sinks_) {
      FrameSinkId id(bundle_->id().client_id(), sink_id);
      if (BeginFrameObserver* observer = manager_->GetFrameSinkForId(id)) {
        observer->OnBeginFrame(args);
      }
    }
    defer_on_begin_frames_ = false;
    FlushMessages();
  }

  const BeginFrameArgs& LastUsedBeginFrameArgs() const override {
    return last_used_begin_frame_args_;
  }

  void OnBeginFrameSourcePausedChanged(bool paused) override {}

  bool WantsAnimateOnlyBeginFrames() const override { return false; }

  void FlushMessages() {
    std::vector<mojom::BundledReturnedResourcesPtr> pending_received_frame_acks;
    std::swap(pending_received_frame_acks_, pending_received_frame_acks);

    std::vector<mojom::BeginFrameInfoPtr> pending_on_begin_frames;
    std::swap(pending_on_begin_frames_, pending_on_begin_frames);

    std::vector<mojom::BundledReturnedResourcesPtr> pending_reclaimed_resources;
    std::swap(pending_reclaimed_resources_, pending_reclaimed_resources);

    if (!pending_received_frame_acks.empty() ||
        !pending_on_begin_frames.empty() ||
        !pending_reclaimed_resources.empty()) {
      client_->FlushNotifications(std::move(pending_received_frame_acks),
                                  std::move(pending_on_begin_frames),
                                  std::move(pending_reclaimed_resources));
    }
  }

  void DidFinishFrame() {
    source_->DidFinishFrame(this, DisplaySchedulerDrawResult::kUnknown);
  }

 private:
  void UpdateBeginFrameObservation() {
    bool should_observe_begin_frame = !frame_sinks_needing_begin_frame_.empty();
    if (should_observe_begin_frame && !is_observing_begin_frame_) {
      // NOTE: It's important to set this flag before adding the observer,
      // because AddObserver() can synchronously enter CFSS::OnBeginFrame(),
      // which can in turn re-enter this method.
      is_observing_begin_frame_ = true;
      source_->AddObserver(this);
      return;
    }

    if (is_observing_begin_frame_ && !should_observe_begin_frame) {
      source_->RemoveObserver(this);
      is_observing_begin_frame_ = false;
    }
  }

  const raw_ref<FrameSinkManagerImpl> manager_;
  const raw_ref<FrameSinkBundleImpl> bundle_;
  const raw_ref<BeginFrameSource> source_;
  const raw_ref<mojom::FrameSinkBundleClient> client_;

  bool defer_on_begin_frames_ = false;
  std::vector<mojom::BundledReturnedResourcesPtr> pending_received_frame_acks_;
  std::vector<mojom::BundledReturnedResourcesPtr> pending_reclaimed_resources_;
  std::vector<mojom::BeginFrameInfoPtr> pending_on_begin_frames_;
  std::set<uint32_t> frame_sinks_;
  std::set<uint32_t> frame_sinks_needing_begin_frame_;
  bool is_observing_begin_frame_ = false;

  // Tracks which sinks in the group are still expecting an ack for a previously
  // submitted frame.
  std::set<uint32_t> unacked_submissions_;

  BeginFrameArgs last_used_begin_frame_args_;

  base::WeakPtrFactory<SinkGroup> weak_ptr_factory_{this};
};

FrameSinkBundleImpl::FrameSinkBundleImpl(
    FrameSinkManagerImpl& manager,
    const FrameSinkBundleId& id,
    mojo::PendingReceiver<mojom::FrameSinkBundle> receiver,
    mojo::PendingRemote<mojom::FrameSinkBundleClient> client)
    : manager_(manager),
      id_(id),
      receiver_(this, std::move(receiver)),
      client_(std::move(client)) {
  receiver_.set_disconnect_handler(base::BindOnce(
      &FrameSinkBundleImpl::OnDisconnect, base::Unretained(this)));
}

FrameSinkBundleImpl::~FrameSinkBundleImpl() = default;

void FrameSinkBundleImpl::SetSinkNeedsBeginFrame(uint32_t sink_id,
                                                 bool needs_begin_frame) {
  if (auto* group = GetSinkGroup(sink_id)) {
    group->SetNeedsBeginFrame(sink_id, needs_begin_frame);
  }
}

void FrameSinkBundleImpl::AddFrameSink(CompositorFrameSinkSupport* support) {
  uint32_t sink_id = support->frame_sink_id().sink_id();
  auto* source = support->begin_frame_source();
  if (!source) {
    sourceless_sinks_.insert(sink_id);
    return;
  }

  auto& group = sink_groups_[source];
  if (!group) {
    group =
        std::make_unique<SinkGroup>(*manager_, *this, *source, *client_.get());
  }
  group->AddFrameSink(sink_id);
}

void FrameSinkBundleImpl::UpdateFrameSink(CompositorFrameSinkSupport* support,
                                          BeginFrameSource* old_source) {
  uint32_t sink_id = support->frame_sink_id().sink_id();
  RemoveFrameSinkImpl(old_source, sink_id);
  AddFrameSink(support);
}

void FrameSinkBundleImpl::RemoveFrameSink(CompositorFrameSinkSupport* support) {
  auto sink_id = support->frame_sink_id().sink_id();
  auto* source = support->begin_frame_source();
  RemoveFrameSinkImpl(source, sink_id);
}

void FrameSinkBundleImpl::SetNeedsBeginFrame(uint32_t sink_id,
                                             bool needs_begin_frame) {
  if (auto* sink = GetFrameSink(sink_id)) {
    sink->SetNeedsBeginFrame(needs_begin_frame);
  }
}

void FrameSinkBundleImpl::Submit(
    std::vector<mojom::BundledFrameSubmissionPtr> submissions) {
  std::map<raw_ptr<SinkGroup>, base::WeakPtr<SinkGroup>> groups;
  std::map<raw_ptr<SinkGroup>, base::WeakPtr<SinkGroup>> affected_groups;

  // Count the frame submissions before processing anything. This ensures that
  // any frames submitted here will be acked together in a batch, and not acked
  // individually in case they happen to ack synchronously within
  // SubmitCompositorFrame below.
  //
  // For sinks which can't currently be associated with a SinkGroup (because
  // they have no BeginFrameSource), we count nothing and their acks will pass
  // through to the client without batching.
  for (auto& submission : submissions) {
    if (auto* group = GetSinkGroup(submission->sink_id)) {
      groups.emplace(group, group->GetWeakPtr());
      if (submission->data->is_frame()) {
        group->WillSubmitFrame(submission->sink_id);
        affected_groups.emplace(group, group->GetWeakPtr());
      }
    }
  }

  for (auto& submission : submissions) {
    if (auto* sink = GetFrameSink(submission->sink_id)) {
      switch (submission->data->which()) {
        case mojom::BundledFrameSubmissionData::Tag::kFrame: {
          mojom::BundledCompositorFramePtr& frame =
              submission->data->get_frame();
          sink->SubmitCompositorFrame(
              frame->local_surface_id, std::move(frame->frame),
              std::move(frame->hit_test_region_list), frame->submit_time);
          break;
        }

        case mojom::BundledFrameSubmissionData::Tag::kDidNotProduceFrame:
          sink->DidNotProduceFrame(
              submission->data->get_did_not_produce_frame());
          break;
      }
    }
  }

  for (const auto& [unsafe_group, weak_group] : groups) {
    if (weak_group) {
      weak_group->DidFinishFrame();
    }
  }

  for (const auto& [unsafe_group, weak_group] : affected_groups) {
    if (weak_group) {
      weak_group->FlushMessages();
    }
  }
}

#if BUILDFLAG(IS_ANDROID)
void FrameSinkBundleImpl::SetThreads(uint32_t sink_id,
                                     const std::vector<Thread>& threads) {
  if (auto* sink = GetFrameSink(sink_id)) {
    sink->SetThreads(threads);
  }
}
#endif

void FrameSinkBundleImpl::EnqueueDidReceiveCompositorFrameAck(
    uint32_t sink_id,
    std::vector<ReturnedResource> resources) {
  if (auto* group = GetSinkGroup(sink_id)) {
    group->EnqueueDidReceiveCompositorFrameAck(sink_id, std::move(resources));
  } else {
    // The sink has no BeginFrameSource at the moment and therefore does not
    // belong to a SinkGroup. Forward directly without batching.
    std::vector<mojom::BundledReturnedResourcesPtr> acks;
    acks.push_back(
        mojom::BundledReturnedResources::New(sink_id, std::move(resources)));
    client_->FlushNotifications(std::move(acks), {}, {});
  }
}

void FrameSinkBundleImpl::EnqueueOnBeginFrame(
    uint32_t sink_id,
    const BeginFrameArgs& args,
    const base::flat_map<uint32_t, FrameTimingDetails>& details,
    std::vector<ReturnedResource> resources) {
  if (auto* group = GetSinkGroup(sink_id)) {
    group->EnqueueOnBeginFrame(sink_id, args, details, std::move(resources));
  } else {
    // The sink has no BeginFrameSource at the moment and therefore does not
    // belong to a SinkGroup. Forward directly without batching.
    std::vector<mojom::BeginFrameInfoPtr> begin_frames;
    begin_frames.push_back(mojom::BeginFrameInfo::New(sink_id, args, details,
                                                      std::move(resources)));
    client_->FlushNotifications({}, std::move(begin_frames), {});
  }
}

void FrameSinkBundleImpl::EnqueueReclaimResources(
    uint32_t sink_id,
    std::vector<ReturnedResource> resources) {
  if (auto* group = GetSinkGroup(sink_id)) {
    group->EnqueueReclaimResources(sink_id, std::move(resources));
  } else {
    // The sink has no BeginFrameSource at the moment and therefore does not
    // belong to a SinkGroup. Forward directly without batching.
    std::vector<mojom::BundledReturnedResourcesPtr> reclaims;
    reclaims.push_back(
        mojom::BundledReturnedResources::New(sink_id, std::move(resources)));
    client_->FlushNotifications({}, {}, std::move(reclaims));
  }
}

void FrameSinkBundleImpl::SendOnBeginFramePausedChanged(uint32_t sink_id,
                                                        bool paused) {
  client_->OnBeginFramePausedChanged(sink_id, paused);
}

void FrameSinkBundleImpl::SendOnCompositorFrameTransitionDirectiveProcessed(
    uint32_t sink_id,
    uint32_t sequence_id) {
  client_->OnCompositorFrameTransitionDirectiveProcessed(sink_id, sequence_id);
}

void FrameSinkBundleImpl::RemoveFrameSinkImpl(BeginFrameSource* source,
                                              uint32_t sink_id) {
  if (!source) {
    sourceless_sinks_.erase(sink_id);
    return;
  }

  auto it = sink_groups_.find(source);
  if (it == sink_groups_.end()) {
    DVLOG(1) << "Unexpected missing SinkGroup entry for sink " << sink_id;
    return;
  }

  it->second->RemoveFrameSink(sink_id);
  if (it->second->IsEmpty()) {
    sink_groups_.erase(it);
  }
}

CompositorFrameSinkImpl* FrameSinkBundleImpl::GetFrameSink(
    uint32_t sink_id) const {
  return manager_->GetFrameSinkImpl(FrameSinkId(id_.client_id(), sink_id));
}

CompositorFrameSinkSupport* FrameSinkBundleImpl::GetFrameSinkSupport(
    uint32_t sink_id) const {
  return manager_->GetFrameSinkForId(FrameSinkId(id_.client_id(), sink_id));
}

FrameSinkBundleImpl::SinkGroup* FrameSinkBundleImpl::GetSinkGroup(
    uint32_t sink_id) const {
  auto* support = GetFrameSinkSupport(sink_id);
  if (!support) {
    return nullptr;
  }

  auto* source = support->begin_frame_source();
  auto it = sink_groups_.find(source);
  if (it == sink_groups_.end()) {
    return nullptr;
  }
  return it->second.get();
}

void FrameSinkBundleImpl::OnDisconnect() {
  manager_->DestroyFrameSinkBundle(id_);
}

}  // namespace viz
