// 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_model_associator.h"

#include <algorithm>
#include <utility>

#include "base/logging.h"
#include "chrome/browser/extensions/extension_tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/profile_sync_service.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/browser/tabs/tab_strip_model.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/url_constants.h"
#include "content/browser/tab_contents/navigation_controller.h"
#include "content/browser/tab_contents/navigation_entry.h"
#include "content/common/notification_details.h"
#include "content/common/notification_service.h"

namespace browser_sync {

namespace {
static const char kNoSessionsFolderError[] =
    "Server did not create the top-level sessions node. We "
    "might be running against an out-of-date server.";

// The maximum number of navigations in each direction we care to sync.
static const int max_sync_navigation_count = 6;
}  // namespace

SessionModelAssociator::SessionModelAssociator(ProfileSyncService* sync_service)
    : tab_pool_(sync_service),
      local_session_syncid_(sync_api::kInvalidId),
      sync_service_(sync_service),
      setup_for_test_(false) {
  DCHECK(CalledOnValidThread());
  DCHECK(sync_service_);
}

SessionModelAssociator::SessionModelAssociator(ProfileSyncService* sync_service,
                                               bool setup_for_test)
    : tab_pool_(sync_service),
      local_session_syncid_(sync_api::kInvalidId),
      sync_service_(sync_service),
      setup_for_test_(setup_for_test) {
  DCHECK(CalledOnValidThread());
  DCHECK(sync_service_);
}

SessionModelAssociator::~SessionModelAssociator() {
  DCHECK(CalledOnValidThread());
}

bool SessionModelAssociator::InitSyncNodeFromChromeId(
    const std::string& id,
    sync_api::BaseNode* sync_node) {
  NOTREACHED();
  return false;
}

bool SessionModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) {
  DCHECK(CalledOnValidThread());
  CHECK(has_nodes);
  *has_nodes = false;
  sync_api::ReadTransaction trans(sync_service_->GetUserShare());
  sync_api::ReadNode root(&trans);
  if (!root.InitByTagLookup(kSessionsTag)) {
    LOG(ERROR) << kNoSessionsFolderError;
    return false;
  }
  // The sync model has user created nodes iff the sessions folder has
  // any children.
  *has_nodes = root.GetFirstChildId() != sync_api::kInvalidId;
  return true;
}

int64 SessionModelAssociator::GetSyncIdFromChromeId(const size_t& id) {
  DCHECK(CalledOnValidThread());
  return GetSyncIdFromSessionTag(TabIdToTag(GetCurrentMachineTag(), id));
}

int64 SessionModelAssociator::GetSyncIdFromSessionTag(const std::string& tag) {
  DCHECK(CalledOnValidThread());
  sync_api::ReadTransaction trans(sync_service_->GetUserShare());
  sync_api::ReadNode node(&trans);
  if (!node.InitByClientTagLookup(syncable::SESSIONS, tag))
    return sync_api::kInvalidId;
  return node.GetId();
}

const TabContents*
SessionModelAssociator::GetChromeNodeFromSyncId(int64 sync_id) {
  NOTREACHED();
  return NULL;
}

bool SessionModelAssociator::InitSyncNodeFromChromeId(
    const size_t& id,
    sync_api::BaseNode* sync_node) {
  NOTREACHED();
  return false;
}

void SessionModelAssociator::ReassociateWindows(bool reload_tabs) {
  DCHECK(CalledOnValidThread());
  sync_pb::SessionSpecifics specifics;
  specifics.set_session_tag(GetCurrentMachineTag());
  sync_pb::SessionHeader* header_s = specifics.mutable_header();

  for (BrowserList::const_iterator i = BrowserList::begin();
       i != BrowserList::end(); ++i) {
    // Make sure the browser has tabs and a window. Browsers destructor
    // removes itself from the BrowserList. When a browser is closed the
    // destructor is not necessarily run immediately. This means its possible
    // for us to get a handle to a browser that is about to be removed. If
    // the tab count is 0 or the window is NULL, the browser is about to be
    // deleted, so we ignore it.
    if (ShouldSyncWindowType((*i)->type()) && (*i)->tab_count() &&
        (*i)->window()) {
      sync_pb::SessionWindow window_s;
      SessionID::id_type window_id = (*i)->session_id().id();
      VLOG(1) << "Reassociating window " << window_id << " with " <<
          (*i)->tab_count() << " tabs.";
      window_s.set_window_id(window_id);
      window_s.set_selected_tab_index((*i)->active_index());
      if ((*i)->type() ==
          Browser::TYPE_NORMAL) {
        window_s.set_browser_type(
            sync_pb::SessionWindow_BrowserType_TYPE_NORMAL);
      } else {
        window_s.set_browser_type(
            sync_pb::SessionWindow_BrowserType_TYPE_POPUP);
      }

      // Store the order of tabs.
      bool found_tabs = false;
      for (int j = 0; j < (*i)->tab_count(); ++j) {
        TabContents* tab = (*i)->GetTabContentsAt(j);
        DCHECK(tab);
        if (IsValidTab(*tab)) {
          found_tabs = true;
          window_s.add_tab(tab->controller().session_id().id());
          if (reload_tabs) {
            ReassociateTab(*tab);
          }
        }
      }
      // Only add a window if it contains valid tabs.
      if (found_tabs) {
        sync_pb::SessionWindow* header_window = header_s->add_window();
        *header_window = window_s;
      }
    }
  }

  sync_api::WriteTransaction trans(sync_service_->GetUserShare());
  sync_api::WriteNode header_node(&trans);
  if (!header_node.InitByIdLookup(local_session_syncid_)) {
    LOG(ERROR) << "Failed to load local session header node.";
    return;
  }
  header_node.SetSessionSpecifics(specifics);
}

// Static.
bool SessionModelAssociator::ShouldSyncWindowType(const Browser::Type& type) {
  switch (type) {
    case Browser::TYPE_POPUP:
      return true;
    case Browser::TYPE_APP:
      return false;
    case Browser::TYPE_APP_POPUP:
      return false;
    case Browser::TYPE_DEVTOOLS:
      return false;
    case Browser::TYPE_APP_PANEL:
      return false;
    case Browser::TYPE_NORMAL:
    default:
      return true;
  }
}

void SessionModelAssociator::ReassociateTabs(
    const std::vector<TabContents*>& tabs) {
  DCHECK(CalledOnValidThread());
  for (std::vector<TabContents*>::const_iterator i = tabs.begin();
       i != tabs.end();
       ++i) {
    ReassociateTab(**i);
  }
}

void SessionModelAssociator::ReassociateTab(const TabContents& tab) {
  DCHECK(CalledOnValidThread());
  if (!IsValidTab(tab))
    return;

  int64 sync_id;
  SessionID::id_type id = tab.controller().session_id().id();
  if (tab.is_being_destroyed()) {
    // This tab is closing.
    TabLinksMap::iterator tab_iter = tab_map_.find(id);
    if (tab_iter == tab_map_.end()) {
      // We aren't tracking this tab (for example, sync setting page).
      return;
    }
    tab_pool_.FreeTabNode(tab_iter->second.sync_id());
    tab_map_.erase(tab_iter);
    return;
  }

  TabLinksMap::const_iterator tablink = tab_map_.find(id);
  if (tablink == tab_map_.end()) {
    // This is a new tab, get a sync node for it.
    sync_id = tab_pool_.GetFreeTabNode();
  } else {
    // This tab is already associated with a sync node, reuse it.
    sync_id = tablink->second.sync_id();
  }
  Associate(&tab, sync_id);
}

void SessionModelAssociator::Associate(const TabContents* tab, int64 sync_id) {
  DCHECK(CalledOnValidThread());
  SessionID::id_type session_id = tab->controller().session_id().id();
  Browser* browser = BrowserList::FindBrowserWithID(
      tab->controller().window_id().id());
  if (!browser)  // Can happen for weird things like developer console.
    return;

  TabLinks t(sync_id, tab);
  tab_map_[session_id] = t;

  sync_api::WriteTransaction trans(sync_service_->GetUserShare());
  WriteTabContentsToSyncModel(*browser, *tab, sync_id, &trans);
}

bool SessionModelAssociator::WriteTabContentsToSyncModel(
    const Browser& browser,
    const TabContents& tab,
    int64 sync_id,
    sync_api::WriteTransaction* trans) {
  DCHECK(CalledOnValidThread());
  sync_api::WriteNode tab_node(trans);
  if (!tab_node.InitByIdLookup(sync_id)) {
    LOG(ERROR) << "Failed to look up tab node " << sync_id;
    return false;
  }

  sync_pb::SessionSpecifics session_s;
  session_s.set_session_tag(GetCurrentMachineTag());
  sync_pb::SessionTab* tab_s = session_s.mutable_tab();

  SessionID::id_type tab_id = tab.controller().session_id().id();
  tab_s->set_tab_id(tab_id);
  tab_s->set_window_id(tab.controller().window_id().id());
  const int current_index = tab.controller().GetCurrentEntryIndex();
  const int min_index = std::max(0,
                                 current_index - max_sync_navigation_count);
  const int max_index = std::min(current_index + max_sync_navigation_count,
                                 tab.controller().entry_count());
  const int pending_index = tab.controller().pending_entry_index();
  int index_in_window = browser.tabstrip_model()->GetWrapperIndex(&tab);
  DCHECK(index_in_window != TabStripModel::kNoTab);
  tab_s->set_pinned(browser.tabstrip_model()->IsTabPinned(index_in_window));
  TabContentsWrapper* wrapper =
      TabContentsWrapper::GetCurrentWrapperForContents(
          const_cast<TabContents*>(&tab));
  if (wrapper->extension_tab_helper()->extension_app()) {
    tab_s->set_extension_app_id(
        wrapper->extension_tab_helper()->extension_app()->id());
  }
  for (int i = min_index; i < max_index; ++i) {
    const NavigationEntry* entry = (i == pending_index) ?
       tab.controller().pending_entry() : tab.controller().GetEntryAtIndex(i);
    DCHECK(entry);
    if (entry->virtual_url().is_valid()) {
      if (i == max_index - 1) {
        VLOG(1) << "Associating tab " << tab_id << " with sync id " << sync_id
            << " and url " << entry->virtual_url().possibly_invalid_spec();
      }
      TabNavigation tab_nav;
      tab_nav.SetFromNavigationEntry(*entry);
      sync_pb::TabNavigation* nav_s = tab_s->add_navigation();
      PopulateSessionSpecificsNavigation(&tab_nav, nav_s);
    }
  }
  tab_s->set_current_navigation_index(current_index);

  tab_node.SetSessionSpecifics(session_s);
  return true;
}

// Static
// TODO(zea): perhaps sync state (scroll position, form entries, etc.) as well?
// See http://crbug.com/67068.
void SessionModelAssociator::PopulateSessionSpecificsNavigation(
    const TabNavigation* navigation,
    sync_pb::TabNavigation* tab_navigation) {
  tab_navigation->set_index(navigation->index());
  tab_navigation->set_virtual_url(navigation->virtual_url().spec());
  tab_navigation->set_referrer(navigation->referrer().spec());
  tab_navigation->set_title(UTF16ToUTF8(navigation->title()));
  switch (navigation->transition()) {
    case PageTransition::LINK:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_LINK);
      break;
    case PageTransition::TYPED:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_TYPED);
      break;
    case PageTransition::AUTO_BOOKMARK:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_AUTO_BOOKMARK);
      break;
    case PageTransition::AUTO_SUBFRAME:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_AUTO_SUBFRAME);
      break;
    case PageTransition::MANUAL_SUBFRAME:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_MANUAL_SUBFRAME);
      break;
    case PageTransition::GENERATED:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_GENERATED);
      break;
    case PageTransition::START_PAGE:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_START_PAGE);
      break;
    case PageTransition::FORM_SUBMIT:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_FORM_SUBMIT);
      break;
    case PageTransition::RELOAD:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_RELOAD);
      break;
    case PageTransition::KEYWORD:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_KEYWORD);
      break;
    case PageTransition::KEYWORD_GENERATED:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_KEYWORD_GENERATED);
      break;
    case PageTransition::CHAIN_START:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_CHAIN_START);
      break;
    case PageTransition::CHAIN_END:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_CHAIN_END);
      break;
    case PageTransition::CLIENT_REDIRECT:
      tab_navigation->set_navigation_qualifier(
        sync_pb::TabNavigation_PageTransitionQualifier_CLIENT_REDIRECT);
      break;
    case PageTransition::SERVER_REDIRECT:
      tab_navigation->set_navigation_qualifier(
        sync_pb::TabNavigation_PageTransitionQualifier_SERVER_REDIRECT);
      break;
    default:
      tab_navigation->set_page_transition(
        sync_pb::TabNavigation_PageTransition_TYPED);
  }
}

void SessionModelAssociator::Disassociate(int64 sync_id) {
  DCHECK(CalledOnValidThread());
  NOTIMPLEMENTED();
  // TODO(zea): we will need this once we support deleting foreign sessions.
}

bool SessionModelAssociator::AssociateModels() {
  DCHECK(CalledOnValidThread());

  // Ensure that we disassociated properly, otherwise memory might leak.
  DCHECK(foreign_session_tracker_.empty());
  DCHECK_EQ(0U, tab_pool_.capacity());

  local_session_syncid_ = sync_api::kInvalidId;

  // Read any available foreign sessions and load any session data we may have.
  // If we don't have any local session data in the db, create a header node.
  {
    sync_api::WriteTransaction trans(sync_service_->GetUserShare());

    sync_api::ReadNode root(&trans);
    if (!root.InitByTagLookup(kSessionsTag)) {
      LOG(ERROR) << kNoSessionsFolderError;
      return false;
    }

    // Make sure we have a machine tag.
    if (current_machine_tag_.empty())
      InitializeCurrentMachineTag(&trans);

    UpdateAssociationsFromSyncModel(root, &trans);

    if (local_session_syncid_ == sync_api::kInvalidId) {
      // The sync db didn't have a header node for us, we need to create one.
      sync_api::WriteNode write_node(&trans);
      if (!write_node.InitUniqueByCreation(syncable::SESSIONS, root,
          current_machine_tag_)) {
        LOG(ERROR) << "Failed to create sessions header sync node.";
        return false;
      }
      write_node.SetTitle(UTF8ToWide(current_machine_tag_));
      local_session_syncid_ = write_node.GetId();
    }
  }

  // Check if anything has changed on the client side.
  UpdateSyncModelDataFromClient();

  VLOG(1) << "Session models associated.";

  return true;
}

bool SessionModelAssociator::DisassociateModels() {
  DCHECK(CalledOnValidThread());
  foreign_session_tracker_.clear();
  tab_map_.clear();
  tab_pool_.clear();
  local_session_syncid_ = sync_api::kInvalidId;

  // There is no local model stored with which to disassociate, just notify
  // foreign session handlers.
  NotificationService::current()->Notify(
      NotificationType::FOREIGN_SESSION_DISABLED,
      NotificationService::AllSources(),
      NotificationService::NoDetails());
  return true;
}

void SessionModelAssociator::InitializeCurrentMachineTag(
    sync_api::WriteTransaction* trans) {
  DCHECK(CalledOnValidThread());
  syncable::Directory* dir = trans->GetWrappedWriteTrans()->directory();

  // TODO(zea): We need a better way of creating a machine tag. The directory
  // kernel's cache_guid changes every time syncing is turned on and off. This
  // will result in session's associated with stale machine tags persisting on
  // the server since that tag will not be reused. Eventually this should
  // become some string identifiable to the user. (Home, Work, Laptop, etc.)
  // See issue at http://crbug.com/59672
  current_machine_tag_ = "session_sync";
  current_machine_tag_.append(dir->cache_guid());
  VLOG(1) << "Creating machine tag: " << current_machine_tag_;
  tab_pool_.set_machine_tag(current_machine_tag_);
}

bool SessionModelAssociator::UpdateAssociationsFromSyncModel(
    const sync_api::ReadNode& root,
    const sync_api::BaseTransaction* trans) {
  DCHECK(CalledOnValidThread());

  // Iterate through the nodes and associate any foreign sessions.
  int64 id = root.GetFirstChildId();
  while (id != sync_api::kInvalidId) {
    sync_api::ReadNode sync_node(trans);
    if (!sync_node.InitByIdLookup(id)) {
      LOG(ERROR) << "Failed to fetch sync node for id " << id;
      return false;
    }

    const sync_pb::SessionSpecifics& specifics =
        sync_node.GetSessionSpecifics();
    const int64 modification_time = sync_node.GetModificationTime();
    if (specifics.session_tag() != GetCurrentMachineTag()) {
      if (!AssociateForeignSpecifics(specifics, modification_time)) {
        return false;
      }
    } else if (id != local_session_syncid_) {
      // This is previously stored local session information.
      if (specifics.has_header()) {
        DCHECK_EQ(sync_api::kInvalidId, local_session_syncid_);

        // This is our previous header node, reuse it.
        local_session_syncid_ = id;
      } else {
        DCHECK(specifics.has_tab());

        // This is a tab node. We want to track these to reuse them in our free
        // tab node pool. They will be overwritten eventually, so need to do
        // anything else.
        tab_pool_.AddTabNode(id);
      }
    }

    id = sync_node.GetSuccessorId();
  }

  // After updating from sync model all tabid's should be free.
  DCHECK(tab_pool_.full());

  return true;
}

bool SessionModelAssociator::AssociateForeignSpecifics(
    const sync_pb::SessionSpecifics& specifics,
    const int64 modification_time) {
  DCHECK(CalledOnValidThread());
  std::string foreign_session_tag = specifics.session_tag();
  DCHECK(foreign_session_tag != GetCurrentMachineTag() || setup_for_test_);

  if (specifics.has_header()) {
    // Read in the header data for this foreign session.
    // Header data contains window information and ordered tab id's for each
    // window.

    // Load (or create) the ForeignSession object for this client.
    ForeignSession* foreign_session =
        foreign_session_tracker_.GetForeignSession(foreign_session_tag);

    const sync_pb::SessionHeader& header = specifics.header();
    foreign_session->windows.reserve(header.window_size());
    VLOG(1) << "Associating " << foreign_session_tag << " with " <<
        header.window_size() << " windows.";
    size_t i;
    for (i = 0; i < static_cast<size_t>(header.window_size()); ++i) {
      if (i >= foreign_session->windows.size()) {
        // This a new window, create it.
        foreign_session->windows.push_back(new SessionWindow());
      }
      const sync_pb::SessionWindow& window_s = header.window(i);
      PopulateSessionWindowFromSpecifics(foreign_session_tag,
                                         window_s,
                                         modification_time,
                                         foreign_session->windows[i],
                                         &foreign_session_tracker_);
    }
    // Remove any remaining windows (in case windows were closed)
    for (; i < foreign_session->windows.size(); ++i) {
      delete foreign_session->windows[i];
    }
    foreign_session->windows.resize(header.window_size());
  } else if (specifics.has_tab()) {
    const sync_pb::SessionTab& tab_s = specifics.tab();
    SessionID::id_type tab_id = tab_s.tab_id();
    SessionTab* tab =
        foreign_session_tracker_.GetSessionTab(foreign_session_tag,
                                               tab_id,
                                               false);
    PopulateSessionTabFromSpecifics(tab_s, modification_time, tab);
  } else {
    NOTREACHED();
    return false;
  }

  return true;
}

void SessionModelAssociator::DisassociateForeignSession(
    const std::string& foreign_session_tag) {
  DCHECK(CalledOnValidThread());
  foreign_session_tracker_.DeleteForeignSession(foreign_session_tag);
}

// Static
void SessionModelAssociator::PopulateSessionWindowFromSpecifics(
    const std::string& foreign_session_tag,
    const sync_pb::SessionWindow& specifics,
    int64 mtime,
    SessionWindow* session_window,
    ForeignSessionTracker* tracker) {
  if (specifics.has_window_id())
    session_window->window_id.set_id(specifics.window_id());
  if (specifics.has_selected_tab_index())
    session_window->selected_tab_index = specifics.selected_tab_index();
  if (specifics.has_browser_type()) {
    if (specifics.browser_type() ==
        sync_pb::SessionWindow_BrowserType_TYPE_NORMAL) {
      session_window->type = 1;
    } else {
      session_window->type = 2;
    }
  }
  session_window->timestamp = base::Time::FromInternalValue(mtime);
  session_window->tabs.resize(specifics.tab_size());
  for (int i = 0; i < specifics.tab_size(); i++) {
    SessionID::id_type tab_id = specifics.tab(i);
    session_window->tabs[i] =
        tracker->GetSessionTab(foreign_session_tag, tab_id, true);
  }
}

// Static
void SessionModelAssociator::PopulateSessionTabFromSpecifics(
    const sync_pb::SessionTab& specifics,
    const int64 mtime,
    SessionTab* tab) {
  if (specifics.has_tab_id())
    tab->tab_id.set_id(specifics.tab_id());
  if (specifics.has_window_id())
    tab->window_id.set_id(specifics.window_id());
  if (specifics.has_tab_visual_index())
    tab->tab_visual_index = specifics.tab_visual_index();
  if (specifics.has_current_navigation_index())
    tab->current_navigation_index = specifics.current_navigation_index();
  if (specifics.has_pinned())
    tab->pinned = specifics.pinned();
  if (specifics.has_extension_app_id())
    tab->extension_app_id = specifics.extension_app_id();
  tab->timestamp = base::Time::FromInternalValue(mtime);
  tab->navigations.clear();  // In case we are reusing a previous SessionTab.
  for (int i = 0; i < specifics.navigation_size(); i++) {
    AppendSessionTabNavigation(specifics.navigation(i), &tab->navigations);
  }
}

// Static
void SessionModelAssociator::AppendSessionTabNavigation(
    const sync_pb::TabNavigation& specifics,
    std::vector<TabNavigation>* navigations) {
  int index = 0;
  GURL virtual_url;
  GURL referrer;
  string16 title;
  std::string state;
  PageTransition::Type transition(PageTransition::LINK);
  if (specifics.has_index())
    index = specifics.index();
  if (specifics.has_virtual_url()) {
    GURL gurl(specifics.virtual_url());
    virtual_url = gurl;
  }
  if (specifics.has_referrer()) {
    GURL gurl(specifics.referrer());
    referrer = gurl;
  }
  if (specifics.has_title())
    title = UTF8ToUTF16(specifics.title());
  if (specifics.has_state())
    state = specifics.state();
  if (specifics.has_page_transition() ||
      specifics.has_navigation_qualifier()) {
    switch (specifics.page_transition()) {
      case sync_pb::TabNavigation_PageTransition_LINK:
        transition = PageTransition::LINK;
        break;
      case sync_pb::TabNavigation_PageTransition_TYPED:
        transition = PageTransition::TYPED;
        break;
      case sync_pb::TabNavigation_PageTransition_AUTO_BOOKMARK:
        transition = PageTransition::AUTO_BOOKMARK;
        break;
      case sync_pb::TabNavigation_PageTransition_AUTO_SUBFRAME:
        transition = PageTransition::AUTO_SUBFRAME;
        break;
      case sync_pb::TabNavigation_PageTransition_MANUAL_SUBFRAME:
        transition = PageTransition::MANUAL_SUBFRAME;
        break;
      case sync_pb::TabNavigation_PageTransition_GENERATED:
        transition = PageTransition::GENERATED;
        break;
      case sync_pb::TabNavigation_PageTransition_START_PAGE:
        transition = PageTransition::START_PAGE;
        break;
      case sync_pb::TabNavigation_PageTransition_FORM_SUBMIT:
        transition = PageTransition::FORM_SUBMIT;
        break;
      case sync_pb::TabNavigation_PageTransition_RELOAD:
        transition = PageTransition::RELOAD;
        break;
      case sync_pb::TabNavigation_PageTransition_KEYWORD:
        transition = PageTransition::KEYWORD;
        break;
      case sync_pb::TabNavigation_PageTransition_KEYWORD_GENERATED:
        transition = PageTransition::KEYWORD_GENERATED;
        break;
      case sync_pb::TabNavigation_PageTransition_CHAIN_START:
        transition = sync_pb::TabNavigation_PageTransition_CHAIN_START;
        break;
      case sync_pb::TabNavigation_PageTransition_CHAIN_END:
        transition = PageTransition::CHAIN_END;
        break;
      default:
        switch (specifics.navigation_qualifier()) {
          case sync_pb::
              TabNavigation_PageTransitionQualifier_CLIENT_REDIRECT:
            transition = PageTransition::CLIENT_REDIRECT;
            break;
            case sync_pb::
                TabNavigation_PageTransitionQualifier_SERVER_REDIRECT:
            transition = PageTransition::SERVER_REDIRECT;
              break;
            default:
            transition = PageTransition::TYPED;
        }
    }
  }
  TabNavigation tab_navigation(index, virtual_url, referrer, title, state,
                               transition);
  navigations->insert(navigations->end(), tab_navigation);
}

void SessionModelAssociator::UpdateSyncModelDataFromClient() {
  DCHECK(CalledOnValidThread());
  // TODO(zea): the logic for determining if we want to sync and the loading of
  // the previous session should go here. We can probably reuse the code for
  // loading the current session from the old session implementation.
  // SessionService::SessionCallback* callback =
  //     NewCallback(this, &SessionModelAssociator::OnGotSession);
  // GetSessionService()->GetCurrentSession(&consumer_, callback);

  // Associate all open windows and their tabs.
  ReassociateWindows(true);
}

SessionModelAssociator::TabNodePool::TabNodePool(
    ProfileSyncService* sync_service)
    : tab_pool_fp_(-1),
      sync_service_(sync_service) {
}

SessionModelAssociator::TabNodePool::~TabNodePool() {}

void SessionModelAssociator::TabNodePool::AddTabNode(int64 sync_id) {
  tab_syncid_pool_.resize(tab_syncid_pool_.size() + 1);
  tab_syncid_pool_[static_cast<size_t>(++tab_pool_fp_)] = sync_id;
}

int64 SessionModelAssociator::TabNodePool::GetFreeTabNode() {
  DCHECK_GT(machine_tag_.length(), 0U);
  if (tab_pool_fp_ == -1) {
    // Tab pool has no free nodes, allocate new one.
    sync_api::WriteTransaction trans(sync_service_->GetUserShare());
    sync_api::ReadNode root(&trans);
    if (!root.InitByTagLookup(kSessionsTag)) {
      LOG(ERROR) << kNoSessionsFolderError;
      return 0;
    }
    size_t tab_node_id = tab_syncid_pool_.size();
    std::string tab_node_tag = TabIdToTag(machine_tag_, tab_node_id);
    sync_api::WriteNode tab_node(&trans);
    if (!tab_node.InitUniqueByCreation(syncable::SESSIONS, root,
                                       tab_node_tag)) {
      LOG(ERROR) << "Could not create new node!";
      return -1;
    }
    tab_node.SetTitle(UTF8ToWide(tab_node_tag));

    // Grow the pool by 1 since we created a new node. We don't actually need
    // to put the node's id in the pool now, since the pool is still empty.
    // The id will be added when that tab is closed and the node is freed.
    tab_syncid_pool_.resize(tab_node_id + 1);
    VLOG(1) << "Adding sync node " << tab_node.GetId() << " to tab syncid pool";
    return tab_node.GetId();
  } else {
    // There are nodes available, grab next free and decrement free pointer.
    return tab_syncid_pool_[static_cast<size_t>(tab_pool_fp_--)];
  }
}

void SessionModelAssociator::TabNodePool::FreeTabNode(int64 sync_id) {
  // Pool size should always match # of free tab nodes.
  DCHECK_LT(tab_pool_fp_, static_cast<int64>(tab_syncid_pool_.size()));
  tab_syncid_pool_[static_cast<size_t>(++tab_pool_fp_)] = sync_id;
}

bool SessionModelAssociator::GetAllForeignSessions(
    std::vector<const ForeignSession*>* sessions) {
  DCHECK(CalledOnValidThread());
  return foreign_session_tracker_.LookupAllForeignSessions(sessions);
}

bool SessionModelAssociator::GetForeignSession(
    const std::string& tag,
    std::vector<SessionWindow*>* windows) {
  DCHECK(CalledOnValidThread());
  return foreign_session_tracker_.LookupSessionWindows(tag, windows);
}

bool SessionModelAssociator::GetForeignTab(
    const std::string& tag,
    const SessionID::id_type tab_id,
    const SessionTab** tab) {
  DCHECK(CalledOnValidThread());
  return foreign_session_tracker_.LookupSessionTab(tag, tab_id, tab);
}

// Static
bool SessionModelAssociator::SessionWindowHasNoTabsToSync(
    const SessionWindow& window) {
  int num_populated = 0;
  for (std::vector<SessionTab*>::const_iterator i = window.tabs.begin();
      i != window.tabs.end(); ++i) {
    const SessionTab* tab = *i;
    if (IsValidSessionTab(*tab))
      num_populated++;
  }
  if (num_populated == 0)
    return true;
  return false;
}

// Valid local tab?
bool SessionModelAssociator::IsValidTab(const TabContents& tab) {
  DCHECK(CalledOnValidThread());
  if ((tab.profile() == sync_service_->profile() ||
       sync_service_->profile() == NULL)) {
    const NavigationEntry* entry = tab.controller().GetActiveEntry();
    if (!entry)
      return false;
    if (entry->virtual_url().is_valid() &&
        (entry->virtual_url() != GURL(chrome::kChromeUINewTabURL) ||
         tab.controller().entry_count() > 1)) {
      return true;
    }
  }
  return false;
}

// Static
bool SessionModelAssociator::IsValidSessionTab(const SessionTab& tab) {
  if (tab.navigations.empty())
    return false;
  int selected_index = tab.current_navigation_index;
  selected_index = std::max(
      0,
      std::min(selected_index,
          static_cast<int>(tab.navigations.size() - 1)));
  if (selected_index == 0 &&
      tab.navigations.size() == 1 &&
      tab.navigations.at(selected_index).virtual_url() ==
          GURL(chrome::kChromeUINewTabURL)) {
    // This is a new tab with no further history, skip.
    return false;
  }
  return true;
}

// ==========================================================================
// The following methods are not currently used but will likely become useful
// if we choose to sync the previous browser session.

SessionService* SessionModelAssociator::GetSessionService() {
  DCHECK(CalledOnValidThread());
  DCHECK(sync_service_);
  Profile* profile = sync_service_->profile();
  DCHECK(profile);
  SessionService* sessions_service = profile->GetSessionService();
  DCHECK(sessions_service);
  return sessions_service;
}

void SessionModelAssociator::OnGotSession(
    int handle,
    std::vector<SessionWindow*>* windows) {
  DCHECK(CalledOnValidThread());
  DCHECK(local_session_syncid_);

  sync_pb::SessionSpecifics specifics;
  specifics.set_session_tag(GetCurrentMachineTag());
  sync_pb::SessionHeader* header_s = specifics.mutable_header();
  PopulateSessionSpecificsHeader(*windows, header_s);

  sync_api::WriteTransaction trans(sync_service_->GetUserShare());
  sync_api::ReadNode root(&trans);
  if (!root.InitByTagLookup(kSessionsTag)) {
    LOG(ERROR) << kNoSessionsFolderError;
    return;
  }

  sync_api::WriteNode header_node(&trans);
  if (!header_node.InitByIdLookup(local_session_syncid_)) {
    LOG(ERROR) << "Failed to load local session header node.";
    return;
  }

  header_node.SetSessionSpecifics(specifics);
}

void SessionModelAssociator::PopulateSessionSpecificsHeader(
    const std::vector<SessionWindow*>& windows,
    sync_pb::SessionHeader* header_s) {
  DCHECK(CalledOnValidThread());

  // Iterate through the vector of windows, extracting the window data, along
  // with the tab data to populate the session specifics.
  for (size_t i = 0; i < windows.size(); ++i) {
    if (SessionWindowHasNoTabsToSync(*(windows[i])))
      continue;
    sync_pb::SessionWindow* window_s = header_s->add_window();
    PopulateSessionSpecificsWindow(*(windows[i]), window_s);
    if (!SyncLocalWindowToSyncModel(*(windows[i])))
      return;
  }
}

// Called when populating session specifics to send to the sync model, called
// when associating models, or updating the sync model.
void SessionModelAssociator::PopulateSessionSpecificsWindow(
    const SessionWindow& window,
    sync_pb::SessionWindow* session_window) {
  DCHECK(CalledOnValidThread());
  session_window->set_window_id(window.window_id.id());
  session_window->set_selected_tab_index(window.selected_tab_index);
  if (window.type == Browser::TYPE_NORMAL) {
    session_window->set_browser_type(
      sync_pb::SessionWindow_BrowserType_TYPE_NORMAL);
  } else if (window.type == Browser::TYPE_POPUP) {
    session_window->set_browser_type(
      sync_pb::SessionWindow_BrowserType_TYPE_POPUP);
  } else {
    // ignore
    LOG(WARNING) << "Session Sync unable to handle windows of type" <<
        window.type;
    return;
  }
  for (std::vector<SessionTab*>::const_iterator i = window.tabs.begin();
      i != window.tabs.end(); ++i) {
    const SessionTab* tab = *i;
    if (!IsValidSessionTab(*tab))
      continue;
    session_window->add_tab(tab->tab_id.id());
  }
}

bool SessionModelAssociator::SyncLocalWindowToSyncModel(
    const SessionWindow& window) {
  DCHECK(CalledOnValidThread());
  DCHECK(tab_map_.empty());
  for (size_t i = 0; i < window.tabs.size(); ++i) {
    SessionTab* tab = window.tabs[i];
    int64 id = tab_pool_.GetFreeTabNode();
    if (id == -1) {
      LOG(ERROR) << "Failed to find/generate free sync node for tab.";
      return false;
    }

    sync_api::WriteTransaction trans(sync_service_->GetUserShare());
    if (!WriteSessionTabToSyncModel(*tab, id, &trans)) {
      return false;
    }

    TabLinks t(id, tab);
    tab_map_[tab->tab_id.id()] = t;
  }
  return true;
}

bool SessionModelAssociator::WriteSessionTabToSyncModel(
    const SessionTab& tab,
    const int64 sync_id,
    sync_api::WriteTransaction* trans) {
  DCHECK(CalledOnValidThread());
  sync_api::WriteNode tab_node(trans);
  if (!tab_node.InitByIdLookup(sync_id)) {
    LOG(ERROR) << "Failed to look up tab node " << sync_id;
    return false;
  }

  sync_pb::SessionSpecifics specifics;
  specifics.set_session_tag(GetCurrentMachineTag());
  sync_pb::SessionTab* tab_s = specifics.mutable_tab();
  PopulateSessionSpecificsTab(tab, tab_s);
  tab_node.SetSessionSpecifics(specifics);
  return true;
}

// See PopulateSessionSpecificsWindow for use.
void SessionModelAssociator::PopulateSessionSpecificsTab(
    const SessionTab& tab,
    sync_pb::SessionTab* session_tab) {
  DCHECK(CalledOnValidThread());
  session_tab->set_tab_id(tab.tab_id.id());
  session_tab->set_window_id(tab.window_id.id());
  session_tab->set_tab_visual_index(tab.tab_visual_index);
  session_tab->set_current_navigation_index(
      tab.current_navigation_index);
  session_tab->set_pinned(tab.pinned);
  session_tab->set_extension_app_id(tab.extension_app_id);
  for (std::vector<TabNavigation>::const_iterator i =
      tab.navigations.begin(); i != tab.navigations.end(); ++i) {
    const TabNavigation navigation = *i;
    sync_pb::TabNavigation* tab_navigation =
        session_tab->add_navigation();
    PopulateSessionSpecificsNavigation(&navigation, tab_navigation);
  }
}

bool SessionModelAssociator::CryptoReadyIfNecessary() {
  // We only access the cryptographer while holding a transaction.
  sync_api::ReadTransaction trans(sync_service_->GetUserShare());
  syncable::ModelTypeSet encrypted_types;
  sync_service_->GetEncryptedDataTypes(&encrypted_types);
  return encrypted_types.count(syncable::SESSIONS) == 0 ||
         sync_service_->IsCryptographerReady(&trans);
}

}  // namespace browser_sync