// Copyright 2014 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 "google_apis/gcm/engine/heartbeat_manager.h"

#include "base/callback.h"
#include "base/metrics/histogram.h"
#include "base/power_monitor/power_monitor.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "google_apis/gcm/protocol/mcs.pb.h"
#include "net/base/network_change_notifier.h"

namespace gcm {

namespace {
// The default heartbeat when on a mobile or unknown network .
const int kCellHeartbeatDefaultMs = 1000 * 60 * 28;  // 28 minutes.
// The default heartbeat when on WiFi (also used for ethernet).
const int kWifiHeartbeatDefaultMs = 1000 * 60 * 15;  // 15 minutes.
// The default heartbeat ack interval.
const int kHeartbeatAckDefaultMs = 1000 * 60 * 1;  // 1 minute.
// Minimum allowed client default heartbeat interval.
const int kMinClientHeartbeatIntervalMs = 1000 * 60 * 2;  // 2 minutes.

#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
// The period at which to check if the heartbeat time has passed. Used to
// protect against platforms where the timer is delayed by the system being
// suspended.  Only needed on linux because the other OSes provide a standard
// way to be notified of system suspend and resume events.
const int kHeartbeatMissedCheckMs = 1000 * 60 * 5;  // 5 minutes.
#endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)

}  // namespace

HeartbeatManager::HeartbeatManager()
    : waiting_for_ack_(false),
      heartbeat_interval_ms_(0),
      server_interval_ms_(0),
      client_interval_ms_(0),
      heartbeat_timer_(new base::Timer(true /* retain_user_task */,
                                       false /* is_repeating */)),
      weak_ptr_factory_(this) {
  // Listen for system suspend and resume events.
  base::PowerMonitor* monitor = base::PowerMonitor::Get();
  if (monitor)
    monitor->AddObserver(this);
}

HeartbeatManager::~HeartbeatManager() {
  // Stop listening for system suspend and resume events.
  base::PowerMonitor* monitor = base::PowerMonitor::Get();
  if (monitor)
    monitor->RemoveObserver(this);
}

void HeartbeatManager::Start(
    const base::Closure& send_heartbeat_callback,
    const ReconnectCallback& trigger_reconnect_callback) {
  DCHECK(!send_heartbeat_callback.is_null());
  DCHECK(!trigger_reconnect_callback.is_null());
  send_heartbeat_callback_ = send_heartbeat_callback;
  trigger_reconnect_callback_ = trigger_reconnect_callback;

  // Kicks off the timer.
  waiting_for_ack_ = false;
  RestartTimer();
}

void HeartbeatManager::Stop() {
  heartbeat_expected_time_ = base::Time();
  heartbeat_interval_ms_ = 0;
  heartbeat_timer_->Stop();
  waiting_for_ack_ = false;
}

void HeartbeatManager::OnHeartbeatAcked() {
  if (!heartbeat_timer_->IsRunning())
    return;

  DCHECK(!send_heartbeat_callback_.is_null());
  DCHECK(!trigger_reconnect_callback_.is_null());
  waiting_for_ack_ = false;
  RestartTimer();
}

void HeartbeatManager::UpdateHeartbeatConfig(
    const mcs_proto::HeartbeatConfig& config) {
  if (!config.IsInitialized() ||
      !config.has_interval_ms() ||
      config.interval_ms() <= 0) {
    return;
  }
  DVLOG(1) << "Updating server heartbeat interval to " << config.interval_ms();
  server_interval_ms_ = config.interval_ms();
}

base::TimeTicks HeartbeatManager::GetNextHeartbeatTime() const {
  if (heartbeat_timer_->IsRunning())
    return heartbeat_timer_->desired_run_time();
  else
    return base::TimeTicks();
}

void HeartbeatManager::UpdateHeartbeatTimer(scoped_ptr<base::Timer> timer) {
  bool was_running = heartbeat_timer_->IsRunning();
  base::TimeDelta remaining_delay =
      heartbeat_timer_->desired_run_time() - base::TimeTicks::Now();
  base::Closure timer_task(heartbeat_timer_->user_task());

  heartbeat_timer_->Stop();
  heartbeat_timer_ = timer.Pass();

  if (was_running)
    heartbeat_timer_->Start(FROM_HERE, remaining_delay, timer_task);
}

void HeartbeatManager::OnResume() {
  CheckForMissedHeartbeat();
}

void HeartbeatManager::OnHeartbeatTriggered() {
  // Reset the weak pointers used for heartbeat checks.
  weak_ptr_factory_.InvalidateWeakPtrs();

  if (waiting_for_ack_) {
    LOG(WARNING) << "Lost connection to MCS, reconnecting.";
    ResetConnection(ConnectionFactory::HEARTBEAT_FAILURE);
    return;
  }

  waiting_for_ack_ = true;
  RestartTimer();
  send_heartbeat_callback_.Run();
}

void HeartbeatManager::RestartTimer() {
  if (!waiting_for_ack_) {
    // Recalculate the timer interval based network type.
    // Server interval takes precedence over client interval, even if the latter
    // is less.
    if (server_interval_ms_ != 0) {
      // If a server interval is set, it overrides any local one.
      heartbeat_interval_ms_ = server_interval_ms_;
    } else if (HasClientHeartbeatInterval()) {
      // Client interval might have been adjusted up, which should only take
      // effect during a reconnection.
      if (client_interval_ms_ < heartbeat_interval_ms_ ||
          heartbeat_interval_ms_ == 0) {
        heartbeat_interval_ms_ = client_interval_ms_;
      }
    } else {
      heartbeat_interval_ms_ = GetDefaultHeartbeatInterval();
    }
    DVLOG(1) << "Sending next heartbeat in "
             << heartbeat_interval_ms_ << " ms.";
  } else {
    heartbeat_interval_ms_ = kHeartbeatAckDefaultMs;
    DVLOG(1) << "Resetting timer for ack with "
             << heartbeat_interval_ms_ << " ms interval.";
  }

  heartbeat_expected_time_ =
      base::Time::Now() +
      base::TimeDelta::FromMilliseconds(heartbeat_interval_ms_);
  heartbeat_timer_->Start(FROM_HERE,
                         base::TimeDelta::FromMilliseconds(
                             heartbeat_interval_ms_),
                         base::Bind(&HeartbeatManager::OnHeartbeatTriggered,
                                    weak_ptr_factory_.GetWeakPtr()));

#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
  // Windows, Mac, Android, iOS, and Chrome OS all provide a way to be notified
  // when the system is suspending or resuming.  The only one that does not is
  // Linux so we need to poll to check for missed heartbeats.
  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&HeartbeatManager::CheckForMissedHeartbeat,
                 weak_ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(kHeartbeatMissedCheckMs));
#endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)
}

void HeartbeatManager::CheckForMissedHeartbeat() {
  // If there's no heartbeat pending, return without doing anything.
  if (heartbeat_expected_time_.is_null())
    return;

  // If the heartbeat has been missed, manually trigger it.
  if (base::Time::Now() > heartbeat_expected_time_) {
    UMA_HISTOGRAM_LONG_TIMES("GCM.HeartbeatMissedDelta",
                             base::Time::Now() - heartbeat_expected_time_);
    OnHeartbeatTriggered();
    return;
  }

#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
  // Otherwise check again later.
  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&HeartbeatManager::CheckForMissedHeartbeat,
                 weak_ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(kHeartbeatMissedCheckMs));
#endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)
}

int HeartbeatManager::GetDefaultHeartbeatInterval() {
  // For unknown connections, use the longer cellular heartbeat interval.
  int heartbeat_interval_ms = kCellHeartbeatDefaultMs;
  if (net::NetworkChangeNotifier::GetConnectionType() ==
          net::NetworkChangeNotifier::CONNECTION_WIFI ||
      net::NetworkChangeNotifier::GetConnectionType() ==
          net::NetworkChangeNotifier::CONNECTION_ETHERNET) {
    heartbeat_interval_ms = kWifiHeartbeatDefaultMs;
  }
  return heartbeat_interval_ms;
}

int HeartbeatManager::GetMaxClientHeartbeatIntervalMs() {
  return GetDefaultHeartbeatInterval();
}

int HeartbeatManager::GetMinClientHeartbeatIntervalMs() {
  // Returning a constant. This should be adjusted for connection type, like the
  // default/max interval.
  return kMinClientHeartbeatIntervalMs;
}

void HeartbeatManager::SetClientHeartbeatIntervalMs(int interval_ms) {
  if ((interval_ms != 0 && !IsValidClientHeartbeatInterval(interval_ms)) ||
      interval_ms == client_interval_ms_) {
    return;
  }

  client_interval_ms_ = interval_ms;
  // Only reset connection if the new heartbeat interval is shorter. If it is
  // longer, the connection will reset itself at some point and interval will be
  // fixed.
  if (client_interval_ms_ > 0 && client_interval_ms_ < heartbeat_interval_ms_) {
    ResetConnection(ConnectionFactory::NEW_HEARTBEAT_INTERVAL);
  }
}

int HeartbeatManager::GetClientHeartbeatIntervalMs() {
  return client_interval_ms_;
}

bool HeartbeatManager::HasClientHeartbeatInterval() {
  return client_interval_ms_ != 0;
}

bool HeartbeatManager::IsValidClientHeartbeatInterval(int interval) {
  int max_heartbeat_interval = GetDefaultHeartbeatInterval();
  return kMinClientHeartbeatIntervalMs <= interval &&
      interval <= max_heartbeat_interval;
}

void HeartbeatManager::ResetConnection(
    ConnectionFactory::ConnectionResetReason reason) {
  Stop();
  trigger_reconnect_callback_.Run(reason);
}

}  // namespace gcm