package awais.instagrabber.fragments.directmessages;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;

import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsAnimationControlListenerCompat;
import androidx.core.view.WindowInsetsAnimationControllerCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.transition.TransitionManager;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;

import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.badge.BadgeUtils;
import com.google.android.material.internal.ToolbarUtils;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.collect.ImmutableList;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import awais.instagrabber.R;
import awais.instagrabber.activities.CameraActivity;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.DirectItemsAdapter;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemLongClickListener;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader;
import awais.instagrabber.adapters.DirectReactionsAdapter;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
import awais.instagrabber.animations.CubicBezierInterpolator;
import awais.instagrabber.customviews.InsetsAnimationLinearLayout;
import awais.instagrabber.customviews.KeyNotifyingEmojiEditText;
import awais.instagrabber.customviews.RecordView;
import awais.instagrabber.customviews.Tooltip;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog;
import awais.instagrabber.customviews.emoji.EmojiPicker;
import awais.instagrabber.customviews.helpers.ControlFocusInsetsAnimationCallback;
import awais.instagrabber.customviews.helpers.EmojiPickerInsetsAnimationCallback;
import awais.instagrabber.customviews.helpers.HeaderItemDecoration;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
import awais.instagrabber.customviews.helpers.SimpleImeAnimationController;
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.customviews.helpers.TranslateDeferringInsetsAnimationCallback;
import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding;
import awais.instagrabber.dialogs.DirectItemReactionDialogFragment;
import awais.instagrabber.dialogs.GifPickerBottomDialogFragment;
import awais.instagrabber.fragments.UserSearchMode;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
import awais.instagrabber.repositories.responses.directmessages.DirectItemLink;
import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions;
import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.DMUtils;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.PermissionUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.DirectThreadViewModel;
import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory;

public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener,
        EmojiPicker.OnEmojiClickListener {
    private static final String TAG = DirectMessageThreadFragment.class.getSimpleName();
    private static final int AUDIO_RECORD_PERM_REQUEST_CODE = 1000;
    private static final int CAMERA_REQUEST_CODE = 200;
    private static final int FILE_PICKER_REQUEST_CODE = 500;
    private static final String TRANSLATION_Y = "translationY";

    private DirectItemsAdapter itemsAdapter;
    private MainActivity fragmentActivity;
    private DirectThreadViewModel viewModel;
    private InsetsAnimationLinearLayout root;
    private boolean shouldRefresh = true;
    private List<DirectItemOrHeader> itemOrHeaders;
    private FragmentDirectMessagesThreadBinding binding;
    private Tooltip tooltip;
    private float initialSendX;
    private ActionBar actionBar;
    private AppStateViewModel appStateViewModel;
    private Runnable prevTitleRunnable;
    private AnimatorSet animatorSet;
    private boolean isRecording;
    private DirectItemReactionDialogFragment reactionDialogFragment;
    private DirectItem itemToForward;
    private MutableLiveData<Object> backStackSavedStateResultLiveData;
    private int prevLength;
    private BadgeDrawable pendingRequestCountBadgeDrawable;
    private boolean isPendingRequestCountBadgeAttached = false;
    private ItemTouchHelper itemTouchHelper;
    private LiveData<Boolean> pendingLiveData;
    private LiveData<DirectThread> threadLiveData;
    private LiveData<Integer> inputModeLiveData;
    private LiveData<String> threadTitleLiveData;
    private LiveData<Resource<Object>> fetchingLiveData;
    private LiveData<List<DirectItem>> itemsLiveData;
    private LiveData<DirectItem> replyToItemLiveData;
    private LiveData<Integer> pendingRequestsCountLiveData;
    private LiveData<List<User>> usersLiveData;
    private boolean autoMarkAsSeen = false;
    private MenuItem markAsSeenMenuItem;
    private DirectItem addReactionItem;
    private TranslateDeferringInsetsAnimationCallback inputHolderAnimationCallback;
    private TranslateDeferringInsetsAnimationCallback chatsAnimationCallback;
    private EmojiPickerInsetsAnimationCallback emojiPickerAnimationCallback;
    private boolean hasKbOpenedOnce;
    private boolean wasToggled;
    private SwipeAndRestoreItemTouchHelperCallback touchHelperCallback;

    private final AppExecutors appExecutors = AppExecutors.INSTANCE;
    private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() {
        @Override
        public void onAnimationEnd(final Drawable drawable) {
            AnimatedVectorDrawableCompat.unregisterAnimationCallback(drawable, this);
            setSendToMicIcon();
        }
    };
    private final Animatable2Compat.AnimationCallback sendToMicAnimationCallback = new Animatable2Compat.AnimationCallback() {
        @Override
        public void onAnimationEnd(final Drawable drawable) {
            AnimatedVectorDrawableCompat.unregisterAnimationCallback(drawable, this);
            setMicToSendIcon();
        }
    };
    private final DirectItemCallback directItemCallback = new DirectItemCallback() {
        @Override
        public void onHashtagClick(final String hashtag) {
            try {
                final NavDirections action = DirectMessageThreadFragmentDirections.actionToHashtag(hashtag);
                NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action);
            } catch (Exception e) {
                Log.e(TAG, "onHashtagClick: ", e);
            }
        }

        @Override
        public void onMentionClick(final String mention) {
            navigateToUser(mention);
        }

        @Override
        public void onLocationClick(final long locationId) {
            try {
                final NavDirections action = DirectMessageThreadFragmentDirections.actionToLocation(locationId);
                NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action);
            } catch (Exception e) {
                Log.e(TAG, "onLocationClick: ", e);
            }
        }

        @Override
        public void onURLClick(final String url) {
            final Context context = getContext();
            if (context == null) return;
            Utils.openURL(context, url);
        }

        @Override
        public void onEmailClick(final String email) {
            final Context context = getContext();
            if (context == null) return;
            Utils.openEmailAddress(context, email);
        }

        @Override
        public void onMediaClick(final Media media, final int index) {
            if (media.isReelMedia()) {
                try {
                    final String pk = media.getPk();
                    if (pk == null) return;
                    final long mediaId = Long.parseLong(pk);
                    final User user = media.getUser();
                    if (user == null) return;
                    final String username = user.getUsername();
                    final NavDirections action = DirectMessageThreadFragmentDirections.actionToStory(StoryViewerOptions.forStory(mediaId, username));
                    NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action);
                } catch (Exception e) {
                    Log.e(TAG, "onMediaClick (story): ", e);
                }
                return;
            }
            try {
                final NavDirections actionToPost = DirectMessageThreadFragmentDirections.actionToPost(media, index);
                NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(actionToPost);
            } catch (Exception e) {
                Log.e(TAG, "openPostDialog: ", e);
            }
        }

        @Override
        public void onStoryClick(final DirectItemStoryShare storyShare) {
            try {
                final String pk = storyShare.getReelId();
                if (pk == null) return;
                final long mediaId = Long.parseLong(pk);
                final Media media = storyShare.getMedia();
                if (media == null) return;
                final User user = media.getUser();
                if (user == null) return;
                final String username = user.getUsername();
                final NavDirections action = DirectMessageThreadFragmentDirections.actionToStory(StoryViewerOptions.forUser(mediaId, username));
                NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action);
            } catch (Exception e) {
                Log.e(TAG, "onStoryClick: ", e);
            }
        }

        @Override
        public void onReaction(final DirectItem item, final Emoji emoji) {
            if (item == null || emoji == null) return;
            final LiveData<Resource<Object>> resourceLiveData = viewModel.sendReaction(item, emoji);
            resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData));
        }

        @Override
        public void onReactionClick(final DirectItem item, final int position) {
            showReactionsDialog(item);
        }

        @Override
        public void onOptionSelect(final DirectItem item, final int itemId, final Function<DirectItem, Void> cb) {
            if (itemId == R.id.unsend) {
                handleSentMessage(viewModel.unsend(item));
                return;
            }
            if (itemId == R.id.forward) {
                itemToForward = item;
                final NavDirections actionGlobalUserSearch = DirectMessageThreadFragmentDirections
                        .actionToUserSearch()
                        .setTitle(getString(R.string.forward))
                        .setActionLabel(getString(R.string.send))
                        .setShowGroups(true)
                        .setMultiple(true)
                        .setSearchMode(UserSearchMode.RAVEN);
                NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(actionGlobalUserSearch);
            }
            if (itemId == R.id.download) {
                downloadItem(item);
                return;
            }
            // otherwise call callback if present
            if (cb != null) {
                cb.apply(item);
            }
        }

        @Override
        public void onAddReactionListener(final DirectItem item) {
            if (item == null) return;
            addReactionItem = item;
            final EmojiBottomSheetDialog emojiBottomSheetDialog = EmojiBottomSheetDialog.newInstance();
            emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG);
        }
    };
    private final DirectItemLongClickListener directItemLongClickListener = position -> {
        // viewModel.setSelectedPosition(position);
    };
    private final Observer<Object> backStackSavedStateObserver = result -> {
        if (result == null) return;
        if (result instanceof Uri) {
            final Uri uri = (Uri) result;
            handleSentMessage(viewModel.sendUri(uri));
        } else if ((result instanceof RankedRecipient)) {
            // Log.d(TAG, "result: " + result);
            if (itemToForward != null) {
                viewModel.forward((RankedRecipient) result, itemToForward);
            }
        } else if ((result instanceof Set)) {
            try {
                // Log.d(TAG, "result: " + result);
                if (itemToForward != null) {
                    //noinspection unchecked
                    viewModel.forward((Set<RankedRecipient>) result, itemToForward);
                }
            } catch (Exception e) {
                Log.e(TAG, "forward result: ", e);
            }
        }
        // clear result
        backStackSavedStateResultLiveData.postValue(null);
    };
    private final MutableLiveData<Integer> inputLength = new MutableLiveData<>(0);
    private final MutableLiveData<Boolean> emojiPickerVisible = new MutableLiveData<>(false);
    private final MutableLiveData<Boolean> kbVisible = new MutableLiveData<>(false);
    private final OnBackPressedCallback onEmojiPickerBackPressedCallback = new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            emojiPickerVisible.postValue(false);
        }
    };

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fragmentActivity = (MainActivity) requireActivity();
        appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class);
        autoMarkAsSeen = Utils.settingsHelper.getBoolean(PreferenceKeys.DM_MARK_AS_SEEN);
        final Bundle arguments = getArguments();
        if (arguments == null) return;
        final DirectMessageThreadFragmentArgs fragmentArgs = DirectMessageThreadFragmentArgs.fromBundle(arguments);
        final Resource<User> currentUserResource = appStateViewModel.getCurrentUser();
        if (currentUserResource == null) return;
        final User currentUser = currentUserResource.data;
        if (currentUser == null) return;
        final DirectThreadViewModelFactory viewModelFactory = new DirectThreadViewModelFactory(
                fragmentActivity.getApplication(),
                fragmentArgs.getThreadId(),
                fragmentArgs.getPending(),
                currentUser
        );
        viewModel = new ViewModelProvider(this, viewModelFactory).get(DirectThreadViewModel.class);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(@NonNull final LayoutInflater inflater,
                             final ViewGroup container,
                             final Bundle savedInstanceState) {
        if (root != null) {
            shouldRefresh = false;
            return root;
        }
        binding = FragmentDirectMessagesThreadBinding.inflate(inflater, container, false);
        binding.send.setRecordView(binding.recordView);
        root = binding.getRoot();
        final Context context = getContext();
        if (context == null) {
            return root;
        }
        tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black));
        // todo check has camera and remove view
        return root;
    }

    @Override
    public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
        // WindowCompat.setDecorFitsSystemWindows(fragmentActivity.getWindow(), false);
        if (!shouldRefresh) return;
        init();
        binding.send.post(() -> initialSendX = binding.send.getX());
        shouldRefresh = false;
    }

    @Override
    public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
        inflater.inflate(R.menu.dm_thread_menu, menu);
        markAsSeenMenuItem = menu.findItem(R.id.mark_as_seen);
        if (markAsSeenMenuItem != null) {
            if (autoMarkAsSeen) {
                markAsSeenMenuItem.setVisible(false);
            } else {
                markAsSeenMenuItem.setEnabled(false);
            }
        }
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
        final int itemId = item.getItemId();
        if (itemId == R.id.info) {
            final Boolean pending = viewModel.isPending().getValue();
            final NavDirections directions = DirectMessageThreadFragmentDirections
                    .actionToSettings(viewModel.getThreadId(), null)
                    .setPending(pending != null && pending);
            NavHostFragment.findNavController(this).navigate(directions);
            return true;
        }
        if (itemId == R.id.mark_as_seen) {
            handleMarkAsSeen(item);
            return true;
        }
        if (itemId == R.id.refresh && viewModel != null) {
            viewModel.refreshChats();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void handleMarkAsSeen(@NonNull final MenuItem item) {
        final LiveData<Resource<Object>> resourceLiveData = viewModel.markAsSeen();
        resourceLiveData.observe(getViewLifecycleOwner(), new Observer<Resource<Object>>() {
            @Override
            public void onChanged(final Resource<Object> resource) {
                try {
                    if (resource == null) return;
                    final Context context = getContext();
                    if (context == null) return;
                    switch (resource.status) {
                        case SUCCESS:
                            Toast.makeText(context, R.string.marked_as_seen, Toast.LENGTH_SHORT).show();
                        case LOADING:
                            item.setEnabled(false);
                            break;
                        case ERROR:
                            item.setEnabled(true);
                            if (resource.message != null) {
                                Snackbar.make(context, binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
                                return;
                            }
                            if (resource.resId != 0) {
                                Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
                                return;
                            }
                            break;
                    }
                } finally {
                    resourceLiveData.removeObserver(this);
                }
            }
        });
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_PICKER_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            if (data == null || data.getData() == null) {
                Log.w(TAG, "data is null!");
                return;
            }
            final Context context = getContext();
            if (context == null) {
                Log.w(TAG, "conetxt is null!");
                return;
            }
            final Uri uri = data.getData();
            final String mimeType = Utils.getMimeType(uri, context.getContentResolver());
            if (mimeType != null && mimeType.startsWith("image")) {
                navigateToImageEditFragment(uri);
                return;
            }
            handleSentMessage(viewModel.sendUri(uri));
        }
        if (requestCode == CAMERA_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            if (data == null || data.getData() == null) {
                Log.w(TAG, "data is null!");
                return;
            }
            final Uri uri = data.getData();
            navigateToImageEditFragment(uri);
        }
    }

    @Override
    public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        final Context context = getContext();
        if (context == null) return;
        if (requestCode == AUDIO_RECORD_PERM_REQUEST_CODE) {
            if (PermissionUtils.hasAudioRecordPerms(context)) {
                Toast.makeText(context, "You can send voice messages now!", Toast.LENGTH_LONG).show();
                return;
            }
            Toast.makeText(context, "Require RECORD_AUDIO permission", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onPause() {
        if (isRecording) {
            binding.recordView.cancelRecording(binding.send);
        }
        emojiPickerVisible.postValue(false);
        kbVisible.postValue(false);
        binding.inputHolder.setTranslationY(0);
        binding.chats.setTranslationY(0);
        binding.emojiPicker.setTranslationY(0);
        removeObservers();
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        if (initialSendX != 0) {
            binding.send.setX(initialSendX);
        }
        binding.send.stopScale();
        final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
        onBackPressedDispatcher.addCallback(onEmojiPickerBackPressedCallback);
        setupBackStackResultObserver();
        setObservers();
        // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue());
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        cleanup();
    }

    @Override
    public void onDestroy() {
        viewModel.deleteThreadIfRequired();
        super.onDestroy();
    }

    @SuppressLint("UnsafeOptInUsageError")
    private void cleanup() {
        if (prevTitleRunnable != null) {
            appExecutors.getMainThread().cancel(prevTitleRunnable);
        }
        for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) {
            final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i));
            if (holder == null) continue;
            if (holder instanceof DirectItemViewHolder) {
                ((DirectItemViewHolder) holder).cleanup();
            }
        }
        isPendingRequestCountBadgeAttached = false;
        if (pendingRequestCountBadgeDrawable != null) {
            @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils
                    .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info);
            if (menuItemView != null) {
                BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info);
            }
            pendingRequestCountBadgeDrawable = null;
        }
    }

    private void init() {
        final Context context = getContext();
        if (context == null) return;
        if (getArguments() == null) return;
        actionBar = fragmentActivity.getSupportActionBar();
        setupList();
    }

    private void setupList() {
        final Context context = getContext();
        if (context == null) return;
        binding.chats.setItemViewCacheSize(20);
        final LinearLayoutManager layoutManager = new LinearLayoutManager(context);
        layoutManager.setReverseLayout(true);
        // layoutManager.setStackFromEnd(false);
        // binding.messageList.addItemDecoration(new VerticalSpaceItemDecoration(3));
        final RecyclerView.ItemAnimator animator = binding.chats.getItemAnimator();
        if (animator instanceof SimpleItemAnimator) {
            final SimpleItemAnimator itemAnimator = (SimpleItemAnimator) animator;
            itemAnimator.setSupportsChangeAnimations(false);
        }
        binding.chats.setLayoutManager(layoutManager);
        binding.chats.addOnScrollListener(new RecyclerLazyLoaderAtEdge(layoutManager, true, page -> viewModel.fetchChats()));
        final HeaderItemDecoration headerItemDecoration = new HeaderItemDecoration(binding.chats, itemPosition -> {
            if (itemOrHeaders == null || itemOrHeaders.isEmpty()) return false;
            try {
                final DirectItemOrHeader itemOrHeader = itemOrHeaders.get(itemPosition);
                return itemOrHeader.isHeader();
            } catch (IndexOutOfBoundsException e) {
                return false;
            }
        });
        binding.chats.addItemDecoration(headerItemDecoration);
    }

    private void setObservers() {
        if (viewModel == null) return;
        threadLiveData = viewModel.getThread();
        // if (threadLiveData == null) {
        //     final NavController navController = NavHostFragment.findNavController(this);
        //     navController.navigateUp();
        //     return;
        // }
        pendingLiveData = viewModel.isPending();
        pendingLiveData.observe(getViewLifecycleOwner(), isPending -> {
            if (isPending == null) {
                hideInput();
                return;
            }
            if (isPending) {
                showPendingOptions();
                return;
            }
            hidePendingOptions();
            final Integer inputMode = viewModel.getInputMode().getValue();
            if (inputMode != null && inputMode == 1) return;
            showInput();
        });
        inputModeLiveData = viewModel.getInputMode();
        inputModeLiveData.observe(getViewLifecycleOwner(), inputMode -> {
            final Boolean isPending = viewModel.isPending().getValue();
            if (isPending != null && isPending || inputMode == null) return;
            setupInput(inputMode);
            if (inputMode == 0) {
                setupTouchHelper();
                return;
            }
            if (inputMode == 1) {
                hideInput();
            }
        });
        threadTitleLiveData = viewModel.getThreadTitle();
        threadTitleLiveData.observe(getViewLifecycleOwner(), this::setTitle);
        fetchingLiveData = viewModel.isFetching();
        fetchingLiveData.observe(getViewLifecycleOwner(), fetchingResource -> {
            if (fetchingResource == null) return;
            switch (fetchingResource.status) {
                case SUCCESS:
                case ERROR:
                    setTitle(viewModel.getThreadTitle().getValue());
                    if (fetchingResource.message != null) {
                        Snackbar.make(binding.getRoot(), fetchingResource.message, Snackbar.LENGTH_LONG).show();
                    }
                    if (fetchingResource.resId != 0) {
                        Snackbar.make(binding.getRoot(), fetchingResource.resId, Snackbar.LENGTH_LONG).show();
                    }
                    break;
                case LOADING:
                    setTitle(getString(R.string.dms_thread_updating));
                    break;
            }
        });
        // final ItemsAdapterDataMerger itemsAdapterDataMerger = new ItemsAdapterDataMerger(appStateViewModel.getCurrentUser(), viewModel.getThread());
        // itemsAdapterDataMerger.observe(getViewLifecycleOwner(), userThreadPair -> {
        //     viewModel.setCurrentUser(userThreadPair.first);
        //     setupItemsAdapter(userThreadPair.first, userThreadPair.second);
        // });
        threadLiveData.observe(getViewLifecycleOwner(), this::setupItemsAdapter);
        itemsLiveData = viewModel.getItems();
        itemsLiveData.observe(getViewLifecycleOwner(), this::submitItemsToAdapter);
        replyToItemLiveData = viewModel.getReplyToItem();
        replyToItemLiveData.observe(getViewLifecycleOwner(), item -> {
            if (item == null) {
                if (binding.input.length() == 0) {
                    showExtraInputOption(true);
                }
                binding.getRoot().post(() -> {
                    TransitionManager.beginDelayedTransition(binding.getRoot());
                    binding.replyBg.setVisibility(View.GONE);
                    binding.replyInfo.setVisibility(View.GONE);
                    binding.replyPreviewImage.setVisibility(View.GONE);
                    binding.replyCancel.setVisibility(View.GONE);
                    binding.replyPreviewText.setVisibility(View.GONE);
                });
                return;
            }
            showExtraInputOption(false);
            binding.getRoot().postDelayed(() -> {
                binding.replyBg.setVisibility(View.VISIBLE);
                binding.replyInfo.setVisibility(View.VISIBLE);
                binding.replyPreviewImage.setVisibility(View.VISIBLE);
                binding.replyCancel.setVisibility(View.VISIBLE);
                binding.replyPreviewText.setVisibility(View.VISIBLE);
                if (item.getUserId() == viewModel.getViewerId()) {
                    binding.replyInfo.setText(R.string.replying_to_yourself);
                } else {
                    final User user = viewModel.getUser(item.getUserId());
                    if (user != null) {
                        binding.replyInfo.setText(getString(R.string.replying_to_user, user.getFullName()));
                    } else {
                        binding.replyInfo.setVisibility(View.GONE);
                    }
                }
                final String previewText = getDirectItemPreviewText(item);
                binding.replyPreviewText.setText(TextUtils.isEmpty(previewText) ? getString(R.string.message) : previewText);
                final String previewImageUrl = getDirectItemPreviewImageUrl(item);
                if (TextUtils.isEmpty(previewImageUrl)) {
                    binding.replyPreviewImage.setVisibility(View.GONE);
                } else {
                    binding.replyPreviewImage.setImageURI(previewImageUrl);
                }
                binding.replyCancel.setOnClickListener(v -> viewModel.setReplyToItem(null));
            }, 200);
        });
        inputLength.observe(getViewLifecycleOwner(), length -> {
            if (length == null) return;
            final boolean hasReplyToItem = viewModel.getReplyToItem().getValue() != null;
            if (hasReplyToItem) {
                prevLength = length;
                return;
            }
            if ((prevLength == 0 && length != 0) || (prevLength != 0 && length == 0)) {
                showExtraInputOption(length == 0);
            }
            prevLength = length;
        });
        pendingRequestsCountLiveData = viewModel.getPendingRequestsCount();
        pendingRequestsCountLiveData.observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge);
        usersLiveData = viewModel.getUsers();
        usersLiveData.observe(getViewLifecycleOwner(), users -> {
            if (users == null || users.isEmpty()) return;
            final User user = users.get(0);
            binding.acceptPendingRequestQuestion.setText(getString(R.string.accept_request_from_user, user.getUsername(), user.getFullName()));
        });
    }

    private void setupTouchHelper() {
        final Context context = getContext();
        if (context == null) return;
        touchHelperCallback = new SwipeAndRestoreItemTouchHelperCallback(
                context,
                (adapterPosition, viewHolder) -> {
                    if (itemsAdapter == null) return;
                    final DirectItemOrHeader directItemOrHeader = itemsAdapter.getList().get(adapterPosition);
                    if (directItemOrHeader.isHeader()) return;
                    viewModel.setReplyToItem(directItemOrHeader.item);
                }
        );
        itemTouchHelper = new ItemTouchHelper(touchHelperCallback);
        itemTouchHelper.attachToRecyclerView(binding.chats);
    }

    private void removeObservers() {
        pendingLiveData.removeObservers(getViewLifecycleOwner());
        inputModeLiveData.removeObservers(getViewLifecycleOwner());
        threadTitleLiveData.removeObservers(getViewLifecycleOwner());
        fetchingLiveData.removeObservers(getViewLifecycleOwner());
        threadLiveData.removeObservers(getViewLifecycleOwner());
        itemsLiveData.removeObservers(getViewLifecycleOwner());
        replyToItemLiveData.removeObservers(getViewLifecycleOwner());
        inputLength.removeObservers(getViewLifecycleOwner());
        pendingRequestsCountLiveData.removeObservers(getViewLifecycleOwner());
        usersLiveData.removeObservers(getViewLifecycleOwner());

    }

    private void hidePendingOptions() {
        binding.acceptPendingRequestQuestion.setVisibility(View.GONE);
        binding.decline.setVisibility(View.GONE);
        binding.accept.setVisibility(View.GONE);
    }

    private void showPendingOptions() {
        binding.acceptPendingRequestQuestion.setVisibility(View.VISIBLE);
        binding.decline.setVisibility(View.VISIBLE);
        binding.accept.setVisibility(View.VISIBLE);
        binding.accept.setOnClickListener(v -> {
            final LiveData<Resource<Object>> resourceLiveData = viewModel.acceptRequest();
            handlePendingChangeResource(resourceLiveData, false);
        });
        binding.decline.setOnClickListener(v -> {
            final LiveData<Resource<Object>> resourceLiveData = viewModel.declineRequest();
            handlePendingChangeResource(resourceLiveData, true);
        });
    }

    private void handlePendingChangeResource(final LiveData<Resource<Object>> resourceLiveData, final boolean isDecline) {
        resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
            if (resource == null) return;
            final Resource.Status status = resource.status;
            switch (status) {
                case SUCCESS:
                    resourceLiveData.removeObservers(getViewLifecycleOwner());
                    if (isDecline) {
                        removeObservers();
                        viewModel.removeThread();
                        final NavController navController = NavHostFragment.findNavController(this);
                        navController.navigateUp();
                        return;
                    }
                    removeObservers();
                    viewModel.moveFromPending();
                    setObservers();
                    break;
                case LOADING:
                    break;
                case ERROR:
                    if (resource.message != null) {
                        Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
                    }
                    if (resource.resId != 0) {
                        Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
                    }
                    resourceLiveData.removeObservers(getViewLifecycleOwner());
                    break;
            }
        });
    }

    private void hideInput() {
        binding.emojiToggle.setVisibility(View.GONE);
        binding.gif.setVisibility(View.GONE);
        binding.camera.setVisibility(View.GONE);
        binding.gallery.setVisibility(View.GONE);
        binding.input.setVisibility(View.GONE);
        binding.inputBg.setVisibility(View.GONE);
        binding.recordView.setVisibility(View.GONE);
        binding.send.setVisibility(View.GONE);
        if (itemTouchHelper != null) {
            itemTouchHelper.attachToRecyclerView(null);
        }
    }

    private void showInput() {
        binding.emojiToggle.setVisibility(View.VISIBLE);
        binding.gif.setVisibility(View.VISIBLE);
        binding.camera.setVisibility(View.VISIBLE);
        binding.gallery.setVisibility(View.VISIBLE);
        binding.input.setVisibility(View.VISIBLE);
        binding.inputBg.setVisibility(View.VISIBLE);
        binding.recordView.setVisibility(View.VISIBLE);
        binding.send.setVisibility(View.VISIBLE);
        if (itemTouchHelper != null) {
            itemTouchHelper.attachToRecyclerView(binding.chats);
        }
    }

    @SuppressLint("UnsafeOptInUsageError")
    private void attachPendingRequestsBadge(@Nullable final Integer count) {
        if (pendingRequestCountBadgeDrawable == null) {
            final Context context = getContext();
            if (context == null) return;
            pendingRequestCountBadgeDrawable = BadgeDrawable.create(context);
        }
        if (count == null || count == 0) {
            @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils
                    .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info);
            if (menuItemView != null) {
                BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info);
            }
            isPendingRequestCountBadgeAttached = false;
            pendingRequestCountBadgeDrawable.setNumber(0);
            return;
        }
        if (pendingRequestCountBadgeDrawable.getNumber() == count) return;
        pendingRequestCountBadgeDrawable.setNumber(count);
        if (!isPendingRequestCountBadgeAttached) {
            BadgeUtils.attachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info);
            isPendingRequestCountBadgeAttached = true;
        }
    }

    private void showExtraInputOption(final boolean show) {
        if (show) {
            if (!binding.send.isListenForRecord()) {
                binding.send.setListenForRecord(true);
                startIconAnimation();
            }
            binding.gif.setVisibility(View.VISIBLE);
            binding.camera.setVisibility(View.VISIBLE);
            binding.gallery.setVisibility(View.VISIBLE);
            return;
        }
        if (binding.send.isListenForRecord()) {
            binding.send.setListenForRecord(false);
            startIconAnimation();
        }
        binding.gif.setVisibility(View.GONE);
        binding.camera.setVisibility(View.GONE);
        binding.gallery.setVisibility(View.GONE);
    }

    private String getDirectItemPreviewText(@NonNull final DirectItem item) {
        final DirectItemType itemType = item.getItemType();
        if (itemType == null) return "";
        switch (itemType) {
            case TEXT:
                return item.getText();
            case LINK:
                final DirectItemLink link = item.getLink();
                if (link == null) return "";
                return link.getText();
            case MEDIA: {
                final Media media = item.getMedia();
                if (media == null) return "";
                return getMediaPreviewTextString(media);
            }
            case RAVEN_MEDIA: {
                final DirectItemVisualMedia visualMedia = item.getVisualMedia();
                if (visualMedia == null) return "";
                final Media media = visualMedia.getMedia();
                if (media == null) return "";
                return getMediaPreviewTextString(media);
            }
            case VOICE_MEDIA:
                return getString(R.string.voice_message);
            case MEDIA_SHARE:
                return getString(R.string.post);
            case REEL_SHARE:
                final DirectItemReelShare reelShare = item.getReelShare();
                if (reelShare == null) return "";
                return reelShare.getText();
        }
        return "";
    }

    @NonNull
    private String getMediaPreviewTextString(@NonNull final Media media) {
        final MediaItemType mediaType = media.getType();
        if (mediaType == null) return "";
        switch (mediaType) {
            case MEDIA_TYPE_IMAGE:
                return getString(R.string.photo);
            case MEDIA_TYPE_VIDEO:
                return getString(R.string.video);
            default:
                return "";
        }
    }

    @Nullable
    private String getDirectItemPreviewImageUrl(@NonNull final DirectItem item) {
        final DirectItemType itemType = item.getItemType();
        if (itemType == null) return null;
        switch (itemType) {
            case TEXT:
            case LINK:
            case VOICE_MEDIA:
            case REEL_SHARE:
                return null;
            case MEDIA: {
                final Media media = item.getMedia();
                return ResponseBodyUtils.getThumbUrl(media);
            }
            case RAVEN_MEDIA: {
                final DirectItemVisualMedia visualMedia = item.getVisualMedia();
                if (visualMedia == null) return null;
                final Media media = visualMedia.getMedia();
                return ResponseBodyUtils.getThumbUrl(media);
            }
            case MEDIA_SHARE: {
                final Media media = item.getMediaShare();
                return ResponseBodyUtils.getThumbUrl(media);
            }
        }
        return null;
    }

    private void setupBackStackResultObserver() {
        final NavController navController = NavHostFragment.findNavController(this);
        final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry();
        if (backStackEntry != null) {
            backStackSavedStateResultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result");
            backStackSavedStateResultLiveData.observe(getViewLifecycleOwner(), backStackSavedStateObserver);
        }
    }

    private void submitItemsToAdapter(final List<DirectItem> items) {
        binding.chats.post(() -> {
            if (autoMarkAsSeen) {
                viewModel.markAsSeen();
                return;
            }
            final DirectThread thread = threadLiveData.getValue();
            if (thread == null) return;
            if (markAsSeenMenuItem != null) {
                markAsSeenMenuItem.setEnabled(!DMUtils.isRead(thread));
            }
        });
        if (itemsAdapter == null) return;
        itemsAdapter.submitList(items, () -> {
            itemOrHeaders = itemsAdapter.getList();
            binding.chats.post(() -> {
                final RecyclerView.LayoutManager layoutManager = binding.chats.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    final int position = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
                    if (position < 0) return;
                    if (position == itemsAdapter.getItemCount() - 1) {
                        viewModel.fetchChats();
                    }
                }
            });
        });
    }

    private void setupItemsAdapter(final DirectThread thread) {
        if (thread == null) return;
        if (itemsAdapter != null) {
            if (itemsAdapter.getThread() == thread) return;
            itemsAdapter.setThread(thread);
            return;
        }
        final Resource<User> currentUserResource = appStateViewModel.getCurrentUser();
        if (currentUserResource == null) return;
        final User currentUser = currentUserResource.data;
        if (currentUser == null) return;
        itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback, directItemLongClickListener);
        itemsAdapter.setHasStableIds(true);
        itemsAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
        binding.chats.setAdapter(itemsAdapter);
        registerDataObserver();
        final List<DirectItem> items = viewModel.getItems().getValue();
        if (items != null && itemsAdapter.getItems() != items) {
            submitItemsToAdapter(items);
        }
    }

    private void registerDataObserver() {
        itemsAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {

            @Override
            public void onItemRangeInserted(final int positionStart, final int itemCount) {
                super.onItemRangeInserted(positionStart, itemCount);
                final LinearLayoutManager layoutManager = (LinearLayoutManager) binding.chats.getLayoutManager();
                if (layoutManager == null) return;
                int firstVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
                if ((firstVisiblePosition == -1 || firstVisiblePosition == 0) && (positionStart == 0)) {
                    binding.chats.scrollToPosition(0);
                }
            }
        });
    }

    private void setupInput(@Nullable final Integer inputMode) {
        if (inputMode != null && inputMode == 1) return;
        final Context context = getContext();
        if (context == null) return;
        tooltip.setText(R.string.dms_thread_audio_hint);
        setMicToSendIcon();
        binding.recordView.setMinMillis(1000);
        binding.recordView.setOnRecordListener(new RecordView.OnRecordListener() {
            @Override
            public void onStart() {
                isRecording = true;
                binding.input.setHint(null);
                binding.gif.setVisibility(View.GONE);
                binding.camera.setVisibility(View.GONE);
                binding.gallery.setVisibility(View.GONE);
                if (PermissionUtils.hasAudioRecordPerms(context)) {
                    viewModel.startRecording();
                    return;
                }
                PermissionUtils.requestAudioRecordPerms(DirectMessageThreadFragment.this, AUDIO_RECORD_PERM_REQUEST_CODE);
            }

            @Override
            public void onCancel() {
                Log.d(TAG, "onCancel");
                // binding.input.setHint("Message");
                viewModel.stopRecording(true);
                isRecording = false;
            }

            @Override
            public void onFinish(final long recordTime) {
                Log.d(TAG, "onFinish");
                binding.input.setHint("Message");
                binding.gif.setVisibility(View.VISIBLE);
                binding.camera.setVisibility(View.VISIBLE);
                binding.gallery.setVisibility(View.VISIBLE);
                viewModel.stopRecording(false);
                isRecording = false;
            }

            @Override
            public void onLessThanMin() {
                Log.d(TAG, "onLessThanMin");
                binding.input.setHint("Message");
                if (PermissionUtils.hasAudioRecordPerms(context)) {
                    tooltip.show(binding.send);
                }
                binding.gif.setVisibility(View.VISIBLE);
                binding.camera.setVisibility(View.VISIBLE);
                binding.gallery.setVisibility(View.VISIBLE);
                viewModel.stopRecording(true);
                isRecording = false;
            }
        });
        binding.recordView.setOnBasketAnimationEndListener(() -> {
            binding.input.setHint(R.string.dms_thread_message_hint);
            binding.gif.setVisibility(View.VISIBLE);
            binding.camera.setVisibility(View.VISIBLE);
            binding.gallery.setVisibility(View.VISIBLE);
        });
        binding.input.addTextChangedListener(new TextWatcherAdapter() {
            // int prevLength = 0;

            @Override
            public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
                final int length = s.length();
                inputLength.postValue(length);
            }
        });
        binding.send.setOnRecordClickListener(v -> {
            final Editable text = binding.input.getText();
            if (TextUtils.isEmpty(text)) return;
            final LiveData<Resource<Object>> resourceLiveData = viewModel.sendText(text.toString());
            resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData));
            binding.input.setText("");
            viewModel.setReplyToItem(null);
        });
        binding.send.setOnRecordLongClickListener(v -> {
            Log.d(TAG, "setOnRecordLongClickListener");
            return true;
        });
        binding.input.setOnFocusChangeListener((v, hasFocus) -> {
            if (!hasFocus) return;
            final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue();
            if (emojiPickerVisibleValue == null || !emojiPickerVisibleValue) return;
            inputHolderAnimationCallback.setShouldTranslate(false);
            chatsAnimationCallback.setShouldTranslate(false);
            emojiPickerAnimationCallback.setShouldTranslate(false);
        });
        setupInsetsCallback();
        setupEmojiPicker();
        binding.gallery.setOnClickListener(v -> {
            final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.setType("*/*");
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{
                    "image/*",
                    "video/mp4"
            });
            startActivityForResult(intent, FILE_PICKER_REQUEST_CODE);
        });
        binding.gif.setOnClickListener(v -> {
            final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance();
            gifPicker.setOnSelectListener(giphyGif -> {
                gifPicker.dismiss();
                if (giphyGif == null) return;
                handleSentMessage(viewModel.sendAnimatedMedia(giphyGif));
            });
            gifPicker.show(getChildFragmentManager(), "GifPicker");
        });
        binding.camera.setOnClickListener(v -> {
            final Intent intent = new Intent(context, CameraActivity.class);
            startActivityForResult(intent, CAMERA_REQUEST_CODE);
        });
    }

    private void setupInsetsCallback() {
        inputHolderAnimationCallback = new TranslateDeferringInsetsAnimationCallback(
                binding.inputHolder,
                WindowInsetsCompat.Type.systemBars(),
                WindowInsetsCompat.Type.ime(),
                WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE
        );
        ViewCompat.setWindowInsetsAnimationCallback(binding.inputHolder, inputHolderAnimationCallback);
        chatsAnimationCallback = new TranslateDeferringInsetsAnimationCallback(
                binding.chats,
                WindowInsetsCompat.Type.systemBars(),
                WindowInsetsCompat.Type.ime()
        );
        ViewCompat.setWindowInsetsAnimationCallback(binding.chats, chatsAnimationCallback);
        emojiPickerAnimationCallback = new EmojiPickerInsetsAnimationCallback(
                binding.emojiPicker,
                WindowInsetsCompat.Type.systemBars(),
                WindowInsetsCompat.Type.ime()
        );
        emojiPickerAnimationCallback.setKbVisibilityListener(this::onKbVisibilityChange);
        ViewCompat.setWindowInsetsAnimationCallback(binding.emojiPicker, emojiPickerAnimationCallback);
        ViewCompat.setWindowInsetsAnimationCallback(
                binding.input,
                new ControlFocusInsetsAnimationCallback(binding.input)
        );
        final SimpleImeAnimationController imeAnimController = root.getImeAnimController();
        if (imeAnimController != null) {
            imeAnimController.setAnimationControlListener(new WindowInsetsAnimationControlListenerCompat() {
                @Override
                public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {}

                @Override
                public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) {
                    checkKbVisibility();
                }

                @Override
                public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) {
                    checkKbVisibility();
                }

                private void checkKbVisibility() {
                    final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(binding.getRoot());
                    final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
                    onKbVisibilityChange(visible);
                }
            });
        }
    }

    private void onKbVisibilityChange(final boolean kbVisible) {
        this.kbVisible.postValue(kbVisible);
        if (wasToggled) {
            emojiPickerVisible.postValue(!kbVisible);
            wasToggled = false;
            return;
        }
        final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue();
        if (kbVisible && emojiPickerVisibleValue != null && emojiPickerVisibleValue) {
            emojiPickerVisible.postValue(false);
            return;
        }
        if (!kbVisible) {
            emojiPickerVisible.postValue(false);
        }
    }

    private void startIconAnimation() {
        final Drawable icon = binding.send.getIcon();
        if (icon instanceof Animatable) {
            final Animatable animatable = (Animatable) icon;
            if (animatable.isRunning()) {
                animatable.stop();
            }
            animatable.start();
        }
    }

    private void navigateToImageEditFragment(final String path) {
        navigateToImageEditFragment(Uri.fromFile(new File(path)));
    }

    private void navigateToImageEditFragment(final Uri uri) {
        try {
            final NavDirections navDirections = DirectMessageThreadFragmentDirections.actionToImageEdit(uri);
            NavHostFragment.findNavController(this).navigate(navDirections);
        } catch (Exception e) {
            Log.e(TAG, "navigateToImageEditFragment: ", e);
        }
    }

    private void handleSentMessage(final LiveData<Resource<Object>> resourceLiveData) {
        final Resource<Object> resource = resourceLiveData.getValue();
        if (resource == null) return;
        final Resource.Status status = resource.status;
        switch (status) {
            case SUCCESS:
                resourceLiveData.removeObservers(getViewLifecycleOwner());
                break;
            case LOADING:
                break;
            case ERROR:
                if (resource.message != null) {
                    Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
                }
                if (resource.resId != 0) {
                    Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show();
                }
                resourceLiveData.removeObservers(getViewLifecycleOwner());
                break;
        }
    }

    private void setupEmojiPicker() {
        root.post(() -> binding.emojiPicker.init(
                root,
                (view, emoji) -> {
                    final KeyNotifyingEmojiEditText input = binding.input;
                    final int start = input.getSelectionStart();
                    final int end = input.getSelectionEnd();
                    if (start < 0) {
                        input.append(emoji.getUnicode());
                        return;
                    }
                    input.getText().replace(
                            Math.min(start, end),
                            Math.max(start, end),
                            emoji.getUnicode(),
                            0,
                            emoji.getUnicode().length()
                    );
                },
                () -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
        ));
        binding.emojiToggle.setOnClickListener(v -> {
            Boolean isEmojiPickerVisible = emojiPickerVisible.getValue();
            if (isEmojiPickerVisible == null) isEmojiPickerVisible = false;
            Boolean isKbVisible = kbVisible.getValue();
            if (isKbVisible == null) isKbVisible = false;
            wasToggled = isEmojiPickerVisible || isKbVisible;

            if (isEmojiPickerVisible) {
                if (hasKbOpenedOnce && binding.emojiPicker.getTranslationY() != 0) {
                    inputHolderAnimationCallback.setShouldTranslate(false);
                    chatsAnimationCallback.setShouldTranslate(false);
                    emojiPickerAnimationCallback.setShouldTranslate(false);
                }
                // trigger ime.
                // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here
                showKeyboard();
                return;
            }

            if (isKbVisible) {
                // hide the keyboard, but don't translate the views
                // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here
                inputHolderAnimationCallback.setShouldTranslate(false);
                chatsAnimationCallback.setShouldTranslate(false);
                emojiPickerAnimationCallback.setShouldTranslate(false);
                hideKeyboard();
            }
            emojiPickerVisible.postValue(true);
        });
        final LiveData<Pair<Boolean, Boolean>> emojiKbVisibilityLD = Utils.zipLiveData(emojiPickerVisible, kbVisible);
        emojiKbVisibilityLD.observe(getViewLifecycleOwner(), pair -> {
            Boolean isEmojiPickerVisible = pair.first;
            Boolean isKbVisible = pair.second;
            if (isEmojiPickerVisible == null) isEmojiPickerVisible = false;
            if (isKbVisible == null) isKbVisible = false;
            root.setScrollImeOffScreenWhenVisible(!isEmojiPickerVisible);
            root.setScrollImeOnScreenWhenNotVisible(!isEmojiPickerVisible);
            onEmojiPickerBackPressedCallback.setEnabled(isEmojiPickerVisible && !isKbVisible);
            if (isEmojiPickerVisible && !isKbVisible) {
                animatePan(binding.emojiPicker.getMeasuredHeight(), unused -> {
                    binding.emojiPicker.setAlpha(1);
                    binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24);
                    return null;
                }, null);
                return;
            }
            if (!isEmojiPickerVisible && !isKbVisible) {
                animatePan(0, null, unused -> {
                    binding.emojiPicker.setAlpha(0);
                    binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
                    return null;
                });
                return;
            }
            // isKbVisible will always be true going forward
            hasKbOpenedOnce = true;
            if (!isEmojiPickerVisible) {
                binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
                binding.emojiPicker.setAlpha(0);
                return;
            }
            binding.emojiPicker.setAlpha(1);
        });
    }

    public void showKeyboard() {
        final Context context = getContext();
        if (context == null) return;
        final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm == null) return;
        if (!binding.input.isFocused()) {
            binding.input.requestFocus();
        }
        final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT);
        if (!shown) {
            Log.e(TAG, "showKeyboard: System did not display the keyboard");
        }
    }

    public void hideKeyboard() {
        final Context context = getContext();
        if (context == null) return;
        final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm == null) return;
        imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
    }

    private void setSendToMicIcon() {
        final Context context = getContext();
        if (context == null) return;
        final Drawable sendToMicDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_send_to_mic_anim);
        if (sendToMicDrawable instanceof Animatable) {
            AnimatedVectorDrawableCompat.registerAnimationCallback(sendToMicDrawable, sendToMicAnimationCallback);
        }
        binding.send.setIcon(sendToMicDrawable);
    }

    private void setMicToSendIcon() {
        final Context context = getContext();
        if (context == null) return;
        final Drawable micToSendDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_mic_to_send_anim);
        if (micToSendDrawable instanceof Animatable) {
            AnimatedVectorDrawableCompat.registerAnimationCallback(micToSendDrawable, micToSendAnimationCallback);
        }
        binding.send.setIcon(micToSendDrawable);
    }

    private void setTitle(final String title) {
        if (actionBar == null) return;
        if (prevTitleRunnable != null) {
            appExecutors.getMainThread().cancel(prevTitleRunnable);
        }
        prevTitleRunnable = () -> actionBar.setTitle(title);
        // set title delayed to avoid title blink if fetch is fast
        appExecutors.getMainThread().execute(prevTitleRunnable, 1000);
    }

    private void downloadItem(final DirectItem item) {
        final Context context = getContext();
        if (context == null) return;
        final DirectItemType itemType = item.getItemType();
        if (itemType == null) return;
        //noinspection SwitchStatementWithTooFewBranches
        switch (itemType) {
            case VOICE_MEDIA:
                downloadItem(context, item.getVoiceMedia() == null ? null : item.getVoiceMedia().getMedia());
                break;
            default:
                break;
        }
    }

    // currently ONLY for voice
    private void downloadItem(@NonNull final Context context, final Media media) {
        if (media == null) {
            Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
            return;
        }
        DownloadUtils.download(context, media);
        Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show();
    }

    // Sets the translationY of views to height with animation
    private void animatePan(final int height,
                            @Nullable final Function<Void, Void> onAnimationStart,
                            @Nullable final Function<Void, Void> onAnimationEnd) {
        if (animatorSet != null && animatorSet.isStarted()) {
            animatorSet.cancel();
        }
        final ImmutableList.Builder<Animator> builder = ImmutableList.builder();
        builder.add(
                ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height),
                ObjectAnimator.ofFloat(binding.inputHolder, TRANSLATION_Y, -height),
                ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, -height)
        );
        // if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) {
        //     builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height));
        // }
        animatorSet = new AnimatorSet();
        animatorSet.playTogether(builder.build());
        animatorSet.setDuration(200);
        animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN);
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(final Animator animation) {
                super.onAnimationStart(animation);
                if (onAnimationStart != null) {
                    onAnimationStart.apply(null);
                }
            }

            @Override
            public void onAnimationEnd(final Animator animation) {
                super.onAnimationEnd(animation);
                animatorSet = null;
                if (onAnimationEnd != null) {
                    onAnimationEnd.apply(null);
                }
            }
        });
        animatorSet.start();
    }

    private void showReactionsDialog(final DirectItem item) {
        final LiveData<List<User>> users = viewModel.getUsers();
        final LiveData<List<User>> leftUsers = viewModel.getLeftUsers();
        final ArrayList<User> allUsers = new ArrayList<>();
        allUsers.add(viewModel.getCurrentUser());
        if (users.getValue() != null) {
            allUsers.addAll(users.getValue());
        }
        if (leftUsers.getValue() != null) {
            allUsers.addAll(leftUsers.getValue());
        }
        final String itemId = item.getItemId();
        if (itemId == null) return;
        final DirectItemReactions reactions = item.getReactions();
        if (reactions == null) return;
        reactionDialogFragment = DirectItemReactionDialogFragment.newInstance(
                viewModel.getViewerId(),
                allUsers,
                itemId,
                reactions
        );
        reactionDialogFragment.show(getChildFragmentManager(), "reactions_dialog");
    }

    @Override
    public void onReactionClick(final String itemId, final DirectItemEmojiReaction reaction) {
        if (reactionDialogFragment != null) {
            reactionDialogFragment.dismiss();
        }
        if (itemId == null || reaction == null) return;
        if (reaction.getSenderId() == viewModel.getViewerId()) {
            final LiveData<Resource<Object>> resourceLiveData = viewModel.sendDeleteReaction(itemId);
            resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData));
            return;
        }
        // navigate to user
        final User user = viewModel.getUser(reaction.getSenderId());
        if (user == null) return;
        navigateToUser(user.getUsername());
    }

    private void navigateToUser(@NonNull final String username) {
        try {
            final NavDirections direction = DirectMessageThreadFragmentDirections.actionToProfile().setUsername(username);
            NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(direction);
        } catch (Exception e) {
            Log.e(TAG, "navigateToUser: ", e);
        }
    }

    @Override
    public void onClick(final View view, final Emoji emoji) {
        if (addReactionItem == null || emoji == null) return;
        final LiveData<Resource<Object>> resourceLiveData = viewModel.sendReaction(addReactionItem, emoji);
        resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData));
    }
}
