// 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 "chrome/browser/extensions/extensions_quota_service.h"

#include "base/message_loop.h"
#include "base/stl_util.h"
#include "chrome/browser/extensions/extension_function.h"
#include "chrome/common/extensions/extension_error_utils.h"

namespace {

// If the browser stays open long enough, we reset state once a day.
// Whatever this value is, it should be an order of magnitude longer than
// the longest interval in any of the QuotaLimitHeuristics in use.
const int kPurgeIntervalInDays = 1;

const char kOverQuotaError[] = "This request exceeds the * quota.";

}  // namespace

ExtensionsQuotaService::ExtensionsQuotaService() {
  if (MessageLoop::current() != NULL) {  // Null in unit tests.
    purge_timer_.Start(FROM_HERE,
                       base::TimeDelta::FromDays(kPurgeIntervalInDays),
                       this, &ExtensionsQuotaService::Purge);
  }
}

ExtensionsQuotaService::~ExtensionsQuotaService() {
  DCHECK(CalledOnValidThread());
  purge_timer_.Stop();
  Purge();
}

std::string ExtensionsQuotaService::Assess(
    const std::string& extension_id,
    ExtensionFunction* function,
    const ListValue* args,
    const base::TimeTicks& event_time) {
  DCHECK(CalledOnValidThread());

  if (function->ShouldSkipQuotaLimiting())
    return "";

  // Lookup function list for extension.
  FunctionHeuristicsMap& functions = function_heuristics_[extension_id];

  // Lookup heuristics for function, create if necessary.
  QuotaLimitHeuristics& heuristics = functions[function->name()];
  if (heuristics.empty())
    function->GetQuotaLimitHeuristics(&heuristics);

  if (heuristics.empty())
    return "";  // No heuristic implies no limit.

  ViolationErrorMap::iterator violation_error =
      violation_errors_.find(extension_id);
  if (violation_error != violation_errors_.end())
    return violation_error->second;  // Repeat offender.

  QuotaLimitHeuristic* failed_heuristic = NULL;
  for (QuotaLimitHeuristics::iterator heuristic = heuristics.begin();
       heuristic != heuristics.end(); ++heuristic) {
    // Apply heuristic to each item (bucket).
    if (!(*heuristic)->ApplyToArgs(args, event_time)) {
      failed_heuristic = *heuristic;
      break;
    }
  }

  if (!failed_heuristic)
    return "";

  std::string error = failed_heuristic->GetError();
  DCHECK_GT(error.length(), 0u);

  PurgeFunctionHeuristicsMap(&functions);
  function_heuristics_.erase(extension_id);
  violation_errors_[extension_id] = error;
  return error;
}

void ExtensionsQuotaService::PurgeFunctionHeuristicsMap(
    FunctionHeuristicsMap* map) {
  FunctionHeuristicsMap::iterator heuristics = map->begin();
  while (heuristics != map->end()) {
    STLDeleteElements(&heuristics->second);
    map->erase(heuristics++);
  }
}

void ExtensionsQuotaService::Purge() {
  DCHECK(CalledOnValidThread());
  std::map<std::string, FunctionHeuristicsMap>::iterator it =
      function_heuristics_.begin();
  for (; it != function_heuristics_.end(); function_heuristics_.erase(it++))
    PurgeFunctionHeuristicsMap(&it->second);
}

void QuotaLimitHeuristic::Bucket::Reset(const Config& config,
    const base::TimeTicks& start) {
  num_tokens_ = config.refill_token_count;
  expiration_ = start + config.refill_interval;
}

void QuotaLimitHeuristic::SingletonBucketMapper::GetBucketsForArgs(
    const ListValue* args,
    BucketList* buckets) {
  buckets->push_back(&bucket_);
}

QuotaLimitHeuristic::QuotaLimitHeuristic(const Config& config,
                                         BucketMapper* map,
                                         const std::string& name)
    : config_(config), bucket_mapper_(map), name_(name) {
}

QuotaLimitHeuristic::~QuotaLimitHeuristic() {}

bool QuotaLimitHeuristic::ApplyToArgs(const ListValue* args,
    const base::TimeTicks& event_time) {
  BucketList buckets;
  bucket_mapper_->GetBucketsForArgs(args, &buckets);
  for (BucketList::iterator i = buckets.begin(); i != buckets.end(); ++i) {
    if ((*i)->expiration().is_null())  // A brand new bucket.
      (*i)->Reset(config_, event_time);
    if (!Apply(*i, event_time))
      return false;  // It only takes one to spoil it for everyone.
  }
  return true;
}

std::string QuotaLimitHeuristic::GetError() const {
  return ExtensionErrorUtils::FormatErrorMessage(kOverQuotaError, name_);
}

ExtensionsQuotaService::SustainedLimit::SustainedLimit(
    const base::TimeDelta& sustain,
    const Config& config,
    BucketMapper* map,
    const std::string& name)
    : QuotaLimitHeuristic(config, map, name),
      repeat_exhaustion_allowance_(sustain.InSeconds() /
                                   config.refill_interval.InSeconds()),
      num_available_repeat_exhaustions_(repeat_exhaustion_allowance_) {
}

bool ExtensionsQuotaService::TimedLimit::Apply(Bucket* bucket,
    const base::TimeTicks& event_time) {
  if (event_time > bucket->expiration())
    bucket->Reset(config(), event_time);

  return bucket->DeductToken();
}

bool ExtensionsQuotaService::SustainedLimit::Apply(Bucket* bucket,
    const base::TimeTicks& event_time) {
  if (event_time > bucket->expiration()) {
    // We reset state for this item and start over again if this request breaks
    // the bad cycle that was previously being tracked.  This occurs if the
    // state in the bucket expired recently (it has been long enough since the
    // event that we don't care about the last event), but the bucket still has
    // tokens (so pressure was not sustained over that time), OR we are more
    // than 1 full refill interval away from the last event (so even if we used
    // up all the tokens in the last bucket, nothing happened in the entire
    // next refill interval, so it doesn't matter).
    if (bucket->has_tokens() || event_time > bucket->expiration() +
        config().refill_interval) {
      bucket->Reset(config(), event_time);
      num_available_repeat_exhaustions_ = repeat_exhaustion_allowance_;
    } else if (--num_available_repeat_exhaustions_ > 0) {
      // The last interval was saturated with requests, and this is the first
      // event in the next interval. If this happens
      // repeat_exhaustion_allowance_ times, it's a violation. Reset the bucket
      // state to start timing from the end of the last interval (and we'll
      // deduct the token below) so we can detect this each time it happens.
      bucket->Reset(config(), bucket->expiration());
    } else {
      // No allowances left; this request is a violation.
      return false;
    }
  }

  // We can go negative since we check has_tokens when we get to *next* bucket,
  // and for the small interval all that matters is whether we used up all the
  // tokens (which is true if num_tokens_ <= 0).
  bucket->DeductToken();
  return true;
}