package org.thoughtcrime.securesms.conversation;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewKt;
import androidx.core.view.WindowInsetsCompat;
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;

import org.signal.core.util.DimensionUnit;
import org.signal.core.util.Util;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ViewUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import java.util.stream.Stream;

import kotlin.Unit;

public final class ConversationReactionOverlay extends FrameLayout {

  private static final String       TAG          = Log.tag(ConversationReactionOverlay.class);
  private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();

  private final Rect  emojiViewGlobalRect  = new Rect();
  private final Rect  emojiStripViewBounds = new Rect();
  private       float segmentSize;

  private final Boundary horizontalEmojiBoundary = new Boundary();
  private final Boundary verticalScrubBoundary   = new Boundary();
  private final PointF   deadzoneTouchPoint      = new PointF();

  private Recipient                 conversationRecipient;
  private MessageRecord             messageRecord;
  private SelectedConversationModel selectedConversationModel;
  private OverlayState              overlayState = OverlayState.HIDDEN;
  private boolean                   isNonAdminInAnnouncementGroup;
  private boolean                   canEditGroupInfo;

  private boolean downIsOurs;
  private int     selected = -1;
  private int     customEmojiIndex;

  private View             dropdownAnchor;
  private View             conversationItem;
  private View             backgroundView;
  private ConstraintLayout foregroundView;
  private View             selectedView;
  private EmojiImageView[] emojiViews;

  private ConversationContextMenu contextMenu;

  private float touchDownDeadZoneSize;
  private float distanceFromTouchDownPointToBottomOfScrubberDeadZone;
  private int   scrubberWidth;
  private int   selectedVerticalTranslation;
  private int   scrubberHorizontalMargin;
  private int   animationEmojiStartDelayFactor;
  private int   statusBarHeight;
  private int   bottomNavigationBarHeight;

  private OnReactionSelectedListener onReactionSelectedListener;
  private OnActionSelectedListener   onActionSelectedListener;
  private OnHideListener             onHideListener;

  private final AnimatorSet revealAnimatorSet = new AnimatorSet();
  private       AnimatorSet hideAnimatorSet   = new AnimatorSet();

  public ConversationReactionOverlay(@NonNull Context context) {
    super(context);
  }

  public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  protected void onFinishInflate() {
    super.onFinishInflate();

    dropdownAnchor   = findViewById(R.id.dropdown_anchor);
    conversationItem = findViewById(R.id.conversation_item);
    backgroundView   = findViewById(R.id.conversation_reaction_scrubber_background);
    foregroundView   = findViewById(R.id.conversation_reaction_scrubber_foreground);
    selectedView     = findViewById(R.id.conversation_reaction_current_selection_indicator);

    emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
                                        findViewById(R.id.reaction_2),
                                        findViewById(R.id.reaction_3),
                                        findViewById(R.id.reaction_4),
                                        findViewById(R.id.reaction_5),
                                        findViewById(R.id.reaction_6),
                                        findViewById(R.id.reaction_7) };

    customEmojiIndex = emojiViews.length - 1;

    distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);

    touchDownDeadZoneSize       = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
    scrubberWidth               = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
    selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
    scrubberHorizontalMargin    = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);

    animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor);

    initAnimators();
  }

  public void show(@NonNull Activity activity,
                   @NonNull Recipient conversationRecipient,
                   @NonNull ConversationMessage conversationMessage,
                   @NonNull PointF lastSeenDownPoint,
                   boolean isNonAdminInAnnouncementGroup,
                   @NonNull SelectedConversationModel selectedConversationModel,
                   boolean canEditGroupInfo)
  {
    if (overlayState != OverlayState.HIDDEN) {
      return;
    }

    this.messageRecord                 = conversationMessage.getMessageRecord();
    this.conversationRecipient         = conversationRecipient;
    this.selectedConversationModel     = selectedConversationModel;
    this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup;
    this.canEditGroupInfo              = canEditGroupInfo;
    overlayState                       = OverlayState.UNINITAILIZED;
    selected                           = -1;

    setupSelectedEmoji();

    View               root             = activity.findViewById(android.R.id.content).getRootView();
    WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(root);

    if (rootWindowInsets != null) {
      Log.i(TAG, "Capturing insets from root view.");

      Insets insets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
      statusBarHeight           = insets.top;
      bottomNavigationBarHeight = insets.bottom;
    } else {
      Log.i(TAG, "Capturing insets from util methods.");

      statusBarHeight           = ViewUtil.getStatusBarHeight(root);
      bottomNavigationBarHeight = ViewUtil.getNavigationBarHeight(root);
    }

    if (zeroNavigationBarHeightForConfiguration()) {
      bottomNavigationBarHeight = 0;
    }

    Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();

    conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
    conversationItem.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));

    boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);

    conversationItem.setScaleX(ConversationItem.LONG_PRESS_SCALE_FACTOR);
    conversationItem.setScaleY(ConversationItem.LONG_PRESS_SCALE_FACTOR);

    setVisibility(View.INVISIBLE);

    ViewKt.doOnLayout(this, v -> {
      showAfterLayout(conversationMessage, lastSeenDownPoint, isMessageOnLeft);
      return Unit.INSTANCE;
    });
  }

  private void showAfterLayout(@NonNull ConversationMessage conversationMessage,
                               @NonNull PointF lastSeenDownPoint,
                               boolean isMessageOnLeft)
  {
    contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));

    conversationItem.setX(selectedConversationModel.getSnapshotMetrics().getSnapshotOffset());
    conversationItem.setY(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight);

    Bitmap  conversationItemSnapshot = selectedConversationModel.getBitmap();
    boolean isWideLayout             = contextMenu.getMaxWidth() + scrubberWidth < getWidth();

    int overlayHeight = getHeight() - bottomNavigationBarHeight;
    int bubbleWidth   = selectedConversationModel.getBubbleWidth();

    float endX           = selectedConversationModel.getSnapshotMetrics().getSnapshotOffset();
    float endY           = conversationItem.getY();
    float endApparentTop = endY;
    float endScale       = 1f;

    float menuPadding           = DimensionUnit.DP.toPixels(12f);
    float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f);
    int   reactionBarHeight     = backgroundView.getHeight();

    float reactionBarBackgroundY;

    if (isWideLayout) {
      boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight;
      if (everythingFitsVertically) {
        boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding;

        if (reactionBarFitsAboveItem) {
          reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
        } else {
          endY                   = reactionBarHeight + menuPadding + reactionBarTopPadding;
          reactionBarBackgroundY = reactionBarTopPadding;
        }
      } else {
        float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding;

        endScale               = spaceAvailableForItem / conversationItem.getHeight();
        endX                   += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
        endY                   = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
        reactionBarBackgroundY = reactionBarTopPadding;
      }
    } else {
      float   reactionBarOffset        = DimensionUnit.DP.toPixels(48);
      float   spaceForReactionBar      = Math.max(reactionBarHeight + reactionBarOffset - conversationItemSnapshot.getHeight(), 0);
      boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight;

      if (everythingFitsVertically) {
        float   bubbleBottom      = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
        boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight;

        if (menuFitsBelowItem) {
          if (conversationItem.getY() < 0) {
            endY = 0;
          }
          float contextMenuTop = endY + conversationItemSnapshot.getHeight();
          reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);

          if (reactionBarBackgroundY <= reactionBarTopPadding) {
            endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding;
          }
        } else {
          endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();

          float contextMenuTop = endY + conversationItemSnapshot.getHeight();
          reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
        }

        endApparentTop = endY;
      } else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
        float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar;

        endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
        endX     += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
        endY     = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);

        float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale);
        reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
        endApparentTop         = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
      } else {
        contextMenu.setHeight(contextMenu.getMaxHeight() / 2);

        int     menuHeight     = contextMenu.getHeight();
        boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight;

        if (fitsVertically) {
          float   bubbleBottom      = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
          boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight;

          if (menuFitsBelowItem) {
            reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;

            if (reactionBarBackgroundY < reactionBarTopPadding) {
              endY                   = reactionBarTopPadding + reactionBarHeight + menuPadding;
              reactionBarBackgroundY = reactionBarTopPadding;
            }
          } else {
            endY                   = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
            reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
          }
          endApparentTop = endY;
        } else {
          float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;

          endScale               = spaceAvailableForItem / conversationItemSnapshot.getHeight();
          endX                   += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
          endY                   = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding;
          reactionBarBackgroundY = reactionBarTopPadding;
          endApparentTop         = reactionBarHeight + menuPadding + reactionBarTopPadding;
        }
      }
    }

    reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);

    hideAnimatorSet.end();
    setVisibility(View.VISIBLE);

    float scrubberX;
    if (isMessageOnLeft) {
      scrubberX = scrubberHorizontalMargin;
    } else {
      scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
    }

    foregroundView.setX(scrubberX);
    foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);

    backgroundView.setX(scrubberX);
    backgroundView.setY(reactionBarBackgroundY);

    verticalScrubBoundary.update(reactionBarBackgroundY,
                                 lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);

    updateBoundsOnLayoutChanged();

    revealAnimatorSet.start();

    if (isWideLayout) {
      float scrubberRight = scrubberX + scrubberWidth;
      float offsetX       = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
      contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight()));
    } else {
      float contentX = selectedConversationModel.getSnapshotMetrics().getContextMenuPadding();
      float offsetX  = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth;

      float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
      contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
    }

    int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);

    conversationItem.animate()
                    .x(endX)
                    .y(endY)
                    .scaleX(endScale)
                    .scaleY(endScale)
                    .setDuration(revealDuration);
  }

  private float getReactionBarOffsetForTouch(@NonNull PointF touchPoint,
                                             float contextMenuTop,
                                             float contextMenuPadding,
                                             float reactionBarOffset,
                                             int reactionBarHeight,
                                             float spaceNeededBetweenTopOfScreenAndTopOfReactionBar,
                                             float messageTop)
  {
    float adjustedTouchY        = touchPoint.y - statusBarHeight;
    float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop);

    float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop);

    if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) {
      float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding;
      reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding;
    }

    return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
  }

  /**
   * Returns true when the device is in a configuration where the navigation bar doesn't take up
   * space at the bottom of the screen.
   */
  private boolean zeroNavigationBarHeightForConfiguration() {
    boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;

    WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this);
    return (insets == null || insets.getInsets(WindowInsetsCompat.Type.systemGestures()).bottom == 0) && isLandscape;
  }

  public void hide() {
    hideInternal(onHideListener);
  }

  private void hideInternal(@Nullable OnHideListener onHideListener) {
    if (overlayState == OverlayState.HIDDEN || selectedConversationModel == null) {
      return;
    }

    overlayState = OverlayState.HIDDEN;

    AnimatorSet animatorSet = newHideAnimatorSet();
    hideAnimatorSet = animatorSet;

    revealAnimatorSet.end();
    animatorSet.start();

    if (onHideListener != null) {
      onHideListener.startHide(selectedConversationModel.getFocusedView());
    }

    animatorSet.addListener(new AnimationCompleteListener() {
      @Override public void onAnimationEnd(Animator animation) {
        animatorSet.removeListener(this);

        if (onHideListener != null) {
          onHideListener.onHide();
        }
      }
    });

    if (contextMenu != null) {
      contextMenu.dismiss();
    }
  }

  public boolean isShowing() {
    return overlayState != OverlayState.HIDDEN;
  }

  public @NonNull MessageRecord getMessageRecord() {
    return messageRecord;
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    updateBoundsOnLayoutChanged();
  }

  private void updateBoundsOnLayoutChanged() {
    backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
    emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
    emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
    emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect);
    emojiStripViewBounds.right = getEnd(emojiViewGlobalRect);

    segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length;
  }

  private int getStart(@NonNull Rect rect) {
    if (ViewUtil.isLtr(this)) {
      return rect.left;
    } else {
      return rect.right;
    }
  }

  private int getEnd(@NonNull Rect rect) {
    if (ViewUtil.isLtr(this)) {
      return rect.right;
    } else {
      return rect.left;
    }
  }

  public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
    if (!isShowing()) {
      throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber.");
    }

    if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {
      return true;
    }

    if (overlayState == OverlayState.UNINITAILIZED) {
      downIsOurs = false;

      deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());

      overlayState = OverlayState.DEADZONE;
    }

    if (overlayState == OverlayState.DEADZONE) {
      float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getX());
      float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getY());

      if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
        overlayState = OverlayState.SCRUB;
      } else {
        if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
          overlayState = OverlayState.TAP;

          if (downIsOurs) {
            handleUpEvent();
            return true;
          }
        }

        return MotionEvent.ACTION_MOVE == motionEvent.getAction();
      }
    }

    switch (motionEvent.getAction()) {
      case MotionEvent.ACTION_DOWN:
        selected = getSelectedIndexViaDownEvent(motionEvent);

        deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
        overlayState = OverlayState.DEADZONE;
        downIsOurs = true;
        return true;
      case MotionEvent.ACTION_MOVE:
        selected = getSelectedIndexViaMoveEvent(motionEvent);
        return true;
      case MotionEvent.ACTION_UP:
        handleUpEvent();
        return downIsOurs;
      case MotionEvent.ACTION_CANCEL:
        hide();
        return downIsOurs;
      default:
        return false;
    }
  }

  private void setupSelectedEmoji() {
    final List<String> emojis   = SignalStore.emoji().getReactions();
    final String       oldEmoji = getOldEmoji(messageRecord);

    if (oldEmoji == null) {
      selectedView.setVisibility(View.GONE);
    }

    boolean foundSelected = false;

    for (int i = 0; i < emojiViews.length; i++) {
      final EmojiImageView view = emojiViews[i];

      view.setScaleX(1.0f);
      view.setScaleY(1.0f);
      view.setTranslationY(0);

      boolean isAtCustomIndex                      = i == customEmojiIndex;
      boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && EmojiUtil.isCanonicallyEqual(emojis.get(i), oldEmoji);
      boolean isAtCustomIndexAndOldEmojiExists     = isAtCustomIndex && oldEmoji != null;

      if (!foundSelected &&
          (isNotAtCustomIndexAndOldEmojiMatches || isAtCustomIndexAndOldEmojiExists))
      {
        foundSelected = true;
        selectedView.setVisibility(View.VISIBLE);

        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(foregroundView);
        constraintSet.clear(selectedView.getId(), ConstraintSet.LEFT);
        constraintSet.clear(selectedView.getId(), ConstraintSet.RIGHT);
        constraintSet.connect(selectedView.getId(), ConstraintSet.LEFT, view.getId(), ConstraintSet.LEFT);
        constraintSet.connect(selectedView.getId(), ConstraintSet.RIGHT, view.getId(), ConstraintSet.RIGHT);
        constraintSet.applyTo(foregroundView);

        if (isAtCustomIndex) {
          view.setImageEmoji(oldEmoji);
          view.setTag(oldEmoji);
        } else {
          view.setImageEmoji(SignalStore.emoji().getPreferredVariation(emojis.get(i)));
        }
      } else if (isAtCustomIndex) {
        view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
        view.setTag(null);
      } else {
        view.setImageEmoji(SignalStore.emoji().getPreferredVariation(emojis.get(i)));
      }
    }
  }

  private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) {
    return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom));
  }

  private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) {
    return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary);
  }

  private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) {
    int selected = -1;

    if (backgroundView.getVisibility() != View.VISIBLE) {
      return selected;
    }

    for (int i = 0; i < emojiViews.length; i++) {
      final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left;
      horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize);

      if (horizontalEmojiBoundary.contains(motionEvent.getX()) && boundary.contains(motionEvent.getY())) {
        selected = i;
      }
    }

    if (this.selected != -1 && this.selected != selected) {
      shrinkView(emojiViews[this.selected]);
    }

    if (this.selected != selected && selected != -1) {
      growView(emojiViews[selected]);
    }

    return selected;
  }

  private void growView(@NonNull View view) {
    view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
    view.animate()
        .scaleY(1.5f)
        .scaleX(1.5f)
        .translationY(-selectedVerticalTranslation)
        .setDuration(200)
        .setInterpolator(INTERPOLATOR)
        .start();
  }

  private void shrinkView(@NonNull View view) {
    view.animate()
        .scaleX(1.0f)
        .scaleY(1.0f)
        .translationY(0)
        .setDuration(200)
        .setInterpolator(INTERPOLATOR)
        .start();
  }

  private void handleUpEvent() {
    if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) {
      if (selected == customEmojiIndex) {
        onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
      } else {
        onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emoji().getPreferredVariation(SignalStore.emoji().getReactions().get(selected)));
      }
    } else {
      hide();
    }
  }

  public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) {
    this.onReactionSelectedListener = onReactionSelectedListener;
  }

  public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
    this.onActionSelectedListener = onActionSelectedListener;
  }

  public void setOnHideListener(@Nullable OnHideListener onHideListener) {
    this.onHideListener = onHideListener;
  }

  private static @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) {
    return messageRecord.getReactions().stream()
                        .filter(record -> record.getAuthor()
                                                .serialize()
                                                .equals(Recipient.self()
                                                                 .getId()
                                                                 .serialize()))
                        .findFirst()
                        .map(ReactionRecord::getEmoji)
                        .orElse(null);
  }

  private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) {
    MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup, canEditGroupInfo);

    List<ActionItem> items = new ArrayList<>();

    if (menuState.shouldShowReplyAction()) {
      items.add(new ActionItem(R.drawable.symbol_reply_24, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
    }

    if (menuState.shouldShowEditAction()) {
      items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_edit_24, getResources().getString(R.string.conversation_selection__menu_edit), () -> handleActionItemClicked(Action.EDIT)));
    }

    if (menuState.shouldShowForwardAction()) {
      items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_forward_24, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD)));
    }

    if (menuState.shouldShowResendAction()) {
      items.add(new ActionItem(R.drawable.symbol_refresh_24, getResources().getString(R.string.conversation_selection__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
    }

    if (menuState.shouldShowSaveAttachmentAction()) {
      items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_save_android_24, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
    }

    if (menuState.shouldShowCopyAction()) {
      items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_copy_android_24, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY)));
    }

    if (menuState.shouldShowPaymentDetails()) {
      items.add(new ActionItem(R.drawable.symbol_payment_24, getResources().getString(R.string.conversation_selection__menu_payment_details), () -> handleActionItemClicked(Action.PAYMENT_DETAILS)));
    }

    items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_check_circle_24, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT)));

    if (menuState.shouldShowDetailsAction()) {
      items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_info_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
    }

    if (menuState.shouldShowPollTerminateAction()) {
      items.add(new ActionItem(R.drawable.symbol_stop_24, getResources().getString(R.string.conversation_selection__menu_end_poll), () -> handleActionItemClicked(Action.END_POLL)));
    }

    if (menuState.shouldShowPinMessage()) {
      items.add(new ActionItem(R.drawable.symbol_pin_24, getResources().getString(R.string.conversation_selection__menu_pin_message), () -> handleActionItemClicked(Action.PIN_MESSAGE)));
    }

    if (menuState.showShowUnpinMessage()) {
      items.add(new ActionItem(R.drawable.symbol_pin_slash_24, getResources().getString(R.string.conversation_selection__menu_unpin_message), () -> handleActionItemClicked(Action.UNPIN_MESSAGE)));
    }

    if (menuState.shouldShowStarMessage()) {
      items.add(new ActionItem(R.drawable.symbol_star_outline_24, getResources().getString(R.string.conversation_selection__menu_star), () -> handleActionItemClicked(Action.STAR_MESSAGE)));
    }

    if (menuState.shouldShowUnstarMessage()) {
      items.add(new ActionItem(R.drawable.symbol_star_outline_24, getResources().getString(R.string.conversation_selection__menu_unstar), () -> handleActionItemClicked(Action.UNSTAR_MESSAGE)));
    }

    backgroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);
    foregroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);

    items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_trash_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> handleActionItemClicked(Action.DELETE)));

    return items;
  }

  private void handleActionItemClicked(@NonNull Action action) {
    hideInternal(new OnHideListener() {
      @Override
      public void startHide(@Nullable View focusedView) {
        if (onHideListener != null) {
          onHideListener.startHide(action == Action.VIEW_INFO ? null : focusedView);
        }
      }

      @Override
      public void onHide() {
        if (onHideListener != null) {
          onHideListener.onHide();
        }

        if (onActionSelectedListener != null) {
          onActionSelectedListener.onActionSelected(action);
        }
      }
    });
  }

  private void initAnimators() {

    int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
    int revealOffset   = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset);

    List<Animator> reveals = LongStream.range(0, emojiViews.length)
                                       .boxed()
                                       .map(idx -> {
                                         Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal);
                                         anim.setTarget(emojiViews[idx.intValue()]);
                                         anim.setStartDelay(idx * animationEmojiStartDelayFactor);
                                         return anim;
                                       }).collect(Collectors.toList());

    Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
    backgroundRevealAnim.setTarget(backgroundView);
    backgroundRevealAnim.setDuration(revealDuration);
    backgroundRevealAnim.setStartDelay(revealOffset);
    reveals.add(backgroundRevealAnim);

    Animator selectedRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
    selectedRevealAnim.setTarget(selectedView);
    backgroundRevealAnim.setDuration(revealDuration);
    backgroundRevealAnim.setStartDelay(revealOffset);
    reveals.add(selectedRevealAnim);

    revealAnimatorSet.setInterpolator(INTERPOLATOR);
    revealAnimatorSet.playTogether(reveals);
  }

  private @NonNull AnimatorSet newHideAnimatorSet() {
    AnimatorSet set = new AnimatorSet();

    set.addListener(new AnimationCompleteListener() {
      @Override
      public void onAnimationEnd(Animator animation) {
        setVisibility(View.GONE);
      }
    });
    set.setInterpolator(INTERPOLATOR);

    set.playTogether(newHideAnimators());

    return set;
  }

  private @NonNull List<Animator> newHideAnimators() {
    int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration);

    List<Animator> animators = new ArrayList<>(Stream.of(emojiViews)
                                                     .map(v -> {
                                                       Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
                                                       anim.setTarget(v);
                                                       return anim;
                                                     })
                                                     .collect(Collectors.toList()));

    Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
    backgroundHideAnim.setTarget(backgroundView);
    backgroundHideAnim.setDuration(duration);
    animators.add(backgroundHideAnim);

    Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
    selectedHideAnim.setTarget(selectedView);
    selectedHideAnim.setDuration(duration);
    animators.add(selectedHideAnim);

    ObjectAnimator itemScaleXAnim = new ObjectAnimator();
    itemScaleXAnim.setProperty(View.SCALE_X);
    itemScaleXAnim.setFloatValues(1f);
    itemScaleXAnim.setTarget(conversationItem);
    itemScaleXAnim.setDuration(duration);
    animators.add(itemScaleXAnim);

    ObjectAnimator itemScaleYAnim = new ObjectAnimator();
    itemScaleYAnim.setProperty(View.SCALE_Y);
    itemScaleYAnim.setFloatValues(1f);
    itemScaleYAnim.setTarget(conversationItem);
    itemScaleYAnim.setDuration(duration);
    animators.add(itemScaleYAnim);

    ObjectAnimator itemXAnim = new ObjectAnimator();
    itemXAnim.setProperty(View.X);
    itemXAnim.setFloatValues(selectedConversationModel.getSnapshotMetrics().getSnapshotOffset());
    itemXAnim.setTarget(conversationItem);
    itemXAnim.setDuration(duration);
    animators.add(itemXAnim);

    ObjectAnimator itemYAnim = new ObjectAnimator();
    itemYAnim.setProperty(View.Y);
    itemYAnim.setFloatValues(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight);
    itemYAnim.setTarget(conversationItem);
    itemYAnim.setDuration(duration);
    animators.add(itemYAnim);

    return animators;
  }

  public interface OnHideListener {
    void startHide(@Nullable View focusedView);

    void onHide();
  }

  public interface OnReactionSelectedListener {
    void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji);

    void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
  }

  public interface OnActionSelectedListener {
    void onActionSelected(@NonNull Action action);
  }

  private static class Boundary {
    private float min;
    private float max;

    Boundary() {}

    Boundary(float min, float max) {
      update(min, max);
    }

    private void update(float min, float max) {
      this.min = min;
      this.max = max;
    }

    public boolean contains(float value) {
      if (min < max) {
        return this.min < value && this.max > value;
      } else {
        return this.min > value && this.max < value;
      }
    }
  }

  private enum OverlayState {
    HIDDEN,
    UNINITAILIZED,
    DEADZONE,
    SCRUB,
    TAP
  }

  public enum Action {
    REPLY,
    EDIT,
    FORWARD,
    RESEND,
    DOWNLOAD,
    COPY,
    MULTISELECT,
    PAYMENT_DETAILS,
    VIEW_INFO,
    DELETE,
    END_POLL,
    PIN_MESSAGE,
    UNPIN_MESSAGE,
    STAR_MESSAGE,
    UNSTAR_MESSAGE
  }
}