// Copyright (c) 2006-2008 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/sessions/base_session_service.h"

#include "base/pickle.h"
#include "base/stl_util-inl.h"
#include "base/threading/thread.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_backend.h"
#include "chrome/browser/sessions/session_types.h"
#include "content/browser/tab_contents/navigation_entry.h"
#include "webkit/glue/webkit_glue.h"

// InternalGetCommandsRequest -------------------------------------------------

BaseSessionService::InternalGetCommandsRequest::~InternalGetCommandsRequest() {
  STLDeleteElements(&commands);
}

// BaseSessionService ---------------------------------------------------------

namespace {

// Helper used by CreateUpdateTabNavigationCommand(). It writes |str| to
// |pickle|, if and only if |str| fits within (|max_bytes| - |*bytes_written|).
// |bytes_written| is incremented to reflect the data written.
void WriteStringToPickle(Pickle& pickle, int* bytes_written, int max_bytes,
                         const std::string& str) {
  int num_bytes = str.size() * sizeof(char);
  if (*bytes_written + num_bytes < max_bytes) {
    *bytes_written += num_bytes;
    pickle.WriteString(str);
  } else {
    pickle.WriteString(std::string());
  }
}

// string16 version of WriteStringToPickle.
void WriteString16ToPickle(Pickle& pickle, int* bytes_written, int max_bytes,
                           const string16& str) {
  int num_bytes = str.size() * sizeof(char16);
  if (*bytes_written + num_bytes < max_bytes) {
    *bytes_written += num_bytes;
    pickle.WriteString16(str);
  } else {
    pickle.WriteString16(string16());
  }
}

}  // namespace

// Delay between when a command is received, and when we save it to the
// backend.
static const int kSaveDelayMS = 2500;

// static
const int BaseSessionService::max_persist_navigation_count = 6;

BaseSessionService::BaseSessionService(SessionType type,
                                       Profile* profile,
                                       const FilePath& path)
    : profile_(profile),
      path_(path),
      backend_thread_(NULL),
      ALLOW_THIS_IN_INITIALIZER_LIST(save_factory_(this)),
      pending_reset_(false),
      commands_since_reset_(0) {
  if (profile) {
    // We should never be created when incognito.
    DCHECK(!profile->IsOffTheRecord());
  }
  backend_ = new SessionBackend(type,
      profile_ ? profile_->GetPath() : path_);
  DCHECK(backend_.get());
  backend_thread_ = g_browser_process->file_thread();
  if (!backend_thread_)
    backend_->Init();
  // If backend_thread is non-null, backend will init itself as appropriate.
}

BaseSessionService::~BaseSessionService() {
}

void BaseSessionService::DeleteLastSession() {
  if (!backend_thread()) {
    backend()->DeleteLastSession();
  } else {
    backend_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
        backend(), &SessionBackend::DeleteLastSession));
  }
}

void BaseSessionService::ScheduleCommand(SessionCommand* command) {
  DCHECK(command);
  commands_since_reset_++;
  pending_commands_.push_back(command);
  StartSaveTimer();
}

void BaseSessionService::StartSaveTimer() {
  // Don't start a timer when testing (profile == NULL or
  // MessageLoop::current() is NULL).
  if (MessageLoop::current() && profile() && save_factory_.empty()) {
    MessageLoop::current()->PostDelayedTask(FROM_HERE,
        save_factory_.NewRunnableMethod(&BaseSessionService::Save),
        kSaveDelayMS);
  }
}

void BaseSessionService::Save() {
  DCHECK(backend());

  if (pending_commands_.empty())
    return;

  if (!backend_thread()) {
    backend()->AppendCommands(
        new std::vector<SessionCommand*>(pending_commands_), pending_reset_);
  } else {
    backend_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
        backend(), &SessionBackend::AppendCommands,
        new std::vector<SessionCommand*>(pending_commands_),
        pending_reset_));
  }
  // Backend took ownership of commands.
  pending_commands_.clear();

  if (pending_reset_) {
    commands_since_reset_ = 0;
    pending_reset_ = false;
  }
}

SessionCommand* BaseSessionService::CreateUpdateTabNavigationCommand(
    SessionID::id_type command_id,
    SessionID::id_type tab_id,
    int index,
    const NavigationEntry& entry) {
  // Use pickle to handle marshalling.
  Pickle pickle;
  pickle.WriteInt(tab_id);
  pickle.WriteInt(index);

  // We only allow navigations up to 63k (which should be completely
  // reasonable). On the off chance we get one that is too big, try to
  // keep the url.

  // Bound the string data (which is variable length) to
  // |max_state_size bytes| bytes.
  static const SessionCommand::size_type max_state_size =
      std::numeric_limits<SessionCommand::size_type>::max() - 1024;

  int bytes_written = 0;

  WriteStringToPickle(pickle, &bytes_written, max_state_size,
                      entry.virtual_url().spec());

  WriteString16ToPickle(pickle, &bytes_written, max_state_size, entry.title());

  if (entry.has_post_data()) {
    // Remove the form data, it may contain sensitive information.
    WriteStringToPickle(pickle, &bytes_written, max_state_size,
        webkit_glue::RemoveFormDataFromHistoryState(entry.content_state()));
  } else {
    WriteStringToPickle(pickle, &bytes_written, max_state_size,
                        entry.content_state());
  }

  pickle.WriteInt(entry.transition_type());
  int type_mask = entry.has_post_data() ? TabNavigation::HAS_POST_DATA : 0;
  pickle.WriteInt(type_mask);

  WriteStringToPickle(pickle, &bytes_written, max_state_size,
      entry.referrer().is_valid() ? entry.referrer().spec() : std::string());

  // Adding more data? Be sure and update TabRestoreService too.
  return new SessionCommand(command_id, pickle);
}

SessionCommand* BaseSessionService::CreateSetTabExtensionAppIDCommand(
    SessionID::id_type command_id,
    SessionID::id_type tab_id,
    const std::string& extension_id) {
  // Use pickle to handle marshalling.
  Pickle pickle;
  pickle.WriteInt(tab_id);

  // Enforce a max for ids. They should never be anywhere near this size.
  static const SessionCommand::size_type max_id_size =
      std::numeric_limits<SessionCommand::size_type>::max() - 1024;

  int bytes_written = 0;

  WriteStringToPickle(pickle, &bytes_written, max_id_size, extension_id);

  return new SessionCommand(command_id, pickle);
}

bool BaseSessionService::RestoreUpdateTabNavigationCommand(
    const SessionCommand& command,
    TabNavigation* navigation,
    SessionID::id_type* tab_id) {
  scoped_ptr<Pickle> pickle(command.PayloadAsPickle());
  if (!pickle.get())
    return false;
  void* iterator = NULL;
  std::string url_spec;
  if (!pickle->ReadInt(&iterator, tab_id) ||
      !pickle->ReadInt(&iterator, &(navigation->index_)) ||
      !pickle->ReadString(&iterator, &url_spec) ||
      !pickle->ReadString16(&iterator, &(navigation->title_)) ||
      !pickle->ReadString(&iterator, &(navigation->state_)) ||
      !pickle->ReadInt(&iterator,
                       reinterpret_cast<int*>(&(navigation->transition_))))
    return false;
  // type_mask did not always exist in the written stream. As such, we
  // don't fail if it can't be read.
  bool has_type_mask = pickle->ReadInt(&iterator, &(navigation->type_mask_));

  if (has_type_mask) {
    // the "referrer" property was added after type_mask to the written
    // stream. As such, we don't fail if it can't be read.
    std::string referrer_spec;
    pickle->ReadString(&iterator, &referrer_spec);
    if (!referrer_spec.empty())
      navigation->referrer_ = GURL(referrer_spec);
  }

  navigation->virtual_url_ = GURL(url_spec);
  return true;
}

bool BaseSessionService::RestoreSetTabExtensionAppIDCommand(
    const SessionCommand& command,
    SessionID::id_type* tab_id,
    std::string* extension_app_id) {
  scoped_ptr<Pickle> pickle(command.PayloadAsPickle());
  if (!pickle.get())
    return false;

  void* iterator = NULL;
  return pickle->ReadInt(&iterator, tab_id) &&
      pickle->ReadString(&iterator, extension_app_id);
}

bool BaseSessionService::ShouldTrackEntry(const NavigationEntry& entry) {
  return entry.virtual_url().is_valid();
}

bool BaseSessionService::ShouldTrackEntry(const TabNavigation& navigation) {
  return navigation.virtual_url().is_valid();
}

BaseSessionService::Handle BaseSessionService::ScheduleGetLastSessionCommands(
    InternalGetCommandsRequest* request,
    CancelableRequestConsumerBase* consumer) {
  scoped_refptr<InternalGetCommandsRequest> request_wrapper(request);
  AddRequest(request, consumer);
  if (backend_thread()) {
    backend_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
        backend(), &SessionBackend::ReadLastSessionCommands, request_wrapper));
  } else {
    backend()->ReadLastSessionCommands(request);
  }
  return request->handle();
}

BaseSessionService::Handle
    BaseSessionService::ScheduleGetCurrentSessionCommands(
        InternalGetCommandsRequest* request,
        CancelableRequestConsumerBase* consumer) {
  scoped_refptr<InternalGetCommandsRequest> request_wrapper(request);
  AddRequest(request, consumer);
  if (backend_thread()) {
    backend_thread()->message_loop()->PostTask(FROM_HERE,
        NewRunnableMethod(backend(),
                          &SessionBackend::ReadCurrentSessionCommands,
                          request_wrapper));
  } else {
    backend()->ReadCurrentSessionCommands(request);
  }
  return request->handle();
}