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

#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url.h"
#include "content/common/notification_service.h"

namespace events {
const char kOnInputStarted[] = "omnibox.onInputStarted";
const char kOnInputChanged[] = "omnibox.onInputChanged";
const char kOnInputEntered[] = "omnibox.onInputEntered";
const char kOnInputCancelled[] = "omnibox.onInputCancelled";
};  // namespace events

namespace {
const char kDescriptionStylesOrderError[] =
    "Suggestion descriptionStyles must be in increasing non-overlapping order.";
const char kDescriptionStylesLengthError[] =
    "Suggestion descriptionStyles contains an offset longer than the"
    " description text";

const char kSuggestionContent[] = "content";
const char kSuggestionDescription[] = "description";
const char kSuggestionDescriptionStyles[] = "descriptionStyles";
const char kDescriptionStylesType[] = "type";
const char kDescriptionStylesOffset[] = "offset";
const char kDescriptionStylesLength[] = "length";

static base::LazyInstance<PropertyAccessor<ExtensionOmniboxSuggestion> >
    g_extension_omnibox_suggestion_property_accessor(base::LINKER_INITIALIZED);

PropertyAccessor<ExtensionOmniboxSuggestion>& GetPropertyAccessor() {
  return g_extension_omnibox_suggestion_property_accessor.Get();
}

// Returns the suggestion object set by the extension via the
// omnibox.setDefaultSuggestion call, or NULL if it was never set.
const ExtensionOmniboxSuggestion* GetDefaultSuggestionForExtension(
    Profile* profile, const std::string& extension_id) {
  const Extension* extension =
      profile->GetExtensionService()->GetExtensionById(extension_id, false);
  if (!extension)
    return NULL;
  return GetPropertyAccessor().GetProperty(
      profile->GetExtensionService()->GetPropertyBag(extension));
}

};  // namespace

// static
void ExtensionOmniboxEventRouter::OnInputStarted(
    Profile* profile, const std::string& extension_id) {
  profile->GetExtensionEventRouter()->DispatchEventToExtension(
      extension_id, events::kOnInputStarted, "[]", profile, GURL());
}

// static
bool ExtensionOmniboxEventRouter::OnInputChanged(
    Profile* profile, const std::string& extension_id,
    const std::string& input, int suggest_id) {
  if (!profile->GetExtensionEventRouter()->ExtensionHasEventListener(
        extension_id, events::kOnInputChanged))
    return false;

  ListValue args;
  args.Set(0, Value::CreateStringValue(input));
  args.Set(1, Value::CreateIntegerValue(suggest_id));
  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);

  profile->GetExtensionEventRouter()->DispatchEventToExtension(
      extension_id, events::kOnInputChanged, json_args, profile, GURL());
  return true;
}

// static
void ExtensionOmniboxEventRouter::OnInputEntered(
    Profile* profile, const std::string& extension_id,
    const std::string& input) {
  ListValue args;
  args.Set(0, Value::CreateStringValue(input));
  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);

  profile->GetExtensionEventRouter()->DispatchEventToExtension(
      extension_id, events::kOnInputEntered, json_args, profile, GURL());

  NotificationService::current()->Notify(
      NotificationType::EXTENSION_OMNIBOX_INPUT_ENTERED,
      Source<Profile>(profile), NotificationService::NoDetails());
}

// static
void ExtensionOmniboxEventRouter::OnInputCancelled(
    Profile* profile, const std::string& extension_id) {
  profile->GetExtensionEventRouter()->DispatchEventToExtension(
      extension_id, events::kOnInputCancelled, "[]", profile, GURL());
}

bool OmniboxSendSuggestionsFunction::RunImpl() {
  ExtensionOmniboxSuggestions suggestions;
  ListValue* suggestions_value;
  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &suggestions.request_id));
  EXTENSION_FUNCTION_VALIDATE(args_->GetList(1, &suggestions_value));

  suggestions.suggestions.resize(suggestions_value->GetSize());
  for (size_t i = 0; i < suggestions_value->GetSize(); ++i) {
    ExtensionOmniboxSuggestion& suggestion = suggestions.suggestions[i];
    DictionaryValue* suggestion_value;
    EXTENSION_FUNCTION_VALIDATE(suggestions_value->GetDictionary(
        i, &suggestion_value));
    EXTENSION_FUNCTION_VALIDATE(suggestion_value->GetString(
        kSuggestionContent, &suggestion.content));
    EXTENSION_FUNCTION_VALIDATE(suggestion_value->GetString(
        kSuggestionDescription, &suggestion.description));

    if (suggestion_value->HasKey(kSuggestionDescriptionStyles)) {
      ListValue* styles;
      EXTENSION_FUNCTION_VALIDATE(
          suggestion_value->GetList(kSuggestionDescriptionStyles, &styles));
      EXTENSION_FUNCTION_VALIDATE(suggestion.ReadStylesFromValue(*styles));
    } else {
      suggestion.description_styles.clear();
      suggestion.description_styles.push_back(
          ACMatchClassification(0, ACMatchClassification::NONE));
    }
  }

  NotificationService::current()->Notify(
      NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY,
      Source<Profile>(profile_),
      Details<ExtensionOmniboxSuggestions>(&suggestions));

  return true;
}

bool OmniboxSetDefaultSuggestionFunction::RunImpl() {
  ExtensionOmniboxSuggestion suggestion;
  DictionaryValue* suggestion_value;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &suggestion_value));
  EXTENSION_FUNCTION_VALIDATE(suggestion_value->GetString(
      kSuggestionDescription, &suggestion.description));

  if (suggestion_value->HasKey(kSuggestionDescriptionStyles)) {
    ListValue* styles;
    EXTENSION_FUNCTION_VALIDATE(
        suggestion_value->GetList(kSuggestionDescriptionStyles, &styles));
    EXTENSION_FUNCTION_VALIDATE(suggestion.ReadStylesFromValue(*styles));
  } else {
    suggestion.description_styles.clear();
    suggestion.description_styles.push_back(
        ACMatchClassification(0, ACMatchClassification::NONE));
  }

  // Store the suggestion in the extension's runtime data.
  GetPropertyAccessor().SetProperty(
      profile_->GetExtensionService()->GetPropertyBag(GetExtension()),
      suggestion);

  NotificationService::current()->Notify(
      NotificationType::EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
      Source<Profile>(profile_),
      NotificationService::NoDetails());

  return true;
}

ExtensionOmniboxSuggestion::ExtensionOmniboxSuggestion() {}

ExtensionOmniboxSuggestion::~ExtensionOmniboxSuggestion() {}

bool ExtensionOmniboxSuggestion::ReadStylesFromValue(
    const ListValue& styles_value) {
  description_styles.clear();

  // Step 1: Build a vector of styles, 1 per character of description text.
  std::vector<int> styles;
  styles.resize(description.length());  // sets all styles to 0

  for (size_t i = 0; i < styles_value.GetSize(); ++i) {
    DictionaryValue* style;
    std::string type;
    int offset;
    int length;
    if (!styles_value.GetDictionary(i, &style))
      return false;
    if (!style->GetString(kDescriptionStylesType, &type))
      return false;
    if (!style->GetInteger(kDescriptionStylesOffset, &offset))
      return false;
    if (!style->GetInteger(kDescriptionStylesLength, &length) || length < 0)
      length = description.length();

    if (offset < 0)
      offset = std::max(0, static_cast<int>(description.length()) + offset);

    int type_class =
        (type == "url") ? ACMatchClassification::URL :
        (type == "match") ? ACMatchClassification::MATCH :
        (type == "dim") ? ACMatchClassification::DIM : -1;
    if (type_class == -1)
      return false;

    for (int j = offset;
         j < offset + length && j < static_cast<int>(styles.size()); ++j)
      styles[j] |= type_class;
  }

  // Step 2: Convert the vector into continuous runs of common styles.
  for (size_t i = 0; i < styles.size(); ++i) {
    if (i == 0 || styles[i] != styles[i-1])
      description_styles.push_back(ACMatchClassification(i, styles[i]));
  }

  return true;
}

ExtensionOmniboxSuggestions::ExtensionOmniboxSuggestions() : request_id(0) {}

ExtensionOmniboxSuggestions::~ExtensionOmniboxSuggestions() {}

void ApplyDefaultSuggestionForExtensionKeyword(
    Profile* profile,
    const TemplateURL* keyword,
    const string16& remaining_input,
    AutocompleteMatch* match) {
  DCHECK(keyword->IsExtensionKeyword());
  const ExtensionOmniboxSuggestion* suggestion =
      GetDefaultSuggestionForExtension(profile, keyword->GetExtensionId());
  if (!suggestion)
    return;  // fall back to the universal default

  const string16 kPlaceholderText(ASCIIToUTF16("%s"));
  const string16 kReplacementText(ASCIIToUTF16("<input>"));

  string16 description = suggestion->description;
  ACMatchClassifications& description_styles = match->contents_class;
  description_styles = suggestion->description_styles;

  // Replace "%s" with the user's input and adjust the style offsets to the
  // new length of the description.
  size_t placeholder(suggestion->description.find(kPlaceholderText, 0));
  if (placeholder != string16::npos) {
    string16 replacement =
        remaining_input.empty() ? kReplacementText : remaining_input;
    description.replace(placeholder, kPlaceholderText.length(), replacement);

    for (size_t i = 0; i < description_styles.size(); ++i) {
      if (description_styles[i].offset > placeholder)
        description_styles[i].offset += replacement.length() - 2;
    }
  }

  match->contents.assign(description);
}