/*
 * Copyright (C) 2006, 2007 Apple, Inc.  All rights reserved.
 * Copyright (C) 2012 Google, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "third_party/blink/renderer/core/editing/editing_behavior.h"

#include "build/build_config.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/platform/keyboard_codes.h"
#include "third_party/blink/renderer/platform/text/writing_mode_utils.h"

namespace blink {

namespace {

//
// The below code was adapted from the WebKit file webview.cpp
//

const uint32_t kCtrlKey = WebInputEvent::kControlKey;
const uint32_t kAltKey = WebInputEvent::kAltKey;
const uint32_t kShiftKey = WebInputEvent::kShiftKey;
const uint32_t kMetaKey = WebInputEvent::kMetaKey;
#if BUILDFLAG(IS_MAC)
// Aliases for the generic key defintions to make kbd shortcuts definitions more
// readable on OS X.
const uint32_t kOptionKey = kAltKey;

// Do not use this constant for anything but cursor movement commands. Keys
// with cmd set have their |isSystemKey| bit set, so chances are the shortcut
// will not be executed. Another, less important, reason is that shortcuts
// defined in the layoutObject do not blink the menu item that they triggered.
// See http://crbug.com/25856 and the bugs linked from there for details.
const uint32_t kCommandKey = kMetaKey;
#endif

// Keys with special meaning. These will be delegated to the editor using
// the execCommand() method
struct KeyboardCodeKeyDownEntry {
  uint32_t virtual_key;
  uint32_t modifiers;
  const char* name;
};

struct KeyboardCodeKeyPressEntry {
  uint32_t char_code;
  uint32_t modifiers;
  const char* name;
};

// DomKey has a broader range than KeyboardCode, we need DomKey to handle some
// special keys.
// Note: We cannot use DomKey for printable keys since it may vary based on
// locale.
struct DomKeyKeyDownEntry {
  const char* key;
  uint32_t modifiers;
  const char* name;
};

#if BUILDFLAG(IS_MAC)
#define OPTION_OR_CTRL_KEY kOptionKey
#else
#define OPTION_OR_CTRL_KEY kCtrlKey
#endif

// Key bindings with command key on Mac and alt key on other platforms are
// marked as system key events and will be ignored (with the exception
// of Command-B and Command-I) so they shouldn't be added here.
const KeyboardCodeKeyDownEntry kKeyboardCodeKeyDownEntries[] = {
    {VKEY_LEFT, 0, "MoveLeft"},
    {VKEY_LEFT, kShiftKey, "MoveLeftAndModifySelection"},
    {VKEY_LEFT, OPTION_OR_CTRL_KEY, "MoveWordLeft"},
    {VKEY_LEFT, OPTION_OR_CTRL_KEY | kShiftKey,
     "MoveWordLeftAndModifySelection"},
    {VKEY_RIGHT, 0, "MoveRight"},
    {VKEY_RIGHT, kShiftKey, "MoveRightAndModifySelection"},
    {VKEY_RIGHT, OPTION_OR_CTRL_KEY, "MoveWordRight"},
    {VKEY_RIGHT, OPTION_OR_CTRL_KEY | kShiftKey,
     "MoveWordRightAndModifySelection"},
#if BUILDFLAG(IS_ANDROID)
    {VKEY_LEFT, kAltKey, "MoveToBeginningOfLine"},
    {VKEY_RIGHT, kAltKey, "MoveToEndOfLine"},
    {VKEY_LEFT, kAltKey | kShiftKey, "MoveToBeginningOfLineAndModifySelection"},
    {VKEY_RIGHT, kAltKey | kShiftKey, "MoveToEndOfLineAndModifySelection"},
#endif
    {VKEY_UP, 0, "MoveUp"},
    {VKEY_UP, kShiftKey, "MoveUpAndModifySelection"},
    {VKEY_PRIOR, kShiftKey, "MovePageUpAndModifySelection"},
    {VKEY_DOWN, 0, "MoveDown"},
    {VKEY_DOWN, kShiftKey, "MoveDownAndModifySelection"},
    {VKEY_NEXT, kShiftKey, "MovePageDownAndModifySelection"},
    {VKEY_UP, OPTION_OR_CTRL_KEY, "MoveParagraphBackward"},
    {VKEY_DOWN, OPTION_OR_CTRL_KEY, "MoveParagraphForward"},
#if !BUILDFLAG(IS_MAC)
    {VKEY_UP, kCtrlKey | kShiftKey, "MoveParagraphBackwardAndModifySelection"},
    {VKEY_DOWN, kCtrlKey | kShiftKey, "MoveParagraphForwardAndModifySelection"},
    {VKEY_PRIOR, 0, "MovePageUp"},
    {VKEY_NEXT, 0, "MovePageDown"},
#endif
    {VKEY_HOME, 0, "MoveToBeginningOfLine"},
    {VKEY_HOME, kShiftKey, "MoveToBeginningOfLineAndModifySelection"},
#if BUILDFLAG(IS_MAC)
    {VKEY_PRIOR, kOptionKey, "MovePageUp"},
    {VKEY_NEXT, kOptionKey, "MovePageDown"},
#endif
#if !BUILDFLAG(IS_MAC)
    {VKEY_HOME, kCtrlKey, "MoveToBeginningOfDocument"},
    {VKEY_HOME, kCtrlKey | kShiftKey,
     "MoveToBeginningOfDocumentAndModifySelection"},
#endif
    {VKEY_END, 0, "MoveToEndOfLine"},
    {VKEY_END, kShiftKey, "MoveToEndOfLineAndModifySelection"},
#if !BUILDFLAG(IS_MAC)
    {VKEY_END, kCtrlKey, "MoveToEndOfDocument"},
    {VKEY_END, kCtrlKey | kShiftKey, "MoveToEndOfDocumentAndModifySelection"},
#endif
    {VKEY_BACK, 0, "DeleteBackward"},
    {VKEY_BACK, kShiftKey, "DeleteBackward"},
    {VKEY_DELETE, 0, "DeleteForward"},
    {VKEY_BACK, OPTION_OR_CTRL_KEY, "DeleteWordBackward"},
    {VKEY_DELETE, OPTION_OR_CTRL_KEY, "DeleteWordForward"},
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
    {VKEY_BACK, kCtrlKey | kShiftKey, "DeleteToBeginningOfLine"},
#endif
#if BUILDFLAG(IS_MAC)
    {'B', kCommandKey, "ToggleBold"},
    {'I', kCommandKey, "ToggleItalic"},
    {'U', kCommandKey, "ToggleUnderline"},
#else
    {'B', kCtrlKey, "ToggleBold"},
    {'I', kCtrlKey, "ToggleItalic"},
    {'U', kCtrlKey, "ToggleUnderline"},
#endif
    {VKEY_ESCAPE, 0, "Cancel"},
    {VKEY_OEM_PERIOD, kCtrlKey, "Cancel"},
    {VKEY_TAB, 0, "InsertTab"},
    {VKEY_TAB, kShiftKey, "InsertBacktab"},
    {VKEY_RETURN, 0, "InsertNewline"},
    {VKEY_RETURN, kCtrlKey, "InsertNewline"},
    {VKEY_RETURN, kAltKey, "InsertNewline"},
    {VKEY_RETURN, kAltKey | kShiftKey, "InsertNewline"},
    {VKEY_RETURN, kShiftKey, "InsertLineBreak"},
    {VKEY_INSERT, kCtrlKey, "Copy"},
    {VKEY_INSERT, kShiftKey, "Paste"},
    {VKEY_DELETE, kShiftKey, "Cut"},
#if !BUILDFLAG(IS_MAC)
    // On OS X, we pipe these back to the browser, so that it can do menu item
    // blinking.
    {'C', kCtrlKey, "Copy"},
    {'V', kCtrlKey, "Paste"},
    {'V', kCtrlKey | kShiftKey, "PasteAndMatchStyle"},
    {'X', kCtrlKey, "Cut"},
    {'A', kCtrlKey, "SelectAll"},
    {'Z', kCtrlKey, "Undo"},
    {'Z', kCtrlKey | kShiftKey, "Redo"},
    {'Y', kCtrlKey, "Redo"},
#endif
#if BUILDFLAG(IS_WIN)
    {VKEY_BACK, kAltKey, "Undo"},
    {VKEY_BACK, kAltKey | kShiftKey, "Redo"},
#endif
    {VKEY_INSERT, 0, "OverWrite"},
#if BUILDFLAG(IS_ANDROID)
    {VKEY_BACK, kAltKey, "DeleteToBeginningOfLine"},
#endif
};

const KeyboardCodeKeyPressEntry kKeyboardCodeKeyPressEntries[] = {
    {'\t', 0, "InsertTab"},
    {'\t', kShiftKey, "InsertBacktab"},
    {'\r', 0, "InsertNewline"},
    {'\r', kShiftKey, "InsertLineBreak"},
};

const DomKeyKeyDownEntry kDomKeyKeyDownEntries[] = {
    {"Copy", 0, "Copy"},
    {"Cut", 0, "Cut"},
    {"Paste", 0, "Paste"},
};

#undef OPTION_OR_CTRL_KEY

const char* LookupCommandNameFromDomKeyKeyDown(const String& key,
                                               uint32_t modifiers) {
  // This table is not likely to grow, so sequential search is fine here.
  for (const auto& entry : kDomKeyKeyDownEntries) {
    if (key == entry.key && modifiers == entry.modifiers)
      return entry.name;
  }
  return nullptr;
}

const int kVkeyForwardChar = VKEY_RIGHT;
const int kVkeyBackwardChar = VKEY_LEFT;
const int kVkeyNextLine = VKEY_DOWN;
const int kVkeyPreviousLine = VKEY_UP;

int TransposeArrowKey(int key_code, WritingMode writing_mode) {
  LogicalToPhysical<int> key_map({writing_mode, TextDirection::kLtr},
                                 kVkeyBackwardChar, kVkeyForwardChar,
                                 kVkeyPreviousLine, kVkeyNextLine);
  switch (key_code) {
    case VKEY_LEFT:
      return key_map.Left();
    case VKEY_RIGHT:
      return key_map.Right();
    case VKEY_UP:
      return key_map.Top();
    case VKEY_DOWN:
      return key_map.Bottom();
  }
  return key_code;
}

}  // anonymous namespace

const char* EditingBehavior::InterpretKeyEvent(const KeyboardEvent& event,
                                               WritingMode writing_mode) const {
  const WebKeyboardEvent* key_event = event.KeyEvent();
  if (!key_event)
    return "";

  static HashMap<int, const char*>* key_down_commands_map = nullptr;
  static HashMap<int, const char*>* key_press_commands_map = nullptr;

  if (!key_down_commands_map) {
    key_down_commands_map = new HashMap<int, const char*>;
    key_press_commands_map = new HashMap<int, const char*>;

    for (const auto& entry : kKeyboardCodeKeyDownEntries) {
      key_down_commands_map->Set(entry.modifiers << 16 | entry.virtual_key,
                                 entry.name);
    }

    for (const auto& entry : kKeyboardCodeKeyPressEntries) {
      key_press_commands_map->Set(entry.modifiers << 16 | entry.char_code,
                                  entry.name);
    }
  }

  uint32_t modifiers =
      key_event->GetModifiers() & (kShiftKey | kAltKey | kCtrlKey | kMetaKey);

  auto FindName = [=](HashMap<int, const char*>* map, int code) -> const char* {
    int map_key = modifiers << 16 | code;
    if (!map_key)
      return nullptr;
    auto it = map->find(map_key);
    if (it == map->end())
      return nullptr;
    DCHECK(it->value);
    return it->value;
  };

  if (key_event->GetType() == WebInputEvent::Type::kRawKeyDown) {
    const char* name =
        FindName(key_down_commands_map,
                 TransposeArrowKey(event.keyCode(), writing_mode));
    return name ? name
                : LookupCommandNameFromDomKeyKeyDown(event.key(), modifiers);
  }
  return FindName(key_press_commands_map, event.charCode());
}

bool EditingBehavior::ShouldInsertCharacter(const KeyboardEvent& event) const {
  if (event.KeyEvent()->text[1] != 0)
    return true;

  // On Gtk/Linux, it emits key events with ASCII text and ctrl on for ctrl-<x>.
  // In Webkit, EditorClient::handleKeyboardEvent in
  // WebKit/gtk/WebCoreSupport/EditorClientGtk.cpp drop such events.
  // On Mac, it emits key events with ASCII text and meta on for Command-<x>.
  // These key events should not emit text insert event.
  // Alt key would be used to insert alternative character, so we should let
  // through. Also note that Ctrl-Alt combination equals to AltGr key which is
  // also used to insert alternative character.
  // http://code.google.com/p/chromium/issues/detail?id=10846
  // Windows sets both alt and meta are on when "Alt" key pressed.
  // http://code.google.com/p/chromium/issues/detail?id=2215
  // Also, we should not rely on an assumption that keyboards don't
  // send ASCII characters when pressing a control key on Windows,
  // which may be configured to do it so by user.
  // See also http://en.wikipedia.org/wiki/Keyboard_Layout
  // FIXME(ukai): investigate more detail for various keyboard layout.
  UChar ch = event.KeyEvent()->text[0U];

  // Don't insert null or control characters as they can result in
  // unexpected behaviour
  if (ch < ' ')
    return false;
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
  // According to XKB map no keyboard combinations with ctrl key are mapped to
  // printable characters, however we need the filter as the DomKey/text could
  // contain printable characters.
  if (event.ctrlKey())
    return false;
#elif !BUILDFLAG(IS_WIN)
  // Don't insert ASCII character if ctrl w/o alt or meta is on.
  // On Mac, we should ignore events when meta is on (Command-<x>).
  if (ch < 0x80) {
    if (event.ctrlKey() && !event.altKey())
      return false;
#if BUILDFLAG(IS_MAC)
    if (event.metaKey())
      return false;
#endif
  }
#endif

  return true;
}

}  // namespace blink
