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

import org.mozc.android.inputmethod.japanese.accessibility.AccessibilityUtil;
import org.mozc.android.inputmethod.japanese.accessibility.CandidateWindowAccessibilityDelegate;
import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory;
import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList;
import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord;
import org.mozc.android.inputmethod.japanese.ui.CandidateLayout;
import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row;
import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span;
import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer;
import org.mozc.android.inputmethod.japanese.ui.CandidateLayouter;
import org.mozc.android.inputmethod.japanese.ui.SnapScroller;
import org.mozc.android.inputmethod.japanese.view.CarrierEmojiRenderHelper;
import org.mozc.android.inputmethod.japanese.view.Skin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EdgeEffect;

import javax.annotation.Nullable;

/**
 * A view for candidate words.
 *
 */
// TODO(matsuzakit): Optional is introduced partially. Complete introduction.
abstract class CandidateWordView extends View implements MemoryManageable {

  /**
   * Handles gestures to scroll candidate list and choose a candidate.
   */
  class CandidateWordGestureDetector {

    class CandidateWordViewGestureListener extends SimpleOnGestureListener {

      @Override
      public boolean onFling(
          MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
        float velocity = orientationTrait.projectVector(velocityX, velocityY);
        // As fling is started, current action is not tapping.
        // Reset pressing state so that candidate selection is not triggered at touch up event.
        reset();
        // Fling makes scrolling.
        scroller.fling(-(int) velocity);
        invalidate();
        return true;
      }

      @Override
      public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        float distance = orientationTrait.projectVector(distanceX, distanceY);
        int oldScrollPosition = scroller.getScrollPosition();
        int oldMaxScrollPosition = scroller.getMaxScrollPosition();
        scroller.scrollBy((int) distance);
        orientationTrait.scrollTo(CandidateWordView.this, scroller.getScrollPosition());
        // As scroll is started, current action is not tapping.
        // Reset pressing state so that candidate selection is not triggered at touch up event.
        reset();

        // Edge effect. Now, in production, we only support vertical scroll.
        if (oldScrollPosition + distance < 0) {
          topEdgeEffect.onPull(distance / getHeight());
          if (!bottomEdgeEffect.isFinished()) {
            bottomEdgeEffect.onRelease();
          }
        } else if (oldScrollPosition + distance > oldMaxScrollPosition) {
          bottomEdgeEffect.onPull(distance / getHeight());
          if (!topEdgeEffect.isFinished()) {
            topEdgeEffect.onRelease();
          }
        }

        invalidate();
        return true;
      }
    }

    // GestureDetector cannot handle all complex gestures which we need.
    // But we use GestureDetector for some gesture recognition
    // because implementing whole gesture detection logic by ourselves is a bit tedious.
    private final GestureDetector gestureDetector;

    /**
     * Points to an instance of currently pressed candidate word. Or {@code null} if any
     * candidates aren't pressed.
     */
    @Nullable
    private CandidateWord pressedCandidate;
    private final RectF candidateRect = new RectF();
    private Optional<Integer> pressedRowIndex = Optional.absent();

    public CandidateWordGestureDetector(Context context) {
      gestureDetector = new GestureDetector(context, new CandidateWordViewGestureListener());
    }

    private void pressCandidate(int rowIndex, Span span) {
      Row row = calculatedLayout.getRowList().get(rowIndex);
      pressedRowIndex = Optional.of(rowIndex);
      pressedCandidate = span.getCandidateWord().orNull();
      // TODO(yamaguchi):maybe better to make this rect larger by several pixels to avoid that
      // users fail to select a candidate by unconscious small movement of tap point.
      // (i.e. give hysterisis for noise reduction)
      // Needs UX study.
      candidateRect.set(span.getLeft(), row.getTop(),
                        span.getRight(), row.getTop() + row.getHeight());
    }

    void reset() {
      pressedCandidate = null;
      pressedRowIndex = Optional.absent();
      // NOTE: candidateRect doesn't need reset.
    }

    CandidateWord getPressedCandidate() {
      return pressedCandidate;
    }

    /**
     * Checks if a down event is fired inside a candidate rectangle.
     * If so, begin pressing it.
     *
     * It is assumed that rows are stored in up-to-down order,
     * and spans are in left-to-right order.
     *
     * @param scrolledX X coordinate of down event point including scroll offset
     * @param scrolledY Y coordinate of down event point including scroll offset
     * @return true if the down event is fired inside a candidate rectangle.
     */
    private boolean findCandidateAndPress(float scrolledX, float scrolledY) {
      if (calculatedLayout == null) {
        return false;
      }
      for (int rowIndex = 0; rowIndex < calculatedLayout.getRowList().size(); ++rowIndex) {
        Row row = calculatedLayout.getRowList().get(rowIndex);
        if (scrolledY < row.getTop()) {
          break;
        }
        if (scrolledY >= row.getTop() + row.getHeight()) {
          continue;
        }
        for (Span span : row.getSpanList()) {
          if (scrolledX < span.getLeft()) {
            break;
          }
          if (scrolledX >= span.getRight()) {
            continue;
          }
          pressCandidate(rowIndex, span);
          invalidate();
          return true;
        }
        return false;
      }
      return false;
    }

    boolean onTouchEvent(MotionEvent event) {
      // Before delegation to gesture detector, handle ACTION_UP event
      // in order to release edge effect.
      if (event.getAction() == MotionEvent.ACTION_UP) {
        topEdgeEffect.onRelease();
        bottomEdgeEffect.onRelease();
        invalidate();
      }

      if (gestureDetector.onTouchEvent(event)) {
        return true;
      }

      float scrolledX = event.getX() + getScrollX();
      float scrolledY = event.getY() + getScrollY();
      switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
          findCandidateAndPress(scrolledX, scrolledY);
          scroller.stopScrolling();
          if (!topEdgeEffect.isFinished()) {
            topEdgeEffect.onRelease();
            invalidate();
          }
          if (!bottomEdgeEffect.isFinished()) {
            bottomEdgeEffect.onRelease();
            invalidate();
          }
          return true;
        case MotionEvent.ACTION_MOVE:
          if (pressedCandidate != null) {
            // Turn off highlighting if contact point gets out of the candidate.
            if (!candidateRect.contains(scrolledX, scrolledY)) {
              reset();
              invalidate();
            }
          }
          return true;
        case MotionEvent.ACTION_CANCEL:
          if (pressedCandidate != null) {
            reset();
            invalidate();
          }
          return true;
        case MotionEvent.ACTION_UP:
          if (pressedCandidate != null) {
            if (candidateRect.contains(scrolledX, scrolledY) && candidateSelectListener != null) {
              candidateSelectListener.onCandidateSelected(pressedCandidate, pressedRowIndex);
            }
            reset();
            invalidate();
          }
          return true;
      }
      return false;
    }
  }

  /**
   * Polymorphic behavior based on scroll orientation.
   */
  // TODO(hidehiko): rename OrientationTrait to OrientationTraits.
  interface OrientationTrait {
    /** @return scroll position of which direction corresponds to the orientation. */
    int getScrollPosition(View view);

    /** @return the projected value. */
    float projectVector(float x, float y);

    /** Scrolls to {@code position}. {@code position} is applied to corresponding axis. */
    void scrollTo(View view, int position);

    /** @return left or top position based on the orientation. */
    float getCandidatePosition(Row row, Span span);

    /** @return width or height based on the orientation. */
    float getCandidateLength(Row row, Span span);

    /** @return view's width or height based on the orientation. */
    int getViewLength(View view);

    /** @return the page size of the layout for the scroll orientation. */
    int getPageSize(CandidateLayouter layouter);

    /** @return the content size for the scroll orientation of the layout. 0 for absent. */
    float getContentSize(Optional<CandidateLayout> layout);
  }

  enum Orientation implements OrientationTrait {
    HORIZONTAL {
      @Override
      public int getScrollPosition(View view) {
        return view.getScrollX();
      }
      @Override
      public void scrollTo(View view, int position) {
        view.scrollTo(position, 0);
      }
      @Override
      public float getCandidatePosition(Row row, Span span) {
        return span.getLeft();
      }
      @Override
      public float getCandidateLength(Row row, Span span) {
        return span.getWidth();
      }
      @Override
      public int getViewLength(View view) {
        return view.getWidth();
      }
      @Override
      public float projectVector(float x, float y) {
        return x;
      }
      @Override
      public int getPageSize(CandidateLayouter layouter) {
        return Preconditions.checkNotNull(layouter).getPageWidth();
      }
      @Override
      public float getContentSize(Optional<CandidateLayout> layout) {
        return layout.isPresent() ? layout.get().getContentWidth() : 0;
      }
    },
    VERTICAL {
      @Override
      public int getScrollPosition(View view) {
        return view.getScrollY();
      }
      @Override
      public void scrollTo(View view, int position) {
        view.scrollTo(0, position);
      }
      @Override
      public float getCandidatePosition(Row row, Span span) {
        return row.getTop();
      }
      @Override
      public float getCandidateLength(Row row, Span span) {
        return row.getHeight();
      }
      @Override
      public int getViewLength(View view) {
        return view.getHeight();
          }
      @Override
      public float projectVector(float x, float y) {
        return y;
      }
      @Override
      public int getPageSize(CandidateLayouter layouter) {
        return Preconditions.checkNotNull(layouter).getPageHeight();
      }
      @Override
      public float getContentSize(Optional<CandidateLayout> layout) {
        return layout.isPresent() ? layout.get().getContentHeight() : 0;
      }
    };
  }

  private CandidateSelectListener candidateSelectListener;

  // Finally, we only need vertical scrolling.
  // TODO(hidehiko): Remove horizontal scrolling related codes.
  private final EdgeEffect topEdgeEffect = new EdgeEffect(getContext());
  private final EdgeEffect bottomEdgeEffect = new EdgeEffect(getContext());

  // The Scroller which manages the status of scrolling the view.
  // Default behavior of ScrollView does not suffice our UX design
  // so we introduced this Scroller.
  // TODO(matsuzakit): The parameter is TBD (needs UX study?).
  protected final SnapScroller scroller = new SnapScroller();
  // The CandidateLayouter which calculates the layout of candidate words.
  // This fields is not final but must be set in initialization in the subclasses.
  @VisibleForTesting CandidateLayouter layouter;
  // The calculated layout, created by this.layouter.
  protected CandidateLayout calculatedLayout;
  // The CandidateList which is currently shown on the view.
  protected CandidateList currentCandidateList;
  // The Y position where the last touch event occurs.
  float lastEventPosition;

  // No padding by default.
  private int horizontalPadding = 0;

  protected final CarrierEmojiRenderHelper carrierEmojiRenderHelper =
      new CarrierEmojiRenderHelper(this);
  protected final CandidateLayoutRenderer candidateLayoutRenderer =
      new CandidateLayoutRenderer();

  CandidateWordGestureDetector candidateWordGestureDetector =
      new CandidateWordGestureDetector(getContext());

  // Scroll orientation.
  private final OrientationTrait orientationTrait;

  protected final BackgroundDrawableFactory backgroundDrawableFactory =
      new BackgroundDrawableFactory(getResources());
  private DrawableType backgroundDrawableType = null;

  private final CandidateWindowAccessibilityDelegate accessibilityDelegate;

  CandidateWordView(Context context, OrientationTrait orientationFeature) {
    super(context);
    this.orientationTrait = orientationFeature;
  }

  CandidateWordView(Context context, AttributeSet attributeSet,
                    OrientationTrait orientationTrait) {
    super(context, attributeSet);
    this.orientationTrait = orientationTrait;
  }

  CandidateWordView(Context context, AttributeSet attributeSet, int defaultStyle,
                    OrientationTrait orientationTrait) {
    super(context, attributeSet, defaultStyle);
    this.orientationTrait = orientationTrait;
  }

  {
    accessibilityDelegate = new CandidateWindowAccessibilityDelegate(this);
    ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate);
  }

  void reset() {
    calculatedLayout = null;
    currentCandidateList = null;
    candidateWordGestureDetector.reset();
  }

  void setCandidateSelectListener(CandidateSelectListener candidateSelectListener) {
    this.candidateSelectListener = candidateSelectListener;
  }

  CandidateLayouter getCandidateLayouter() {
    return layouter;
  }

  protected void setHorizontalPadding(int horizontalPadding) {
    this.horizontalPadding = horizontalPadding;
    updateLayouter();
  }

  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    int width = Math.max(right - left - horizontalPadding * 2, 0);
    int height = bottom - top;
    if (layouter != null && layouter.setViewSize(width, height)) {
      updateCalculatedLayout();
    }
    updateScroller();
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    return candidateWordGestureDetector.onTouchEvent(event);
  }

  @Override
  protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    carrierEmojiRenderHelper.onAttachedToWindow();
  }

  @Override
  protected void onDetachedFromWindow() {
    carrierEmojiRenderHelper.onDetachedFromWindow();
    super.onDetachedFromWindow();
  }

  public void setEmojiProviderType(EmojiProviderType providerType) {
    Preconditions.checkNotNull(providerType);
    carrierEmojiRenderHelper.setEmojiProviderType(providerType);
  }

  @Override
  public void draw(Canvas canvas) {
    super.draw(canvas);

    // Render edge effect.
    boolean postInvalidateIsNeeded = false;
    if (!topEdgeEffect.isFinished()) {
      int saveCount = canvas.save();
      try {
        canvas.translate(0, Math.min(0, getScrollY()));
        topEdgeEffect.setSize(getWidth(), getHeight());
        if (topEdgeEffect.draw(canvas)) {
          postInvalidateIsNeeded = true;
        }
      } finally {
        canvas.restoreToCount(saveCount);
      }
    }

    if (!bottomEdgeEffect.isFinished()) {
      int saveCount = canvas.save();
      try {
        int width = getWidth();
        int height = getHeight();
        canvas.translate(-width, getScrollY() + height);
        canvas.rotate(180, width, 0);
        bottomEdgeEffect.setSize(width, height);
        if (bottomEdgeEffect.draw(canvas)) {
          postInvalidateIsNeeded = true;
        }
      } finally {
        canvas.restoreToCount(saveCount);
      }
    }

    if (postInvalidateIsNeeded) {
      ViewCompat.postInvalidateOnAnimation(this);
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (calculatedLayout == null || currentCandidateList == null) {
      // No layout is available.
      return;
    }

    // Paint the candidates.
    int saveCount = canvas.save();
    try {
      canvas.translate(horizontalPadding, 0);
      CandidateWord pressedCandidate = candidateWordGestureDetector.getPressedCandidate();
      int pressedCandidateIndex = (pressedCandidate != null && pressedCandidate.hasIndex())
          ? pressedCandidate.getIndex() : -1;
      candidateLayoutRenderer.drawCandidateLayout(
          canvas, calculatedLayout, pressedCandidateIndex, carrierEmojiRenderHelper);
    } finally {
      canvas.restoreToCount(saveCount);
    }
  }

  @Override
  public final void computeScroll() {
    if (scroller.isScrolling()) {
      // If still scrolling, update the scroll position and invalidate the window.
      Optional<Float> optionalVelocity = scroller.computeScrollOffset();
      orientationTrait.scrollTo(this, scroller.getScrollPosition());
      if (optionalVelocity.isPresent()) {
        Float velocity = optionalVelocity.get();
        // The end of scrolling. Check edge effect.
        if (velocity < 0) {
          topEdgeEffect.onAbsorb(velocity.intValue());
          if (!bottomEdgeEffect.isFinished()) {
            bottomEdgeEffect.onRelease();
            invalidate();
          }
        } else if (velocity > 0) {
          bottomEdgeEffect.onAbsorb(velocity.intValue());
          if (!topEdgeEffect.isFinished()) {
            topEdgeEffect.onRelease();
            invalidate();
          }
        }
      }

      // This invalidation makes next scrolling.
      ViewCompat.postInvalidateOnAnimation(this);
    }
    super.computeScroll();
  }

  @VisibleForTesting int getUpdatedScrollPosition(Row row, Span span) {
    int scrollPosition = orientationTrait.getScrollPosition(this);
    float candidatePosition = orientationTrait.getCandidatePosition(row, span);
    float candidateLength = orientationTrait.getCandidateLength(row, span);
    int viewLength = orientationTrait.getViewLength(this);
    if (candidatePosition < scrollPosition ||
        candidatePosition + candidateLength > scrollPosition + viewLength) {
      return (int) candidatePosition;
    } else {
      return scrollPosition;
    }
  }

  /**
   * If focused candidate is invisible (including partial invisible),
   * update scroll position to see the candidate.
   */
  protected void updateScrollPositionBasedOnFocusedIndex() {
    int scrollPosition = 0;
    if (calculatedLayout != null && currentCandidateList != null) {
      int focusedIndex = currentCandidateList.getFocusedIndex();
      row_loop: for (Row row : calculatedLayout.getRowList()) {
        for (Span span : row.getSpanList()) {
          if (!span.getCandidateWord().isPresent()) {
            continue;
          }
          if (span.getCandidateWord().get().getIndex() == focusedIndex) {
            scrollPosition = getUpdatedScrollPosition(row, span);
            break row_loop;
          }
        }
      }
    }

    setScrollPosition(scrollPosition);
  }

  void setScrollPosition(int position) {
    scroller.scrollTo(position);
    orientationTrait.scrollTo(this, scroller.getScrollPosition());
    invalidate();
  }

  void update(CandidateList candidateList) {
    CandidateList previousCandidateList = currentCandidateList;
    currentCandidateList = candidateList;
    Optional<CandidateList> optionalCandidateList = Optional.fromNullable(candidateList);
    candidateLayoutRenderer.setCandidateList(optionalCandidateList);
    carrierEmojiRenderHelper.setCandidateList(optionalCandidateList);
    if (layouter != null && !equals(candidateList, previousCandidateList)) {
      updateCalculatedLayout();
    }
    updateScroller();
    invalidate();
  }

  private static boolean equals(CandidateList list1, CandidateList list2) {
    if (list1 == list2) {
      return true;
    }
    if (list1 == null || list2 == null) {
      return false;
    }

    return list1.getCandidatesList().equals(list2.getCandidatesList());
  }

  /**
   * Updates the layouter, and also updates the calculatedLayout based on the updated layouter.
   *
   * TODO(hidehiko): This method is remaining here to reduce a CL size smaller
   * in order to make refactoring step by step. This will be cleaned when CandidateWordView
   * is refactored.
   */
  protected final void updateLayouter() {
    updateCalculatedLayout();
    updateScroller();
  }

  /**
   * Updates the calculatedLayout if possible.
   */
  private void updateCalculatedLayout() {
    if (currentCandidateList == null || layouter == null) {
      calculatedLayout = null;
    } else {
      calculatedLayout = layouter.layout(currentCandidateList).orNull();
    }
    Optional<CandidateLayout> candidateLayout = Optional.fromNullable(calculatedLayout);
    accessibilityDelegate.setCandidateLayout(
        candidateLayout,
        (int) orientationTrait.getContentSize(candidateLayout),
        orientationTrait.getViewLength(this));
  }

  private void updateScroller() {
    if (calculatedLayout == null || layouter == null) {
      scroller.setPageSize(0);
      scroller.setContentSize(0);
    } else {
      int pageSize = orientationTrait.getPageSize(layouter);
      int contentSize =
          (int) orientationTrait.getContentSize(Optional.fromNullable(calculatedLayout));
      if (pageSize != 0) {
        // Ceil to align pages.
        contentSize = (contentSize + pageSize - 1) / pageSize * pageSize;
      }
      scroller.setPageSize(pageSize);
      scroller.setContentSize(contentSize);
    }
    scroller.setViewSize(orientationTrait.getViewLength(this));
  }

  public CandidateList getCandidateList() {
    return currentCandidateList;
  }

  protected void setSpanBackgroundDrawableType(DrawableType drawableType) {
    backgroundDrawableType = drawableType;
    resetSpanBackground();
  }

  private void resetSpanBackground() {
    Drawable drawable = (backgroundDrawableType != null)
        ? backgroundDrawableFactory.getDrawable(backgroundDrawableType) : null;
    candidateLayoutRenderer.setSpanBackgroundDrawable(Optional.fromNullable(drawable));
  }

  /**
   * Returns a Drawable which should be set as the view's background.
   */
  protected abstract Drawable getViewBackgroundDrawable(Skin skin);

  @SuppressWarnings("deprecation")
  void setSkin(Skin skin) {
    backgroundDrawableFactory.setSkin(Preconditions.checkNotNull(skin));
    resetSpanBackground();
    candidateLayoutRenderer.setSkin(skin);
    setBackgroundDrawable(
        getViewBackgroundDrawable(skin).getConstantState().newDrawable());
  }

  @Override
  public void trimMemory() {
    calculatedLayout = null;
    accessibilityDelegate.setCandidateLayout(Optional.<CandidateLayout>absent(), 0, 0);
    currentCandidateList = null;
  }

  @Override
  protected boolean dispatchHoverEvent(MotionEvent event) {
    if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
      return accessibilityDelegate.dispatchHoverEvent(event);
    }
    return false;
  }

  @Override
  protected void onVisibilityChanged(View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);
    // If this view gets invisible, reset the internal state of the gesture detector.
    // Otherwise UP event, which is sent after this view being invisible, will cause
    // unexpected onCandidateSelected callback.
    if (visibility != View.VISIBLE) {
      candidateWordGestureDetector.reset();
    }
  }
}
