// Copyright (c) 2011 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/glue/session_change_processor.h"

#include <sstream>
#include <string>
#include <vector>

#include "base/logging.h"
#include "base/memory/scoped_vector.h"
#include "chrome/browser/extensions/extension_tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/engine/syncapi.h"
#include "chrome/browser/sync/glue/session_model_associator.h"
#include "chrome/browser/sync/profile_sync_service.h"
#include "content/browser/tab_contents/navigation_controller.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_details.h"
#include "content/common/notification_service.h"
#include "content/common/notification_source.h"

namespace browser_sync {

SessionChangeProcessor::SessionChangeProcessor(
    UnrecoverableErrorHandler* error_handler,
    SessionModelAssociator* session_model_associator)
    : ChangeProcessor(error_handler),
      session_model_associator_(session_model_associator),
      profile_(NULL),
      setup_for_test_(false) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(error_handler);
  DCHECK(session_model_associator_);
}

SessionChangeProcessor::SessionChangeProcessor(
    UnrecoverableErrorHandler* error_handler,
    SessionModelAssociator* session_model_associator,
    bool setup_for_test)
    : ChangeProcessor(error_handler),
      session_model_associator_(session_model_associator),
      profile_(NULL),
      setup_for_test_(setup_for_test) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(error_handler);
  DCHECK(session_model_associator_);
}

SessionChangeProcessor::~SessionChangeProcessor() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}

void SessionChangeProcessor::Observe(NotificationType type,
                                     const NotificationSource& source,
                                     const NotificationDetails& details) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(running());
  DCHECK(profile_);

  // Track which windows and/or tabs are modified.
  std::vector<TabContents*> modified_tabs;
  bool windows_changed = false;
  switch (type.value) {
    case NotificationType::BROWSER_OPENED: {
      Browser* browser = Source<Browser>(source).ptr();
      if (browser->profile() != profile_) {
        return;
      }

      windows_changed = true;
      break;
    }

    case NotificationType::TAB_PARENTED: {
      NavigationController* controller =
          Source<NavigationController>(source).ptr();
      if (controller->profile() != profile_) {
        return;
      }
      windows_changed = true;
      modified_tabs.push_back(controller->tab_contents());
      break;
    }

    case NotificationType::TAB_CLOSED: {
      NavigationController* controller =
          Source<NavigationController>(source).ptr();
      if (controller->profile() != profile_) {
        return;
      }
      windows_changed = true;
      modified_tabs.push_back(controller->tab_contents());
      break;
    }

    case NotificationType::NAV_LIST_PRUNED: {
      NavigationController* controller =
          Source<NavigationController>(source).ptr();
      if (controller->profile() != profile_) {
        return;
      }
      modified_tabs.push_back(controller->tab_contents());
      break;
    }

    case NotificationType::NAV_ENTRY_CHANGED: {
      NavigationController* controller =
          Source<NavigationController>(source).ptr();
      if (controller->profile() != profile_) {
        return;
      }
      modified_tabs.push_back(controller->tab_contents());
      break;
    }

    case NotificationType::NAV_ENTRY_COMMITTED: {
      NavigationController* controller =
          Source<NavigationController>(source).ptr();
      if (controller->profile() != profile_) {
        return;
      }
      modified_tabs.push_back(controller->tab_contents());
      break;
    }

    case NotificationType::TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED: {
      ExtensionTabHelper* extension_tab_helper =
          Source<ExtensionTabHelper>(source).ptr();
      if (extension_tab_helper->tab_contents()->profile() != profile_) {
        return;
      }
      if (extension_tab_helper->extension_app()) {
        modified_tabs.push_back(extension_tab_helper->tab_contents());
      }
      break;
    }
    default:
      LOG(ERROR) << "Received unexpected notification of type "
                  << type.value;
      break;
  }

  // Associate windows first to ensure tabs have homes.
  if (windows_changed)
    session_model_associator_->ReassociateWindows(false);
  if (!modified_tabs.empty())
    session_model_associator_->ReassociateTabs(modified_tabs);
}

void SessionChangeProcessor::ApplyChangesFromSyncModel(
    const sync_api::BaseTransaction* trans,
    const sync_api::SyncManager::ChangeRecord* changes,
    int change_count) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (!running()) {
    return;
  }

  StopObserving();

  sync_api::ReadNode root(trans);
  if (!root.InitByTagLookup(kSessionsTag)) {
    error_handler()->OnUnrecoverableError(FROM_HERE,
        "Sessions root node lookup failed.");
    return;
  }

  for (int i = 0; i < change_count; ++i) {
    const sync_api::SyncManager::ChangeRecord& change = changes[i];
    sync_api::SyncManager::ChangeRecord::Action action(change.action);
    if (sync_api::SyncManager::ChangeRecord::ACTION_DELETE == action) {
      // Deletions should only be for a foreign client itself, and hence affect
      // the header node, never a tab node.
      sync_api::ReadNode node(trans);
      if (!node.InitByIdLookup(change.id)) {
        error_handler()->OnUnrecoverableError(FROM_HERE,
                                              "Session node lookup failed.");
        return;
      }
      DCHECK_EQ(node.GetModelType(), syncable::SESSIONS);
      const sync_pb::SessionSpecifics& specifics = node.GetSessionSpecifics();
      session_model_associator_->DisassociateForeignSession(
          specifics.session_tag());
      continue;
    }

    // Handle an update or add.
    sync_api::ReadNode sync_node(trans);
    if (!sync_node.InitByIdLookup(change.id)) {
      error_handler()->OnUnrecoverableError(FROM_HERE,
          "Session node lookup failed.");
      return;
    }

    // Check that the changed node is a child of the session folder.
    DCHECK(root.GetId() == sync_node.GetParentId());
    DCHECK(syncable::SESSIONS == sync_node.GetModelType());

    const sync_pb::SessionSpecifics& specifics(
        sync_node.GetSessionSpecifics());
    if (specifics.session_tag() ==
            session_model_associator_->GetCurrentMachineTag() &&
        !setup_for_test_) {
      // We should only ever receive a change to our own machine's session info
      // if encryption was turned on. In that case, the data is still the same,
      // so we can ignore.
      LOG(WARNING) << "Dropping modification to local session.";
      return;
    }
    const int64 mtime = sync_node.GetModificationTime();
    // Model associator handles foreign session update and add the same.
    session_model_associator_->AssociateForeignSpecifics(specifics, mtime);
  }

  // Notify foreign session handlers that there are new sessions.
  NotificationService::current()->Notify(
      NotificationType::FOREIGN_SESSION_UPDATED,
      NotificationService::AllSources(),
      NotificationService::NoDetails());

  StartObserving();
}

void SessionChangeProcessor::StartImpl(Profile* profile) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(profile);
  DCHECK(profile_ == NULL);
  profile_ = profile;
  StartObserving();
}

void SessionChangeProcessor::StopImpl() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  StopObserving();
  profile_ = NULL;
}

void SessionChangeProcessor::StartObserving() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(profile_);
  notification_registrar_.Add(this, NotificationType::TAB_PARENTED,
      NotificationService::AllSources());
  notification_registrar_.Add(this, NotificationType::TAB_CLOSED,
      NotificationService::AllSources());
  notification_registrar_.Add(this, NotificationType::NAV_LIST_PRUNED,
      NotificationService::AllSources());
  notification_registrar_.Add(this, NotificationType::NAV_ENTRY_CHANGED,
      NotificationService::AllSources());
  notification_registrar_.Add(this, NotificationType::NAV_ENTRY_COMMITTED,
      NotificationService::AllSources());
  notification_registrar_.Add(this, NotificationType::BROWSER_OPENED,
      NotificationService::AllSources());
  notification_registrar_.Add(this,
      NotificationType::TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED,
      NotificationService::AllSources());
}

void SessionChangeProcessor::StopObserving() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(profile_);
  notification_registrar_.RemoveAll();
}

}  // namespace browser_sync