/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 */

import type {MyDocument} from '../../lib/appManagers/appDocsManager';
import type Chat from './chat';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import ButtonMenu, {ButtonMenuItemOptions} from '../buttonMenu';
import PopupDeleteMessages from '../popups/deleteMessages';
import PopupForward from '../popups/forward';
import PopupPinMessage from '../popups/unpinMessage';
import {copyTextToClipboard} from '../../helpers/clipboard';
import PopupSendNow from '../popups/sendNow';
import {toast} from '../toast';
import I18n, {i18n, LangPackKey} from '../../lib/langPack';
import findUpClassName from '../../helpers/dom/findUpClassName';
import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import isSelectionEmpty from '../../helpers/dom/isSelectionEmpty';
import {Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction, MessageEntity, InputStickerSet, StickerSet, Document, Reaction, Photo} from '../../layer';
import PopupReportMessages from '../popups/reportMessages';
import assumeType from '../../helpers/assumeType';
import PopupSponsored from '../popups/sponsored';
import ListenerSetter from '../../helpers/listenerSetter';
import {getMiddleware} from '../../helpers/middleware';
import PeerTitle from '../peerTitle';
import StackedAvatars from '../stackedAvatars';
import {IS_APPLE} from '../../environment/userAgent';
import PopupReactedList from '../popups/reactedList';
import {ChatReactionsMenu, REACTION_CONTAINER_SIZE} from './reactionsMenu';
import getPeerId from '../../lib/appManagers/utils/peers/getPeerId';
import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
import {AppManagers} from '../../lib/appManagers/managers';
import positionMenu, {MenuPositionPadding} from '../../helpers/positionMenu';
import contextMenuController from '../../helpers/contextMenuController';
import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuListener';
import filterAsync from '../../helpers/array/filterAsync';
import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {SERVICE_PEER_ID} from '../../lib/mtproto/mtproto_config';
import {MessagesStorageKey, MyMessage} from '../../lib/appManagers/appMessagesManager';
import filterUnique from '../../helpers/array/filterUnique';
import replaceContent from '../../helpers/dom/replaceContent';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import PopupStickers from '../popups/stickers';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
import canSaveMessageMedia from '../../lib/appManagers/utils/messages/canSaveMessageMedia';
import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText';

export default class ChatContextMenu {
  private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise<boolean>, notDirect?: () => boolean, withSelection?: true, isSponsored?: true, localName?: 'views' | 'emojis'})[];
  private element: HTMLElement;

  private isSelectable: boolean;
  private isSelected: boolean;
  private target: HTMLElement;
  private isTargetAGroupedItem: boolean;
  private isTextSelected: boolean;
  private isAnchorTarget: boolean;
  private isUsernameTarget: boolean;
  private isSponsored: boolean;
  private isOverBubble: boolean;
  private peerId: PeerId;
  private mid: number;
  private message: Message.message | Message.messageService;
  private noForwards: boolean;

  private reactionsMenu: ChatReactionsMenu;
  private listenerSetter: ListenerSetter;
  private attachListenerSetter: ListenerSetter;

  private viewerPeerId: PeerId;
  private middleware: ReturnType<typeof getMiddleware>;
  private canOpenReactedList: boolean;

  private emojiInputsPromise: CancellablePromise<InputStickerSet.inputStickerSetID[]>;
  private albumMessages: Message.message[];
  private linkToMessage: Awaited<ReturnType<ChatContextMenu['getUrlToMessage']>>;
  private selectedMessagesText: string;
  private selectedMessages: MyMessage[];

  constructor(
    private chat: Chat,
    private managers: AppManagers
  ) {
    this.listenerSetter = new ListenerSetter();
    this.attachListenerSetter = new ListenerSetter();
    this.middleware = getMiddleware();
  }

  public attachTo(element: HTMLElement) {
    this.attachListenerSetter.removeAll();

    if(IS_TOUCH_SUPPORTED/*  && false */) {
      attachClickEvent(element, (e) => {
        if(this.chat.selection.isSelecting) {
          return;
        }

        this.chat.log('touchend', e);

        const badSelectors = [
          '.name',
          '.peer-title',
          '.reply',
          '.document',
          'audio-element',
          'avatar-element',
          'a',
          '.bubble-beside-button',
          'replies-element',
          '[data-saved-from]:not(.bubble)',
          'poll-element',
          '.attachment',
          '.reply-markup-button'
        ];
        const good = !(e.target as HTMLElement).closest(badSelectors.join(', '));
        if(good) {
          cancelEvent(e);
          // onContextMenu((e as TouchEvent).changedTouches[0]);
          // onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent);
          this.onContextMenu(e);
        }
      }, {listenerSetter: this.attachListenerSetter});
    } else attachContextMenuListener({
      element,
      callback: this.onContextMenu,
      listenerSetter: this.attachListenerSetter
    });
  }

  private onContextMenu = (e: MouseEvent | Touch | TouchEvent) => {
    let bubble: HTMLElement, contentWrapper: HTMLElement;

    try {
      contentWrapper = findUpClassName(e.target, 'bubble-content-wrapper');
      bubble = contentWrapper ? contentWrapper.parentElement : findUpClassName(e.target, 'bubble');
    } catch(e) {}

    // ! context menu click by date bubble (there is no pointer-events)
    if(!bubble || bubble.classList.contains('bubble-first')) return;

    let element = this.element;
    if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault();
    if(element && element.classList.contains('active')) {
      return false;
    }
    if(e instanceof MouseEvent || e.hasOwnProperty('cancelBubble')) (e as any).cancelBubble = true;

    let mid = +bubble.dataset.mid;
    if(!mid) return;

    const r = async() => {
      const isSponsored = this.isSponsored = mid < 0;
      this.isSelectable = this.chat.selection.canSelectBubble(bubble);
      this.peerId = this.chat.peerId;
      // this.msgID = msgID;
      this.target = e.target as HTMLElement;
      this.isTextSelected = !isSelectionEmpty();
      this.isAnchorTarget = this.target.tagName === 'A' && (
        (this.target as HTMLAnchorElement).target === '_blank' ||
        this.target.classList.contains('anchor-url')
      );
      this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention');

      const mids = await this.chat.getMidsByMid(mid);
      // * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
      if(this.chat.selection.isSelecting && !contentWrapper) {
        if(isSponsored) {
          return;
        }

        if(mids.length > 1) {
          const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ?
            mid :
            mids.find((mid) => this.chat.selection.isMidSelected(this.peerId, mid));
          if(selectedMid) {
            mid = selectedMid;
          }
        }
      }

      this.isOverBubble = !!contentWrapper;

      const groupedItem = findUpClassName(this.target, 'grouped-item');
      this.isTargetAGroupedItem = !!groupedItem;
      if(groupedItem) {
        this.mid = +groupedItem.dataset.mid;
      } else {
        this.mid = mid;
      }

      this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
      this.message = (bubble as any).message || await this.chat.getMessage(this.mid);
      this.albumMessages = (this.message as Message.message).grouped_id ? await this.managers.appMessagesManager.getMessagesByAlbum((this.message as Message.message).grouped_id) : undefined;
      this.noForwards = !isSponsored && !(await this.managers.appMessagesManager.canForward(this.message));
      this.viewerPeerId = undefined;
      this.canOpenReactedList = undefined;
      this.linkToMessage = await this.getUrlToMessage();
      this.selectedMessagesText = await this.getSelectedMessagesText();
      this.selectedMessages = this.chat.selection.isSelecting ? await this.chat.selection.getSelectedMessages() : undefined;

      const initResult = await this.init();
      if(!initResult) {
        return;
      }

      element = initResult.element;
      const {cleanup, destroy, menuPadding, reactionsMenu, reactionsMenuPosition} = initResult;
      let isReactionsMenuVisible = false;
      if(reactionsMenu) {
        const className = 'is-visible';
        isReactionsMenuVisible = reactionsMenu.container.classList.contains(className);
        if(isReactionsMenuVisible) reactionsMenu.container.classList.remove(className);

        if(reactionsMenuPosition === 'horizontal') {
          const offsetSize = element[/* reactionsMenuPosition === 'vertical' ? 'offsetHeight' :  */'offsetWidth'];
          // if(reactionsMenu.scrollable.container.scrollWidth > offsetWidth) {
          const INNER_CONTAINER_PADDING = 8;
          const visibleLength = (offsetSize - INNER_CONTAINER_PADDING) / REACTION_CONTAINER_SIZE;
          const nextVisiblePart = visibleLength % 1;
          const MIN_NEXT_VISIBLE_PART = 0.65;
          if(nextVisiblePart < MIN_NEXT_VISIBLE_PART) {
            const minSize = (offsetSize + (MIN_NEXT_VISIBLE_PART - nextVisiblePart) * REACTION_CONTAINER_SIZE) | 0;
            element.style[/* reactionsMenuPosition === 'vertical' ? 'minHeight' :  */'minWidth'] = minSize + 'px';
          }
          // }
        }
      }

      const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
      // bubble.parentElement.append(element);
      // appImManager.log('contextmenu', e, bubble, side);
      positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side, menuPadding);

      if(reactionsMenu) {
        reactionsMenu.widthContainer.style.top = element.style.top;
        reactionsMenu.widthContainer.style.left = element.style.left;
        reactionsMenu.widthContainer.style.setProperty('--menu-width', element[reactionsMenuPosition === 'vertical' ? 'offsetHeight' : 'offsetWidth'] + 'px');
        element.parentElement.append(reactionsMenu.widthContainer);
        if(isReactionsMenuVisible) void reactionsMenu.container.offsetLeft; // reflow
      }

      contextMenuController.openBtnMenu(element, () => {
        if(reactionsMenu) {
          reactionsMenu.container.classList.remove('is-visible');
        }

        this.mid = 0;
        this.peerId = undefined;
        this.target = null;
        this.viewerPeerId = undefined;
        this.canOpenReactedList = undefined;
        cleanup();

        setTimeout(() => {
          destroy();
        }, 300);
      });

      if(isReactionsMenuVisible) {
        reactionsMenu.container.classList.add('is-visible');
      }
    };

    r();
  };

  public cleanup() {
    this.listenerSetter.removeAll();
    this.reactionsMenu && this.reactionsMenu.cleanup();
    this.middleware.clean();
  }

  public destroy() {
    this.cleanup();
    this.attachListenerSetter.removeAll();
  }

  private async filterButtons(buttons: ChatContextMenu['buttons']) {
    if(this.isSponsored) {
      return buttons.filter((button) => {
        return button.isSponsored;
      });
    } else {
      return filterAsync(buttons, async(button) => {
        let good: boolean;

        // if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
        if(this.chat.selection.isSelecting && !button.withSelection) {
          good = false;
        } else {
          good = this.isOverBubble || IS_TOUCH_SUPPORTED || true ?
            await button.verify() :
            button.notDirect && await button.verify() && button.notDirect();
        }

        return !!good;
      });
    }
  }

  private setButtons() {
    const verifyFavoriteSticker = async(toAdd: boolean) => {
      const doc = ((this.message as Message.message).media as MessageMedia.messageMediaDocument)?.document;
      if(!(doc as MyDocument)?.sticker) {
        return false;
      }

      const favedStickers = await this.managers.acknowledged.appStickersManager.getFavedStickersStickers();
      if(!favedStickers.cached) {
        return false;
      }

      const found = (await favedStickers.result).some((_doc) => _doc.id === doc.id);
      return toAdd ? !found : found;
    };

    this.buttons = [{
      icon: 'send2',
      text: 'MessageScheduleSend',
      onClick: this.onSendScheduledClick,
      verify: () => this.chat.type === 'scheduled' && !this.message.pFlags.is_outgoing
    }, {
      icon: 'send2',
      text: 'Message.Context.Selection.SendNow',
      onClick: this.onSendScheduledClick,
      verify: () => this.chat.type === 'scheduled' && this.isSelected && !this.chat.selection.selectionSendNowBtn.hasAttribute('disabled'),
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'schedule',
      text: 'MessageScheduleEditTime',
      onClick: () => {
        this.chat.input.scheduleSending(() => {
          assumeType<Message.message>(this.message);
          this.managers.appMessagesManager.editMessage(this.message, this.message.message, {
            scheduleDate: this.chat.input.scheduleDate,
            entities: this.message.entities
          });

          this.chat.input.onMessageSent(false, false);
        }, new Date(this.message.date * 1000));
      },
      verify: () => this.chat.type === 'scheduled'
    }, {
      icon: 'reply',
      text: 'Reply',
      onClick: this.onReplyClick,
      verify: async() => await this.chat.canSend() &&
        !this.message.pFlags.is_outgoing &&
        !!this.chat.input.messageInput &&
        this.chat.type !== 'scheduled'/* ,
      cancelEvent: true */
    }, {
      icon: 'favourites',
      text: 'AddToFavorites',
      onClick: this.onFaveStickerClick.bind(this, false),
      verify: () => verifyFavoriteSticker(true)
    }, {
      icon: 'favourites',
      text: 'DeleteFromFavorites',
      onClick: this.onFaveStickerClick.bind(this, true),
      verify: () => verifyFavoriteSticker(false)
    }, {
      icon: 'edit',
      text: 'Edit',
      onClick: this.onEditClick,
      verify: async() => (await this.managers.appMessagesManager.canEditMessage(this.message, 'text')) && !!this.chat.input.messageInput
    }, {
      icon: 'copy',
      text: 'Copy',
      onClick: this.onCopyClick,
      verify: () => !this.noForwards && !!(this.message as Message.message).message && !this.isTextSelected && (!this.isAnchorTarget || (this.message as Message.message).message !== this.target.innerText)
    }, {
      icon: 'copy',
      text: 'Chat.CopySelectedText',
      onClick: this.onCopyClick,
      verify: () => !this.noForwards && !!(this.message as Message.message).message && this.isTextSelected
    }, {
      icon: 'copy',
      text: 'Message.Context.Selection.Copy',
      onClick: this.onCopyClick,
      verify: async() => {
        if(!this.isSelected || this.noForwards) {
          return false;
        }

        for(const [peerId, mids] of this.chat.selection.selectedMids) {
          const storageKey: MessagesStorageKey = `${peerId}_${this.chat.type === 'scheduled' ? 'scheduled' : 'history'}`;
          for(const mid of mids) {
            const message = (await this.managers.appMessagesManager.getMessageFromStorage(storageKey, mid)) as Message.message;
            if(!!message.message) {
              return true;
            }
          }
        }

        return false;
      },
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'copy',
      text: 'CopyLink',
      onClick: this.onCopyAnchorLinkClick,
      verify: () => this.isAnchorTarget,
      withSelection: true
    }, {
      icon: 'copy',
      text: 'Text.Context.Copy.Username',
      onClick: () => {
        copyTextToClipboard(this.target.innerHTML);
      },
      verify: () => this.isUsernameTarget,
      withSelection: true
    }, {
      icon: 'copy',
      text: 'Text.Context.Copy.Hashtag',
      onClick: () => {
        copyTextToClipboard(this.target.innerHTML);
      },
      verify: () => this.target.classList.contains('anchor-hashtag'),
      withSelection: true
    }, {
      icon: 'link',
      text: 'MessageContext.CopyMessageLink1',
      onClick: this.onCopyLinkClick,
      verify: async() => await this.managers.appPeersManager.isChannel(this.peerId) && !this.message.pFlags.is_outgoing
    }, {
      icon: 'pin',
      text: 'Message.Context.Pin',
      onClick: this.onPinClick,
      verify: async() => !this.message.pFlags.is_outgoing &&
        this.message._ !== 'messageService' &&
        !this.message.pFlags.pinned &&
        await this.managers.appPeersManager.canPinMessage(this.peerId) &&
        this.chat.type !== 'scheduled'
    }, {
      icon: 'unpin',
      text: 'Message.Context.Unpin',
      onClick: this.onUnpinClick,
      verify: async() => (this.message as Message.message).pFlags.pinned && await this.managers.appPeersManager.canPinMessage(this.peerId)
    }, {
      icon: 'download',
      text: 'MediaViewer.Context.Download',
      onClick: () => {
        appDownloadManager.downloadToDisc({media: getMediaFromMessage(this.message, true)});
      },
      verify: () => this.canDownload(this.message, true)
    }, {
      icon: 'checkretract',
      text: 'Chat.Poll.Unvote',
      onClick: this.onRetractVote,
      verify: () => {
        const poll = (this.message as any).media?.poll as Poll;
        return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz;
      }/* ,
      cancelEvent: true */
    }, {
      icon: 'stop',
      text: 'Chat.Poll.Stop',
      onClick: this.onStopPoll,
      verify: async() => {
        const poll = (this.message as any).media?.poll;
        return await this.managers.appMessagesManager.canEditMessage(this.message, 'poll') && poll && !poll.pFlags.closed && !this.message.pFlags.is_outgoing;
      }/* ,
      cancelEvent: true */
    }, {
      icon: 'forward',
      text: 'Forward',
      onClick: this.onForwardClick, // let forward the message if it's outgoing but not ours (like a changelog)
      verify: () => !this.noForwards && this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || this.message.fromId === SERVICE_PEER_ID) && this.message._ !== 'messageService'
    }, {
      icon: 'forward',
      text: 'Message.Context.Selection.Forward',
      onClick: this.onForwardClick,
      verify: () => this.chat.selection.selectionForwardBtn &&
        this.isSelected &&
        !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'),
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'download',
      text: 'Message.Context.Selection.Download',
      onClick: () => {
        this.selectedMessages.forEach((message) => {
          appDownloadManager.downloadToDisc({media: getMediaFromMessage(message, true)});
        });
      },
      verify: () => this.selectedMessages ? this.selectedMessages.some((message) => this.canDownload(message, false)) : false,
      withSelection: true
    }, {
      icon: 'flag',
      text: 'ReportChat',
      onClick: () => {
        new PopupReportMessages(this.peerId, [this.mid]);
      },
      verify: async() => !this.message.pFlags.out && this.message._ === 'message' && !this.message.pFlags.is_outgoing && await this.managers.appPeersManager.isChannel(this.peerId),
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'select',
      text: 'Message.Context.Select',
      onClick: this.onSelectClick,
      verify: () => !(this.message as Message.messageService).action && !this.isSelected && this.isSelectable,
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'select',
      text: 'Message.Context.Selection.Clear',
      onClick: this.onClearSelectionClick,
      verify: () => this.isSelected,
      notDirect: () => true,
      withSelection: true
    }, {
      onClick: () => {
        if(this.viewerPeerId) {
          this.chat.appImManager.setInnerPeer({
            peerId: this.viewerPeerId
          });
        } else if(this.canOpenReactedList) {
          new PopupReactedList(this.message as Message.message);
        } else {
          return false;
        }
      },
      verify: async() => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactions?.length || await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message)),
      notDirect: () => true,
      localName: 'views'
    }, {
      icon: 'delete danger',
      text: 'Delete',
      onClick: this.onDeleteClick,
      verify: async() => this.managers.appMessagesManager.canDeleteMessage(this.message)
    }, {
      icon: 'delete danger',
      text: 'Message.Context.Selection.Delete',
      onClick: this.onDeleteClick,
      verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'),
      notDirect: () => true,
      withSelection: true
    }, {
      icon: 'info',
      text: 'Chat.Message.Sponsored.What',
      onClick: () => {
        new PopupSponsored();
      },
      verify: () => false,
      isSponsored: true
    }, {
      // icon: 'smile',
      text: 'Loading',
      onClick: () => {
        this.emojiInputsPromise.then((inputs) => {
          new PopupStickers(inputs, true).show();
        });
      },
      verify: () => !!this.getUniqueCustomEmojisFromMessage().length,
      notDirect: () => true,
      localName: 'emojis'
    }];
  }

  private canDownload(message: MyMessage, withTarget?: boolean) {
    if(!canSaveMessageMedia(message) || this.noForwards) {
      return false;
    }

    const isPhoto: boolean = !!((message as Message.message).media as MessageMedia.messageMediaPhoto)?.photo;
    let isGoodType = false

    if(isPhoto) {
      isGoodType = true;
    } else {
      const doc: MyDocument = ((message as Message.message).media as MessageMedia.messageMediaDocument)?.document as any;
      if(!doc) return false;
      // isGoodType = doc.type && (['gif', 'video', 'audio', 'voice', 'sticker'] as MyDocument['type'][]).includes(doc.type)
      isGoodType = true;
    }

    let hasTarget = !withTarget || !!IS_TOUCH_SUPPORTED;

    if(isGoodType) hasTarget ||= !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio') || !!findUpClassName(this.target, 'media-sticker-wrapper') || !!findUpClassName(this.target, 'media-photo') || !!findUpClassName(this.target, 'media-video');

    return isGoodType && hasTarget;
  }

  private getMessageWithText() {
    return (this.albumMessages && getAlbumText(this.albumMessages)) || this.message;
  }

  private getUniqueCustomEmojisFromMessage() {
    const docIds: DocId[] = [];

    const message = this.getMessageWithText();

    const entities = (message as Message.message).entities;
    if(entities) {
      const filtered = entities.filter((entity) => entity._ === 'messageEntityCustomEmoji') as MessageEntity.messageEntityCustomEmoji[];
      docIds.push(...filtered.map((entity) => entity.document_id));
    }

    const reactions = (message as Message.message).reactions;
    if(reactions) {
      const results = reactions.results.filter((reactionCount) => reactionCount.reaction._ === 'reactionCustomEmoji');
      docIds.push(...results.map((reactionCount) => (reactionCount.reaction as Reaction.reactionCustomEmoji).document_id));
    }

    return filterUnique(docIds);
  }

  private async init() {
    this.cleanup();
    this.setButtons();

    const filteredButtons = await this.filterButtons(this.buttons);
    if(!filteredButtons.length) {
      return;
    }

    const element = this.element = await ButtonMenu({
      buttons: filteredButtons,
      listenerSetter: this.listenerSetter
    });
    element.id = 'bubble-contextmenu';
    element.classList.add('contextmenu');

    const viewsButton = filteredButtons.find((button) => button.localName === 'views');
    if(viewsButton) {
      const reactions = (this.message as Message.message).reactions;
      const recentReactions = reactions?.recent_reactions;
      const isViewingReactions = !!recentReactions?.length;
      const participantsCount = await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message) ?
        ((await this.managers.appPeersManager.getPeer(this.peerId)) as MTChat.chat).participants_count :
        undefined;
      const reactedLength = reactions ? reactions.results.reduce((acc, r) => acc + r.count, 0) : undefined;

      viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks'));
      const i18nElem = new I18n.IntlElement({
        key: isViewingReactions ? (
          participantsCount === undefined ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted'
        ) : 'NobodyViewed',
        args: isViewingReactions ? (
          participantsCount === undefined ? [reactedLength] : [participantsCount, participantsCount]
        ) : undefined,
        element: viewsButton.textElement
      });

      let fakeText: HTMLElement;
      if(isViewingReactions) {
        if(participantsCount === undefined) {
          fakeText = i18n('Chat.Context.ReactedFast', [reactedLength]);
        } else {
          fakeText = i18n(
            recentReactions.length === participantsCount ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
            [recentReactions.length, participantsCount]
          );
        }
      } else {
        fakeText = i18n('Loading');
      }

      fakeText.classList.add('btn-menu-item-text-fake');
      viewsButton.element.append(fakeText);

      const AVATAR_SIZE = 22;
      const MAX_AVATARS = 3;
      const PADDING_PER_AVATAR = 1.125;
      i18nElem.element.style.visibility = 'hidden';
      i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * Math.min(MAX_AVATARS, recentReactions.length) + 'rem' : '1rem';
      const middleware = this.middleware.get();
      this.managers.appMessagesManager.getMessageReactionsListAndReadParticipants(this.message as Message.message).then((result) => {
        if(!middleware()) {
          return;
        }

        if(fakeText) {
          fakeText.remove();
        }

        const reactions = result.combined;
        const reactedLength = participantsCount === undefined ?
          result.reactionsCount :
          (
            isViewingReactions ?
              reactions.filter((reaction) => reaction.reaction).length :
              reactions.length
          );

        let fakeElem: HTMLElement;
        if(reactions.length === 1) {
          fakeElem = new PeerTitle({
            peerId: reactions[0].peerId,
            onlyFirstName: true,
            dialog: false
          }).element;

          if(!isViewingReactions || result.readParticipants.length <= 1) {
            this.viewerPeerId = reactions[0].peerId;
          }
        } else if(isViewingReactions) {
          const isFull = reactedLength === reactions.length || participantsCount === undefined;
          fakeElem = i18n(
            isFull ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted',
            isFull ? [reactedLength] : [reactedLength, reactions.length]
          );
        } else {
          if(!reactions.length) {
            i18nElem.element.style.visibility = '';
          } else {
            fakeElem = i18n('MessageSeen', [reactions.length]);
          }
        }

        if(fakeElem) {
          fakeElem.style.paddingRight = PADDING_PER_AVATAR * Math.min(MAX_AVATARS, reactedLength) + 'rem';
          fakeElem.classList.add('btn-menu-item-text-fake');
          viewsButton.element.append(fakeElem);
        }

        if(reactions.length) {
          const avatars = new StackedAvatars({avatarSize: AVATAR_SIZE});
          avatars.render(recentReactions ? recentReactions.map((r) => getPeerId(r.peer_id)) : reactions.map((reaction) => reaction.peerId));
          viewsButton.element.append(avatars.container);

          // if(reactions.length > 1) {
          // if(isViewingReactions) {
          this.canOpenReactedList = true;
          // }
        }
      });
    }

    let menuPadding: MenuPositionPadding;
    let reactionsMenu: ChatReactionsMenu;
    let reactionsMenuPosition: 'horizontal' | 'vertical';
    if(
      this.message._ === 'message' &&
      !this.chat.selection.isSelecting &&
      !this.message.pFlags.is_outgoing &&
      !this.message.pFlags.is_scheduled &&
      !this.message.pFlags.local
    ) {
      reactionsMenuPosition = (IS_APPLE || IS_TOUCH_SUPPORTED)/*  && false */ ? 'horizontal' : 'vertical';
      reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.managers, reactionsMenuPosition, this.middleware);
      reactionsMenu.init(await this.managers.appMessagesManager.getGroupsFirstMessage(this.message));
      // element.prepend(reactionsMenu.widthContainer);

      const size = 36;
      const margin = 8;
      const totalSize = size + margin;
      const paddingLeft = 0, paddingRight = 0;
      if(reactionsMenuPosition === 'vertical') {
        menuPadding = {
          top: paddingLeft,
          // bottom: 36, // positionMenu will detect it itself somehow
          left: totalSize
        };
      } else {
        menuPadding = {
          top: totalSize,
          right: paddingRight,
          left: paddingLeft
        };
      }
    }

    const emojisButton = filteredButtons.find((button) => button.localName === 'emojis');
    if(emojisButton) {
      emojisButton.element.classList.add('is-multiline');
      emojisButton.element.parentElement.insertBefore(document.createElement('hr'), emojisButton.element);

      const setPadding = () => {
        menuPadding ??= {};
        menuPadding.bottom = 24;
      };

      const docIds = this.getUniqueCustomEmojisFromMessage();
      const inputsPromise = this.emojiInputsPromise = deferredPromise();

      await this.managers.appEmojiManager.getCachedCustomEmojiDocuments(docIds).then(async(docs) => {
        const p = async(docs: Document.document[]) => {
          const s: Map<StickerSet['id'], InputStickerSet.inputStickerSetID> = new Map();
          docs.forEach((doc) => {
            if(!doc || s.has(doc.stickerSetInput.id)) {
              return;
            }

            s.set(doc.stickerSetInput.id, doc.stickerSetInput);
          });

          const inputs = [...s.values()];
          inputsPromise.resolve(inputs);
          if(s.size === 1) {
            const result = await this.managers.acknowledged.appStickersManager.getStickerSet(inputs[0]);
            const promise = result.result.then((set) => {
              const el = i18n('MessageContainsEmojiPack', [wrapEmojiText(set.set.title)]);
              replaceContent(emojisButton.textElement, el);
            });

            return result.cached ? promise : (setPadding(), undefined);
          }

          replaceContent(emojisButton.textElement, i18n('MessageContainsEmojiPacks', [s.size]));
        };

        if(docs.some((doc) => !doc)) {
          setPadding();
          this.managers.appEmojiManager.getCustomEmojiDocuments(docIds).then(p);
        } else {
          return p(docs);
        }
      });
      // emojisButton.element.append(i18n('Loading'));
    }

    this.chat.container.append(element);

    return {
      element,
      cleanup: () => {
        this.cleanup();
        reactionsMenu && reactionsMenu.cleanup();
      },
      destroy: () => {
        element.remove();
        reactionsMenu && reactionsMenu.widthContainer.remove();
      },
      menuPadding,
      reactionsMenu,
      reactionsMenuPosition
    };
  }

  private async getUrlToMessage() {
    if(this.peerId.isUser()) {
      return;
    }

    let threadMessage: Message.message;
    const {peerId, mid} = this;
    const threadId = this.chat.threadId;
    if(this.chat.type === 'discussion') {
      threadMessage = (await this.managers.appMessagesManager.getMessageByPeer(peerId, threadId)) as Message.message;
    }

    const username = await this.managers.appPeersManager.getPeerUsername(threadMessage ? threadMessage.fromId : peerId);
    const msgId = getServerMessageId(mid);
    let url = 'https://t.me/';
    if(username) {
      url += username;
      if(threadMessage) url += `/${getServerMessageId(threadMessage.fwd_from.channel_post)}?comment=${msgId}`;
      else if(threadId) url += `/${getServerMessageId(threadId)}/${msgId}`;
      else url += '/' + msgId;
    } else {
      url += 'c/' + peerId.toChatId();
      if(threadMessage) url += `/${msgId}?thread=${getServerMessageId(threadMessage.mid)}`;
      else if(threadId) url += `/${getServerMessageId(threadId)}/${msgId}`;
      else url += '/' + msgId;
    }

    return {url, isPrivate: !username};
  }

  private async getSelectedMessagesText() {
    if(!isSelectionEmpty()) {
      return '';
    }

    const mids = this.chat.selection.isSelecting ?
      [...this.chat.selection.selectedMids.get(this.peerId)].sort((a, b) => a - b) :
      [this.mid];

    const parts: string[] = await Promise.all(mids.map(async(mid) => {
      const message = (await this.chat.getMessage(mid)) as Message.message;
      return message?.message ? message.message + '\n' : '';
    }));

    return parts.join('');
  }

  private onSendScheduledClick = async() => {
    if(this.chat.selection.isSelecting) {
      simulateClickEvent(this.chat.selection.selectionSendNowBtn);
    } else {
      new PopupSendNow(this.peerId, await this.chat.getMidsByMid(this.mid));
    }
  };

  private onReplyClick = () => {
    this.chat.input.initMessageReply(this.mid);
  };

  private onFaveStickerClick = (unfave?: boolean) => {
    const docId = ((this.message as Message.message).media as MessageMedia.messageMediaDocument).document.id;
    this.managers.appStickersManager.faveSticker(docId, unfave);
  };

  private onEditClick = () => {
    const message = this.getMessageWithText();
    this.chat.input.initMessageEditing(this.isTargetAGroupedItem ? this.mid : message.mid);
  };

  private onCopyClick = async() => {
    if(isSelectionEmpty()) {
      copyTextToClipboard(this.selectedMessagesText);
    } else {
      document.execCommand('copy');
      // cancelSelection();
    }
  };

  private onCopyAnchorLinkClick = () => {
    copyTextToClipboard((this.target as HTMLAnchorElement).href);
  };

  private onCopyLinkClick = () => {
    const {url, isPrivate} = this.linkToMessage;
    const key: LangPackKey = isPrivate ? 'LinkCopiedPrivateInfo' : 'LinkCopied';
    toast(I18n.format(key, true));
    copyTextToClipboard(url);
  };

  private onPinClick = () => {
    new PopupPinMessage(this.peerId, this.mid);
  };

  private onUnpinClick = () => {
    new PopupPinMessage(this.peerId, this.mid, true);
  };

  private onRetractVote = () => {
    this.managers.appPollsManager.sendVote(this.message as Message.message, []);
  };

  private onStopPoll = () => {
    this.managers.appPollsManager.stopPoll(this.message as Message.message);
  };

  private onForwardClick = async() => {
    if(this.chat.selection.isSelecting) {
      simulateClickEvent(this.chat.selection.selectionForwardBtn);
    } else {
      const peerId = this.peerId;
      const mids = this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid);
      PopupForward.create({
        [peerId]: mids
      });
    }
  };

  private onSelectClick = () => {
    this.chat.selection.toggleByElement(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble'));
  };

  private onClearSelectionClick = () => {
    this.chat.selection.cancelSelection();
  };

  private onDeleteClick = async() => {
    if(this.chat.selection.isSelecting) {
      simulateClickEvent(this.chat.selection.selectionDeleteBtn);
    } else {
      new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type);
    }
  };
}
