/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "src/traced/probes/android_game_intervention_list/android_game_intervention_list_data_source.h"

#include <stddef.h>

#include "perfetto/base/logging.h"
#include "perfetto/ext/base/optional.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/tracing/core/trace_writer.h"

#include "perfetto/tracing/core/data_source_config.h"
#include "protos/perfetto/config/android/android_game_intervention_list_config.pbzero.h"
#include "protos/perfetto/trace/android/android_game_intervention_list.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"

namespace perfetto {

const char kAndroidGameInterventionListFileName[] =
    "/data/system/game_mode_intervention.list";

// making the descriptor static
const ProbesDataSource::Descriptor
    AndroidGameInterventionListDataSource::descriptor = {
        /* name */ "android.game_interventions",
        /* flags */ Descriptor::kFlagsNone,
        /* fill_descriptor_func */ nullptr,
};

AndroidGameInterventionListDataSource::AndroidGameInterventionListDataSource(
    const DataSourceConfig& ds_config,
    TracingSessionID session_id,
    std::unique_ptr<TraceWriter> trace_writer)
    : ProbesDataSource(session_id, &descriptor),
      trace_writer_(std::move(trace_writer)) {
  perfetto::protos::pbzero::AndroidGameInterventionListConfig::Decoder cfg(
      ds_config.android_game_intervention_list_config_raw());
  for (auto name = cfg.package_name_filter(); name; ++name) {
    package_name_filter_.emplace_back((*name).ToStdString());
  }
}

AndroidGameInterventionListDataSource::
    ~AndroidGameInterventionListDataSource() = default;

void AndroidGameInterventionListDataSource::Start() {
  auto trace_packet = trace_writer_->NewTracePacket();
  auto* android_game_intervention_list_packet =
      trace_packet->set_android_game_intervention_list();

  base::ScopedFstream fs(fopen(kAndroidGameInterventionListFileName, "r"));
  if (!fs) {
    PERFETTO_ELOG("Failed to open %s", kAndroidGameInterventionListFileName);
    android_game_intervention_list_packet->set_read_error(true);
  } else {
    bool is_parsed_fully = ParseAndroidGameInterventionListStream(
        android_game_intervention_list_packet, fs, package_name_filter_);
    if (!is_parsed_fully) {
      android_game_intervention_list_packet->set_parse_error(true);
    }
    if (ferror(*fs)) {
      android_game_intervention_list_packet->set_read_error(true);
    }
  }

  trace_packet->Finalize();
  trace_writer_->Flush();
}

void AndroidGameInterventionListDataSource::Flush(
    FlushRequestID,
    std::function<void()> callback) {
  callback();
}

bool AndroidGameInterventionListDataSource::
    ParseAndroidGameInterventionListStream(
        protos::pbzero::AndroidGameInterventionList* packet,
        const base::ScopedFstream& fs,
        const std::vector<std::string>& package_name_filter) {
  bool is_parsed_fully = true;
  char line_buf[2048];
  while (fgets(line_buf, sizeof(line_buf), *fs) != nullptr) {
    // removing trailing '\n'
    // for processing fields with CStringTo* functions
    line_buf[strlen(line_buf) - 1] = '\0';

    if (!ParseAndroidGameInterventionListLine(line_buf, package_name_filter,
                                              packet)) {
      // marking parsed with error and continue with this line skipped
      is_parsed_fully = false;
    }
  }
  return is_parsed_fully;
}

bool AndroidGameInterventionListDataSource::
    ParseAndroidGameInterventionListLine(
        char* line,
        const std::vector<std::string>& package_name_filter,
        protos::pbzero::AndroidGameInterventionList* packet) {
  size_t idx = 0;
  perfetto::protos::pbzero::AndroidGameInterventionList_GamePackageInfo*
      package = nullptr;
  perfetto::protos::pbzero::AndroidGameInterventionList_GameModeInfo*
      game_mode_info = nullptr;
  for (base::StringSplitter string_splitter(line, '\t'); string_splitter.Next();
       ++idx) {
    // check if package name is in the name filter
    // if not we skip parsing this line.
    if (idx == 0) {
      if (!package_name_filter.empty() &&
          std::count(package_name_filter.begin(), package_name_filter.end(),
                     string_splitter.cur_token()) == 0) {
        return true;
      }
      package = packet->add_game_packages();
    }

    switch (idx) {
      case 0: {
        package->set_name(string_splitter.cur_token(),
                          string_splitter.cur_token_size());
        break;
      }
      case 1: {
        base::Optional<uint64_t> uid =
            base::CStringToUInt64(string_splitter.cur_token());
        if (uid == base::nullopt) {
          PERFETTO_DLOG("Failed to parse game_mode_intervention.list uid.");
          return false;
        }
        package->set_uid(uid.value());
        break;
      }
      case 2: {
        base::Optional<uint32_t> cur_mode =
            base::CStringToUInt32(string_splitter.cur_token());
        if (cur_mode == base::nullopt) {
          PERFETTO_DLOG(
              "Failed to parse game_mode_intervention.list cur_mode.");
          return false;
        }
        package->set_current_mode(cur_mode.value());
        break;
      }
      case 3:
      case 5:
      case 7: {
        base::Optional<uint32_t> game_mode =
            base::CStringToUInt32(string_splitter.cur_token());
        if (game_mode == base::nullopt) {
          PERFETTO_DLOG(
              "Failed to parse game_mode_intervention.list game_mode.");
          return false;
        }
        game_mode_info = package->add_game_mode_info();
        game_mode_info->set_mode(game_mode.value());
        break;
      }
      case 4:
      case 6:
      case 8: {
        for (base::StringSplitter intervention_splitter(
                 string_splitter.cur_token(), ',');
             intervention_splitter.Next();) {
          base::StringSplitter value_splitter(intervention_splitter.cur_token(),
                                              '=');
          value_splitter.Next();
          char* key = value_splitter.cur_token();
          if (strcmp(key, "angle") == 0) {
            value_splitter.Next();
            base::Optional<uint32_t> use_angle =
                base::CStringToUInt32(value_splitter.cur_token());
            if (use_angle == base::nullopt) {
              PERFETTO_DLOG(
                  "Failed to parse game_mode_intervention.list use_angle.");
              return false;
            }
            game_mode_info->set_use_angle(use_angle.value());
          } else if (strcmp(key, "scaling") == 0) {
            value_splitter.Next();
            base::Optional<double> resolution_downscale =
                base::CStringToDouble(value_splitter.cur_token());
            if (resolution_downscale == base::nullopt) {
              PERFETTO_DLOG(
                  "Failed to parse game_mode_intervention.list "
                  "resolution_downscale.");
              return false;
            }
            game_mode_info->set_resolution_downscale(
                static_cast<float>(resolution_downscale.value()));
          } else if (strcmp(key, "fps") == 0) {
            value_splitter.Next();
            base::Optional<double> fps =
                base::CStringToDouble(value_splitter.cur_token());
            if (fps == base::nullopt) {
              PERFETTO_DLOG("Failed to parse game_mode_intervention.list fps.");
              return false;
            }
            game_mode_info->set_fps(static_cast<float>(fps.value()));
          }
        }
        break;
      }
    }
  }
  return true;
}

}  // namespace perfetto
