//
// Copyright (C) 2013 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 "update_engine/payload_consumer/install_plan.h"

#include <algorithm>
#include <utility>

#include <base/format_macros.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <android-base/stringprintf.h>

#include "update_engine/common/utils.h"
#include "update_engine/update_metadata.pb.h"

using std::string;
using std::vector;

namespace chromeos_update_engine {

namespace {
string PayloadUrlsToString(
    const decltype(InstallPlan::Payload::payload_urls)& payload_urls) {
  return "(" + android::base::Join(payload_urls, ",") + ")";
}

string VectorToString(const vector<std::pair<string, string>>& input,
                      const string& separator) {
  vector<string> vec;
  std::transform(input.begin(),
                 input.end(),
                 std::back_inserter(vec),
                 [](const auto& pair) {
                   return android::base::Join(vector{pair.first, pair.second},
                                              ": ");
                 });
  return android::base::Join(vec, separator);
}
}  // namespace

string InstallPayloadTypeToString(InstallPayloadType type) {
  switch (type) {
    case InstallPayloadType::kUnknown:
      return "unknown";
    case InstallPayloadType::kFull:
      return "full";
    case InstallPayloadType::kDelta:
      return "delta";
  }
  return "invalid type";
}

bool InstallPlan::operator==(const InstallPlan& that) const {
  return ((is_resume == that.is_resume) &&
          (download_url == that.download_url) && (payloads == that.payloads) &&
          (source_slot == that.source_slot) &&
          (target_slot == that.target_slot) && (partitions == that.partitions));
}

bool InstallPlan::operator!=(const InstallPlan& that) const {
  return !((*this) == that);
}

void InstallPlan::Dump() const {
  LOG(INFO) << "InstallPlan: \n" << ToString();
}

string InstallPlan::ToString() const {
  string url_str = download_url;
  if (android::base::StartsWith(ToLower(url_str), "fd://")) {
    int fd = std::stoi(url_str.substr(strlen("fd://")));
    url_str = utils::GetFilePath(fd);
  }

  vector<string> result_str;
  result_str.emplace_back(VectorToString(
      {
          {"type", (is_resume ? "resume" : "new_update")},
          {"version", version},
          {"source_slot", BootControlInterface::SlotName(source_slot)},
          {"target_slot", BootControlInterface::SlotName(target_slot)},
          {"initial url", url_str},
          {"hash_checks_mandatory", utils::ToString(hash_checks_mandatory)},
          {"powerwash_required", utils::ToString(powerwash_required)},
          {"switch_slot_on_reboot", utils::ToString(switch_slot_on_reboot)},
          {"run_post_install", utils::ToString(run_post_install)},
          {"rollback_data_save_requested",
           utils::ToString(rollback_data_save_requested)},
          {"write_verity", utils::ToString(write_verity)},
      },
      "\n"));

  for (const auto& partition : partitions) {
    result_str.emplace_back(VectorToString(
        {
            {"Partition", partition.name},
            {"source_size", std::format("{}", partition.source_size)},
            {"source_path", partition.source_path},
            {"source_hash",
             base::HexEncode(partition.source_hash.data(),
                             partition.source_hash.size())},
            {"target_size", std::format("{}", partition.target_size)},
            {"target_path", partition.target_path},
            {"target_hash",
             base::HexEncode(partition.target_hash.data(),
                             partition.target_hash.size())},
            {"run_postinstall", utils::ToString(partition.run_postinstall)},
            {"postinstall_path", partition.postinstall_path},
            {"readonly_target_path", partition.readonly_target_path},
            {"filesystem_type", partition.filesystem_type},
        },
        "\n  "));
  }

  for (unsigned int i = 0; i < payloads.size(); ++i) {
    const auto& payload = payloads[i];
    result_str.emplace_back(VectorToString(
        {
            {"Payload", std::format("{}", i)},
            {"urls", PayloadUrlsToString(payload.payload_urls)},
            {"size", std::format("{}", payload.size)},
            {"metadata_size", std::format("{}", payload.metadata_size)},
            {"metadata_signature", payload.metadata_signature},
            {"hash", base::HexEncode(payload.hash.data(), payload.hash.size())},
            {"type", InstallPayloadTypeToString(payload.type)},
            {"fingerprint", payload.fp},
            {"app_id", payload.app_id},
            {"already_applied", utils::ToString(payload.already_applied)},
        },
        "\n  "));
  }

  return android::base::Join(result_str, "\n");
}

bool InstallPlan::LoadPartitionsFromSlots(BootControlInterface* boot_control) {
  bool result = true;
  for (Partition& partition : partitions) {
    if (source_slot != BootControlInterface::kInvalidSlot &&
        partition.source_size > 0) {
      TEST_AND_RETURN_FALSE(boot_control->GetPartitionDevice(
          partition.name, source_slot, &partition.source_path));
    } else {
      partition.source_path.clear();
    }

    if (target_slot != BootControlInterface::kInvalidSlot &&
        partition.target_size > 0) {
      auto device = boot_control->GetPartitionDevice(
          partition.name, target_slot, source_slot);
      TEST_AND_RETURN_FALSE(device.has_value());
      partition.target_path = device->rw_device_path;
      partition.readonly_target_path = device->readonly_device_path;
    } else {
      partition.target_path.clear();
    }
  }
  return result;
}

bool InstallPlan::Partition::operator==(
    const InstallPlan::Partition& that) const {
  return (name == that.name && source_path == that.source_path &&
          source_size == that.source_size && source_hash == that.source_hash &&
          target_path == that.target_path && target_size == that.target_size &&
          target_hash == that.target_hash &&
          run_postinstall == that.run_postinstall &&
          postinstall_path == that.postinstall_path &&
          filesystem_type == that.filesystem_type &&
          postinstall_optional == that.postinstall_optional);
}

bool InstallPlan::Partition::ParseVerityConfig(
    const PartitionUpdate& partition) {
  if (partition.has_hash_tree_extent()) {
    Extent extent = partition.hash_tree_data_extent();
    hash_tree_data_offset = extent.start_block() * block_size;
    hash_tree_data_size = extent.num_blocks() * block_size;
    extent = partition.hash_tree_extent();
    hash_tree_offset = extent.start_block() * block_size;
    hash_tree_size = extent.num_blocks() * block_size;
    uint64_t hash_tree_data_end = hash_tree_data_offset + hash_tree_data_size;
    if (hash_tree_offset < hash_tree_data_end) {
      LOG(ERROR) << "Invalid hash tree extents, hash tree data ends at "
                 << hash_tree_data_end << ", but hash tree starts at "
                 << hash_tree_offset;
      return false;
    }
    hash_tree_algorithm = partition.hash_tree_algorithm();
    hash_tree_salt.assign(partition.hash_tree_salt().begin(),
                          partition.hash_tree_salt().end());
  }
  if (partition.has_fec_extent()) {
    Extent extent = partition.fec_data_extent();
    fec_data_offset = extent.start_block() * block_size;
    fec_data_size = extent.num_blocks() * block_size;
    extent = partition.fec_extent();
    fec_offset = extent.start_block() * block_size;
    fec_size = extent.num_blocks() * block_size;
    uint64_t fec_data_end = fec_data_offset + fec_data_size;
    if (fec_offset < fec_data_end) {
      LOG(ERROR) << "Invalid fec extents, fec data ends at " << fec_data_end
                 << ", but fec starts at " << fec_offset;
      return false;
    }
    fec_roots = partition.fec_roots();
  }
  return true;
}

template <typename PartitinoUpdateArray>
bool InstallPlan::ParseManifestToInstallPlan(
    const PartitinoUpdateArray& partitions,
    BootControlInterface* boot_control,
    size_t block_size,
    InstallPlan* install_plan,
    ErrorCode* error) {
  // Fill in the InstallPlan::partitions based on the partitions from the
  // payload.
  for (const PartitionUpdate& partition : partitions) {
    InstallPlan::Partition install_part;
    install_part.name = partition.partition_name();
    install_part.run_postinstall =
        partition.has_run_postinstall() && partition.run_postinstall();
    if (install_part.run_postinstall) {
      install_part.postinstall_path =
          (partition.has_postinstall_path() ? partition.postinstall_path()
                                            : kPostinstallDefaultScript);
      install_part.filesystem_type = partition.filesystem_type();
      install_part.postinstall_optional = partition.postinstall_optional();
    }

    if (partition.has_old_partition_info()) {
      const PartitionInfo& info = partition.old_partition_info();
      install_part.source_size = info.size();
      install_part.source_hash.assign(info.hash().begin(), info.hash().end());
    }

    if (!partition.has_new_partition_info()) {
      LOG(ERROR) << "Unable to get new partition hash info on partition "
                 << install_part.name << ".";
      *error = ErrorCode::kDownloadNewPartitionInfoError;
      return false;
    }
    const PartitionInfo& info = partition.new_partition_info();
    install_part.target_size = info.size();
    install_part.target_hash.assign(info.hash().begin(), info.hash().end());

    install_part.block_size = block_size;
    if (!install_part.ParseVerityConfig(partition)) {
      *error = ErrorCode::kDownloadNewPartitionInfoError;
      LOG(INFO) << "Failed to parse partition `" << partition.partition_name()
                << "` verity configs";
      return false;
    }

    install_plan->partitions.push_back(install_part);
  }

  // TODO(xunchang) only need to load the partitions for those in payload.
  // Because we have already loaded the other once when generating SOURCE_COPY
  // operations.
  if (!install_plan->LoadPartitionsFromSlots(boot_control)) {
    LOG(ERROR) << "Unable to determine all the partition devices.";
    *error = ErrorCode::kInstallDeviceOpenError;
    return false;
  }
  return true;
}

bool InstallPlan::ParsePartitions(
    const std::vector<PartitionUpdate>& partitions,
    BootControlInterface* boot_control,
    size_t block_size,
    ErrorCode* error) {
  return ParseManifestToInstallPlan(
      partitions, boot_control, block_size, this, error);
}

bool InstallPlan::ParsePartitions(
    const google::protobuf::RepeatedPtrField<PartitionUpdate>& partitions,
    BootControlInterface* boot_control,
    size_t block_size,
    ErrorCode* error) {
  return ParseManifestToInstallPlan(
      partitions, boot_control, block_size, this, error);
}

}  // namespace chromeos_update_engine
