content: /*
   * Copyright (C) 2011 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.amnesica.kryptey.inputmethod.keyboard;
  
  import android.content.Context;
  import android.graphics.Paint;
  import android.util.Log;
  
  import com.amnesica.kryptey.inputmethod.R;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardBuilder;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.MoreKeySpec;
  import com.amnesica.kryptey.inputmethod.latin.common.StringUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.TypefaceUtils;
  
  public final class MoreKeysKeyboard extends Keyboard {
    private static final String TAG = MoreKeysKeyboard.class.getSimpleName();
    private final int mDefaultKeyCoordX;
    private static final float FLOAT_THRESHOLD = 0.0001f;
  
    MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
      super(params);
      mDefaultKeyCoordX = Math.round(params.getDefaultKeyCoordX() + params.mOffsetX
          + (params.mDefaultKeyPaddedWidth - params.mHorizontalGap) / 2);
    }
  
    public int getDefaultCoordX() {
      return mDefaultKeyCoordX;
    }
  
    static class MoreKeysKeyboardParams extends KeyboardParams {
      public boolean mIsMoreKeysFixedOrder;
      /* package */ int mTopRowAdjustment;
      public int mNumRows;
      public int mNumColumns;
      public int mTopKeys;
      public int mLeftKeys;
      public int mRightKeys; // includes default key.
      public float mColumnWidth;
      public float mOffsetX;
  
      public MoreKeysKeyboardParams() {
        super();
      }
  
      /**
       * Set keyboard parameters of more keys keyboard.
       *
       * @param numKeys               number of keys in this more keys keyboard.
       * @param numColumn             number of columns of this more keys keyboard.
       * @param keyPaddedWidth        more keys keyboard key width in pixel, including horizontal gap.
       * @param rowHeight             more keys keyboard row height in pixel, including vertical gap.
       * @param coordXInParent        coordinate x of the key preview in parent keyboard.
       * @param parentKeyboardWidth   parent keyboard width in pixel.
       * @param isMoreKeysFixedColumn true if more keys keyboard should have
       *                              <code>numColumn</code> columns. Otherwise more keys keyboard should have
       *                              <code>numColumn</code> columns at most.
       * @param isMoreKeysFixedOrder  true if the order of more keys is determined by the order in
       *                              the more keys' specification. Otherwise the order of more keys is automatically
       *                              determined.
       */
      public void setParameters(final int numKeys, final int numColumn,
                                final float keyPaddedWidth, final float rowHeight,
                                final float coordXInParent, final int parentKeyboardWidth,
                                final boolean isMoreKeysFixedColumn,
                                final boolean isMoreKeysFixedOrder) {
        // Add the horizontal padding because there is no horizontal gap on the outside edge,
        // but it is included in the key width, so this compensates for simple division and
        // comparison.
        final float availableWidth = parentKeyboardWidth - mLeftPadding - mRightPadding
            + mHorizontalGap;
        if (availableWidth < keyPaddedWidth) {
          throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
              + availableWidth + " " + keyPaddedWidth);
        }
        mIsMoreKeysFixedOrder = isMoreKeysFixedOrder;
        mDefaultKeyPaddedWidth = keyPaddedWidth;
        mDefaultRowHeight = rowHeight;
  
        final int maxColumns = getMaxKeys(availableWidth, keyPaddedWidth);
        if (isMoreKeysFixedColumn) {
          int requestedNumColumns = Math.min(numKeys, numColumn);
          if (maxColumns < requestedNumColumns) {
            Log.e(TAG, "Keyboard is too small to hold the requested more keys columns: "
                + availableWidth + " " + keyPaddedWidth + " " + numKeys + " "
                + requestedNumColumns + ". The number of columns was reduced.");
            mNumColumns = maxColumns;
          } else {
            mNumColumns = requestedNumColumns;
          }
          mNumRows = getNumRows(numKeys, mNumColumns);
        } else {
          int defaultNumColumns = Math.min(maxColumns, numColumn);
          mNumRows = getNumRows(numKeys, defaultNumColumns);
          mNumColumns = getOptimizedColumns(numKeys, defaultNumColumns, mNumRows);
        }
        final int topKeys = numKeys % mNumColumns;
        mTopKeys = topKeys == 0 ? mNumColumns : topKeys;
  
        final int numLeftKeys = (mNumColumns - 1) / 2;
        final int numRightKeys = mNumColumns - numLeftKeys; // including default key.
        // Determine the maximum number of keys we can lay out on both side of the left edge of
        // a key centered on the parent key. Also, account for horizontal padding because there
        // is no horizontal gap on the outside edge.
        final float leftWidth = Math.max(coordXInParent - mLeftPadding - keyPaddedWidth / 2
            + mHorizontalGap / 2, 0);
        final float rightWidth = Math.max(parentKeyboardWidth - coordXInParent
            + keyPaddedWidth / 2 - mRightPadding + mHorizontalGap / 2, 0);
        int maxLeftKeys = getMaxKeys(leftWidth, keyPaddedWidth);
        int maxRightKeys = getMaxKeys(rightWidth, keyPaddedWidth);
        // Handle the case where the number of columns fits but doesn't have enough room
        // for the default key to be centered on the parent key.
        if (numKeys >= mNumColumns && mNumColumns == maxColumns
            && maxLeftKeys + maxRightKeys < maxColumns) {
          final float extraLeft = leftWidth - maxLeftKeys * keyPaddedWidth;
          final float extraRight = rightWidth - maxRightKeys * keyPaddedWidth;
          // Put the extra key on whatever side has more space
          if (extraLeft > extraRight) {
            maxLeftKeys++;
          } else {
            maxRightKeys++;
          }
        }
  
        int leftKeys, rightKeys;
        if (numLeftKeys > maxLeftKeys) {
          leftKeys = maxLeftKeys;
          rightKeys = mNumColumns - leftKeys;
        } else if (numRightKeys > maxRightKeys) {
          // Make sure the default key is included even if it doesn't exactly fit (the default
          // key just won't be completely centered on the parent key)
          rightKeys = Math.max(maxRightKeys, 1);
          leftKeys = mNumColumns - rightKeys;
        } else {
          leftKeys = numLeftKeys;
          rightKeys = numRightKeys;
        }
        mLeftKeys = leftKeys;
        mRightKeys = rightKeys;
  
        // Adjustment of the top row.
        mTopRowAdjustment = getTopRowAdjustment();
        mColumnWidth = mDefaultKeyPaddedWidth;
        mBaseWidth = mNumColumns * mColumnWidth;
        // Need to subtract the right most column's gutter only.
        mOccupiedWidth = Math.round(mBaseWidth + mLeftPadding + mRightPadding - mHorizontalGap);
        mBaseHeight = mNumRows * mDefaultRowHeight;
        // Need to subtract the bottom row's gutter only.
        mOccupiedHeight = Math.round(mBaseHeight + mTopPadding + mBottomPadding - mVerticalGap);
  
        // The proximity grid size can be reduced because the more keys keyboard is probably
        // smaller and doesn't need extra precision from smaller cells.
        mGridWidth = Math.min(mGridWidth, mNumColumns);
        mGridHeight = Math.min(mGridHeight, mNumRows);
      }
  
      private int getTopRowAdjustment() {
        final int numOffCenterKeys = Math.abs(mRightKeys - 1 - mLeftKeys);
        // Don't center if there are more keys in the top row than can be centered around the
        // default more key or if there is an odd number of keys in the top row (already will
        // be centered).
        if (mTopKeys > mNumColumns - numOffCenterKeys || mTopKeys % 2 == 1) {
          return 0;
        }
        return -1;
      }
  
      // Return key position according to column count (0 is default).
      /* package */int getColumnPos(final int n) {
        return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
      }
  
      private int getFixedOrderColumnPos(final int n) {
        final int col = n % mNumColumns;
        final int row = n / mNumColumns;
        if (!isTopRow(row)) {
          return col - mLeftKeys;
        }
        final int rightSideKeys = mTopKeys / 2;
        final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
        final int pos = col - leftSideKeys;
        final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
        final int numRightKeys = mRightKeys - 1;
        if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
          return pos;
        } else if (numRightKeys < rightSideKeys) {
          return pos - (rightSideKeys - numRightKeys);
        } else { // numLeftKeys < leftSideKeys
          return pos + (leftSideKeys - numLeftKeys);
        }
      }
  
      private int getAutomaticColumnPos(final int n) {
        final int col = n % mNumColumns;
        final int row = n / mNumColumns;
        int leftKeys = mLeftKeys;
        if (isTopRow(row)) {
          leftKeys += mTopRowAdjustment;
        }
        if (col == 0) {
          // default position.
          return 0;
        }
  
        int pos = 0;
        int right = 1; // include default position key.
        int left = 0;
        int i = 0;
        while (true) {
          // Assign right key if available.
          if (right < mRightKeys) {
            pos = right;
            right++;
            i++;
          }
          if (i >= col)
            break;
          // Assign left key if available.
          if (left < leftKeys) {
            left++;
            pos = -left;
            i++;
          }
          if (i >= col)
            break;
        }
        return pos;
      }
  
      private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
        final int remainings = numKeys % numColumns;
        return remainings == 0 ? 0 : numColumns - remainings;
      }
  
      private static int getOptimizedColumns(final int numKeys, final int maxColumns,
                                             final int numRows) {
        int numColumns = Math.min(numKeys, maxColumns);
        while (getTopRowEmptySlots(numKeys, numColumns) >= numRows) {
          numColumns--;
        }
        return numColumns;
      }
  
      private static int getNumRows(final int numKeys, final int numColumn) {
        return (numKeys + numColumn - 1) / numColumn;
      }
  
      private static int getMaxKeys(final float keyboardWidth, final float keyPaddedWidth) {
        // This is effectively the same as returning (int)(keyboardWidth / keyPaddedWidth)
        // except this handles floating point errors better since rounding in the wrong
        // directing here doesn't cause an issue, but truncating incorrectly from an error
        // could be a problem (eg: the keyboard width is an exact multiple of the key width
        // could return one less than the expected number).
        final int maxKeys = Math.round(keyboardWidth / keyPaddedWidth);
        if (maxKeys * keyPaddedWidth > keyboardWidth + FLOAT_THRESHOLD) {
          return maxKeys - 1;
        }
        return maxKeys;
      }
  
      public float getDefaultKeyCoordX() {
        return mLeftKeys * mColumnWidth + mLeftPadding;
      }
  
      public float getX(final int n, final int row) {
        final float x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
        if (isTopRow(row)) {
          return x + mTopRowAdjustment * (mColumnWidth / 2);
        }
        return x;
      }
  
      public float getY(final int row) {
        return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
      }
  
      private boolean isTopRow(final int rowCount) {
        return mNumRows > 1 && rowCount == mNumRows - 1;
      }
    }
  
    public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
      private final Key mParentKey;
  
      private static final float LABEL_PADDING_RATIO = 0.2f;
  
      /**
       * The builder of MoreKeysKeyboard.
       *
       * @param context                    the context of {@link MoreKeysKeyboardView}.
       * @param key                        the {@link Key} that invokes more keys keyboard.
       * @param keyboard                   the {@link Keyboard} that contains the parentKey.
       * @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single
       *                                   "more key" and its key popup preview is enabled.
       * @param keyPreviewVisibleWidth     the width of visible part of key popup preview.
       * @param keyPreviewVisibleHeight    the height of visible part of key popup preview
       * @param paintToMeasure             the {@link Paint} object to measure a "more key" width
       */
      public Builder(final Context context, final Key key, final Keyboard keyboard,
                     final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
                     final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
        super(context, new MoreKeysKeyboardParams());
        load(keyboard.mMoreKeysTemplate, keyboard.mId);
  
        // TODO: More keys keyboard's vertical gap is currently calculated heuristically.
        // Should revise the algorithm.
        mParams.mVerticalGap = keyboard.mVerticalGap / 2;
        // This {@link MoreKeysKeyboard} is invoked from the <code>key</code>.
        mParentKey = key;
  
        final float keyPaddedWidth, rowHeight;
        if (isSingleMoreKeyWithPreview) {
          // Use pre-computed width and height if this more keys keyboard has only one key to
          // mitigate visual flicker between key preview and more keys keyboard.
          // The bottom paddings don't need to be considered because the vertical positions
          // of both backgrounds and the keyboard were already adjusted with their bottom
          // paddings deducted. The keyboard's left/right/top paddings do need to be deducted
          // so the key including the paddings matches the key preview.
          final float keyboardHorizontalPadding = mParams.mLeftPadding
              + mParams.mRightPadding;
          final float baseKeyPaddedWidth = keyPreviewVisibleWidth + mParams.mHorizontalGap;
          if (keyboardHorizontalPadding > baseKeyPaddedWidth - FLOAT_THRESHOLD) {
            // If the padding doesn't fit we'll just add it outside of the key preview.
            keyPaddedWidth = baseKeyPaddedWidth;
          } else {
            keyPaddedWidth = baseKeyPaddedWidth - keyboardHorizontalPadding;
            // Keep the more keys keyboard with uneven padding lined up with the key
            // preview rather than centering the more keys keyboard's key with the parent
            // key.
            mParams.mOffsetX = (mParams.mRightPadding - mParams.mLeftPadding) / 2;
          }
          final float baseKeyPaddedHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
          if (mParams.mTopPadding > baseKeyPaddedHeight - FLOAT_THRESHOLD) {
            // If the padding doesn't fit we'll just add it outside of the key preview.
            rowHeight = baseKeyPaddedHeight;
          } else {
            rowHeight = baseKeyPaddedHeight - mParams.mTopPadding;
          }
        } else {
          final float defaultKeyWidth = mParams.mDefaultKeyPaddedWidth
              - mParams.mHorizontalGap;
          final float padding = context.getResources().getDimension(
              R.dimen.config_more_keys_keyboard_key_horizontal_padding)
              + (key.hasLabelsInMoreKeys()
              ? defaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
          keyPaddedWidth = getMaxKeyWidth(key, defaultKeyWidth, padding, paintToMeasure)
              + mParams.mHorizontalGap;
          rowHeight = keyboard.mMostCommonKeyHeight + keyboard.mVerticalGap;
        }
        final MoreKeySpec[] moreKeys = key.getMoreKeys();
        mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyPaddedWidth,
            rowHeight, key.getX() + key.getWidth() / 2f, keyboard.mId.mWidth,
            key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder());
      }
  
      private static float getMaxKeyWidth(final Key parentKey, final float minKeyWidth,
                                          final float padding, final Paint paint) {
        float maxWidth = minKeyWidth;
        for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
          final String label = spec.mLabel;
          // If the label is single letter, minKeyWidth is enough to hold the label.
          if (label != null && StringUtils.codePointCount(label) > 1) {
            maxWidth = Math.max(maxWidth,
                TypefaceUtils.getStringWidth(label, paint) + padding);
          }
        }
        return maxWidth;
      }
  
      @Override
      public MoreKeysKeyboard build() {
        final MoreKeysKeyboardParams params = mParams;
        final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
        final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
        for (int n = 0; n < moreKeys.length; n++) {
          final MoreKeySpec moreKeySpec = moreKeys[n];
          final int row = n / params.mNumColumns;
          final float width = params.mDefaultKeyPaddedWidth - params.mHorizontalGap;
          final float height = params.mDefaultRowHeight - params.mVerticalGap;
          final float keyLeftEdge = params.getX(n, row);
          final float keyTopEdge = params.getY(row);
          final float keyRightEdge = keyLeftEdge + width;
          final float keyBottomEdge = keyTopEdge + height;
  
          final float keyboardLeftEdge = params.mLeftPadding;
          final float keyboardRightEdge = params.mOccupiedWidth - params.mRightPadding;
          final float keyboardTopEdge = params.mTopPadding;
          final float keyboardBottomEdge = params.mOccupiedHeight - params.mBottomPadding;
  
          final float keyLeftPadding = keyLeftEdge < keyboardLeftEdge + FLOAT_THRESHOLD
              ? params.mLeftPadding : params.mHorizontalGap / 2;
          final float keyRightPadding = keyRightEdge > keyboardRightEdge - FLOAT_THRESHOLD
              ? params.mRightPadding : params.mHorizontalGap / 2;
          final float keyTopPadding = keyTopEdge < keyboardTopEdge + FLOAT_THRESHOLD
              ? params.mTopPadding : params.mVerticalGap / 2;
          final float keyBottomPadding = keyBottomEdge > keyboardBottomEdge - FLOAT_THRESHOLD
              ? params.mBottomPadding : params.mVerticalGap / 2;
  
          final Key key = moreKeySpec.buildKey(keyLeftEdge, keyTopEdge, width, height,
              keyLeftPadding, keyRightPadding, keyTopPadding, keyBottomPadding,
              moreKeyFlags);
          params.onAddKey(key);
        }
        return new MoreKeysKeyboard(params);
      }
    }
  }
  