// Copyright 2020 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/display/delegated_ink_point_renderer_base.h"

#include <algorithm>

#include "base/debug/dump_without_crashing.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "components/viz/service/display/delegated_ink_trail_data.h"
#include "third_party/perfetto/include/perfetto/tracing/track_event_args.h"

namespace viz {

DelegatedInkPointRendererBase::DelegatedInkPointRendererBase() = default;
DelegatedInkPointRendererBase::~DelegatedInkPointRendererBase() = default;

void DelegatedInkPointRendererBase::InitMessagePipeline(
    mojo::PendingReceiver<gfx::mojom::DelegatedInkPointRenderer> receiver) {
  // The remote end of this pipeline exists on a per-tab basis, so if tab A
  // is using the feature and then tab B starts trying to use it, a new
  // PendingReceiver will arrive here while |receiver_| is still bound to the
  // remote in tab A. In this case, just reset |receiver_| so that tab A's
  // remote is unbound and bind the new receiver to use the feature in tab B.
  if (receiver_.is_bound()) {
    receiver_.reset();
    metadata_.reset();
    pointer_ids_.clear();
  }
  receiver_.Bind(std::move(receiver));
}

void DelegatedInkPointRendererBase::SetDelegatedInkMetadata(
    std::unique_ptr<gfx::DelegatedInkMetadata> metadata) {
  // Frame time is set later than everything else due to what is available
  // at time of creation, so confirm that it was actually set.
  CHECK_NE(metadata->frame_time(), base::TimeTicks());
  metadata_ = std::move(metadata);
  const bool metadata_is_new = previous_metadata_ != *metadata_;
  previous_metadata_ = *metadata_;

  TRACE_EVENT("delegated_ink_trails",
              "DelegatedInkPointRendererBase::SetDelegatedInkMetadata",
              perfetto::TerminatingFlow::Global(metadata_->trace_id()),
              "metadata", metadata_->ToString());

  // If we already have a cached pointer ID, check if the same pointer ID
  // matches the new metadata.
  if (pointer_id_.has_value() &&
      pointer_ids_[pointer_id_.value()].ContainsMatchingPoint(
          metadata_.get())) {
    if (metadata_is_new) {
      metadata_paint_time_ = pointer_ids_[pointer_id_.value()]
                                 .GetMatchingPoint(metadata_.get())
                                 .paint_timestamp();
    }
    return;
  }

  // If not, find the pointer ID that does match it, if any, and cache it.
  for (auto& it : pointer_ids_) {
    if (it.second.ContainsMatchingPoint(metadata_.get())) {
      pointer_id_ = it.first;
      metadata_paint_time_ =
          it.second.GetMatchingPoint(metadata_.get()).paint_timestamp();
      return;
    }
  }

  // If we aren't able to find any matching point, set the pointer ID to null
  // so that FilterPoints and PredictPoints can early out.
  pointer_id_ = std::nullopt;
}

void DelegatedInkPointRendererBase::ResetPoints() {
  CHECK(!metadata_);
  pointer_ids_.clear();
  pointer_id_.reset();
}

std::vector<gfx::DelegatedInkPoint>
DelegatedInkPointRendererBase::FilterPoints() {
  if (pointer_ids_.empty()) {
    return {};
  }

  CHECK(metadata_);

  // Any stored point with a timestamp earlier than the metadata's has already
  // been drawn as part of the ink stroke and therefore should not be part of
  // the delegated ink trail. Do this before checking if |pointer_id_| is valid
  // because it helps manage the number of DelegatedInkPoints that are being
  // stored and isn't dependent on |pointer_id_| at all.
  for (auto& it : pointer_ids_) {
    it.second.ErasePointsOlderThanMetadata(metadata_.get());
  }

  // TODO(crbug.com/40118757): Add additional filtering to prevent points in
  // |points_| from having a timestamp that is far ahead of |metadata_|'s
  // timestamp. This could occur if the renderer stalls before sending a
  // metadata while the browser continues to pump points through to viz. Then
  // when the renderer starts back up again, the metadata it sends may be
  // significantly older than the points stored here, resulting in a long
  // possibly incorrect trail if the max number of points to store was reached.

  // If no point with any pointer id exactly matches the metadata, then we can't
  // confirm which set of points to use for the delegated ink trail, so just
  // return an empty vector so that nothing will be drawn. This happens most
  // often at the beginning of delegated ink trail use. The metadata is created
  // using a PointerEvent earlier than any DelegatedInkPoint is created,
  // resulting in the metadata having an earlier timestamp and a point that
  // doesn't match anything that is sent here from viz. Even if only a single
  // pointer ID is in use, we can't know with any certainty what happened
  // between the metadata point and the earliest DelegatedInkPoint we have, so
  // we choose to just not draw anything.
  if (!pointer_id_.has_value()) {
    return {};
  }

  DelegatedInkTrailData& trail_data = pointer_ids_[pointer_id_.value()];

  // Make sure the metrics handler is provided the new real events to accurately
  // measure the prediction later.
  trail_data.UpdateMetrics(metadata_.get());

  // Any remaining points must be the points that should be part of the
  // delegated ink trail
  std::vector<gfx::DelegatedInkPoint> points_to_draw;
  for (const auto& [_, point] : trail_data.GetPoints()) {
    points_to_draw.emplace_back(point);
    TRACE_EVENT("delegated_ink_trails", "Filtering to draw point",
                perfetto::TerminatingFlow::Global(point.trace_id()), "point",
                point.ToString());
  }

  if (!points_to_draw.front().MatchesDelegatedInkMetadata(metadata_.get())) {
    base::debug::DumpWithoutCrashing();
  }

  return points_to_draw;
}

std::optional<AggregatedRenderPassId>
DelegatedInkPointRendererBase::GetLatestMetadataRenderPassId() const {
  if (metadata_) {
    return AggregatedRenderPassId::FromUnsafeValue(metadata_->render_pass_id());
  }
  return std::nullopt;
}

void DelegatedInkPointRendererBase::PredictPoints(
    std::vector<gfx::DelegatedInkPoint>* ink_points_to_draw) {
  DCHECK(metadata_);

  if (!pointer_id_.has_value() ||
      static_cast<int>(ink_points_to_draw->size()) == 0)
    return;

  pointer_ids_[pointer_id_.value()].PredictPoints(ink_points_to_draw,
                                                  metadata_.get());
}

void DelegatedInkPointRendererBase::ResetPrediction() {
  for (auto& it : pointer_ids_)
    it.second.Reset();
  TRACE_EVENT_INSTANT("delegated_ink_trails",
                      "Delegated ink prediction reset.");
}

void DelegatedInkPointRendererBase::ReportPointsDrawn() {
  const base::TimeTicks now = base::TimeTicks::Now();
  // If there is a point that matches the metadata and the histogram has not yet
  // been fired, then this is the first frame that the metadata point will be
  // recorded by `updateInkTrailStartPoint`.
  if (metadata_paint_time_.has_value()) {
    base::UmaHistogramCustomTimes(
        "Renderer.DelegatedInkTrail.Skia.TimeFromDelegatedInkToApiPaint",
        now - metadata_paint_time_.value(), base::Milliseconds(1),
        base::Seconds(1), 50);
    metadata_paint_time_ = std::nullopt;
  }

  if (!pointer_id_.has_value()) {
    return;
  }
  CHECK(pointer_ids_.contains(pointer_id_.value()));
  auto& points_trail = pointer_ids_.at(pointer_id_.value()).GetPoints();
  base::UmaHistogramCounts100(
      "Renderer.DelegatedInkTrail.Skia.OutstandingPointsToDraw",
      points_trail.size());

  for (auto& [_, point] : points_trail) {
    UMA_HISTOGRAM_TIMES(
        "Renderer.DelegatedInkTrail.Skia.TimeToDrawPointsMillis",
        now - point.timestamp());
    if (!point.paint_timestamp().has_value()) {
      point.set_paint_timestamp(now);
    }
  }
}

void DelegatedInkPointRendererBase::StoreDelegatedInkPoint(
    const gfx::DelegatedInkPoint& point) {
  TRACE_EVENT("delegated_ink_trails",
              "DelegatedInkPointRendererImpl::StoreDelegatedInkPoint",
              perfetto::Flow::Global(point.trace_id()), "point",
              point.ToString());

  pointer_ids_[point.pointer_id()].AddPoint(point);
}

}  // namespace viz
