/*
 * Copyright (C) 2020 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 "host/libs/config/custom_actions.h"

#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <json/json.h>

#include <fstream>
#include <optional>
#include <string>
#include <vector>

#include "common/libs/utils/files.h"
#include "common/libs/utils/flag_parser.h"
#include "common/libs/utils/json.h"
#include "host/libs/config/cuttlefish_config.h"

namespace cuttlefish {
namespace {

const char* kCustomActionInstanceID = "instance_id";
const char* kCustomActionShellCommand = "shell_command";
const char* kCustomActionServer = "server";
const char* kCustomActionDeviceStates = "device_states";
const char* kCustomActionDeviceStateLidSwitchOpen = "lid_switch_open";
const char* kCustomActionDeviceStateHingeAngleValue = "hinge_angle_value";
const char* kCustomActionButton = "button";
const char* kCustomActionButtons = "buttons";
const char* kCustomActionButtonCommand = "command";
const char* kCustomActionButtonTitle = "title";
const char* kCustomActionButtonIconName = "icon_name";

CustomActionInstanceID GetCustomActionInstanceIDFromJson(
    const Json::Value& dictionary) {
  CustomActionInstanceID config;
  config.instance_id = dictionary[kCustomActionInstanceID].asString();
  return config;
}

CustomShellActionConfig GetCustomShellActionConfigFromJson(
    const Json::Value& dictionary) {
  CustomShellActionConfig config;
  // Shell command with one button.
  Json::Value button_entry = dictionary[kCustomActionButton];
  config.button = {button_entry[kCustomActionButtonCommand].asString(),
    button_entry[kCustomActionButtonTitle].asString(),
    button_entry[kCustomActionButtonIconName].asString()};
  config.shell_command = dictionary[kCustomActionShellCommand].asString();
  return config;
}

CustomActionServerConfig GetCustomActionServerConfigFromJson(
    const Json::Value& dictionary) {
  CustomActionServerConfig config;
  // Action server with possibly multiple buttons.
  for (const Json::Value& button_entry : dictionary[kCustomActionButtons]) {
    config.buttons.push_back(
        {button_entry[kCustomActionButtonCommand].asString(),
        button_entry[kCustomActionButtonTitle].asString(),
        button_entry[kCustomActionButtonIconName].asString()});
  }
  config.server = dictionary[kCustomActionServer].asString();
  return config;
}

CustomDeviceStateActionConfig GetCustomDeviceStateActionConfigFromJson(
    const Json::Value& dictionary) {
  CustomDeviceStateActionConfig config;
  // Device state(s) with one button.
  // Each button press cycles to the next state, then repeats to the first.
  Json::Value button_entry = dictionary[kCustomActionButton];
  config.button = {button_entry[kCustomActionButtonCommand].asString(),
    button_entry[kCustomActionButtonTitle].asString(),
    button_entry[kCustomActionButtonIconName].asString()};
  for (const Json::Value& device_state_entry :
      dictionary[kCustomActionDeviceStates]) {
    DeviceState state;
    if (device_state_entry.isMember(
          kCustomActionDeviceStateLidSwitchOpen)) {
      state.lid_switch_open =
        device_state_entry[kCustomActionDeviceStateLidSwitchOpen].asBool();
    }
    if (device_state_entry.isMember(
          kCustomActionDeviceStateHingeAngleValue)) {
      state.hinge_angle_value =
        device_state_entry[kCustomActionDeviceStateHingeAngleValue].asInt();
    }
    config.device_states.push_back(state);
  }
  return config;
}

Json::Value ToJson(const CustomActionInstanceID& custom_action) {
  Json::Value json;
  json[kCustomActionInstanceID] = custom_action.instance_id;
  return json;
}

Json::Value ToJson(const CustomShellActionConfig& custom_action) {
  Json::Value json;
  // Shell command with one button.
  json[kCustomActionShellCommand] = custom_action.shell_command;
  json[kCustomActionButton] = Json::Value();
  json[kCustomActionButton][kCustomActionButtonCommand] =
      custom_action.button.command;
  json[kCustomActionButton][kCustomActionButtonTitle] =
      custom_action.button.title;
  json[kCustomActionButton][kCustomActionButtonIconName] =
      custom_action.button.icon_name;
  return json;
}

Json::Value ToJson(const CustomActionServerConfig& custom_action) {
  Json::Value json;
  // Action server with possibly multiple buttons.
  json[kCustomActionServer] = custom_action.server;
  json[kCustomActionButtons] = Json::Value(Json::arrayValue);
  for (const auto& button : custom_action.buttons) {
    Json::Value button_entry;
    button_entry[kCustomActionButtonCommand] = button.command;
    button_entry[kCustomActionButtonTitle] = button.title;
    button_entry[kCustomActionButtonIconName] = button.icon_name;
    json[kCustomActionButtons].append(button_entry);
  }
  return json;
}

Json::Value ToJson(const CustomDeviceStateActionConfig& custom_action) {
  Json::Value json;
  // Device state(s) with one button.
  json[kCustomActionDeviceStates] = Json::Value(Json::arrayValue);
  for (const auto& device_state : custom_action.device_states) {
    Json::Value device_state_entry;
    if (device_state.lid_switch_open) {
      device_state_entry[kCustomActionDeviceStateLidSwitchOpen] =
          *device_state.lid_switch_open;
    }
    if (device_state.hinge_angle_value) {
      device_state_entry[kCustomActionDeviceStateHingeAngleValue] =
          *device_state.hinge_angle_value;
    }
    json[kCustomActionDeviceStates].append(device_state_entry);
  }
  json[kCustomActionButton] = Json::Value();
  json[kCustomActionButton][kCustomActionButtonCommand] =
      custom_action.button.command;
  json[kCustomActionButton][kCustomActionButtonTitle] =
      custom_action.button.title;
  json[kCustomActionButton][kCustomActionButtonIconName] =
      custom_action.button.icon_name;
  return json;
}

std::string DefaultCustomActionConfig() {
  auto custom_action_config_dir =
      DefaultHostArtifactsPath("etc/cvd_custom_action_config");
  if (DirectoryExists(custom_action_config_dir)) {
    auto directory_contents_result =
        DirectoryContents(custom_action_config_dir);
    CHECK(directory_contents_result.ok())
        << directory_contents_result.error().FormatForEnv();
    auto custom_action_configs = std::move(*directory_contents_result);
    if (custom_action_configs.size() > 1) {
      LOG(ERROR) << "Expected at most one custom action config in "
                 << custom_action_config_dir << ". Please delete extras.";
    } else if (custom_action_configs.size() == 1) {
      for (const auto& config : custom_action_configs) {
        if (android::base::EndsWithIgnoreCase(config, ".json")) {
          return custom_action_config_dir + "/" + config;
        }
      }
    }
  }
  return "";
}

int get_instance_order(const std::string& id_str) {
  int instance_index = 0;
  const auto& config = CuttlefishConfig::Get();
  for (const auto& instance : config->Instances()) {
    if (instance.id() == id_str) {
      break;
    }
    instance_index++;
  }
  return instance_index;
}

class CustomActionConfigImpl : public CustomActionConfigProvider {
 public:
  INJECT(CustomActionConfigImpl(ConfigFlag& config)) : config_(config) {
    custom_action_config_flag_ = GflagsCompatFlag("custom_action_config");
    custom_action_config_flag_.Help(
        "Path to a custom action config JSON. Defaults to the file provided by "
        "build variable CVD_CUSTOM_ACTION_CONFIG. If this build variable is "
        "empty then the custom action config will be empty as well.");
    custom_action_config_flag_.Getter(
        [this]() { return custom_action_config_[0]; });
    custom_action_config_flag_.Setter(
        [this](const FlagMatch& match) -> Result<void> {
          if (!match.value.empty() &&
              (match.value == "unset" || match.value == "\"unset\"")) {
            custom_action_config_.push_back(DefaultCustomActionConfig());
          } else if (!match.value.empty() && !FileExists(match.value)) {
            return CF_ERRF("custom_action_config file \"{}\" does not exist.",
                           match.value);
          } else {
            custom_action_config_.push_back(match.value);
          }
          return {};
        });
    // TODO(schuffelen): Access ConfigFlag directly for these values.
    custom_actions_flag_ = GflagsCompatFlag("custom_actions");
    custom_actions_flag_.Help(
        "Serialized JSON of an array of custom action objects (in the same "
        "format as custom action config JSON files). For use within --config "
        "preset config files; prefer --custom_action_config to specify a "
        "custom config file on the command line. Actions in this flag are "
        "combined with actions in --custom_action_config.");
    custom_actions_flag_.Setter([this](const FlagMatch& match) -> Result<void> {
      // Load the custom action from the --config preset file.
      if (match.value == "unset" || match.value == "\"unset\"") {
        AddEmptyJsonCustomActionConfigs();
        return {};
      }
      auto custom_action_array = CF_EXPECT(
          ParseJson(match.value), "Could not read custom actions config flag");
      CF_EXPECT(AddJsonCustomActionConfigs(custom_action_array));
      return {};
    });
  }

  const std::vector<CustomShellActionConfig> CustomShellActions(
      const std::string& id_str = std::string()) const override {
    int instance_index = 0;
    if (instance_actions_.empty()) {
      // No Custom Action input, return empty vector
      return {};
    }

    if (!id_str.empty()) {
      instance_index = get_instance_order(id_str);
    }
    if (instance_index >= instance_actions_.size()) {
      instance_index = 0;
    }
    return instance_actions_[instance_index].custom_shell_actions_;
  }

  const std::vector<CustomActionServerConfig> CustomActionServers(
      const std::string& id_str = std::string()) const override {
    int instance_index = 0;
    if (instance_actions_.empty()) {
      // No Custom Action input, return empty vector
      return {};
    }

    if (!id_str.empty()) {
      instance_index = get_instance_order(id_str);
    }
    if (instance_index >= instance_actions_.size()) {
      instance_index = 0;
    }
    return instance_actions_[instance_index].custom_action_servers_;
  }

  const std::vector<CustomDeviceStateActionConfig> CustomDeviceStateActions(
      const std::string& id_str = std::string()) const override {
    int instance_index = 0;
    if (instance_actions_.empty()) {
      // No Custom Action input, return empty vector
      return {};
    }

    if (!id_str.empty()) {
      instance_index = get_instance_order(id_str);
    }
    if (instance_index >= instance_actions_.size()) {
      instance_index = 0;
    }
    return instance_actions_[instance_index].custom_device_state_actions_;
  }

  // ConfigFragment
  Json::Value Serialize() const override {
    Json::Value actions_array(Json::arrayValue);
    for (const auto& each_instance_actions_ : instance_actions_) {
      actions_array.append(
          ToJson(each_instance_actions_.custom_action_instance_id_));
      for (const auto& action : each_instance_actions_.custom_shell_actions_) {
        actions_array.append(ToJson(action));
      }
      for (const auto& action : each_instance_actions_.custom_action_servers_) {
        actions_array.append(ToJson(action));
      }
      for (const auto& action :
           each_instance_actions_.custom_device_state_actions_) {
        actions_array.append(ToJson(action));
      }
    }
    return actions_array;
  }
  bool Deserialize(const Json::Value& custom_actions_json) override {
    return AddJsonCustomActionConfigs(custom_actions_json);
  }

  // FlagFeature
  std::string Name() const override { return "CustomActionConfig"; }
  std::unordered_set<FlagFeature*> Dependencies() const override {
    return {static_cast<FlagFeature*>(&config_)};
  }

  Result<void> Process(std::vector<std::string>& args) override {
    CF_EXPECT(ConsumeFlags(Flags(), args));
    if (custom_action_config_.empty()) {
      // no custom action flag input
      custom_action_config_.push_back(DefaultCustomActionConfig());
    }
    for (const auto& config : custom_action_config_) {
      if (config != "") {
        std::string config_contents;
        CF_EXPECT(android::base::ReadFileToString(config, &config_contents));
        auto custom_action_array = CF_EXPECT(ParseJson(config_contents));
        CF_EXPECTF(AddJsonCustomActionConfigs(custom_action_array),
                   "Failed to parse config at \"{}\"", config);
      } else {
        AddEmptyJsonCustomActionConfigs();
      }
    }
    return {};
  }
  bool WriteGflagsCompatHelpXml(std::ostream& out) const override {
    return WriteGflagsCompatXml(Flags(), out);
  }

 private:
  struct InstanceActions {
    std::vector<CustomShellActionConfig> custom_shell_actions_;
    std::vector<CustomActionServerConfig> custom_action_servers_;
    std::vector<CustomDeviceStateActionConfig> custom_device_state_actions_;
    CustomActionInstanceID custom_action_instance_id_;
  };

  std::vector<Flag> Flags() const {
    return {custom_action_config_flag_, custom_actions_flag_};
  }

  void AddEmptyJsonCustomActionConfigs() {
    InstanceActions instance_action;
    instance_action.custom_action_instance_id_.instance_id =
        std::to_string(instance_actions_.size());
    instance_actions_.push_back(instance_action);
  }

  bool AddJsonCustomActionConfigs(const Json::Value& custom_action_array) {
    if (custom_action_array.type() != Json::arrayValue) {
      LOG(ERROR) << "Expected a JSON array of custom actions";
      return false;
    }
    InstanceActions instance_action;
    instance_action.custom_action_instance_id_.instance_id = "-1";

    for (const auto& custom_action : custom_action_array) {
      // for multi-instances case, assume instance_id, shell_command,
      // server and device_states comes together before next instance
      bool has_instance_id = custom_action.isMember(kCustomActionInstanceID);
      bool has_shell_command =
          custom_action.isMember(kCustomActionShellCommand);
      bool has_server = custom_action.isMember(kCustomActionServer);
      bool has_device_states =
          custom_action.isMember(kCustomActionDeviceStates);
      if (!!has_shell_command + !!has_server + !!has_device_states +
              !!has_instance_id !=
          1) {
        LOG(ERROR) << "Custom action must contain exactly one of "
                      "shell_command, server, device_states or instance_id";
        return false;
      }

      if (has_shell_command) {
        auto config = GetCustomShellActionConfigFromJson(custom_action);
        instance_action.custom_shell_actions_.push_back(config);
      } else if (has_server) {
        auto config = GetCustomActionServerConfigFromJson(custom_action);
        instance_action.custom_action_servers_.push_back(config);
      } else if (has_device_states) {
        auto config = GetCustomDeviceStateActionConfigFromJson(custom_action);
        instance_action.custom_device_state_actions_.push_back(config);
      } else if (has_instance_id) {
        auto config = GetCustomActionInstanceIDFromJson(custom_action);
        if (instance_action.custom_action_instance_id_.instance_id != "-1") {
          // already has instance id, start a new instance
          instance_actions_.push_back(instance_action);
          instance_action = InstanceActions();
        }
        instance_action.custom_action_instance_id_ = config;
      } else {
        LOG(ERROR) << "Unknown custom action type.";
        return false;
      }
    }
    if (instance_action.custom_action_instance_id_.instance_id == "-1") {
      // default id "-1" which means no instance id assigned yet
      // at this time, just assign the # of instance as ID
      instance_action.custom_action_instance_id_.instance_id =
          std::to_string(instance_actions_.size());
    }
    instance_actions_.push_back(instance_action);
    return true;
  }

    ConfigFlag& config_;
    Flag custom_action_config_flag_;
    std::vector<std::string> custom_action_config_;
    Flag custom_actions_flag_;
    std::vector<InstanceActions> instance_actions_;
  };

}  // namespace

fruit::Component<fruit::Required<ConfigFlag>, CustomActionConfigProvider>
CustomActionsComponent() {
  return fruit::createComponent()
      .bind<CustomActionConfigProvider, CustomActionConfigImpl>()
      .addMultibinding<ConfigFragment, CustomActionConfigProvider>()
      .addMultibinding<FlagFeature, CustomActionConfigProvider>();
}

}  // namespace cuttlefish
