/*
 * Copyright 2019 The Chromium OS 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 "hal_adapter/camera_metadata_inspector.h"

#include <algorithm>
#include <utility>

#include <base/bind.h>
#include <base/command_line.h>
#include <base/memory/ptr_util.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <system/camera_metadata.h>

#include "cros-camera/common.h"

namespace {

const char kSeparator[] = " | ";
const int kSeparatorWidth = 3;

const char kArrow[] = " => ";
const int kArrowWidth = 4;

const char kEllipsis[] = "...";
const int kEllipsisWidth = 3;

// The ASCII escape color code for request metadata. It's green.
const int kRequestColor = 32;

// The ASCII escape color code for result metadata. It's yellow.
const int kResultColor = 33;

// The max column width for an output line.
const int kMaxLineWidth = 100;

// The format is HH:MM:ss.SSS, such as 23:59:59.999.
const int kTimestampWidth = 12;

// The format is (Req|Res)(frame % 1000), such as Req087.
const int kIdentifierWidth = 6;

// The length of the longest request/result key name (lensShadingCorrectionMap).
const int kMaxKeyWidth = 24;

// The remaining line width is the space for value.
const int kMaxValueWidth =
    kMaxLineWidth - (kTimestampWidth + kSeparatorWidth + kIdentifierWidth +
                     kSeparatorWidth + kMaxKeyWidth + kSeparatorWidth);

// Formats the key of |entry| as {tag_section}.{tag_name}.  Returns empty string
// on error.
std::string FormatEntryKey(const camera_metadata_ro_entry_t& entry) {
  const char* tag_section = get_camera_metadata_section_name(entry.tag);
  if (tag_section == nullptr) {
    LOGF(ERROR) << "Failed to get section name of " << entry.tag;
    return "";
  }

  const char* tag_name = get_camera_metadata_tag_name(entry.tag);
  if (tag_name == nullptr) {
    LOGF(ERROR) << "Failed to get tag name of " << entry.tag;
    return "";
  }

  return base::StringPrintf("%s.%s", tag_section, tag_name);
}

// Formats the value at index |idx| of |entry| according to its type.  Uses the
// name of enum if possible, and falls back to numeric value if not found.
// Returns empty string on error.
std::string FormatEntryValueAt(const camera_metadata_ro_entry_t& entry,
                               size_t idx) {
  switch (entry.type) {
    case TYPE_BYTE: {
      uint8_t val = entry.data.u8[idx];
      char buf[64];
      if (camera_metadata_enum_snprint(entry.tag, val, buf, sizeof(buf)) == 0) {
        return buf;
      }
      return std::to_string(val);
    }
    case TYPE_INT32: {
      int32_t val = entry.data.i32[idx];
      char buf[64];
      if (camera_metadata_enum_snprint(entry.tag, val, buf, sizeof(buf)) == 0) {
        return buf;
      }
      return std::to_string(val);
    }
    case TYPE_INT64:
      return std::to_string(entry.data.i64[idx]);
    case TYPE_FLOAT:
      return base::StringPrintf("%.2g", entry.data.f[idx]);
    case TYPE_DOUBLE:
      return base::StringPrintf("%.2g", entry.data.d[idx]);
    case TYPE_RATIONAL: {
      camera_metadata_rational_t val = entry.data.r[idx];
      return base::StringPrintf("%d/%d", val.numerator, val.denominator);
    }
    default:
      LOGF(ERROR) << "Unknown entry type " << entry.type;
      return "";
  }
}

}  // namespace

namespace cros {

std::string DiffData::FormatKey(int width) {
  DCHECK_GE(width, kEllipsisWidth);
  if (width >= key.size()) {
    return key;
  }
  auto dot_pos = key.find('.', key.size() - width - 1);
  if (dot_pos != std::string::npos) {
    return key.substr(dot_pos + 1);
  } else {
    return kEllipsis + key.substr(key.size() - (width - kEllipsisWidth));
  }
}

std::string DiffData::FormatValue(int width) {
  DCHECK_GE(width, kEllipsisWidth + kArrowWidth + kEllipsisWidth);

  int old_width = old_val.size();
  int new_width = new_val.size();
  int limit = width - kArrowWidth;

  if (old_width + new_width > limit) {
    if (std::min(old_width, new_width) * 2 <= limit) {
      // It can fit into |limit| by only trimming the longer one.
      if (old_width > new_width) {
        old_width = limit - new_width;
      } else {
        new_width = limit - old_width;
      }
    } else {
      // Distribute the width evenly if we need to trim both.
      old_width = limit / 2;
      new_width = limit - old_width;
    }
  }

  auto Format = [&](const std::string& s, int w) {
    if (s.size() <= w) {
      return s;
    }
    return s.substr(0, w - kEllipsisWidth) + kEllipsis;
  };

  return Format(old_val, old_width) + kArrow + Format(new_val, new_width);
}

// static
std::unique_ptr<CameraMetadataInspector> CameraMetadataInspector::Create(
    int partial_result_count) {
  auto cl = base::CommandLine::ForCurrentProcess();
  base::FilePath output_path =
      cl->GetSwitchValuePath("metadata_inspector_output");
  if (output_path.empty()) {
    return nullptr;
  }

  base::File output_file = {
      output_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND};
  if (!output_file.IsValid()) {
    LOGF(ERROR) << "Failed to open output file " << output_path.value();
    return nullptr;
  }

  auto GetRE2FromSwitch = [&](base::StringPiece name) -> std::unique_ptr<RE2> {
    std::string value = cl->GetSwitchValueASCII(name);
    if (value.empty()) {
      return nullptr;
    }
    RE2::Options option;
    option.set_case_sensitive(false);
    return std::make_unique<RE2>(value, option);
  };

  std::unique_ptr<RE2> allowlist =
      GetRE2FromSwitch("metadata_inspector_allowlist");
  if (allowlist && !allowlist->ok()) {
    LOGF(ERROR) << "Failed to build regex for allowlist: "
                << allowlist->error();
    return nullptr;
  }

  std::unique_ptr<RE2> denylist =
      GetRE2FromSwitch("metadata_inspector_denylist");
  if (denylist && !denylist->ok()) {
    LOGF(ERROR) << "Failed to build regex for denylist: " << denylist->error();
    return nullptr;
  }

  auto thread = std::make_unique<base::Thread>("CameraMetadataInspectorThread");
  if (!thread->Start()) {
    LOGF(ERROR) << "Failed to start thread";
    return nullptr;
  }

  return base::WrapUnique(new CameraMetadataInspector(
      partial_result_count, std::move(output_file), std::move(allowlist),
      std::move(denylist), std::move(thread)));
}

CameraMetadataInspector::CameraMetadataInspector(
    int partial_result_count,
    base::File output_file,
    std::unique_ptr<RE2> allowlist,
    std::unique_ptr<RE2> denylist,
    std::unique_ptr<base::Thread> thread)
    : partial_result_count_(partial_result_count),
      output_file_(std::move(output_file)),
      allowlist_(std::move(allowlist)),
      denylist_(std::move(denylist)),
      thread_(std::move(thread)) {
  result_sequence_checker_.DetachFromSequence();
}

void CameraMetadataInspector::Write(base::StringPiece msg) {
  output_file_.WriteAtCurrentPos(msg.data(), msg.size());
  output_file_.Flush();
}

bool CameraMetadataInspector::ShouldIgnoreKey(const std::string& key) {
  if (key.empty()) {
    return true;
  }
  if (allowlist_ && !RE2::PartialMatch(key, *allowlist_)) {
    return true;
  }
  if (denylist_ && RE2::PartialMatch(key, *denylist_)) {
    return true;
  }
  return false;
}

CameraMetadataInspector::DataMap CameraMetadataInspector::MapFromMetadata(
    const camera_metadata_t* metadata) {
  CameraMetadataInspector::DataMap map;
  size_t n = get_camera_metadata_entry_count(metadata);
  for (size_t i = 0; i < n; i++) {
    camera_metadata_ro_entry_t entry;
    int ret = get_camera_metadata_ro_entry(metadata, i, &entry);
    if (ret != 0) {
      LOGF(ERROR) << "Failed to get the metadata entry at " << i;
      continue;
    }
    std::string key = FormatEntryKey(entry);
    if (ShouldIgnoreKey(key)) {
      continue;
    }
    std::vector<std::string> values;
    values.reserve(entry.count);
    for (size_t j = 0; j < entry.count; j++) {
      values.push_back(FormatEntryValueAt(entry, j));
    }
    map[key] = base::JoinString(values, " ");
  }
  return map;
}

std::vector<DiffData> CameraMetadataInspector::Compare(const DataMap& old_map,
                                                       const DataMap& new_map) {
  std::vector<std::string> keys;
  for (auto& map : {old_map, new_map}) {
    for (auto& elem : map) {
      keys.push_back(elem.first);
    }
  }
  std::sort(keys.begin(), keys.end());
  keys.erase(std::unique(keys.begin(), keys.end()), keys.end());

  auto GetValue = [&](const DataMap& map, const std::string& key) {
    auto it = map.find(key);
    return it != map.end() ? it->second : "nil";
  };

  std::vector<DiffData> diffs;
  for (const auto& key : keys) {
    std::string old_val = GetValue(old_map, key);
    std::string new_val = GetValue(new_map, key);
    if (old_val != new_val) {
      DiffData diff = {key, old_val, new_val};
      diffs.push_back(diff);
    }
  }

  return diffs;
}

void CameraMetadataInspector::InspectOnThread(Kind kind,
                                              const std::string& kind_name,
                                              int color,
                                              base::Time time,
                                              int frame_number,
                                              camera_metadata_t* metadata) {
  DCHECK(thread_->task_runner()->BelongsToCurrentThread());
  auto map = MapFromMetadata(metadata);
  base::Time::Exploded exploded;
  time.LocalExplode(&exploded);
  auto diffs = Compare(latest_map[static_cast<size_t>(kind)], map);
  std::stringstream ss;
  for (auto diff : diffs) {
    ss << "\e[" << color << "m";
    ss << base::StringPrintf("%02d:%02d:%02d.%03d", exploded.hour,
                             exploded.minute, exploded.second,
                             exploded.millisecond);
    ss << kSeparator;
    ss << base::StringPrintf("%s%03d", kind_name.c_str(), frame_number % 1000);
    ss << kSeparator;
    ss << base::StringPrintf("%*s", kMaxKeyWidth,
                             diff.FormatKey(kMaxKeyWidth).c_str());
    ss << kSeparator;
    ss << diff.FormatValue(kMaxValueWidth);
    ss << "\e[0m\n";
  }
  Write(ss.str());
  latest_map[static_cast<size_t>(kind)] = map;
  free_camera_metadata(metadata);
}

void CameraMetadataInspector::InspectRequest(
    const camera3_capture_request_t* request) {
  if (request->settings == nullptr) {
    return;
  }
  thread_->task_runner()->PostTask(
      FROM_HERE,
      base::Bind(&CameraMetadataInspector::InspectOnThread,
                 base::Unretained(this), Kind::kRequest, "Req", kRequestColor,
                 base::Time::Now(), request->frame_number,
                 clone_camera_metadata(request->settings)));
}

void CameraMetadataInspector::InspectResult(
    const camera3_capture_result_t* result) {
  DCHECK(result_sequence_checker_.CalledOnValidSequence());
  if (result->result == nullptr) {
    return;
  }
  pending_result_.append(result->result);
  if (result->partial_result != partial_result_count_) {
    return;
  }
  thread_->task_runner()->PostTask(
      FROM_HERE, base::Bind(&CameraMetadataInspector::InspectOnThread,
                            base::Unretained(this), Kind::kResult, "Res",
                            kResultColor, base::Time::Now(),
                            result->frame_number, pending_result_.release()));
}

}  // namespace cros
