// Copyright (c) 2012 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 "content/renderer/dom_storage/dom_storage_cached_area.h"

#include "base/basictypes.h"
#include "base/metrics/histogram.h"
#include "base/time/time.h"
#include "content/common/dom_storage/dom_storage_map.h"
#include "content/renderer/dom_storage/dom_storage_proxy.h"

namespace content {

namespace {

static const int kMaxLogGetMessagesToSend = 16 * 1024;

}  // namespace

DOMStorageCachedArea::DOMStorageCachedArea(int64 namespace_id,
                                           const GURL& origin,
                                           DOMStorageProxy* proxy)
    : ignore_all_mutations_(false),
      namespace_id_(namespace_id),
      origin_(origin),
      proxy_(proxy),
      remaining_log_get_messages_(0),
      weak_factory_(this) {}

DOMStorageCachedArea::~DOMStorageCachedArea() {}

unsigned DOMStorageCachedArea::GetLength(int connection_id) {
  PrimeIfNeeded(connection_id);
  return map_->Length();
}

base::NullableString16 DOMStorageCachedArea::GetKey(int connection_id,
                                                    unsigned index) {
  PrimeIfNeeded(connection_id);
  return map_->Key(index);
}

base::NullableString16 DOMStorageCachedArea::GetItem(
    int connection_id,
    const base::string16& key) {
  PrimeIfNeeded(connection_id);
  base::NullableString16 result = map_->GetItem(key);
  if (remaining_log_get_messages_ > 0) {
    remaining_log_get_messages_--;
    proxy_->LogGetItem(connection_id, key, result);
  }
  return result;
}

bool DOMStorageCachedArea::SetItem(int connection_id,
                                   const base::string16& key,
                                   const base::string16& value,
                                   const GURL& page_url) {
  // A quick check to reject obviously overbudget items to avoid
  // the priming the cache.
  if (key.length() + value.length() > kPerStorageAreaQuota)
    return false;

  PrimeIfNeeded(connection_id);
  base::NullableString16 unused;
  if (!map_->SetItem(key, value, &unused))
    return false;

  // Ignore mutations to 'key' until OnSetItemComplete.
  ignore_key_mutations_[key]++;
  proxy_->SetItem(
      connection_id, key, value, page_url,
      base::Bind(&DOMStorageCachedArea::OnSetItemComplete,
                 weak_factory_.GetWeakPtr(), key));
  return true;
}

void DOMStorageCachedArea::RemoveItem(int connection_id,
                                      const base::string16& key,
                                      const GURL& page_url) {
  PrimeIfNeeded(connection_id);
  base::string16 unused;
  if (!map_->RemoveItem(key, &unused))
    return;

  // Ignore mutations to 'key' until OnRemoveItemComplete.
  ignore_key_mutations_[key]++;
  proxy_->RemoveItem(
      connection_id, key, page_url,
      base::Bind(&DOMStorageCachedArea::OnRemoveItemComplete,
                 weak_factory_.GetWeakPtr(), key));
}

void DOMStorageCachedArea::Clear(int connection_id, const GURL& page_url) {
  // No need to prime the cache in this case.
  Reset();
  map_ = new DOMStorageMap(kPerStorageAreaQuota);

  // Ignore all mutations until OnClearComplete time.
  ignore_all_mutations_ = true;
  proxy_->ClearArea(connection_id,
                    page_url,
                    base::Bind(&DOMStorageCachedArea::OnClearComplete,
                               weak_factory_.GetWeakPtr()));
}

void DOMStorageCachedArea::ApplyMutation(
    const base::NullableString16& key,
    const base::NullableString16& new_value) {
  if (!map_.get() || ignore_all_mutations_)
    return;

  if (key.is_null()) {
    // It's a clear event.
    scoped_refptr<DOMStorageMap> old = map_;
    map_ = new DOMStorageMap(kPerStorageAreaQuota);

    // We have to retain local additions which happened after this
    // clear operation from another process.
    std::map<base::string16, int>::iterator iter =
        ignore_key_mutations_.begin();
    while (iter != ignore_key_mutations_.end()) {
      base::NullableString16 value = old->GetItem(iter->first);
      if (!value.is_null()) {
        base::NullableString16 unused;
        map_->SetItem(iter->first, value.string(), &unused);
      }
      ++iter;
    }
    return;
  }

  // We have to retain local changes.
  if (should_ignore_key_mutation(key.string()))
    return;

  if (new_value.is_null()) {
    // It's a remove item event.
    base::string16 unused;
    map_->RemoveItem(key.string(), &unused);
    return;
  }

  // It's a set item event.
  // We turn off quota checking here to accomodate the over budget
  // allowance that's provided in the browser process.
  base::NullableString16 unused;
  map_->set_quota(kint32max);
  map_->SetItem(key.string(), new_value.string(), &unused);
  map_->set_quota(kPerStorageAreaQuota);
}

size_t DOMStorageCachedArea::MemoryBytesUsedByCache() const {
  return map_.get() ? map_->bytes_used() : 0;
}

void DOMStorageCachedArea::Prime(int connection_id) {
  DCHECK(!map_.get());

  // The LoadArea method is actually synchronous, but we have to
  // wait for an asyncly delivered message to know when incoming
  // mutation events should be applied. Our valuemap is plucked
  // from ipc stream out of order, mutations in front if it need
  // to be ignored.

  // Ignore all mutations until OnLoadComplete time.
  ignore_all_mutations_ = true;
  DOMStorageValuesMap values;
  bool send_log_get_messages = false;
  base::TimeTicks before = base::TimeTicks::Now();
  proxy_->LoadArea(connection_id,
                   &values,
                   &send_log_get_messages,
                   base::Bind(&DOMStorageCachedArea::OnLoadComplete,
                              weak_factory_.GetWeakPtr()));
  base::TimeDelta time_to_prime = base::TimeTicks::Now() - before;
  // Keeping this histogram named the same (without the ForRenderer suffix)
  // to maintain histogram continuity.
  UMA_HISTOGRAM_TIMES("LocalStorage.TimeToPrimeLocalStorage",
                      time_to_prime);
  map_ = new DOMStorageMap(kPerStorageAreaQuota);
  map_->SwapValues(&values);
  if (send_log_get_messages)
    remaining_log_get_messages_ = kMaxLogGetMessagesToSend;

  size_t local_storage_size_kb = map_->bytes_used() / 1024;
  // Track localStorage size, from 0-6MB. Note that the maximum size should be
  // 5MB, but we add some slop since we want to make sure the max size is always
  // above what we see in practice, since histograms can't change.
  UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.RendererLocalStorageSizeInKB",
                              local_storage_size_kb,
                              0, 6 * 1024, 50);
  if (local_storage_size_kb < 100) {
    UMA_HISTOGRAM_TIMES(
        "LocalStorage.RendererTimeToPrimeLocalStorageUnder100KB",
        time_to_prime);
  } else if (local_storage_size_kb < 1000) {
    UMA_HISTOGRAM_TIMES(
        "LocalStorage.RendererTimeToPrimeLocalStorage100KBTo1MB",
        time_to_prime);
  } else {
    UMA_HISTOGRAM_TIMES(
        "LocalStorage.RendererTimeToPrimeLocalStorage1MBTo5MB",
        time_to_prime);
  }
}

void DOMStorageCachedArea::Reset() {
  map_ = NULL;
  weak_factory_.InvalidateWeakPtrs();
  ignore_key_mutations_.clear();
  ignore_all_mutations_ = false;
}

void DOMStorageCachedArea::OnLoadComplete(bool success) {
  DCHECK(success);
  DCHECK(ignore_all_mutations_);
  ignore_all_mutations_ = false;
}

void DOMStorageCachedArea::OnSetItemComplete(const base::string16& key,
                                             bool success) {
  if (!success) {
    Reset();
    return;
  }
  std::map<base::string16, int>::iterator found =
      ignore_key_mutations_.find(key);
  DCHECK(found != ignore_key_mutations_.end());
  if (--found->second == 0)
    ignore_key_mutations_.erase(found);
}

void DOMStorageCachedArea::OnRemoveItemComplete(const base::string16& key,
                                                bool success) {
  DCHECK(success);
  std::map<base::string16, int>::iterator found =
      ignore_key_mutations_.find(key);
  DCHECK(found != ignore_key_mutations_.end());
  if (--found->second == 0)
    ignore_key_mutations_.erase(found);
}

void DOMStorageCachedArea::OnClearComplete(bool success) {
  DCHECK(success);
  DCHECK(ignore_all_mutations_);
  ignore_all_mutations_ = false;
}

}  // namespace content
