// Copyright 2017 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 "extensions/renderer/feature_cache.h"

#include <algorithm>

#include "base/command_line.h"
#include "content/public/common/content_switches.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/features/feature_provider.h"

namespace extensions {

FeatureCache::FeatureCache() {}
FeatureCache::~FeatureCache() = default;

FeatureCache::FeatureNameVector FeatureCache::GetAvailableFeatures(
    Feature::Context context_type,
    const Extension* extension,
    const GURL& url) {
  DCHECK_NE(context_type == Feature::WEBUI_CONTEXT, !!extension)
      << "WebUI contexts shouldn't have extensions.";
  DCHECK_NE(Feature::WEB_PAGE_CONTEXT, context_type)
      << "FeatureCache shouldn't be used for web contexts.";
  DCHECK_NE(Feature::UNSPECIFIED_CONTEXT, context_type)
      << "FeatureCache shouldn't be used for unspecified contexts.";

  const FeatureVector& features =
      GetFeaturesFromCache(context_type, extension, url.GetOrigin());
  FeatureNameVector names;
  names.reserve(features.size());
  for (const Feature* feature : features) {
    // Since we only cache based on extension id and context type, instead of
    // all attributes of a context (like URL), we need to double-check if the
    // feature is actually available to the context. This is still a win, since
    // we only perform this check on the (much smaller) set of features that
    // *may* be available, rather than all known features.
    // TODO(devlin): Optimize this - we should be able to tell if a feature may
    // change based on additional context attributes.
    if (ExtensionAPI::GetSharedInstance()->IsAnyFeatureAvailableToContext(
            *feature, extension, context_type, url,
            CheckAliasStatus::NOT_ALLOWED)) {
      names.push_back(feature->name());
    }
  }
  return names;
}

void FeatureCache::InvalidateExtension(const ExtensionId& extension_id) {
  for (auto iter = extension_cache_.begin(); iter != extension_cache_.end();) {
    if (iter->first.first == extension_id)
      iter = extension_cache_.erase(iter);
    else
      ++iter;
  }
}

const FeatureCache::FeatureVector& FeatureCache::GetFeaturesFromCache(
    Feature::Context context_type,
    const Extension* extension,
    const GURL& origin) {
  if (context_type == Feature::WEBUI_CONTEXT) {
    auto iter = webui_cache_.find(origin);
    if (iter != webui_cache_.end())
      return iter->second;
    return webui_cache_
        .emplace(origin, CreateCacheEntry(context_type, extension, origin))
        .first->second;
  }

  DCHECK(extension);
  ExtensionCacheMapKey key(extension->id(), context_type);
  auto iter = extension_cache_.find(key);
  if (iter != extension_cache_.end())
    return iter->second;
  return extension_cache_
      .emplace(key, CreateCacheEntry(context_type, extension, origin))
      .first->second;
}

FeatureCache::FeatureVector FeatureCache::CreateCacheEntry(
    Feature::Context context_type,
    const Extension* extension,
    const GURL& origin) {
  bool is_webui = context_type == Feature::WEBUI_CONTEXT;
  FeatureVector features;
  const FeatureProvider* api_feature_provider =
      FeatureProvider::GetAPIFeatures();
  GURL empty_url;
  // We ignore the URL if this is an extension context in order to maximize
  // cache hits. For WebUI, we key on origin.
  // Note: Currently, we only ever have matches based on origin, so this is
  // okay. If this changes, we'll have to get more creative about our WebUI
  // caching.
  const GURL& url_to_use = is_webui ? origin : empty_url;
  for (const auto& map_entry : api_feature_provider->GetAllFeatures()) {
    const Feature* feature = map_entry.second.get();
    // Exclude internal APIs.
    if (feature->IsInternal())
      continue;

    // Exclude child features (like events or specific functions).
    // TODO(devlin): Optimize this - instead of skipping child features and then
    // checking IsAnyFeatureAvailableToContext() (which checks child features),
    // we should just check all features directly.
    if (api_feature_provider->GetParent(*feature) != nullptr)
      continue;

    // Skip chrome.test if this isn't a test.
    if (map_entry.first == "test" &&
        !base::CommandLine::ForCurrentProcess()->HasSwitch(
            ::switches::kTestType)) {
      continue;
    }

    if (!ExtensionAPI::GetSharedInstance()->IsAnyFeatureAvailableToContext(
            *feature, extension, context_type, url_to_use,
            CheckAliasStatus::NOT_ALLOWED)) {
      continue;
    }

    features.push_back(feature);
  }

  std::sort(
      features.begin(), features.end(),
      [](const Feature* a, const Feature* b) { return a->name() < b->name(); });
  DCHECK(std::unique(features.begin(), features.end()) == features.end());

  return features;
}

}  // namespace extensions
