/*
 *  Copyright 2019 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */
#include "rtc_base/experiments/field_trial_parser.h"

#include <inttypes.h>

#include <algorithm>
#include <map>
#include <type_traits>
#include <utility>

#include "absl/strings/string_view.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/safe_conversions.h"

namespace webrtc {

FieldTrialParameterInterface::FieldTrialParameterInterface(
    absl::string_view key)
    : key_(key) {}
FieldTrialParameterInterface::~FieldTrialParameterInterface() {
  RTC_DCHECK(used_) << "Field trial parameter with key: '" << key_
                    << "' never used.";
}

void ParseFieldTrial(
    std::initializer_list<FieldTrialParameterInterface*> fields,
    absl::string_view trial_string) {
  std::map<absl::string_view, FieldTrialParameterInterface*> field_map;
  FieldTrialParameterInterface* keyless_field = nullptr;
  for (FieldTrialParameterInterface* field : fields) {
    field->MarkAsUsed();
    if (!field->sub_parameters_.empty()) {
      for (FieldTrialParameterInterface* sub_field : field->sub_parameters_) {
        RTC_DCHECK(!sub_field->key_.empty());
        sub_field->MarkAsUsed();
        field_map[sub_field->key_] = sub_field;
      }
      continue;
    }

    if (field->key_.empty()) {
      RTC_DCHECK(!keyless_field);
      keyless_field = field;
    } else {
      field_map[field->key_] = field;
    }
  }
  bool logged_unknown_key = false;

  absl::string_view tail = trial_string;
  while (!tail.empty()) {
    size_t key_end = tail.find_first_of(",:");
    absl::string_view key = tail.substr(0, key_end);
    std::optional<std::string> opt_value;
    if (key_end == absl::string_view::npos) {
      tail = "";
    } else if (tail[key_end] == ':') {
      tail = tail.substr(key_end + 1);
      size_t value_end = tail.find(',');
      opt_value.emplace(tail.substr(0, value_end));
      if (value_end == absl::string_view::npos) {
        tail = "";
      } else {
        tail = tail.substr(value_end + 1);
      }
    } else {
      RTC_DCHECK_EQ(tail[key_end], ',');
      tail = tail.substr(key_end + 1);
    }

    auto field = field_map.find(key);
    if (field != field_map.end()) {
      if (!field->second->Parse(std::move(opt_value))) {
        RTC_LOG(LS_WARNING) << "Failed to read field with key: '" << key
                            << "' in trial: \"" << trial_string << "\"";
      }
    } else if (!opt_value && keyless_field && !key.empty()) {
      if (!keyless_field->Parse(std::string(key))) {
        RTC_LOG(LS_WARNING) << "Failed to read empty key field with value '"
                            << key << "' in trial: \"" << trial_string << "\"";
      }
    } else if (key.empty() || key[0] != '_') {
      // "_" is be used to prefix keys that are part of the string for
      // debugging purposes but not neccessarily used.
      // e.g. WebRTC-Experiment/param: value, _DebuggingString
      if (!logged_unknown_key) {
        RTC_LOG(LS_INFO) << "No field with key: '" << key
                         << "' (found in trial: \"" << trial_string << "\")";
        std::string valid_keys;
        for (const auto& f : field_map) {
          valid_keys.append(f.first.data(), f.first.size());
          valid_keys += ", ";
        }
        RTC_LOG(LS_INFO) << "Valid keys are: " << valid_keys;
        logged_unknown_key = true;
      }
    }
  }

  for (FieldTrialParameterInterface* field : fields) {
    field->ParseDone();
  }
}

template <>
std::optional<bool> ParseTypedParameter<bool>(absl::string_view str) {
  if (str == "true" || str == "1") {
    return true;
  } else if (str == "false" || str == "0") {
    return false;
  }
  return std::nullopt;
}

template <>
std::optional<double> ParseTypedParameter<double>(absl::string_view str) {
  double value;
  char unit[2]{0, 0};
  if (sscanf(std::string(str).c_str(), "%lf%1s", &value, unit) >= 1) {
    if (unit[0] == '%')
      return value / 100;
    return value;
  } else {
    return std::nullopt;
  }
}

template <>
std::optional<int> ParseTypedParameter<int>(absl::string_view str) {
  int64_t value;
  if (sscanf(std::string(str).c_str(), "%" SCNd64, &value) == 1) {
    if (rtc::IsValueInRangeForNumericType<int, int64_t>(value)) {
      return static_cast<int>(value);
    }
  }
  return std::nullopt;
}

template <>
std::optional<unsigned> ParseTypedParameter<unsigned>(absl::string_view str) {
  int64_t value;
  if (sscanf(std::string(str).c_str(), "%" SCNd64, &value) == 1) {
    if (rtc::IsValueInRangeForNumericType<unsigned, int64_t>(value)) {
      return static_cast<unsigned>(value);
    }
  }
  return std::nullopt;
}

template <>
std::optional<std::string> ParseTypedParameter<std::string>(
    absl::string_view str) {
  return std::string(str);
}

template <>
std::optional<std::optional<bool>> ParseTypedParameter<std::optional<bool>>(
    absl::string_view str) {
  return ParseOptionalParameter<bool>(str);
}
template <>
std::optional<std::optional<int>> ParseTypedParameter<std::optional<int>>(
    absl::string_view str) {
  return ParseOptionalParameter<int>(str);
}
template <>
std::optional<std::optional<unsigned>>
ParseTypedParameter<std::optional<unsigned>>(absl::string_view str) {
  return ParseOptionalParameter<unsigned>(str);
}
template <>
std::optional<std::optional<double>> ParseTypedParameter<std::optional<double>>(
    absl::string_view str) {
  return ParseOptionalParameter<double>(str);
}

FieldTrialFlag::FieldTrialFlag(absl::string_view key)
    : FieldTrialFlag(key, false) {}

FieldTrialFlag::FieldTrialFlag(absl::string_view key, bool default_value)
    : FieldTrialParameterInterface(key), value_(default_value) {}

bool FieldTrialFlag::Get() const {
  return value_;
}

webrtc::FieldTrialFlag::operator bool() const {
  return value_;
}

bool FieldTrialFlag::Parse(std::optional<std::string> str_value) {
  // Only set the flag if there is no argument provided.
  if (str_value) {
    std::optional<bool> opt_value = ParseTypedParameter<bool>(*str_value);
    if (!opt_value)
      return false;
    value_ = *opt_value;
  } else {
    value_ = true;
  }
  return true;
}

AbstractFieldTrialEnum::AbstractFieldTrialEnum(
    absl::string_view key,
    int default_value,
    std::map<std::string, int> mapping)
    : FieldTrialParameterInterface(key),
      value_(default_value),
      enum_mapping_(mapping) {
  for (auto& key_val : enum_mapping_)
    valid_values_.insert(key_val.second);
}
AbstractFieldTrialEnum::AbstractFieldTrialEnum(const AbstractFieldTrialEnum&) =
    default;
AbstractFieldTrialEnum::~AbstractFieldTrialEnum() = default;

bool AbstractFieldTrialEnum::Parse(std::optional<std::string> str_value) {
  if (str_value) {
    auto it = enum_mapping_.find(*str_value);
    if (it != enum_mapping_.end()) {
      value_ = it->second;
      return true;
    }
    std::optional<int> value = ParseTypedParameter<int>(*str_value);
    if (value.has_value() &&
        (valid_values_.find(*value) != valid_values_.end())) {
      value_ = *value;
      return true;
    }
  }
  return false;
}

template class FieldTrialParameter<bool>;
template class FieldTrialParameter<double>;
template class FieldTrialParameter<int>;
template class FieldTrialParameter<unsigned>;
template class FieldTrialParameter<std::string>;

template class FieldTrialConstrained<double>;
template class FieldTrialConstrained<int>;
template class FieldTrialConstrained<unsigned>;

template class FieldTrialOptional<double>;
template class FieldTrialOptional<int>;
template class FieldTrialOptional<unsigned>;
template class FieldTrialOptional<bool>;
template class FieldTrialOptional<std::string>;

}  // namespace webrtc
