// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "sync/notifier/registration_manager.h"

#include <algorithm>
#include <cstddef>
#include <iterator>
#include <string>
#include <utility>

#include "base/rand_util.h"
#include "base/stl_util.h"
#include "google/cacheinvalidation/include/invalidation-client.h"
#include "google/cacheinvalidation/include/types.h"
#include "sync/notifier/invalidation_util.h"

namespace syncer {

RegistrationManager::PendingRegistrationInfo::PendingRegistrationInfo() {}

RegistrationManager::RegistrationStatus::RegistrationStatus(
    const invalidation::ObjectId& id, RegistrationManager* manager)
    : id(id),
      registration_manager(manager),
      enabled(true),
      state(invalidation::InvalidationListener::UNREGISTERED) {
  DCHECK(registration_manager);
}

RegistrationManager::RegistrationStatus::~RegistrationStatus() {}

void RegistrationManager::RegistrationStatus::DoRegister() {
  CHECK(enabled);
  // We might be called explicitly, so stop the timer manually and
  // reset the delay.
  registration_timer.Stop();
  delay = base::TimeDelta();
  registration_manager->DoRegisterId(id);
  DCHECK(!last_registration_request.is_null());
}

void RegistrationManager::RegistrationStatus::Disable() {
  enabled = false;
  state = invalidation::InvalidationListener::UNREGISTERED;
  registration_timer.Stop();
  delay = base::TimeDelta();
}

const int RegistrationManager::kInitialRegistrationDelaySeconds = 5;
const int RegistrationManager::kRegistrationDelayExponent = 2;
const double RegistrationManager::kRegistrationDelayMaxJitter = 0.5;
const int RegistrationManager::kMinRegistrationDelaySeconds = 1;
// 1 hour.
const int RegistrationManager::kMaxRegistrationDelaySeconds = 60 * 60;

RegistrationManager::RegistrationManager(
    invalidation::InvalidationClient* invalidation_client)
    : invalidation_client_(invalidation_client) {
  DCHECK(invalidation_client_);
}

RegistrationManager::~RegistrationManager() {
  DCHECK(CalledOnValidThread());
  STLDeleteValues(&registration_statuses_);
}

ObjectIdSet RegistrationManager::UpdateRegisteredIds(const ObjectIdSet& ids) {
  DCHECK(CalledOnValidThread());

  const ObjectIdSet& old_ids = GetRegisteredIds();
  const ObjectIdSet& to_register = ids;
  ObjectIdSet to_unregister;
  std::set_difference(old_ids.begin(), old_ids.end(),
                      ids.begin(), ids.end(),
                      std::inserter(to_unregister, to_unregister.begin()),
                      ObjectIdLessThan());

  for (ObjectIdSet::const_iterator it = to_unregister.begin();
       it != to_unregister.end(); ++it) {
    UnregisterId(*it);
  }

  for (ObjectIdSet::const_iterator it = to_register.begin();
       it != to_register.end(); ++it) {
    if (!ContainsKey(registration_statuses_, *it)) {
      registration_statuses_.insert(
          std::make_pair(*it, new RegistrationStatus(*it, this)));
    }
    if (!IsIdRegistered(*it)) {
      TryRegisterId(*it, false /* is-retry */);
    }
  }

  return to_unregister;
}

void RegistrationManager::MarkRegistrationLost(
    const invalidation::ObjectId& id) {
  DCHECK(CalledOnValidThread());
  RegistrationStatusMap::const_iterator it = registration_statuses_.find(id);
  if (it == registration_statuses_.end()) {
    DVLOG(1) << "Attempt to mark non-existent registration for "
             << ObjectIdToString(id) << " as lost";
    return;
  }
  if (!it->second->enabled) {
    return;
  }
  it->second->state = invalidation::InvalidationListener::UNREGISTERED;
  bool is_retry = !it->second->last_registration_request.is_null();
  TryRegisterId(id, is_retry);
}

void RegistrationManager::MarkAllRegistrationsLost() {
  DCHECK(CalledOnValidThread());
  for (RegistrationStatusMap::const_iterator it =
           registration_statuses_.begin();
       it != registration_statuses_.end(); ++it) {
    if (IsIdRegistered(it->first)) {
      MarkRegistrationLost(it->first);
    }
  }
}

void RegistrationManager::DisableId(const invalidation::ObjectId& id) {
  DCHECK(CalledOnValidThread());
  RegistrationStatusMap::const_iterator it = registration_statuses_.find(id);
  if (it == registration_statuses_.end()) {
    DVLOG(1) << "Attempt to disable non-existent registration for "
             << ObjectIdToString(id);
    return;
  }
  it->second->Disable();
}

// static
double RegistrationManager::CalculateBackoff(
    double retry_interval,
    double initial_retry_interval,
    double min_retry_interval,
    double max_retry_interval,
    double backoff_exponent,
    double jitter,
    double max_jitter) {
  // scaled_jitter lies in [-max_jitter, max_jitter].
  double scaled_jitter = jitter * max_jitter;
  double new_retry_interval =
      (retry_interval == 0.0) ?
      (initial_retry_interval * (1.0 + scaled_jitter)) :
      (retry_interval * (backoff_exponent + scaled_jitter));
  return std::max(min_retry_interval,
                  std::min(max_retry_interval, new_retry_interval));
}

ObjectIdSet RegistrationManager::GetRegisteredIdsForTest() const {
  return GetRegisteredIds();
}

RegistrationManager::PendingRegistrationMap
    RegistrationManager::GetPendingRegistrationsForTest() const {
  DCHECK(CalledOnValidThread());
  PendingRegistrationMap pending_registrations;
  for (RegistrationStatusMap::const_iterator it =
           registration_statuses_.begin();
       it != registration_statuses_.end(); ++it) {
    const invalidation::ObjectId& id = it->first;
    RegistrationStatus* status = it->second;
    if (status->registration_timer.IsRunning()) {
      pending_registrations[id].last_registration_request =
          status->last_registration_request;
      pending_registrations[id].registration_attempt =
          status->last_registration_attempt;
      pending_registrations[id].delay = status->delay;
      pending_registrations[id].actual_delay =
          status->registration_timer.GetCurrentDelay();
    }
  }
  return pending_registrations;
}

void RegistrationManager::FirePendingRegistrationsForTest() {
  DCHECK(CalledOnValidThread());
  for (RegistrationStatusMap::const_iterator it =
           registration_statuses_.begin();
       it != registration_statuses_.end(); ++it) {
    if (it->second->registration_timer.IsRunning()) {
      it->second->DoRegister();
    }
  }
}

double RegistrationManager::GetJitter() {
  // |jitter| lies in [-1.0, 1.0), which is low-biased, but only
  // barely.
  //
  // TODO(akalin): Fix the bias.
  return 2.0 * base::RandDouble() - 1.0;
}

void RegistrationManager::TryRegisterId(const invalidation::ObjectId& id,
                                        bool is_retry) {
  DCHECK(CalledOnValidThread());
  RegistrationStatusMap::const_iterator it = registration_statuses_.find(id);
  if (it == registration_statuses_.end()) {
    NOTREACHED() << "TryRegisterId called on " << ObjectIdToString(id)
                 << " which is not in the registration map";
    return;
  }
  RegistrationStatus* status = it->second;
  if (!status->enabled) {
    // Disabled, so do nothing.
    return;
  }
  status->last_registration_attempt = base::Time::Now();
  if (is_retry) {
    // If we're a retry, we must have tried at least once before.
    DCHECK(!status->last_registration_request.is_null());
    // delay = max(0, (now - last request) + next_delay)
    status->delay =
        (status->last_registration_request -
         status->last_registration_attempt) +
        status->next_delay;
    base::TimeDelta delay =
        (status->delay <= base::TimeDelta()) ?
        base::TimeDelta() : status->delay;
    DVLOG(2) << "Registering "
             << ObjectIdToString(id) << " in "
             << delay.InMilliseconds() << " ms";
    status->registration_timer.Stop();
    status->registration_timer.Start(FROM_HERE,
        delay, status, &RegistrationManager::RegistrationStatus::DoRegister);
    double next_delay_seconds =
        CalculateBackoff(static_cast<double>(status->next_delay.InSeconds()),
                         kInitialRegistrationDelaySeconds,
                         kMinRegistrationDelaySeconds,
                         kMaxRegistrationDelaySeconds,
                         kRegistrationDelayExponent,
                         GetJitter(),
                         kRegistrationDelayMaxJitter);
    status->next_delay =
        base::TimeDelta::FromSeconds(static_cast<int64>(next_delay_seconds));
    DVLOG(2) << "New next delay for "
             << ObjectIdToString(id) << " is "
             << status->next_delay.InSeconds() << " seconds";
  } else {
    DVLOG(2) << "Not a retry -- registering "
             << ObjectIdToString(id) << " immediately";
    status->delay = base::TimeDelta();
    status->next_delay = base::TimeDelta();
    status->DoRegister();
  }
}

void RegistrationManager::DoRegisterId(const invalidation::ObjectId& id) {
  DCHECK(CalledOnValidThread());
  invalidation_client_->Register(id);
  RegistrationStatusMap::const_iterator it = registration_statuses_.find(id);
  if (it == registration_statuses_.end()) {
    NOTREACHED() << "DoRegisterId called on " << ObjectIdToString(id)
                 << " which is not in the registration map";
    return;
  }
  it->second->state = invalidation::InvalidationListener::REGISTERED;
  it->second->last_registration_request = base::Time::Now();
}

void RegistrationManager::UnregisterId(const invalidation::ObjectId& id) {
  DCHECK(CalledOnValidThread());
  invalidation_client_->Unregister(id);
  RegistrationStatusMap::iterator it = registration_statuses_.find(id);
  if (it == registration_statuses_.end()) {
    NOTREACHED() << "UnregisterId called on " << ObjectIdToString(id)
                 << " which is not in the registration map";
    return;
  }
  delete it->second;
  registration_statuses_.erase(it);
}


ObjectIdSet RegistrationManager::GetRegisteredIds() const {
  DCHECK(CalledOnValidThread());
  ObjectIdSet ids;
  for (RegistrationStatusMap::const_iterator it =
           registration_statuses_.begin();
       it != registration_statuses_.end(); ++it) {
    if (IsIdRegistered(it->first)) {
      ids.insert(it->first);
    }
  }
  return ids;
}

bool RegistrationManager::IsIdRegistered(
    const invalidation::ObjectId& id) const {
  DCHECK(CalledOnValidThread());
  RegistrationStatusMap::const_iterator it =
      registration_statuses_.find(id);
  return it != registration_statuses_.end() &&
      it->second->state == invalidation::InvalidationListener::REGISTERED;
}

}  // namespace syncer