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

#include <string>

#include "base/values.h"
#include "base/string_number_conversions.h"
#include "base/string_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension_error_utils.h"

const char kCheckedKey[] = "checked";
const char kContextsKey[] = "contexts";
const char kDocumentUrlPatternsKey[] = "documentUrlPatterns";
const char kGeneratedIdKey[] = "generatedId";
const char kParentIdKey[] = "parentId";
const char kTargetUrlPatternsKey[] = "targetUrlPatterns";
const char kTitleKey[] = "title";
const char kTypeKey[] = "type";

const char kCannotFindItemError[] = "Cannot find menu item with id *";
const char kCheckedError[] =
    "Only items with type \"radio\" or \"checkbox\" can be checked";
const char kInvalidURLPatternError[] = "Invalid url pattern '*'";
const char kInvalidValueError[] = "Invalid value for *";
const char kInvalidTypeStringError[] = "Invalid type string '*'";
const char kParentsMustBeNormalError[] =
    "Parent items must have type \"normal\"";
const char kTitleNeededError[] =
    "All menu items except for separators must have a title";


bool ExtensionContextMenuFunction::ParseContexts(
    const DictionaryValue& properties,
    const char* key,
    ExtensionMenuItem::ContextList* result) {
  ListValue* list = NULL;
  if (!properties.GetList(key, &list)) {
    return true;
  }
  ExtensionMenuItem::ContextList tmp_result;

  std::string value;
  for (size_t i = 0; i < list->GetSize(); i++) {
    if (!list->GetString(i, &value))
      return false;

    if (value == "all") {
      tmp_result.Add(ExtensionMenuItem::ALL);
    } else if (value == "page") {
      tmp_result.Add(ExtensionMenuItem::PAGE);
    } else if (value == "selection") {
      tmp_result.Add(ExtensionMenuItem::SELECTION);
    } else if (value == "link") {
      tmp_result.Add(ExtensionMenuItem::LINK);
    } else if (value == "editable") {
      tmp_result.Add(ExtensionMenuItem::EDITABLE);
    } else if (value == "image") {
      tmp_result.Add(ExtensionMenuItem::IMAGE);
    } else if (value == "video") {
      tmp_result.Add(ExtensionMenuItem::VIDEO);
    } else if (value == "audio") {
      tmp_result.Add(ExtensionMenuItem::AUDIO);
    } else if (value == "frame") {
      tmp_result.Add(ExtensionMenuItem::FRAME);
    } else {
      error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidValueError, key);
      return false;
    }
  }
  *result = tmp_result;
  return true;
}

bool ExtensionContextMenuFunction::ParseType(
    const DictionaryValue& properties,
    const ExtensionMenuItem::Type& default_value,
    ExtensionMenuItem::Type* result) {
  DCHECK(result);
  if (!properties.HasKey(kTypeKey)) {
    *result = default_value;
    return true;
  }

  std::string type_string;
  if (!properties.GetString(kTypeKey, &type_string))
    return false;

  if (type_string == "normal") {
    *result = ExtensionMenuItem::NORMAL;
  } else if (type_string == "checkbox") {
    *result = ExtensionMenuItem::CHECKBOX;
  } else if (type_string == "radio") {
    *result = ExtensionMenuItem::RADIO;
  } else if (type_string == "separator") {
    *result = ExtensionMenuItem::SEPARATOR;
  } else {
    error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidTypeStringError,
                                                     type_string);
    return false;
  }
  return true;
}

bool ExtensionContextMenuFunction::ParseChecked(
    ExtensionMenuItem::Type type,
    const DictionaryValue& properties,
    bool default_value,
    bool* checked) {
  if (!properties.HasKey(kCheckedKey)) {
    *checked = default_value;
    return true;
  }
  if (!properties.GetBoolean(kCheckedKey, checked))
    return false;
  if (checked && type != ExtensionMenuItem::CHECKBOX &&
      type != ExtensionMenuItem::RADIO) {
    error_ = kCheckedError;
    return false;
  }
  return true;
}

bool ExtensionContextMenuFunction::ParseURLPatterns(
    const DictionaryValue& properties,
    const char* key,
    ExtensionExtent* result) {
  if (!properties.HasKey(key))
    return true;
  ListValue* list = NULL;
  if (!properties.GetList(key, &list))
    return false;
  for (ListValue::iterator i = list->begin(); i != list->end(); ++i) {
    std::string tmp;
    if (!(*i)->GetAsString(&tmp))
      return false;

    URLPattern pattern(ExtensionMenuManager::kAllowedSchemes);
    // TODO(skerner):  Consider enabling strict pattern parsing
    // if this extension's location indicates that it is under development.
    if (URLPattern::PARSE_SUCCESS != pattern.Parse(tmp,
                                                   URLPattern::PARSE_LENIENT)) {
      error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidURLPatternError,
                                                       tmp);
      return false;
    }
    result->AddPattern(pattern);
  }
  return true;
}

bool ExtensionContextMenuFunction::SetURLPatterns(
    const DictionaryValue& properties,
    ExtensionMenuItem* item) {
  // Process the documentUrlPattern value.
  ExtensionExtent document_url_patterns;
  if (!ParseURLPatterns(properties, kDocumentUrlPatternsKey,
                        &document_url_patterns))
    return false;

  if (!document_url_patterns.is_empty()) {
    item->set_document_url_patterns(document_url_patterns);
  }

  // Process the targetUrlPattern value.
  ExtensionExtent target_url_patterns;
  if (!ParseURLPatterns(properties, kTargetUrlPatternsKey,
                        &target_url_patterns))
    return false;

  if (!target_url_patterns.is_empty()) {
    item->set_target_url_patterns(target_url_patterns);
  }

  return true;
}

bool ExtensionContextMenuFunction::GetParent(
    const DictionaryValue& properties,
    const ExtensionMenuManager& manager,
    ExtensionMenuItem** result) {
  if (!properties.HasKey(kParentIdKey))
    return true;
  ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
  if (properties.HasKey(kParentIdKey) &&
      !properties.GetInteger(kParentIdKey, &parent_id.uid))
    return false;

  ExtensionMenuItem* parent = manager.GetItemById(parent_id);
  if (!parent) {
    error_ = "Cannot find menu item with id " +
        base::IntToString(parent_id.uid);
    return false;
  }
  if (parent->type() != ExtensionMenuItem::NORMAL) {
    error_ = kParentsMustBeNormalError;
    return false;
  }
  *result = parent;
  return true;
}

bool CreateContextMenuFunction::RunImpl() {
  DictionaryValue* properties;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &properties));
  EXTENSION_FUNCTION_VALIDATE(properties != NULL);

  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
  EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kGeneratedIdKey,
                                                     &id.uid));
  std::string title;
  if (properties->HasKey(kTitleKey) &&
      !properties->GetString(kTitleKey, &title))
    return false;

  ExtensionMenuManager* menu_manager =
      profile()->GetExtensionService()->menu_manager();

  ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::PAGE);
  if (!ParseContexts(*properties, kContextsKey, &contexts))
    return false;

  ExtensionMenuItem::Type type;
  if (!ParseType(*properties, ExtensionMenuItem::NORMAL, &type))
    return false;

  if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
    error_ = kTitleNeededError;
    return false;
  }

  bool checked;
  if (!ParseChecked(type, *properties, false, &checked))
    return false;

  scoped_ptr<ExtensionMenuItem> item(
      new ExtensionMenuItem(id, title, checked, type, contexts));

  if (!SetURLPatterns(*properties, item.get()))
    return false;

  bool success = true;
  if (properties->HasKey(kParentIdKey)) {
    ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
    EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kParentIdKey,
                                                       &parent_id.uid));
    ExtensionMenuItem* parent = menu_manager->GetItemById(parent_id);
    if (!parent) {
      error_ = ExtensionErrorUtils::FormatErrorMessage(
          kCannotFindItemError, base::IntToString(parent_id.uid));
      return false;
    }
    if (parent->type() != ExtensionMenuItem::NORMAL) {
      error_ = kParentsMustBeNormalError;
      return false;
    }
    success = menu_manager->AddChildItem(parent_id, item.release());
  } else {
    success = menu_manager->AddContextItem(GetExtension(), item.release());
  }

  if (!success)
    return false;

  return true;
}

bool UpdateContextMenuFunction::RunImpl() {
  ExtensionMenuItem::Id item_id(profile(), extension_id(), 0);
  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &item_id.uid));

  ExtensionService* service = profile()->GetExtensionService();
  ExtensionMenuManager* manager = service->menu_manager();
  ExtensionMenuItem* item = manager->GetItemById(item_id);
  if (!item || item->extension_id() != extension_id()) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        kCannotFindItemError, base::IntToString(item_id.uid));
    return false;
  }

  DictionaryValue *properties = NULL;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &properties));
  EXTENSION_FUNCTION_VALIDATE(properties != NULL);

  ExtensionMenuManager* menu_manager =
      profile()->GetExtensionService()->menu_manager();

  // Type.
  ExtensionMenuItem::Type type;
  if (!ParseType(*properties, item->type(), &type))
    return false;
  if (type != item->type())
    item->set_type(type);

  // Title.
  if (properties->HasKey(kTitleKey)) {
    std::string title;
    EXTENSION_FUNCTION_VALIDATE(properties->GetString(kTitleKey, &title));
    if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
      error_ = kTitleNeededError;
      return false;
    }
    item->set_title(title);
  }

  // Checked state.
  bool checked;
  if (!ParseChecked(item->type(), *properties, item->checked(), &checked))
    return false;
  if (checked != item->checked()) {
    if (!item->SetChecked(checked))
      return false;
  }

  // Contexts.
  ExtensionMenuItem::ContextList contexts(item->contexts());
  if (!ParseContexts(*properties, kContextsKey, &contexts))
    return false;
  if (contexts != item->contexts())
    item->set_contexts(contexts);

  // Parent id.
  ExtensionMenuItem* parent = NULL;
  if (!GetParent(*properties, *menu_manager, &parent))
    return false;
  if (parent && !menu_manager->ChangeParent(item->id(), &parent->id()))
    return false;

  if (!SetURLPatterns(*properties, item))
    return false;

  return true;
}

bool RemoveContextMenuFunction::RunImpl() {
  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &id.uid));
  ExtensionService* service = profile()->GetExtensionService();
  ExtensionMenuManager* manager = service->menu_manager();

  ExtensionMenuItem* item = manager->GetItemById(id);
  // Ensure one extension can't remove another's menu items.
  if (!item || item->extension_id() != extension_id()) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        kCannotFindItemError, base::IntToString(id.uid));
    return false;
  }

  return manager->RemoveContextMenuItem(id);
}

bool RemoveAllContextMenusFunction::RunImpl() {
  ExtensionService* service = profile()->GetExtensionService();
  ExtensionMenuManager* manager = service->menu_manager();
  manager->RemoveAllContextItems(extension_id());
  return true;
}