// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sync/engine/cycle/nudge_tracker.h"

#include <algorithm>
#include <utility>

#include "components/sync/protocol/data_type_progress_marker.pb.h"
#include "components/sync/protocol/sync_enums.pb.h"

namespace syncer {

namespace {

// Nudge delay for local refresh. Common to all data types.
constexpr base::TimeDelta kLocalRefreshDelay = base::Milliseconds(500);

}  // namespace

NudgeTracker::NudgeTracker() {
  for (DataType type : DataTypeSet::All()) {
    type_trackers_[type] = std::make_unique<DataTypeTracker>(type);
  }
}

NudgeTracker::~NudgeTracker() = default;

bool NudgeTracker::IsSyncRequired(DataTypeSet types) const {
  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    if (tracker_it->second->IsSyncRequired()) {
      return true;
    }
  }

  return false;
}

bool NudgeTracker::IsGetUpdatesRequired(DataTypeSet types) const {
  if (invalidations_out_of_sync_) {
    return true;
  }

  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    if (tracker_it->second->IsGetUpdatesRequired()) {
      return true;
    }
  }
  return false;
}

void NudgeTracker::RecordSuccessfulCommitMessage(DataTypeSet types) {
  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    tracker_it->second->RecordSuccessfulCommitMessage();
  }
}

void NudgeTracker::RecordSuccessfulSyncCycleIfNotBlocked(DataTypeSet types) {
  // A successful cycle while invalidations are enabled puts us back into sync.
  invalidations_out_of_sync_ = !invalidations_enabled_;

  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    tracker_it->second->RecordSuccessfulSyncCycleIfNotBlocked();
  }
}

void NudgeTracker::RecordInitialSyncDone(DataTypeSet types) {
  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    tracker_it->second->RecordInitialSyncDone();
  }
}

base::TimeDelta NudgeTracker::RecordLocalChange(DataType type,
                                                bool is_single_client) {
  DCHECK(type_trackers_.contains(type));
  type_trackers_[type]->RecordLocalChange();
  return type_trackers_[type]->GetLocalChangeNudgeDelay(is_single_client);
}

base::TimeDelta NudgeTracker::RecordLocalRefreshRequest(DataTypeSet types) {
  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    CHECK(tracker_it != type_trackers_.end()) << DataTypeToDebugString(type);
    tracker_it->second->RecordLocalRefreshRequest();
  }
  return kLocalRefreshDelay;
}

base::TimeDelta NudgeTracker::GetRemoteInvalidationDelay(DataType type) const {
  TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());
  return tracker_it->second->GetRemoteInvalidationDelay();
}

void NudgeTracker::RecordInitialSyncRequired(DataType type) {
  TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());
  tracker_it->second->RecordInitialSyncRequired();
}

void NudgeTracker::RecordCommitConflict(DataType type) {
  TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());
  tracker_it->second->RecordCommitConflict();
}

void NudgeTracker::OnInvalidationsEnabled() {
  invalidations_enabled_ = true;
}

void NudgeTracker::OnInvalidationsDisabled() {
  invalidations_enabled_ = false;
  invalidations_out_of_sync_ = true;
}

void NudgeTracker::SetTypesThrottledUntil(DataTypeSet types,
                                          base::TimeDelta length,
                                          base::TimeTicks now) {
  for (DataType type : types) {
    TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
    tracker_it->second->ThrottleType(length, now);
  }
}

void NudgeTracker::SetTypeBackedOff(DataType type,
                                    base::TimeDelta length,
                                    base::TimeTicks now) {
  TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());
  tracker_it->second->BackOffType(length, now);
}

void NudgeTracker::UpdateTypeThrottlingAndBackoffState() {
  for (const auto& [type, tracker] : type_trackers_) {
    tracker->UpdateThrottleOrBackoffState();
  }
}

void NudgeTracker::SetHasPendingInvalidations(DataType type,
                                              bool has_invalidation) {
  TypeTrackerMap::const_iterator tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());
  tracker_it->second->SetHasPendingInvalidations(has_invalidation);
}

bool NudgeTracker::IsAnyTypeBlocked() const {
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->IsBlocked()) {
      return true;
    }
  }
  return false;
}

bool NudgeTracker::IsTypeBlocked(DataType type) const {
  DCHECK(type_trackers_.find(type) != type_trackers_.end())
      << DataTypeToDebugString(type);
  return type_trackers_.find(type)->second->IsBlocked();
}

WaitInterval::BlockingMode NudgeTracker::GetTypeBlockingMode(
    DataType type) const {
  DCHECK(type_trackers_.find(type) != type_trackers_.end());
  return type_trackers_.find(type)->second->GetBlockingMode();
}

base::TimeDelta NudgeTracker::GetTimeUntilNextUnblock() const {
  DCHECK(IsAnyTypeBlocked()) << "This function requires a pending unblock.";

  // Return min of GetTimeUntilUnblock() values for all IsBlocked() types.
  base::TimeDelta time_until_next_unblock = base::TimeDelta::Max();
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->IsBlocked()) {
      time_until_next_unblock =
          std::min(time_until_next_unblock, tracker->GetTimeUntilUnblock());
    }
  }
  DCHECK(!time_until_next_unblock.is_max());

  return time_until_next_unblock;
}

base::TimeDelta NudgeTracker::GetTypeLastBackoffInterval(DataType type) const {
  auto tracker_it = type_trackers_.find(type);
  CHECK(tracker_it != type_trackers_.end());

  return tracker_it->second->GetLastBackoffInterval();
}

DataTypeSet NudgeTracker::GetBlockedTypes() const {
  DataTypeSet result;
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->IsBlocked()) {
      result.Put(type);
    }
  }
  return result;
}

DataTypeSet NudgeTracker::GetNudgedTypes() const {
  DataTypeSet result;
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->HasLocalChangePending()) {
      result.Put(type);
    }
  }
  return result;
}

DataTypeSet NudgeTracker::GetNotifiedTypes() const {
  DataTypeSet result;
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->HasPendingInvalidation()) {
      result.Put(type);
    }
  }
  return result;
}

DataTypeSet NudgeTracker::GetRefreshRequestedTypes() const {
  DataTypeSet result;
  for (const auto& [type, tracker] : type_trackers_) {
    if (tracker->HasRefreshRequestPending()) {
      result.Put(type);
    }
  }
  return result;
}

sync_pb::SyncEnums::GetUpdatesOrigin NudgeTracker::GetOrigin() const {
  // TODO(crbug.com/40252048): This appears trivial after removing GU_RETRY, either
  // simplify the code around or add an explanation why this is needed.
  for (const auto& [type, tracker] : type_trackers_) {
    if (!tracker->IsBlocked() && (tracker->HasPendingInvalidation() ||
                                  tracker->HasRefreshRequestPending() ||
                                  tracker->HasLocalChangePending() ||
                                  tracker->IsInitialSyncRequired())) {
      return sync_pb::SyncEnums::GU_TRIGGER;
    }
  }

  return sync_pb::SyncEnums::UNKNOWN_ORIGIN;
}

void NudgeTracker::FillProtoMessage(DataType type,
                                    sync_pb::GetUpdateTriggers* msg) const {
  DCHECK(type_trackers_.find(type) != type_trackers_.end());

  // Fill what we can from the global data.
  msg->set_invalidations_out_of_sync(invalidations_out_of_sync_);

  // Delegate the type-specific work to the DataTypeTracker class.
  type_trackers_.find(type)->second->FillGetUpdatesTriggersMessage(msg);
}

void NudgeTracker::UpdateLocalChangeDelay(DataType type,
                                          const base::TimeDelta& delay) {
  if (auto it = type_trackers_.find(type); it != type_trackers_.end()) {
    it->second->UpdateLocalChangeNudgeDelay(delay);
  }
}

void NudgeTracker::SetLocalChangeDelayIgnoringMinForTest(
    DataType type,
    const base::TimeDelta& delay) {
  DCHECK(type_trackers_.contains(type));
  type_trackers_[type]->SetLocalChangeNudgeDelayIgnoringMinForTest(delay);
}

void NudgeTracker::SetQuotaParamsForExtensionTypes(
    std::optional<int> max_tokens,
    std::optional<base::TimeDelta> refill_interval,
    std::optional<base::TimeDelta> depleted_quota_nudge_delay) {
  for (const auto& [type, tracker] : type_trackers_) {
    tracker->SetQuotaParamsIfExtensionType(max_tokens, refill_interval,
                                           depleted_quota_nudge_delay);
  }
}

}  // namespace syncer
