// 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/debugger/viz_debugger.h"

#include <algorithm>
#include <atomic>
#include <string>
#include <string_view>
#include <utility>

#include "base/base64.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "skia/ext/codec_utils.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkSwizzle.h"

#if VIZ_DEBUGGER_IS_ON()

#include "base/threading/platform_thread.h"
#include "base/threading/thread_id_name_manager.h"

namespace viz {

// Version the protocol in case we ever want or need backwards compatibility
// support.
static const int kVizDebuggerVersion = 1;

std::atomic<bool> VizDebugger::enabled_ = false;

VizDebugger::BufferInfo::BufferInfo() = default;
VizDebugger::BufferInfo::~BufferInfo() = default;
VizDebugger::BufferInfo::BufferInfo(const BufferInfo& a) = default;

VizDebugger* VizDebugger::GetInstance() {
  static base::NoDestructor<VizDebugger> g_debugger;
  return g_debugger.get();
}

SkColor VizDebugger::HexStringToSkColor(std::string_view hex_string) {
  uint32_t red, green, blue;

  if (hex_string.size() == 7 && hex_string[0] == '#' &&
      base::HexStringToUInt(hex_string.substr(1, 2), &red) &&
      base::HexStringToUInt(hex_string.substr(3, 2), &green) &&
      base::HexStringToUInt(hex_string.substr(5, 2), &blue)) {
    return SkColorSetRGB(red, green, blue);
  }
  return SK_ColorTRANSPARENT;
}

std::string VizDebugger::SkColorToHexString(SkColor color) {
  return absl::StrFormat("#%02x%02x%02x", SkColorGetR(color),
                         SkColorGetG(color), SkColorGetB(color));
}

VizDebugger::FilterBlock::FilterBlock(const std::string file_str,
                                      const std::string func_str,
                                      const std::string anno_str,
                                      bool is_active,
                                      bool is_enabled)
    : file(std::move(file_str)),
      func(std::move(func_str)),
      anno(std::move(anno_str)),
      active(is_active),
      enabled(is_enabled) {}

VizDebugger::FilterBlock::~FilterBlock() = default;

VizDebugger::FilterBlock::FilterBlock(const FilterBlock& other) = default;

base::DictValue VizDebugger::CallSubmitCommon::GetDictionaryValue() const {
  return base::DictValue()
      .Set("drawindex", draw_index)
      .Set("source_index", source_index)
      // Since this is only for debugging, it's ok for thread ids to be
      // truncated.
      .Set("thread_id", static_cast<int32_t>(thread_id))
      .Set("option", base::DictValue()
                         .Set("color", SkColorToHexString(SkColorSetRGB(
                                           option.color_r, option.color_g,
                                           option.color_b)))
                         .Set("alpha", option.color_a));
}

VizDebugger::StaticSource::StaticSource(const char* anno_name,
                                        const char* file_name,
                                        int file_line,
                                        const char* func_name)
    : anno(anno_name), file(file_name), func(func_name), line(file_line) {
  VizDebugger::GetInstance()->RegisterSource(this);
}

VizDebugger::VizDebugger()
    : gpu_thread_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {
  enabled_.store(false);
}

VizDebugger::~VizDebugger() = default;

void VizDebugger::SubmitBuffer(int buff_id, VizDebugger::BufferInfo&& buffer) {
  read_write_lock_.WriteLock();
  VizDebugger::Buffer buff;
  buff.id = buff_id;
  buff.buffer_info = std::move(buffer);
  buffers_.emplace_back(buff);
  read_write_lock_.WriteUnLock();
}

base::Value VizDebugger::FrameAsJson(const uint64_t counter,
                                     const gfx::Size& window_pix,
                                     base::TimeTicks time_ticks) {
  // TODO(petermcneeley): When we move to multithread we need to do something
  // like an atomic swap here. Currently all multithreading concerns are handled
  // by having a lock around the |json_frame_output_| object.
  submission_count_ = 0;

  auto global_dict =
      base::DictValue()
          .Set("version", kVizDebuggerVersion)
          .Set("frame", base::NumberToString(counter))
          .Set("windowx", window_pix.width())
          .Set("windowy", window_pix.height())
          .Set("time", base::NumberToString(
                           time_ticks.since_origin().InMicroseconds()));

  base::ListValue new_sources;
  for (size_t i = last_sent_source_count_; i < sources_.size(); i++) {
    const StaticSource* each = sources_[i];

    new_sources.Append(base::DictValue()
                           .Set("file", each->file)
                           .Set("line", each->line)
                           .Set("func", each->func)
                           .Set("anno", each->anno)
                           .Set("index", each->reg_index));
  }

  // Remote connection will now have acknowledged all the new sources.
  last_sent_source_count_ = sources_.size();
  global_dict.Set("new_sources", std::move(new_sources));

  // We take the minimum between tail index and buffer size to make sure we
  // don't go out of bounds.
  size_t const max_rect_calls_index =
      std::min(static_cast<int>(draw_calls_tail_idx_),
               static_cast<int>(draw_rect_calls_.size()));

  size_t const max_logs_index = std::min(static_cast<int>(logs_tail_idx_),
                                         static_cast<int>(logs_.size()));

  base::ListValue draw_calls;

  // Hash set to keep track of threads that have been registered already.
  base::flat_set<base::PlatformThreadId::UnderlyingType> registered_threads;
  for (size_t i = 0; i < max_rect_calls_index; ++i) {
    base::DictValue dict = draw_rect_calls_[i].GetDictionaryValue();
    dict.Set("size", base::ListValue()
                         .Append(draw_rect_calls_[i].obj_size.width())
                         .Append(draw_rect_calls_[i].obj_size.height()));
    dict.Set("pos",
             base::ListValue()
                 .Append(static_cast<double>(draw_rect_calls_[i].pos.x()))
                 .Append(static_cast<double>(draw_rect_calls_[i].pos.y())));
    if (draw_rect_calls_[i].uv != DBG_DEFAULT_UV) {
      dict.Set(
          "uv_size",
          base::ListValue()
              .Append(static_cast<double>(draw_rect_calls_[i].uv.width()))
              .Append(static_cast<double>(draw_rect_calls_[i].uv.height())));
      dict.Set("uv_pos",
               base::ListValue()
                   .Append(static_cast<double>(draw_rect_calls_[i].uv.x()))
                   .Append(static_cast<double>(draw_rect_calls_[i].uv.y())));
    }
    dict.Set("buff_id", std::move(draw_rect_calls_[i].buff_id));
    if (!draw_rect_calls_[i].text.empty()) {
      dict.Set("text", std::move(draw_rect_calls_[i].text));
    }
    registered_threads.insert(
        base::saturated_cast<base::PlatformThreadId::UnderlyingType>(
            draw_rect_calls_[i].thread_id));
    draw_calls.Append(std::move(dict));
  }

  global_dict.Set("drawcalls", std::move(draw_calls));

  base::DictValue buff_map;

  for (auto&& each : buffers_) {
    std::string uri =
        skia::EncodePngAsDataUri(each.buffer_info.bitmap.pixmap());
    if (uri.empty()) {
      DLOG(ERROR) << "encode failed";
      continue;
    }
    buff_map.Set(base::NumberToString(each.id), std::move(uri));
  }

  global_dict.Set("buff_map", std::move(buff_map));

  base::ListValue logs;
  for (size_t i = 0; i < max_logs_index; ++i) {
    base::DictValue dict = logs_[i].GetDictionaryValue();
    dict.Set("value", std::move(logs_[i].value));
    logs.Append(std::move(dict));
    registered_threads.insert(logs_[i].thread_id);
  }
  global_dict.Set("logs", std::move(logs));

  // Gather thread name:id for all active threads this frame.
  base::ListValue new_threads;
  for (auto&& thread_id : registered_threads) {
    std::string cur_thread_name =
        base::ThreadIdNameManager::GetInstance()->GetName(
            base::PlatformThreadId(thread_id));
    new_threads.Append(base::DictValue()
                           .Set("thread_id", static_cast<int32_t>(thread_id))
                           .Set("thread_name", cur_thread_name));
    registered_threads.insert(thread_id);
  }

  global_dict.Set("threads", std::move(new_threads));

  // Reset index counters for each buffer.
  buffers_.clear();
  draw_calls_tail_idx_ = 0;
  logs_tail_idx_ = 0;

  return base::Value(std::move(global_dict));
}

void VizDebugger::UpdateFilters() {
  if (apply_new_filters_next_frame_) {
    cached_filters_ = new_filters_;
    for (auto&& source : sources_) {
      ApplyFilters(source);
    }
    new_filters_.clear();
    apply_new_filters_next_frame_ = false;
  }
}

void VizDebugger::CompleteFrame(uint64_t counter,
                                const gfx::Size& window_pix,
                                base::TimeTicks time_ticks) {
  if (!enabled_) {
    return;
  }
  read_write_lock_.WriteLock();
  UpdateFilters();
  json_frame_output_ = FrameAsJson(counter, window_pix, time_ticks);
  gpu_thread_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&VizDebugger::AddFrame, base::Unretained(this)));
  read_write_lock_.WriteUnLock();
}

void VizDebugger::ApplyFilters(VizDebugger::StaticSource* src) {
  // In the case of no filters we disable this source.
  src->active = false;
  src->enabled = false;
  // TODO(petermcneeley): We should probably make this string filtering more
  // optimal. However, for the most part it the cost is only paid on the
  // application of new filters.
  auto simple_match = [](const char* source_str,
                         const std::string& filter_match) {
    if (filter_match.empty() || source_str == nullptr) {
      return true;
    }
    return std::string_view(source_str).find(filter_match) !=
           std::string_view::npos;
  };

  for (const auto& filter_block : cached_filters_) {
    if (simple_match(src->file, filter_block.file) &&
        simple_match(src->func, filter_block.func) &&
        simple_match(src->anno, filter_block.anno)) {
      src->active = filter_block.active;
      src->enabled = filter_block.enabled;
    }
  }
}

void VizDebugger::RegisterSource(StaticSource* src) {
  read_write_lock_.WriteLock();
  int index = sources_.size();
  src->reg_index = index;
  ApplyFilters(src);
  sources_.push_back(src);
  read_write_lock_.WriteUnLock();
}

void VizDebugger::Draw(const gfx::SizeF& obj_size,
                       const gfx::Vector2dF& pos,
                       const VizDebugger::StaticSource* dcs,
                       VizDebugger::DrawOption option,
                       int* id,
                       const gfx::RectF& uv,
                       const std::string& text) {
  DrawInternal(obj_size, pos, dcs, option, id, uv, text);
}

void VizDebugger::DrawInternal(const gfx::SizeF& obj_size,
                               const gfx::Vector2dF& pos,
                               const VizDebugger::StaticSource* dcs,
                               VizDebugger::DrawOption option,
                               int* id,
                               const gfx::RectF& uv,
                               const std::string& text) {
  int local_id_buffer = -1;
  if (id != nullptr) {
    local_id_buffer = buffer_id++;
    *id = local_id_buffer;
  }

  //  Store atomic insertion index in local variable to use to insert into
  //  buffer.
  int insertion_index;

  for (;;) {
    read_write_lock_.ReadLock();
    // Get call insertion index.
    insertion_index = draw_calls_tail_idx_++;
    // If the insertion index is within bounds, insert call into buffer.
    if (static_cast<size_t>(insertion_index) < draw_rect_calls_.size()) {
      int64_t cur_thread_id =
          static_cast<int64_t>(base::PlatformThread::CurrentId());
      draw_rect_calls_[insertion_index] = DrawCall{submission_count_++,
                                                   dcs->reg_index,
                                                   cur_thread_id,
                                                   option,
                                                   obj_size,
                                                   pos,
                                                   local_id_buffer,
                                                   uv,
                                                   text};
      // Return when call insertion is successful.
      read_write_lock_.ReadUnlock();
      return;
    }
    read_write_lock_.ReadUnlock();
    // Take write lock to resize and re-adjust buffer tail index after buffer
    // overflow.
    read_write_lock_.WriteLock();
    // If tail index is over buffer size, then resizing is definitely needed.
    // Also re-adjust tail index so it's at the start of the new buffer space.
    if (static_cast<size_t>(draw_calls_tail_idx_) >= draw_rect_calls_.size()) {
      draw_calls_tail_idx_ = draw_rect_calls_.size();
      draw_rect_calls_.resize(draw_rect_calls_.size() * 2);
    }
    read_write_lock_.WriteUnLock();
  }
}

void VizDebugger::AddFrame() {
  // TODO(petermcneeley): This code has duel thread entry. One to launch the
  // task and one for the task to run. We should improve on this design in the
  // future and have a better multithreaded frame data aggregation system.
  read_write_lock_.WriteLock();
  DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence());
  if (debug_output_.is_bound()) {
    debug_output_->LogFrame(std::move(json_frame_output_));
  }
  read_write_lock_.WriteUnLock();
}

void VizDebugger::FilterDebugStream(base::DictValue json) {
  read_write_lock_.WriteLock();
  DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence());
  const base::ListValue* filters = json.FindList("filters");

  if (!filters) {
    LOG(ERROR) << "Missing filter list in json: " << json;
    return;
  }

  new_filters_.clear();

  for (const auto& entry : *filters) {
    const auto& filter = entry.GetDict();
    const base::Value* file = filter.FindByDottedPath("selector.file");
    const base::Value* func = filter.FindByDottedPath("selector.func");
    const base::Value* anno = filter.FindByDottedPath("selector.anno");
    const base::Value* active = filter.Find("active");

    if (!active) {
      LOG(ERROR) << "Missing filter props in json: " << json;
      return;
    }

    if ((file && !file->is_string()) || (func && !func->is_string()) ||
        (anno && !anno->is_string()) || !active->is_bool()) {
      LOG(ERROR) << "Filter props wrong type in json: " << json;
      continue;
    }

    auto check_str = [](const base::Value* filter_str) {
      return (filter_str ? filter_str->GetString() : std::string());
    };

    std::optional<bool> enabled = filter.FindBool("enabled");
    new_filters_.emplace_back(check_str(file), check_str(func), check_str(anno),
                              active->GetBool(), enabled.value_or(true));
  }

  apply_new_filters_next_frame_ = true;
  read_write_lock_.WriteUnLock();
}

void VizDebugger::StartDebugStream(
    mojo::PendingRemote<mojom::VizDebugOutput> pending_debug_output) {
  read_write_lock_.WriteLock();
  DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence());
  debug_output_.Bind(std::move(pending_debug_output));
  debug_output_.reset_on_disconnect();
  last_sent_source_count_ = 0;

  // Reset our filters for our new connection. By default the client will send
  // along the new filters after establishing the connection.
  new_filters_.clear();
  apply_new_filters_next_frame_ = true;

  debug_output_->LogFrame(
      base::Value(base::DictValue().Set("connection", "ok")));
  enabled_.store(true);
  read_write_lock_.WriteUnLock();
}

void VizDebugger::StopDebugStream() {
  read_write_lock_.WriteLock();
  DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence());
  debug_output_.reset();
  enabled_.store(false);
  read_write_lock_.WriteUnLock();
}

void VizDebugger::AddLogMessage(std::string log,
                                const VizDebugger::StaticSource* dcs,
                                DrawOption option) {
  //  Store atomic insertion index in local variable to use to insert into
  //  buffer.
  int insertion_index;

  for (;;) {
    read_write_lock_.ReadLock();
    // Get call insertion index.
    insertion_index = logs_tail_idx_++;
    // If the insertion index is within bounds, insert call into buffer.
    if (static_cast<size_t>(insertion_index) < logs_.size()) {
      int64_t cur_thread_id =
          static_cast<int64_t>(base::PlatformThread::CurrentId());
      logs_[insertion_index] = LogCall{submission_count_++, dcs->reg_index,
                                       cur_thread_id, option, std::move(log)};
      // Return when call insertion is successful.
      read_write_lock_.ReadUnlock();
      return;
    }
    read_write_lock_.ReadUnlock();
    // Take write lock to resize and re-adjust buffer tail index after buffer
    // overflow.
    read_write_lock_.WriteLock();
    // If tail index is over buffer size, then resizing is definitely needed.
    // Also re-adjust tail index so it's at the start of the new buffer space.
    if (static_cast<size_t>(logs_tail_idx_) >= logs_.size()) {
      logs_tail_idx_ = logs_.size();
      logs_.resize(logs_.size() * 2);
    }
    read_write_lock_.WriteUnLock();
  }
}

}  // namespace viz
#else  // !VIZ_DEBUGGER_IS_ON()
namespace viz {
VizDebugger::BufferInfo::BufferInfo() = default;
VizDebugger::BufferInfo::~BufferInfo() = default;
VizDebugger::BufferInfo::BufferInfo(const BufferInfo& a) = default;

std::unique_ptr<base::trace_event::ConvertableToTraceFormat>
DrawRectToTraceValue(const gfx::Vector2dF& pos,
                     const gfx::SizeF& size,
                     const std::string& text) {
  std::unique_ptr<base::trace_event::TracedValue> state(
      new base::trace_event::TracedValue());
  state->SetString("pos_x", base::NumberToString(pos.x()));
  state->SetString("pos_y", base::NumberToString(pos.y()));
  state->SetString("size_x", base::NumberToString(size.width()));
  state->SetString("size_y", base::NumberToString(size.height()));
  state->SetString("text", text);
  return state;
}

}  // namespace viz
#endif
