// Copyright 2010-2016, 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:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * 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.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "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 THE COPYRIGHT
// OWNER 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.

package org.mozc.android.inputmethod.japanese.hardwarekeyboard;

import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
import org.mozc.android.inputmethod.japanese.MozcLog;
import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
import org.mozc.android.inputmethod.japanese.hardwarekeyboard.KeyEventMapperFactory.KeyEventMapper;
import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap;
import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ModifierKey;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.SpecialKey;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;

import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.view.KeyEvent;

import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Converts Hardware KeyEvent to Mozc's KeyEvent
 *
 * Android OS's keyboard management is like following;
 * <ol>
 * <li>Android OS receives LKC (Linux Key Code, ≒ scan code) from the keyboard.
 * <li>Android OS converts LKC into AKC (Android Key Code) using key layout file (.kl).
 * A user/developer cannot override this behavior.
 * <li>Android OS converts AKC into a tuple of (AKC, unicode character)
 * using key character mapping file (.kcm).
 * <ul>
 * <li>Given AKC might be converted into another AKC (e.g. KEYCODE_GRAVE might be converted
 * into KEYCODE_ZENKAKU_HANKAKU).
 * <li>Since API Level 16 a user/developer has been able to override the behavior.
 * </ul>
 * </ol>
 * To use Japanese keyboard, the default behavior is not sufficient.
 * <ul>
 * <li>Even the latest OS (API Level 19) Japanese key character mapping is not shipped by default.
 * <li>There are Japanese unique AKC (e.g. KEYCODE_ZENKAKU_HANKAKU) but they are available
 * since API Level 16. (Note since API Level 16 some Japanese unique ones (e.g. KEYCODE_RO)
 * can be sent though Japanese KCM is not supported)
 * </ul>
 * The main responsibility of this class is to convert android's KeyEvent into Mozc's KeyEvent.
 * To do this above behavior must be respected. CompactKeyEvent (and its internal KeyEventMapper)
 * does this.
 *
 */
public enum HardwareKeyboardSpecification {

  /**
   * System key map.
   */
  DEFAULT(HardwareKeyMap.DEFAULT,
          KeyEventMapperFactory.DEFAULT_KEYBOARD_MAPPER,
          KeyboardSpecification.HARDWARE_QWERTY_KANA,
          KeyboardSpecification.HARDWARE_QWERTY_ALPHABET),

  /**
   * Represents Japanese 109 Keyboard
   */
  JAPANESE109A(HardwareKeyMap.JAPANESE109A,
               KeyEventMapperFactory.JAPANESE_KEYBOARD_MAPPER,
               KeyboardSpecification.HARDWARE_QWERTY_KANA,
               KeyboardSpecification.HARDWARE_QWERTY_ALPHABET);

  /**
   * Returns true if the given {@code codepoint} is printable.
   *
   * {@link KeyEvent#isPrintingKey()} cannot be used for this purpose as
   * the method doesn't take codepoint but keycode.
   */
  @VisibleForTesting
  static boolean isPrintable(int codepoint) {
    Preconditions.checkArgument(codepoint >= 0);
    if (Character.isISOControl(codepoint)) {
      return false;
    }
    Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
    return block != null &&
           block != Character.UnicodeBlock.SPECIALS;
  }

  /**
   * Returns true if composition mode should be changed.
   */
  @SuppressLint("InlinedApi")
  private static boolean isKeyForCompositinoModeChange(int keyCode, int metaState) {
    boolean shift = (metaState & KeyEvent.META_SHIFT_MASK) != 0;
    boolean alt = (metaState & KeyEvent.META_ALT_MASK) != 0;
    boolean ctrl = (metaState & KeyEvent.META_CTRL_MASK) != 0;
    boolean meta = (metaState & KeyEvent.META_META_MASK) != 0;
    // ZEN/HAN with no modifier.
    if (keyCode == KeyEvent.KEYCODE_ZENKAKU_HANKAKU && !shift && !alt && !ctrl && !meta) {
      return true;
    }
    // GRAVE with ALT.
    if (keyCode == KeyEvent.KEYCODE_GRAVE && !shift && alt && !ctrl && !meta) {
      return true;
    }
    return false;
  }

  private final HardwareKeyMap hardwareKeyMap;

  private final KeyboardSpecification kanaKeyboardSpecification;

  private final KeyboardSpecification alphabetKeyboardSpecification;

  private final KeyEventMapper keyEventMapper;

  private static final Map<HardwareKeyMap, HardwareKeyboardSpecification>
      hardwareKeyMapToSpecificationMap;

  static {
    // Initialize preference name to HardwareKeyboardSpecification mapping.
    Map<HardwareKeyMap, HardwareKeyboardSpecification> tmpMap =
        new EnumMap<HardwareKeyMap, HardwareKeyboardSpecification>(HardwareKeyMap.class);
    for (HardwareKeyboardSpecification hardwareKeyboardSpecification :
        HardwareKeyboardSpecification.values()) {
      tmpMap.put(hardwareKeyboardSpecification.hardwareKeyMap, hardwareKeyboardSpecification);
    }
    hardwareKeyMapToSpecificationMap = Collections.unmodifiableMap(tmpMap);
  }

  private HardwareKeyboardSpecification(
      HardwareKeyMap hardwareKeyMap,
      KeyEventMapper keyEventMapper,
      KeyboardSpecification kanaKeyboardSpecification,
      KeyboardSpecification alphabetKeyboardSpecification) {
    this.hardwareKeyMap = hardwareKeyMap;
    this.keyEventMapper = keyEventMapper;
    this.kanaKeyboardSpecification = kanaKeyboardSpecification;
    this.alphabetKeyboardSpecification = alphabetKeyboardSpecification;
  }

  /**
   * Returns HardwareKeyboadSpecification based on given preference.
   */
  static Optional<HardwareKeyboardSpecification> getHardwareKeyboardSpecification(
     Optional<HardwareKeyMap> hardwareKeyMap) {
    return hardwareKeyMap.isPresent()
        ? Optional.of(hardwareKeyMapToSpecificationMap.get(hardwareKeyMap.get()))
        : Optional.<HardwareKeyboardSpecification>absent();
  }

  /**
   * Detects hardware keyboard type and sets it to the given {@code sharedPreferences}.
   * If the {@code sharedPreferences} is {@code null}, or already has valid hardware keyboard type,
   * just does nothing.
   *
   * Note: if the new detected hardware keyboard type is set to the {@code sharedPreferences}
   * and if it has registered callbacks, of course, they will be invoked as usual.
   */
  public static void maybeSetDetectedHardwareKeyMap(
      @Nullable SharedPreferences sharedPreferences, Configuration configuration,
      boolean forceToSet) {
    Preconditions.checkNotNull(configuration);
    if (sharedPreferences == null) {
      return;
    }

    // First, check if the hardware keyboard type has already set to the preference.
    if (getHardwareKeyMap(sharedPreferences) != null) {
      // Found the valid value.
      return;
    }

    // Here, the HardwareKeyMap hasn't set yet, so detect hardware keyboard type.
    HardwareKeyMap detectedKeyMap = null;
    switch(configuration.keyboard) {
      case Configuration.KEYBOARD_12KEY:
      case Configuration.KEYBOARD_QWERTY:
        detectedKeyMap = HardwareKeyMap.DEFAULT;
        break;
      case Configuration.KEYBOARD_NOKEYS:
      case Configuration.KEYBOARD_UNDEFINED:
      default:
        break;
    }

    if (detectedKeyMap != null) {
      setHardwareKeyMap(sharedPreferences, detectedKeyMap);
    } else if (forceToSet) {
      setHardwareKeyMap(sharedPreferences, HardwareKeyMap.DEFAULT);
    }

    MozcLog.i("RUN HARDWARE KEYBOARD DETECTION: " + detectedKeyMap);
  }

  /**
   * Returns currently set key map based on the default SharedPreferences.
   *
   * @param sharedPreferences a SharedPreference to be retrieved
   * @return null if no preference has not been set
   */
  public static HardwareKeyMap getHardwareKeyMap(SharedPreferences sharedPreferences) {
    String prefHardwareKeyMap =
        Preconditions.checkNotNull(sharedPreferences)
            .getString(PreferenceUtil.PREF_HARDWARE_KEYMAP, null);
    if (prefHardwareKeyMap != null) {
      try {
        return HardwareKeyMap.valueOf(prefHardwareKeyMap);
      } catch (IllegalArgumentException e) {
        return null;
      }
    }
    return null;
  }

  @VisibleForTesting
  static void setHardwareKeyMap(
      SharedPreferences sharedPreference, HardwareKeyMap hardwareKeyMap) {
    Preconditions.checkNotNull(sharedPreference);
    Preconditions.checkNotNull(hardwareKeyMap);
    sharedPreference.edit()
         .putString(PreferenceUtil.PREF_HARDWARE_KEYMAP, hardwareKeyMap.name()).commit();
  }

  public HardwareKeyMap getHardwareKeyMap() {
    return hardwareKeyMap;
  }

  public ProtoCommands.KeyEvent getMozcKeyEvent(android.view.KeyEvent keyEvent) {
    Preconditions.checkNotNull(keyEvent);
    CompactKeyEvent compactKeyEvent = new CompactKeyEvent(keyEvent, keyEventMapper);

    // Check if the key is for changing composition mode.
    // The event should be consumed client-side so nothing should be returned.
    if (isKeyForCompositinoModeChange(compactKeyEvent.getKeyCode(),
                                      compactKeyEvent.getMetaState())) {
      return null;
    }

    int keyCode = compactKeyEvent.getKeyCode();
    ProtoCommands.KeyEvent.Builder builder = ProtoCommands.KeyEvent.newBuilder();
    // Check if the event is special key.
    // This check should be done in advance of checking unicode-character (done in default block)
    // because KEYCODE_SPACE/TAB, which are special keys, are sent with unicode-character.
    switch(keyCode) {
      case android.view.KeyEvent.KEYCODE_SPACE:
        builder.setSpecialKey(SpecialKey.SPACE);
        break;
      case android.view.KeyEvent.KEYCODE_FORWARD_DEL:
        builder.setSpecialKey(SpecialKey.DEL);
        break;
      case android.view.KeyEvent.KEYCODE_DEL:
        builder.setSpecialKey(SpecialKey.BACKSPACE);
        break;
      case android.view.KeyEvent.KEYCODE_INSERT:
        builder.setSpecialKey(SpecialKey.INSERT);
        break;
      case android.view.KeyEvent.KEYCODE_HENKAN:
        builder.setSpecialKey(SpecialKey.HENKAN);
        break;
      case android.view.KeyEvent.KEYCODE_MUHENKAN:
        builder.setSpecialKey(SpecialKey.MUHENKAN);
        break;
      case android.view.KeyEvent.KEYCODE_KANA:
        builder.setSpecialKey(SpecialKey.KANA);
        break;
      case android.view.KeyEvent.KEYCODE_KATAKANA_HIRAGANA:
        builder.setSpecialKey(SpecialKey.KATAKANA);
        break;
      case android.view.KeyEvent.KEYCODE_TAB:
        builder.setSpecialKey(SpecialKey.TAB);
        break;
      case android.view.KeyEvent.KEYCODE_ENTER:
        builder.setSpecialKey(SpecialKey.ENTER);
        break;
      case android.view.KeyEvent.KEYCODE_DPAD_LEFT:
        builder.setSpecialKey(SpecialKey.LEFT);
        break;
      case android.view.KeyEvent.KEYCODE_DPAD_RIGHT:
        builder.setSpecialKey(SpecialKey.RIGHT);
        break;
      case android.view.KeyEvent.KEYCODE_DPAD_UP:
        builder.setSpecialKey(SpecialKey.UP);
        break;
      case android.view.KeyEvent.KEYCODE_DPAD_DOWN:
        builder.setSpecialKey(SpecialKey.DOWN);
        break;
      case android.view.KeyEvent.KEYCODE_ESCAPE:
        builder.setSpecialKey(SpecialKey.ESCAPE);
        break;
      case android.view.KeyEvent.KEYCODE_MOVE_HOME:
        builder.setSpecialKey(SpecialKey.HOME);
        break;
      case android.view.KeyEvent.KEYCODE_MOVE_END:
        builder.setSpecialKey(SpecialKey.END);
        break;
      case android.view.KeyEvent.KEYCODE_PAGE_UP:
        builder.setSpecialKey(SpecialKey.PAGE_UP);
        break;
      case android.view.KeyEvent.KEYCODE_PAGE_DOWN:
        builder.setSpecialKey(SpecialKey.PAGE_DOWN);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_DIVIDE:
        builder.setSpecialKey(SpecialKey.DIVIDE);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
        builder.setSpecialKey(SpecialKey.MULTIPLY);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
        builder.setSpecialKey(SpecialKey.SUBTRACT);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_ADD:
        builder.setSpecialKey(SpecialKey.ADD);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_ENTER:
        builder.setSpecialKey(SpecialKey.SEPARATOR);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_DOT:
        builder.setSpecialKey(SpecialKey.DECIMAL);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_0:
        builder.setSpecialKey(SpecialKey.NUMPAD0);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_1:
        builder.setSpecialKey(SpecialKey.NUMPAD1);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_2:
        builder.setSpecialKey(SpecialKey.NUMPAD2);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_3:
        builder.setSpecialKey(SpecialKey.NUMPAD3);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_4:
        builder.setSpecialKey(SpecialKey.NUMPAD4);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_5:
        builder.setSpecialKey(SpecialKey.NUMPAD5);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_6:
        builder.setSpecialKey(SpecialKey.NUMPAD6);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_7:
        builder.setSpecialKey(SpecialKey.NUMPAD7);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_8:
        builder.setSpecialKey(SpecialKey.NUMPAD8);
        break;
      case android.view.KeyEvent.KEYCODE_NUMPAD_9:
        builder.setSpecialKey(SpecialKey.NUMPAD9);
        break;
      case android.view.KeyEvent.KEYCODE_F1:
        builder.setSpecialKey(SpecialKey.F1);
        break;
      case android.view.KeyEvent.KEYCODE_F2:
        builder.setSpecialKey(SpecialKey.F2);
        break;
      case android.view.KeyEvent.KEYCODE_F3:
        builder.setSpecialKey(SpecialKey.F3);
        break;
      case android.view.KeyEvent.KEYCODE_F4:
        builder.setSpecialKey(SpecialKey.F4);
        break;
      case android.view.KeyEvent.KEYCODE_F5:
        builder.setSpecialKey(SpecialKey.F5);
        break;
      case android.view.KeyEvent.KEYCODE_F6:
        builder.setSpecialKey(SpecialKey.F6);
        break;
      case android.view.KeyEvent.KEYCODE_F7:
        builder.setSpecialKey(SpecialKey.F7);
        break;
      case android.view.KeyEvent.KEYCODE_F8:
        builder.setSpecialKey(SpecialKey.F8);
        break;
      case android.view.KeyEvent.KEYCODE_F9:
        builder.setSpecialKey(SpecialKey.F9);
        break;
      case android.view.KeyEvent.KEYCODE_F10:
        builder.setSpecialKey(SpecialKey.F10);
        break;
      case android.view.KeyEvent.KEYCODE_F11:
        builder.setSpecialKey(SpecialKey.F11);
        break;
      case android.view.KeyEvent.KEYCODE_F12:
        builder.setSpecialKey(SpecialKey.F12);
        break;
      default: {
        // Reach here as the key code is not special one.
        // There are some cases.
        // 1. The event has unicode-character. Simple key event should be sent to the engine.
        //    Note that in this case no modifiers should be sent.
        // 2. No unicode-character.
        //   2-1. Printable character with modifier (except for shift key).
        //   2-2. Ignoreable key event.
        int unicodeChar = compactKeyEvent.getUnicodeCharacter();
        if (isPrintable(unicodeChar)) {
          // Case 1.
          return builder.setKeyCode(unicodeChar).build();
        }
        if (keyCode >= android.view.KeyEvent.KEYCODE_A
            && keyCode <= android.view.KeyEvent.KEYCODE_Z) {
          // Case 2-1.
          builder.setKeyCode(keyCode - android.view.KeyEvent.KEYCODE_A + 'a');
        } else if (keyCode >= android.view.KeyEvent.KEYCODE_0
            && keyCode <= android.view.KeyEvent.KEYCODE_9) {
          // Case 2-1.
          builder.setKeyCode(keyCode - android.view.KeyEvent.KEYCODE_0 + '0');
        } else {
          // Case 2-2.
          return null;
        }
      }
    }
    // Reach here because the key event is special one (including printable + modifier).
    int metaState = compactKeyEvent.getMetaState();
    if ((metaState & android.view.KeyEvent.META_SHIFT_MASK) != 0) {
        builder.addModifierKeys(ModifierKey.SHIFT);
    }
    if ((metaState & android.view.KeyEvent.META_ALT_MASK) != 0) {
      builder.addModifierKeys(ModifierKey.ALT);
    }
    if ((metaState & android.view.KeyEvent.META_CTRL_MASK) != 0) {
      builder.addModifierKeys(ModifierKey.CTRL);
    }
    return builder.build();
  }

  public KeyEventInterface getKeyEventInterface(final android.view.KeyEvent keyEvent) {
    Preconditions.checkNotNull(keyEvent);
    final CompactKeyEvent compactKeyEvent = new CompactKeyEvent(keyEvent, keyEventMapper);
    return new KeyEventInterface() {

      @Override
      public int getKeyCode() {
        return compactKeyEvent.getKeyCode();
      }

      @Override
      public Optional<android.view.KeyEvent> getNativeEvent() {
        return Optional.of(keyEvent);
      }
    };
  }

  public Optional<CompositionSwitchMode> getCompositionSwitchMode(
      android.view.KeyEvent keyEvent) {
    CompactKeyEvent compactKeyEvent
        = new CompactKeyEvent(Preconditions.checkNotNull(keyEvent), keyEventMapper);
    return isKeyForCompositinoModeChange(compactKeyEvent.getKeyCode(),
                                         compactKeyEvent.getMetaState())
        ? Optional.of(CompositionSwitchMode.TOGGLE)
        : Optional.<CompositionSwitchMode>absent();
  }

  public KeyboardSpecification getKanaKeyboardSpecification() {
    return kanaKeyboardSpecification;
  }

  public KeyboardSpecification getAlphabetKeyboardSpecification() {
    return alphabetKeyboardSpecification;
  }
}
