// Copyright (c) 2006-2009 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 "chrome/browser/sync/engine/all_status.h"

#include <algorithm>

#include "base/logging.h"
#include "base/port.h"
#include "base/rand_util.h"
#include "chrome/browser/sync/engine/auth_watcher.h"
#include "chrome/browser/sync/engine/net/gaia_authenticator.h"
#include "chrome/browser/sync/engine/net/server_connection_manager.h"
#include "chrome/browser/sync/engine/syncer.h"
#include "chrome/browser/sync/engine/syncer_thread.h"
#include "chrome/browser/sync/notifier/listener/talk_mediator.h"
#include "chrome/browser/sync/protocol/service_constants.h"
#include "chrome/browser/sync/sessions/session_state.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/util/event_sys-inl.h"

namespace browser_sync {

static const time_t kMinSyncObserveInterval = 10;  // seconds

// Backoff interval randomization factor.
static const int kBackoffRandomizationFactor = 2;

const int AllStatus::kMaxBackoffSeconds = 60 * 60 * 4;  // 4 hours.

const char* AllStatus::GetSyncStatusString(SyncStatus icon) {
  const char* strings[] = {"OFFLINE", "OFFLINE_UNSYNCED", "SYNCING", "READY",
      "CONFLICT", "OFFLINE_UNUSABLE"};
  COMPILE_ASSERT(arraysize(strings) == ICON_STATUS_COUNT, enum_indexed_array);
  if (icon < 0 || icon >= static_cast<SyncStatus>(arraysize(strings)))
    LOG(FATAL) << "Illegal Icon State:" << icon;
  return strings[icon];
}

static const AllStatus::Status init_status =
  { AllStatus::OFFLINE };

static const AllStatusEvent shutdown_event =
  { AllStatusEvent::SHUTDOWN, init_status };

AllStatus::AllStatus() : status_(init_status),
                         channel_(new Channel(shutdown_event)) {
  status_.initial_sync_ended = true;
  status_.notifications_enabled = false;
}

AllStatus::~AllStatus() {
  delete channel_;
}

void AllStatus::WatchConnectionManager(ServerConnectionManager* conn_mgr) {
  conn_mgr_hookup_.reset(NewEventListenerHookup(conn_mgr->channel(), this,
                         &AllStatus::HandleServerConnectionEvent));
}

void AllStatus::WatchAuthenticator(GaiaAuthenticator* gaia) {
  gaia_hookup_.reset(NewEventListenerHookup(gaia->channel(), this,
                     &AllStatus::HandleGaiaAuthEvent));
}

void AllStatus::WatchAuthWatcher(AuthWatcher* auth_watcher) {
  authwatcher_hookup_.reset(
      NewEventListenerHookup(auth_watcher->channel(), this,
                             &AllStatus::HandleAuthWatcherEvent));
}

void AllStatus::WatchSyncerThread(SyncerThread* syncer_thread) {
  syncer_thread_hookup_.reset(
      NewEventListenerHookup(syncer_thread->relay_channel(), this,
                             &AllStatus::HandleSyncerEvent));
}

AllStatus::Status AllStatus::CreateBlankStatus() const {
  Status status = status_;
  status.syncing = true;
  status.unsynced_count = 0;
  status.conflicting_count = 0;
  status.initial_sync_ended = false;
  status.syncer_stuck = false;
  status.max_consecutive_errors = 0;
  status.server_broken = false;
  status.updates_available = 0;
  status.updates_received = 0;
  return status;
}

AllStatus::Status AllStatus::CalcSyncing(const SyncerEvent &event) const {
  Status status = CreateBlankStatus();
  const sessions::SyncSessionSnapshot* snapshot = event.snapshot;
  status.unsynced_count += static_cast<int>(snapshot->unsynced_count);
  status.conflicting_count += snapshot->errors.num_conflicting_commits;
  // The syncer may not be done yet, which could cause conflicting updates.
  // But this is only used for status, so it is better to have visibility.
  status.conflicting_count += snapshot->num_conflicting_updates;

  status.syncing |= snapshot->syncer_status.syncing;
  status.syncing = snapshot->has_more_to_sync && snapshot->is_silenced;
  status.initial_sync_ended |= snapshot->is_share_usable;
  status.syncer_stuck |= snapshot->syncer_status.syncer_stuck;

  const sessions::ErrorCounters& errors(snapshot->errors);
  if (errors.consecutive_errors > status.max_consecutive_errors)
    status.max_consecutive_errors = errors.consecutive_errors;

  // 100 is an arbitrary limit.
  if (errors.consecutive_transient_error_commits > 100)
    status.server_broken = true;

  const sessions::ChangelogProgress& progress(snapshot->changelog_progress);
  status.updates_available += progress.num_server_changes_remaining;
  status.updates_received += progress.current_sync_timestamp;
  return status;
}

AllStatus::Status AllStatus::CalcSyncing() const {
  return CreateBlankStatus();
}

int AllStatus::CalcStatusChanges(Status* old_status) {
  int what_changed = 0;

  // Calculate what changed and what the new icon should be.
  if (status_.syncing != old_status->syncing)
    what_changed |= AllStatusEvent::SYNCING;
  if (status_.unsynced_count != old_status->unsynced_count)
    what_changed |= AllStatusEvent::UNSYNCED_COUNT;
  if (status_.server_up != old_status->server_up)
    what_changed |= AllStatusEvent::SERVER_UP;
  if (status_.server_reachable != old_status->server_reachable)
    what_changed |= AllStatusEvent::SERVER_REACHABLE;
  if (status_.notifications_enabled != old_status->notifications_enabled)
    what_changed |= AllStatusEvent::NOTIFICATIONS_ENABLED;
  if (status_.notifications_received != old_status->notifications_received)
    what_changed |= AllStatusEvent::NOTIFICATIONS_RECEIVED;
  if (status_.notifications_sent != old_status->notifications_sent)
    what_changed |= AllStatusEvent::NOTIFICATIONS_SENT;
  if (status_.initial_sync_ended != old_status->initial_sync_ended)
    what_changed |= AllStatusEvent::INITIAL_SYNC_ENDED;
  if (status_.authenticated != old_status->authenticated)
    what_changed |= AllStatusEvent::AUTHENTICATED;

  const bool unsynced_changes = status_.unsynced_count > 0;
  const bool online = status_.authenticated &&
    status_.server_reachable && status_.server_up && !status_.server_broken;
  if (online) {
    if (status_.syncer_stuck)
      status_.icon = CONFLICT;
    else if (unsynced_changes || status_.syncing)
      status_.icon = SYNCING;
    else
      status_.icon = READY;
  } else if (!status_.initial_sync_ended) {
    status_.icon = OFFLINE_UNUSABLE;
  } else if (unsynced_changes) {
    status_.icon = OFFLINE_UNSYNCED;
  } else {
    status_.icon = OFFLINE;
  }

  if (status_.icon != old_status->icon)
    what_changed |= AllStatusEvent::ICON;

  if (0 == what_changed)
    return 0;
  *old_status = status_;
  return what_changed;
}

void AllStatus::HandleGaiaAuthEvent(const GaiaAuthEvent& gaia_event) {
  ScopedStatusLockWithNotify lock(this);
  switch (gaia_event.what_happened) {
    case GaiaAuthEvent::GAIA_AUTH_FAILED:
      status_.authenticated = false;
      break;
    case GaiaAuthEvent::GAIA_AUTH_SUCCEEDED:
      status_.authenticated = true;
      break;
    default:
      lock.set_notify_plan(DONT_NOTIFY);
      break;
  }
}

void AllStatus::HandleAuthWatcherEvent(const AuthWatcherEvent& auth_event) {
  ScopedStatusLockWithNotify lock(this);
  switch (auth_event.what_happened) {
    case AuthWatcherEvent::GAIA_AUTH_FAILED:
    case AuthWatcherEvent::SERVICE_AUTH_FAILED:
    case AuthWatcherEvent::SERVICE_CONNECTION_FAILED:
    case AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START:
      status_.authenticated = false;
      break;
    case AuthWatcherEvent::AUTH_SUCCEEDED:
      // If we've already calculated that the server is reachable, since we've
      // successfully authenticated, we can be confident that the server is up.
      if (status_.server_reachable)
        status_.server_up = true;

      if (!status_.authenticated) {
        status_.authenticated = true;
        status_ = CalcSyncing();
      } else {
        lock.set_notify_plan(DONT_NOTIFY);
      }
      break;
    default:
      lock.set_notify_plan(DONT_NOTIFY);
      break;
  }
}

void AllStatus::HandleSyncerEvent(const SyncerEvent& event) {
  ScopedStatusLockWithNotify lock(this);
  switch (event.what_happened) {
    case SyncerEvent::COMMITS_SUCCEEDED:
      break;
    case SyncerEvent::SYNC_CYCLE_ENDED:
    case SyncerEvent::STATUS_CHANGED:
      status_ = CalcSyncing(event);
      break;
    case SyncerEvent::SHUTDOWN_USE_WITH_CARE:
      // We're safe to use this value here because we don't call into the syncer
      // or block on any processes.
      lock.set_notify_plan(DONT_NOTIFY);
      break;
    case SyncerEvent::OVER_QUOTA:
      LOG(WARNING) << "User has gone over quota.";
      lock.NotifyOverQuota();
      break;
    case SyncerEvent::REQUEST_SYNC_NUDGE:
      lock.set_notify_plan(DONT_NOTIFY);
      break;
    default:
      LOG(ERROR) << "Unrecognized Syncer Event: " << event.what_happened;
      lock.set_notify_plan(DONT_NOTIFY);
      break;
  }
}

void AllStatus::HandleServerConnectionEvent(
    const ServerConnectionEvent& event) {
  if (ServerConnectionEvent::STATUS_CHANGED == event.what_happened) {
    ScopedStatusLockWithNotify lock(this);
    status_.server_up = IsGoodReplyFromServer(event.connection_code);
    status_.server_reachable = event.server_reachable;
  }
}

void AllStatus::WatchTalkMediator(const TalkMediator* mediator) {
  status_.notifications_enabled = false;
  talk_mediator_hookup_.reset(
      NewEventListenerHookup(mediator->channel(), this,
                             &AllStatus::HandleTalkMediatorEvent));
}

void AllStatus::HandleTalkMediatorEvent(
    const TalkMediatorEvent& event) {
  ScopedStatusLockWithNotify lock(this);
  switch (event.what_happened) {
    case TalkMediatorEvent::SUBSCRIPTIONS_ON:
      status_.notifications_enabled = true;
      break;
    case TalkMediatorEvent::LOGOUT_SUCCEEDED:
    case TalkMediatorEvent::SUBSCRIPTIONS_OFF:
    case TalkMediatorEvent::TALKMEDIATOR_DESTROYED:
      status_.notifications_enabled = false;
      break;
    case TalkMediatorEvent::NOTIFICATION_RECEIVED:
      status_.notifications_received++;
      break;
    case TalkMediatorEvent::NOTIFICATION_SENT:
      status_.notifications_sent++;
      break;
    case TalkMediatorEvent::LOGIN_SUCCEEDED:
    default:
      lock.set_notify_plan(DONT_NOTIFY);
      break;
  }
}

AllStatus::Status AllStatus::status() const {
  AutoLock lock(mutex_);
  return status_;
}

int AllStatus::GetRecommendedDelaySeconds(int base_delay_seconds) {
  if (base_delay_seconds >= kMaxBackoffSeconds)
    return kMaxBackoffSeconds;

  // This calculates approx. base_delay_seconds * 2 +/- base_delay_seconds / 2
  int backoff_s = (0 == base_delay_seconds) ? 1 :
      base_delay_seconds * kBackoffRandomizationFactor;

  // Flip a coin to randomize backoff interval by +/- 50%.
  int rand_sign = base::RandInt(0, 1) * 2 - 1;

  // Truncation is adequate for rounding here.
  backoff_s = backoff_s +
      (rand_sign * (base_delay_seconds / kBackoffRandomizationFactor));

  // Cap the backoff interval.
  backoff_s = std::min(backoff_s, kMaxBackoffSeconds);

  return backoff_s;
}

int AllStatus::GetRecommendedDelay(int base_delay_ms) const {
  return GetRecommendedDelaySeconds(base_delay_ms / 1000) * 1000;
}

ScopedStatusLockWithNotify::ScopedStatusLockWithNotify(AllStatus* allstatus)
  : allstatus_(allstatus), plan_(NOTIFY_IF_STATUS_CHANGED) {
  event_.what_changed = 0;
  allstatus->mutex_.Acquire();
  event_.status = allstatus->status_;
}

ScopedStatusLockWithNotify::~ScopedStatusLockWithNotify() {
  if (DONT_NOTIFY == plan_) {
    allstatus_->mutex_.Release();
    return;
  }
  event_.what_changed |= allstatus_->CalcStatusChanges(&event_.status);
  allstatus_->mutex_.Release();
  if (event_.what_changed)
    allstatus_->channel()->NotifyListeners(event_);
}

void ScopedStatusLockWithNotify::NotifyOverQuota() {
  event_.what_changed |= AllStatusEvent::OVER_QUOTA;
}

}  // namespace browser_sync