// 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.keyboard;

import org.mozc.android.inputmethod.japanese.KeyboardSpecificationName;
import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable;
import org.mozc.android.inputmethod.japanese.resources.R;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;

import android.util.SparseIntArray;

import java.util.Collections;
import java.util.List;

/**
 * A simple model class of a keyboard.
 * A keyboard can contain a sequence of {@code Row}s.
 *
 */
public class Keyboard {

  /**
   * Each keyboard has its own specification.
   *
   * For example, some keyboards use a special Romanji table.
   */
  public static enum KeyboardSpecification {
    // 12 keys.
    TWELVE_KEY_TOGGLE_KANA(
        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_KANA", 0, 2, 0),
        R.xml.kbd_12keys_kana,
        false,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.TWELVE_KEYS_TO_HIRAGANA,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        true,
        CrossingEdgeBehavior.DO_NOTHING),

    TWELVE_KEY_TOGGLE_ALPHABET(
        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_ALPHABET", 0, 2, 0),
        R.xml.kbd_12keys_abc,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.TWELVE_KEYS_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    TWELVE_KEY_TOGGLE_QWERTY_ALPHABET(
        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_QWERTY_ALPHABET", 0, 5, 0),
        R.xml.kbd_qwerty_abc,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),

    // Flick mode.
    TWELVE_KEY_FLICK_KANA(
        new KeyboardSpecificationName("TWELVE_KEY_FLICK_KANA", 0, 2, 0),
        R.xml.kbd_12keys_flick_kana,
        false,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.FLICK_TO_HIRAGANA,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        true,
        CrossingEdgeBehavior.DO_NOTHING),

    TWELVE_KEY_FLICK_ALPHABET(
        new KeyboardSpecificationName("TWELVE_KEY_FLICK_ALPHABET", 0, 2, 0),
        R.xml.kbd_12keys_flick_abc,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.FLICK_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),

    TWELVE_KEY_TOGGLE_FLICK_KANA(
        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_KANA", 0, 2, 0),
        R.xml.kbd_12keys_flick_kana,
        false,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.TOGGLE_FLICK_TO_HIRAGANA,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        true,
        CrossingEdgeBehavior.DO_NOTHING),

    TWELVE_KEY_TOGGLE_FLICK_ALPHABET(
        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 2, 0),
        R.xml.kbd_12keys_flick_abc,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.TOGGLE_FLICK_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    // QWERTY keyboard.
    QWERTY_KANA(
        new KeyboardSpecificationName("QWERTY_KANA", 0, 4, 0),
        R.xml.kbd_qwerty_kana,
        false,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    QWERTY_ALPHABET(
        new KeyboardSpecificationName("QWERTY_ALPHABET", 0, 5, 0),
        R.xml.kbd_qwerty_abc,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),

    QWERTY_ALPHABET_NUMBER(
        new KeyboardSpecificationName("QWERTY_ALPHABET_NUMBER", 0, 3, 0),
        R.xml.kbd_qwerty_abc_123,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),

    // Godan keyboard.
    GODAN_KANA(
        new KeyboardSpecificationName("GODAN_KANA", 0, 2, 0),
        R.xml.kbd_godan_kana,
        false,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.GODAN_TO_HIRAGANA,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        true,
        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),

    NUMBER(
        new KeyboardSpecificationName("NUMBER", 0, 1, 0),
        R.xml.kbd_123,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    // Number keyboard on symbol input view.
    SYMBOL_NUMBER(
        new KeyboardSpecificationName("TWELVE_KEY_SYMBOL_NUMBER", 0, 1, 0),
        R.xml.kbd_symbol_123,
        false,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    // HARDWARE QWERTY keyboard.
    HARDWARE_QWERTY_KANA(
        new KeyboardSpecificationName("HARDWARE_QWERTY_KANA", 0, 1, 0),
        0,
        true,
        CompositionMode.HIRAGANA,
        SpecialRomanjiTable.DEFAULT_TABLE,
        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    HARDWARE_QWERTY_ALPHABET(
        new KeyboardSpecificationName("HARDWARE_QWERTY_ALPHABET", 0, 1, 0),
        0,
        true,
        CompositionMode.HALF_ASCII,
        SpecialRomanjiTable.DEFAULT_TABLE,
        SpaceOnAlphanumeric.COMMIT,
        false,
        CrossingEdgeBehavior.DO_NOTHING),

    ;

    private final KeyboardSpecificationName specName;
    private final int resourceId;
    private final boolean isHardwareKeyboard;
    private final CompositionMode compositionMode;
    private final SpecialRomanjiTable specialRomanjiTable;
    private final SpaceOnAlphanumeric spaceOnAlphanumeric;
    private final boolean kanaModifierInsensitiveConversion;
    private final CrossingEdgeBehavior crossingEdgeBehavior;

    private KeyboardSpecification(
        KeyboardSpecificationName specName,
        int resourceId,
        boolean isHardwareKeyboard,
        CompositionMode compositionMode,
        SpecialRomanjiTable specialRomanjiTable,
        SpaceOnAlphanumeric spaceOnAlphanumeric,
        boolean kanaModifierInsensitiveConversion,
        CrossingEdgeBehavior crossingEdgeBehavior) {
      this.specName = Preconditions.checkNotNull(specName);
      this.resourceId = resourceId;
      this.isHardwareKeyboard = isHardwareKeyboard;
      this.compositionMode = Preconditions.checkNotNull(compositionMode);
      this.specialRomanjiTable = Preconditions.checkNotNull(specialRomanjiTable);
      this.spaceOnAlphanumeric = Preconditions.checkNotNull(spaceOnAlphanumeric);
      this.kanaModifierInsensitiveConversion = kanaModifierInsensitiveConversion;
      this.crossingEdgeBehavior = Preconditions.checkNotNull(crossingEdgeBehavior);
    }

    public int getXmlLayoutResourceId() {
      return resourceId;
    }

    public CompositionMode getCompositionMode() {
      return compositionMode;
    }

    public KeyboardSpecificationName getKeyboardSpecificationName() {
      return specName;
    }

    public SpecialRomanjiTable getSpecialRomanjiTable() {
      return specialRomanjiTable;
    }

    public SpaceOnAlphanumeric getSpaceOnAlphanumeric() {
      return spaceOnAlphanumeric;
    }

    public boolean isKanaModifierInsensitiveConversion() {
      return kanaModifierInsensitiveConversion;
    }

    public CrossingEdgeBehavior getCrossingEdgeBehavior() {
      return crossingEdgeBehavior;
    }

    public boolean isHardwareKeyboard() {
      return isHardwareKeyboard;
    }
  }

  private final Optional<String> contentDescription;
  private final float flickThreshold;
  private final List<Row> rowList;

  public final int contentLeft;
  public final int contentRight;
  public final int contentTop;
  public final int contentBottom;
  protected final KeyboardSpecification specification;
  private Optional<SparseIntArray> sourceIdToKeyCode = Optional.absent();

  public Keyboard(Optional<String> contentDescription,
                  List<? extends Row> rowList, float flickThreshold,
                  KeyboardSpecification specification) {
    this.contentDescription = Preconditions.checkNotNull(contentDescription);
    this.flickThreshold = flickThreshold;
    this.rowList = Collections.unmodifiableList(rowList);

    int left = Integer.MAX_VALUE, right = Integer.MIN_VALUE,
        top = Integer.MAX_VALUE, bottom = Integer.MIN_VALUE;
    for (Row row : this.rowList) {
      for (Key key : row.getKeyList()) {
        left = Math.min(left, key.getX());
        right = Math.max(right, key.getX() + key.getWidth());
        top = Math.min(top, key.getY());
        bottom = Math.max(bottom, key.getY() + key.getHeight());
      }
    }
    this.contentLeft = left;
    this.contentRight = right;
    this.contentTop = top;
    this.contentBottom = bottom;
    this.specification = Preconditions.checkNotNull(specification);
  }

  public Optional<String> getContentDescription() {
    return contentDescription;
  }

  public float getFlickThreshold() {
    return flickThreshold;
  }

  public List<Row> getRowList() {
    return rowList;
  }

  public KeyboardSpecification getSpecification() {
    return specification;
  }

  /**
   * Returns keyCode from {@code souceId}.
   *
   * <p>If not found, {@code Integer.MIN_VALUE} is returned.
   */
  public int getKeyCode(int sourceId) {
    ensureSourceIdToKeyCode();
    return sourceIdToKeyCode.get().get(sourceId, Integer.MIN_VALUE);
  }

  private void ensureSourceIdToKeyCode() {
    if (sourceIdToKeyCode.isPresent()) {
      return;
    }
    SparseIntArray result = new SparseIntArray();
    for (Row row : getRowList()) {
      for (Key key : row.getKeyList()) {
        for (KeyState keyState : key.getKeyStates()) {
          for (Direction direction : Direction.values()) {
            Optional<Flick> flick = keyState.getFlick(direction);
            if (flick.isPresent()) {
              KeyEntity keyEntity = flick.get().getKeyEntity();
              result.put(keyEntity.getSourceId(), keyEntity.getKeyCode());
            }
          }
        }
      }
    }
    sourceIdToKeyCode = Optional.of(result);
  }
}
