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

#include "content/browser/tracing/tracing_ui.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_config.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "content/browser/tracing/grit/tracing_resources.h"
#include "content/browser/tracing/tracing_controller_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/tracing_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "services/tracing/public/cpp/perfetto/perfetto_config.h"
#include "services/tracing/public/cpp/perfetto/perfetto_session.h"
#include "third_party/perfetto/include/perfetto/tracing/tracing.h"
#include "third_party/perfetto/protos/perfetto/common/trace_stats.gen.h"

namespace content {
namespace {
constexpr char kStreamFormat[] = "stream_format";
constexpr char kStreamFormatProtobuf[] = "protobuf";
constexpr char kStreamFormatJSON[] = "json";
perfetto::TracingSession* g_tracing_session = nullptr;

void OnGotCategories(WebUIDataSource::GotDataCallback callback,
                     const std::set<std::string>& category_set) {
  base::ListValue category_list;
  for (const std::string& category : category_set) {
    category_list.Append(category);
  }

  auto res = base::MakeRefCounted<base::RefCountedString>();
  res->as_string() = base::WriteJson(category_list).value_or("");
  std::move(callback).Run(res);
}

void OnRecordingEnabledAck(WebUIDataSource::GotDataCallback callback);

bool BeginRecording(const std::string& data64,
                    WebUIDataSource::GotDataCallback callback) {
  base::trace_event::TraceConfig trace_config("", "");
  std::string stream_format;
  if (!TracingUI::GetTracingOptions(data64, trace_config, stream_format))
    return false;

  // TODO(skyostil): Migrate all use cases from TracingController to Perfetto.
  if (stream_format == kStreamFormatProtobuf) {
    delete g_tracing_session;
    g_tracing_session =
        perfetto::Tracing::NewTrace(perfetto::BackendType::kCustomBackend)
            .release();
    g_tracing_session->Setup(tracing::GetDefaultPerfettoConfig(trace_config));

    auto shared_callback = base::MakeRefCounted<
        base::RefCountedData<WebUIDataSource::GotDataCallback>>(
        std::move(callback));
    g_tracing_session->SetOnStartCallback([shared_callback] {
      OnRecordingEnabledAck(std::move(shared_callback->data));
    });
    g_tracing_session->Start();
    return true;
  }
  return TracingController::GetInstance()->StartTracing(
      trace_config,
      base::BindOnce(&OnRecordingEnabledAck, std::move(callback)));
}

void OnTraceBufferUsageResult(WebUIDataSource::GotDataCallback callback,
                              float percent_full,
                              size_t approximate_event_count) {
  std::string str = base::NumberToString(percent_full);
  std::move(callback).Run(
      base::MakeRefCounted<base::RefCountedString>(std::move(str)));
}

bool GetTraceBufferUsage(WebUIDataSource::GotDataCallback callback) {
  if (g_tracing_session) {
    // |callback| is move-only, so in order to pass it through a copied lambda
    // we need to temporarily move it on the heap.
    auto shared_callback = base::MakeRefCounted<
        base::RefCountedData<WebUIDataSource::GotDataCallback>>(
        std::move(callback));
    g_tracing_session->GetTraceStats(
        [shared_callback](
            perfetto::TracingSession::GetTraceStatsCallbackArgs args) {
          std::string usage;
          perfetto::protos::gen::TraceStats trace_stats;
          if (args.success &&
              trace_stats.ParseFromArray(args.trace_stats_data.data(),
                                         args.trace_stats_data.size())) {
            double percent_full = tracing::GetTraceBufferUsage(trace_stats);
            usage = base::NumberToString(percent_full);
          }
          std::move(shared_callback->data)
              .Run(base::MakeRefCounted<base::RefCountedString>(
                  std::move(usage)));
        });
    return true;
  }

  return TracingController::GetInstance()->GetTraceBufferUsage(
      base::BindOnce(OnTraceBufferUsageResult, std::move(callback)));
}

void ReadProtobufTraceData(
    scoped_refptr<TracingController::TraceDataEndpoint> endpoint,
    perfetto::TracingSession::ReadTraceCallbackArgs args) {
  if (args.size) {
    auto data_string = std::make_unique<std::string>(args.data, args.size);
    endpoint->ReceiveTraceChunk(std::move(data_string));
  }
  if (!args.has_more)
    endpoint->ReceivedTraceFinalContents();
}

void TracingCallbackWrapperBase64(WebUIDataSource::GotDataCallback callback,
                                  std::unique_ptr<std::string> data) {
  base::RefCountedString* data_base64 = new base::RefCountedString();
  data_base64->as_string() = base::Base64Encode(*data);
  std::move(callback).Run(data_base64);
}

bool EndRecording(WebUIDataSource::GotDataCallback callback) {
  if (!TracingController::GetInstance()->IsTracing() && !g_tracing_session)
    return false;

  scoped_refptr<TracingController::TraceDataEndpoint> data_endpoint =
      TracingControllerImpl::CreateCompressedStringEndpoint(
          TracingControllerImpl::CreateCallbackEndpoint(base::BindOnce(
              TracingCallbackWrapperBase64, std::move(callback))),
          false /* compress_with_background_priority */);

  if (g_tracing_session) {
    perfetto::TracingSession* session = g_tracing_session;
    g_tracing_session = nullptr;
    session->SetOnStopCallback([session, data_endpoint] {
      session->ReadTrace(
          [session, data_endpoint](
              perfetto::TracingSession::ReadTraceCallbackArgs args) {
            ReadProtobufTraceData(data_endpoint, args);
            if (!args.has_more)
              delete session;
          });
    });
    session->Stop();
    return true;
  }
  return TracingController::GetInstance()->StopTracing(data_endpoint);
}

void OnRecordingEnabledAck(WebUIDataSource::GotDataCallback callback) {
  std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
}

bool OnBeginJSONRequest(const std::string& path,
                        WebUIDataSource::GotDataCallback callback) {
  if (path == "json/categories") {
    return TracingController::GetInstance()->GetCategories(
        base::BindOnce(OnGotCategories, std::move(callback)));
  }

  const char kBeginRecordingPath[] = "json/begin_recording?";
  if (base::StartsWith(path, kBeginRecordingPath,
                       base::CompareCase::SENSITIVE)) {
    std::string data = path.substr(strlen(kBeginRecordingPath));
    return BeginRecording(data, std::move(callback));
  }
  if (path == "json/get_buffer_percent_full") {
    return GetTraceBufferUsage(std::move(callback));
  }
  if (path == "json/end_recording_compressed") {
    return EndRecording(std::move(callback));
  }

  LOG(ERROR) << "Unhandled request to " << path;
  return false;
}

bool OnShouldHandleRequest(const std::string& path) {
  return base::StartsWith(path, "json/", base::CompareCase::SENSITIVE);
}

void OnTracingRequest(const std::string& path,
                      WebUIDataSource::GotDataCallback callback) {
  DCHECK(OnShouldHandleRequest(path));
  // OnBeginJSONRequest() only runs |callback| if it returns true. But it needs
  // to take ownership of |callback| even though it won't call |callback|
  // sometimes, as it binds |callback| into other callbacks before it makes that
  // decision. So we must give it one copy and keep one ourselves.
  auto split_callback = base::SplitOnceCallback(std::move(callback));
  if (!OnBeginJSONRequest(path, std::move(split_callback.first))) {
    std::string error("##ERROR##");
    std::move(split_callback.second)
        .Run(base::MakeRefCounted<base::RefCountedString>(std::move(error)));
  }
}

}  // namespace


////////////////////////////////////////////////////////////////////////////////
//
// TracingUI
//
////////////////////////////////////////////////////////////////////////////////

TracingUI::TracingUI(WebUI* web_ui) : WebUIController(web_ui) {
  // Set up the chrome://tracing/ source.
  WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
      web_ui->GetWebContents()->GetBrowserContext(), kChromeUITracingHost);
  source->DisableTrustedTypesCSP();
  source->UseStringsJs();
  source->SetDefaultResource(IDR_TRACING_ABOUT_TRACING_HTML);
  source->AddResourcePath("tracing.js", IDR_TRACING_ABOUT_TRACING_JS);

  source->SetRequestFilter(base::BindRepeating(OnShouldHandleRequest),
                           base::BindRepeating(OnTracingRequest));
}

TracingUI::~TracingUI() = default;

// static
bool TracingUI::GetTracingOptions(const std::string& data64,
                                  base::trace_event::TraceConfig& trace_config,
                                  std::string& out_stream_format) {
  std::string data;
  if (!base::Base64Decode(data64, &data)) {
    LOG(ERROR) << "Options were not base64 encoded.";
    return false;
  }

  std::optional<base::Value> options =
      base::JSONReader::Read(data, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!options) {
    LOG(ERROR) << "Options were not valid JSON";
    return false;
  }
  base::DictValue* options_dict = options->GetIfDict();
  if (!options_dict) {
    LOG(ERROR) << "Options must be dict";
    return false;
  }

  if (const std::string* stream_format =
          options_dict->FindString(kStreamFormat)) {
    out_stream_format = *stream_format;
  } else {
    out_stream_format = kStreamFormatJSON;
  }

  // New-style options dictionary.
  trace_config = base::trace_event::TraceConfig(*options_dict);
  return true;
}

}  // namespace content
