import { formatDate } from '@angular/common';
import {
  Component,
  ElementRef,
  HostListener,
  OnDestroy,
  OnInit,
  QueryList,
  ViewChild,
  ViewChildren,
} from '@angular/core';
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { AppStorage } from 'src/app/shared/app-storage';
import { AccountService } from 'src/app/shared/server-services/account.service';
import { AuthService } from 'src/app/shared/server-services/auth.service';
import {
  ChatService,
  SubscriptionChatEventType,
} from 'src/app/shared/server-services/chat.service';
import { DialogueService } from 'src/app/shared/server-services/dialogue.service';
import {
  AccountAvatarRecord,
  MeRecord,
} from 'src/app/shared/server-services/query-records/account-records';
import {
  MessageRecord,
  ReactionEnum,
  reactionIndexSrcMap,
  reactionList,
} from 'src/app/shared/server-services/query-records/room-records';
import { ResourceType } from 'src/app/shared/server-services/query-records/sidebar-records';
import {
  WorkspaceSubscriptionDialogueTypingEventRecord,
  WorkspaceSubscriptionRoomAccountPermissionEventRecord,
  WorkspaceSubscriptionRoomMessageEventRecord,
  WorkspaceSubscriptionRoomTypingEventRecord,
} from 'src/app/shared/server-services/query-records/workspace-records';
import { dialogueMessagesQuery, roomMessagesQuery } from 'src/app/shared/server-services/querys';
import { ResourceBase } from 'src/app/shared/server-services/resource-base';
import { RoomService } from 'src/app/shared/server-services/room.service';
import {
  SubscriptionServiceEvent,
  SubscriptionServiceEventType,
} from 'src/app/shared/server-services/subscription-event';
import { SubscriptionService } from 'src/app/shared/server-services/subscription.service';
import { AudioService, Sounds } from 'src/app/shared/services/audio.service';
import { CacheService } from 'src/app/shared/services/cache/cache.service';
import { ClipboardService } from 'src/app/shared/services/clipboard.service';
import { CordovaService } from 'src/app/shared/services/cordova.service';
import { DialogService } from 'src/app/shared/services/dialog.service';
import { RouterHandler } from 'src/app/shared/services/router-handler.service';
import { SnackBarService } from 'src/app/shared/services/snackbar.service';
import { CopyChatMessagesSnackbarComponent } from 'src/app/shared/snackbars/copy-chat-messages-snackbar/copy-chat-messages-snackbar.component';
import { forceMobileView } from 'src/main';
import { SafeRadix32 } from '../../../shared/safe-radix32';
import { ChatMessageGroupingPolicy } from '../../settings/settings.component';
import { ChatData, ChatPermissions, ChatMessagePermission } from './chat-data';
import {
  ChatMessageComponent,
  ChatMessageContextMenuData,
} from './chat-message/chat-message.component';
import { NativeEditorComponent } from './chat-message/native-editor/native-editor.component';
import { SlateEditorComponent } from './chat-message/slate-editor/slate-editor.component';
import {
  WorkspaceSubscriptionRoomEventRecord,
  WorkspaceSubscriptionDialogueEventRecord,
} from 'src/app/shared/server-services/query-records/workspace-records';
import { focusChatInput } from './focus-chat-input-observer';
import { ID } from 'src/app/shared/server-services/query-records/common-records';
import { MatMenuTrigger } from '@angular/material/menu';
import { MatBottomSheet } from '@angular/material/bottom-sheet';
import { ChatLayoutBottomSheetMenuComponent } from './chat-layout-bottom-sheet-menu/chat-layout-bottom-sheet-menu.component';
import { ChatLayoutSheetAction } from 'src/app/shared/consts/enums';

/** @todo make these configurable in settings */
/** Above this amount the chatMessages array will be sliced */
const MESSAGE_LIMIT_COUNT = 90;
/** Amount of messages will be left after slicing the chatMessages array */
const MESSAGE_SLICED_COUNT = 60;

const MAXIMUM_EMOJI_KIND_NEXT_TO_MESSAGE = 3;

enum ContextMenuAction {
  edit = 'edit',
  copy = 'copy',
  select = 'select',
  delete = 'delete',
  react = 'react',
}

@Component({
  selector: 'app-chat-layout',
  templateUrl: './chat-layout.component.html',
  styleUrls: ['./chat-layout.component.scss'],
})
export class ChatLayoutComponent implements OnInit, OnDestroy {
  public RESOURCE_TYPE_ROOM = ResourceType.ROOM;

  @ViewChild('resourceContainer', { read: ElementRef })
  resourceContainerRef: ElementRef;
  @ViewChild(MatMenuTrigger) context_trigger: MatMenuTrigger;
  public contextMenuPosition = { x: '0px', y: '0px' };
  public isExpanded = false;
  public reactions = reactionList;
  public topEmojies = [
    { index: 1, emoji: reactionIndexSrcMap[1] }, // ❤️
    { index: 40, emoji: reactionIndexSrcMap[40] }, // 😆
    { index: 42, emoji: reactionIndexSrcMap[42] }, // 😮
    { index: 18, emoji: reactionIndexSrcMap[18] }, // 😢
    { index: 41, emoji: reactionIndexSrcMap[41] }, // 😠
    { index: 6, emoji: reactionIndexSrcMap[6] }, // 👍
  ];
  public contextTargetMessage: ChatData = null;
  public contextTargetPermissions: ChatMessagePermission = null;

  // at 0, is the newest
  public chatMessages: ChatData[] = [];
  public myPermissions: ChatPermissions = {};

  private isCompactDisplay = AppStorage.getItem('messageDisplay') === 'compact';
  private chatMessageComparePolicy = (d1, d2) => false;

  public resourceId;
  public loaded = false;
  public isMessagesLoading = false;
  public isMessageSending: boolean = false;
  public type: ResourceType = null;
  private service: ResourceBase = null;
  public chatEndReached: boolean = false;
  public isReadOnly: boolean = false;
  public isMemberOfRoom: boolean = false; // we can only send seen mutation if we are member of a room or it is a dialogue
  public resourceName: string = '';
  private isBottomVisible: boolean = true;
  private me: MeRecord;

  public isNewMessageAtBottom: boolean = false;

  private isSelecting: boolean = false;
  private copySnackbarShowing: boolean = false;
  private selectedChatMessages: HTMLElement[] = [];
  private chatTag: string = 'APP-CHAT-MESSAGE';
  private copySnackBarRef: MatSnackBarRef<CopyChatMessagesSnackbarComponent>;
  private canEnterEditOnUpArrow: boolean = true;
  private isChatInputFocused: boolean = true;
  private isTypingTimer = null;
  public isOnDesktop: boolean = false;

  public isOnApp: boolean = false;

  public editedMessage: null | ChatData = null;

  // track the message, need to save it if the user step into edit mode
  // and load it after it goes back to the normal mode
  private lastUnsentMessage: string = '';

  public typingUsers: {
    timeoutId: NodeJS.Timeout;
    user: AccountAvatarRecord;
  }[] = [];

  private userSeenState: { [userId: string]: string } = {};

  @ViewChild('chatScroll', { read: ElementRef }) chatScroll: ElementRef;

  /**
   * If you are at the bottom of the scrollbar, the incoming messages will scroll you to the bottom.
   * But if you out of this scope, we wont scroll you, so you can search and read earlier messages.
   */
  private autoScrollLimit: number = 200;

  @ViewChild(SlateEditorComponent)
  slateEditorComponent: SlateEditorComponent;
  @ViewChild(NativeEditorComponent)
  nativeEditorComponent: NativeEditorComponent;
  @ViewChildren(ChatMessageComponent)
  chatMessagesRef: QueryList<ChatMessageComponent>;

  public runSeenAnimation: boolean = false;

  private popStateCallback = (event) => {
    if (this.editedMessage) {
      this.restoreChatMessage();
      return;
    }
    //  else { // creates a weird history.back loop
    //   history.back();
    // }
  };

  public setEditedMessage(msg: ChatData) {
    if (!this.editedMessage) {
      window.history.pushState({ editor: true }, '');
    }
    this.chatService.saveMessageInStorage(this.resourceId, this.lastUnsentMessage);
    this.editedMessage = msg;
    focusChatInput.next();
  }

  public restoreChatMessage() {
    this.chatService.loadMessageFromStorage(this.resourceId).then((msg) => {
      this.lastUnsentMessage = msg;
      this.editedMessage = null;
    });
  }

  private checkReadOnly() {
    if (this.authService.isAnonym()) {
      this.isReadOnly = true;
      this.isMemberOfRoom = false;
    } else {
      this.roomService
        .getMyRoomPermissions(this.resourceId)
        .then((myPerm) => {
          this.isMemberOfRoom = !!myPerm.account;

          this.isReadOnly =
            !this.isMemberOfRoom ||
            !(
              myPerm.ownerAccountId === myPerm.account?.id ||
              (myPerm.msgBrdReadOnly && myPerm.account?.msgBrdModerate === true) ||
              myPerm.msgBrdReadOnly !== true
            );
        })
        .catch((err) => {
          console.error('room perm req', err);
          this.isReadOnly = true;
          this.isMemberOfRoom = false;
        });
    }
  }

  public onRouterChange = (res) => {
    if (
      this.isMemberOfRoom &&
      (this.lastUnsentMessage.length > 0 || this.lastUnsentMessage === '')
    ) {
      this.chatService.saveMessageInStorage(this.resourceId, this.lastUnsentMessage);
    }

    // removes duplicate message loading when double-clicking on chat (or resource)
    // jumps to chat when in room-settings/drive page and clicking on chat (or resource), because this.resourceId is undefined
    if (this.resourceId === res.params.id) {
      return;
    }

    this.runSeenAnimation = false;

    this.accountService.getMe().then((me) => {
      this.me = me;
      if (this.type === ResourceType.DIALOGUE) {
        this.myPermissions = {
          owner: false,
          administrator: false,
          moderator: false,
          noReactions: false,
        };
      } else {
        // this.type === ResourceType.ROOM
        this.roomService
          .getMyRoomPermissions(this.resourceId)
          .then((roomPermission) => {
            const noReactions = Object.prototype.hasOwnProperty.call(roomPermission, 'noReactions')
              ? roomPermission.noReactions
              : true;
            this.myPermissions = {
              owner: roomPermission?.ownerAccountId === this.me?.id,
              administrator: roomPermission?.account?.administrator === true,
              moderator: roomPermission?.account?.msgBrdModerate === true,
              noReactions: noReactions,
            };
          })
          .catch((err) => {
            console.error('could not get room perm:2', this.resourceId);
          });
      }
    });

    this.isMessagesLoading = true;
    this.chatEndReached = false;
    this.typingUsers = [];

    this.copySnackBarRef?.dismiss();

    if (res.params.type == 'private') {
      this.type = ResourceType.DIALOGUE;
      this.service = this.dialogueService;
    } else {
      this.type = ResourceType.ROOM;
      this.service = this.chatService;
    }

    this.resourceId = res.params.id;
    this.chatMessages = [];

    if (this.type == ResourceType.ROOM) {
      this.checkReadOnly();
    } else {
      this.isReadOnly = false;
      this.isMemberOfRoom = true; // dialoge count as member, so we can send seen mutation
    }

    this.service.getData(this.resourceId).then((data) => {
      this.service.getDetail(this.resourceId).then((roomDetail) => {
        if (this.type == ResourceType.ROOM) this.resourceName = data['data'].name;
        else {
          this.accountService.getAccount(data['id']).then((acc) => {
            if (acc.deleted == true) {
              this.resourceName = this.translateService.instant(marker('Deleted user'));
            } else {
              this.resourceName = acc.avatarName;
            }
          });
        }

        // load these messages and subscribe the changes
        let cachedResourceId = this.resourceId;

        this.service.getLastMessages(this.resourceId).then((messages) => {
          if (messages.length == 0) this.chatEndReached = true;

          // if you switch fast between chats you can get an old chat response on an other chat window
          if (cachedResourceId != this.resourceId) return;

          this.accountService.getMe().then((me) => {
            if (messages.length > 0 && messages[0] !== me.id) {
              this.isMemberOfRoom && this.sendSeen();
              this.loadMessages(messages).then(() => {
                this.isMessagesLoading = false;

                if (this.isBottomVisible) {
                  this.isMemberOfRoom && this.sendSeen();
                }
                setTimeout(() => {
                  // do not allow animation at the start of the chat for a little bit
                  if (this.resourceId === cachedResourceId) {
                    this.runSeenAnimation = true;
                  }
                }, 100);
              });
            }
          });

          // check if there is more message before the init messages
          // it can solve a scroller issue
          // we have to load both payload in one "loadMessages" otherwise
          // iphone safari renderer wont show the content of the reversed flexbox
          // floating container. It is a bug in the safari renderer core.
          let olderMessagesPromise: Promise<MessageRecord[]> = Promise.resolve([]);
          if (messages.length != 0) {
            olderMessagesPromise = this.service.getMoreMessages(
              this.resourceId,
              messages[messages.length - 1].id
            );
          }

          olderMessagesPromise.then((olderMessages) => {
            // if you switch fast between chats you can get an old chat response on an other chat window
            if (cachedResourceId != this.resourceId) return;

            this.loadMessages(olderMessages).then(() => {
              this.isMessagesLoading = false;

              if (this.isBottomVisible) {
                this.sendSeen();
              }
            });
          });
        });

        this.restoreChatMessage();
        // we dont need the callback reference, it will be removed after ngOnDestroy
        if (!this.isMemberOfRoom) {
          this.subscribeToAllChatServiceChanges();
        }
      });
    });
  };

  private subscribeToAllChatServiceChanges(): void {
    this.chatService.subscribeChatChanges(
      this.resourceId,
      SubscriptionChatEventType.CREATE,
      this.handleMessageCreate
    );
    this.chatService.subscribeChatChanges(
      this.resourceId,
      SubscriptionChatEventType.MODIFY,
      this.handleMessageModify
    );
    this.chatService.subscribeChatChanges(
      this.resourceId,
      SubscriptionChatEventType.DELETE,
      this.handleMessageDelete
    );
  }

  private handleRoomMessageCreate = (msg) => {
    this.handleMessageCreate(msg);
  };

  private handleDialogueMessageCreate = (msg) => {
    this.handleMessageCreate(msg);
  };

  private handleMessageCreate = (msg) => {
    if (msg.id == this.resourceId) {
      this.service.getOneMessageById(this.resourceId, msg.messageId).then((data) => {
        this.loadMessage(data).then(() => {
          // scroll to end if we're close to it, so the whole new message will be visible
          if (this.chatScroll.nativeElement.scrollTop * -1 <= this.autoScrollLimit) {
            this.chatScroll.nativeElement.scrollTo({ top: 0, left: 0 });
          }
          // reverse-flexbox safari bug, its scroller does not work properly
          this.triggerIOSGUIRefresh();

          if (this.isBottomVisible) {
            this.isMemberOfRoom && this.sendSeen();
            // limit messages from being drawn to the page
            if (this.chatMessages.length > MESSAGE_LIMIT_COUNT) {
              this.chatMessages = this.chatMessages.slice(0, MESSAGE_SLICED_COUNT);
            }
          } else if (this.chatScroll.nativeElement.scrollTop * -1 < this.autoScrollLimit) {
            // scroll to the bottom, if needed
            setTimeout(() => {
              // watch out, the reverse flexbox cause a negative scroll direction
              //if (this.chatScroll.nativeElement.scrollTop * -1 < this.autoScrollLimit) {
              this.chatScroll.nativeElement.scrollTo({
                top: -1,
                left: 0,
                behavior: 'smooth',
              });
              //}
            }, 10);
          } else {
            this.isNewMessageAtBottom = true;
          }
        });
      });
    }

    if (this.me.id != msg.posterId) this.removeIsTyping(msg.posterId, true);
  };

  private triggerIOSGUIRefresh() {
    /**
     * In some position (rel+abs) the reverse flexbox ui renderer in safari is buggy
     * we have to trigger a view redraw with some viewport matrix or layout change (css hacks would also work)
     */
    if (
      this.deviceDetectorService.browser == 'Safari' ||
      (this.isOnApp &&
        this.cordovaService.isCordova() &&
        this.cordovaService.getDevicePlatform() == 'iOS')
    ) {
      this.chatScroll.nativeElement.scrollBy(0, -1);
      this.chatScroll.nativeElement.scrollBy(0, 1);
    }
  }

  public scrollToBottom() {
    this.chatScroll.nativeElement.scrollTo({
      top: 0,
      left: 0,
      behavior: 'smooth',
    });
    this.isNewMessageAtBottom = false;
  }

  private handleRoomMessageModify = (msg) => {
    this.handleMessageModify(msg);
  };

  private handleDialogueMessageModify = (msg) => {
    this.handleMessageModify(msg);
  };

  private handleMessageModify = (msg: WorkspaceSubscriptionRoomMessageEventRecord) => {
    if (msg.id == this.resourceId) {
      for (let i = 0; i < this.chatMessages.length; i++) {
        if (this.chatMessages[i].id == msg.messageId) {
          if (msg.content !== undefined) this.chatMessages[i].message = msg.content;
          if (msg.edited !== undefined) this.chatMessages[i].edited = msg.edited;
          if (msg.decryptionError !== undefined) this.chatMessages[i].error = msg.decryptionError;
          if (msg.reactionsAdded !== undefined || msg.reactionsRemoved !== undefined) {
            Object.assign(
              this.chatMessages[i],
              this.calcReactionInfo(this.chatMessages[i].messageRef.reactions)
            );
          }
        }
      }
    }
  };

  private handleRoomMessageDelete = (msg) => {
    if (this.type == ResourceType.ROOM) {
      this.handleMessageDelete(msg);
    }
  };

  private handleDialogueMessageDelete = (msg) => {
    if (this.type == ResourceType.DIALOGUE) {
      this.handleMessageDelete(msg);
    }
  };

  private handleMessageDelete = (msg) => {
    if (msg.id == this.resourceId) {
      for (let i = 0; i < this.chatMessages.length; i++) {
        if (this.chatMessages[i].id == msg.messageId) {
          this.chatMessages.splice(i, 1);

          // calculate isGrouping, hasLineBreak around the deleted message
          this.setGroupingProperties(i + 1);
          this.setGroupingProperties(i);
          this.setGroupingProperties(i - 1);
        }
      }
    }
  };

  private handleRoomEventModify = (event: WorkspaceSubscriptionRoomEventRecord) => {
    if (event.id !== this.resourceId) {
      return;
    }

    if (event.peerSeenIdsAdded) {
      // room seen event
      const peerSeenIdsAdded = event.peerSeenIdsAdded;
      // currently the `peerSeenIdsAdded` has only one entry
      // later we should optimize this
      // so we would not fetch the same message more than once

      for (const [userId, msgId] of Object.entries(peerSeenIdsAdded)) {
        const prevMessageId = this.userSeenState?.[userId];
        this.updateSeenMessage(prevMessageId);

        this.userSeenState[userId] = msgId;
        this.updateSeenMessage(msgId);
      }

      setTimeout(() => {
        // trigger gui refresh after the animation event
        this.triggerIOSGUIRefresh();
      }, 100);
    }
  };

  private handleDialogueEventModify = (event: WorkspaceSubscriptionDialogueEventRecord) => {
    // seen logic
    if (event.id !== this.resourceId) {
      return;
    }
    if (event.peerSeenId) {
      // room seen event
      const msgId = event.peerSeenId;
      const userId = event.id;

      const prevMessageId = this.userSeenState?.[userId];
      this.updateSeenMessage(prevMessageId);

      this.userSeenState[userId] = msgId;
      this.updateSeenMessage(msgId);

      setTimeout(() => {
        // trigger gui refresh after the animation event
        this.triggerIOSGUIRefresh();
      }, 100);
    }
  };

  private updateSeenMessage = (msgId) => {
    if (msgId) {
      const foundMessage = this.chatMessages.find((msg) => msg.id === msgId);
      if (foundMessage) {
        this.service.getSeenIdsByMessage(this.resourceId, foundMessage.id).then((seenBy) => {
          foundMessage.seenBy = seenBy.filter(
            (userId) => userId !== this.me.id && userId !== foundMessage.account.id
          );
        });
      }
    }
  };

  private handleTypingEvent = (
    data:
      | WorkspaceSubscriptionDialogueTypingEventRecord
      | WorkspaceSubscriptionRoomTypingEventRecord
  ): void => {
    let currentlyTypingUserId = data['accountId'] ?? data['id'];

    if (!this.canShowTyping(data, currentlyTypingUserId)) return;

    let alreadyTypingUser = this.typingUsers.find(
      (typingUser) => typingUser.user.id == currentlyTypingUserId
    );
    if (alreadyTypingUser) {
      //if user is in the list when this event fires -> user is still typing, we have to reset and restart timer
      clearTimeout(alreadyTypingUser.timeoutId);
    }

    let timeOut = setTimeout(() => {
      this.removeIsTyping(currentlyTypingUserId);
    }, 6000);

    if (alreadyTypingUser) {
      alreadyTypingUser.timeoutId = timeOut;
    } else {
      this.accountService.getAccount(currentlyTypingUserId).then((account) => {
        if (!this.canShowTyping(data, currentlyTypingUserId)) return;
        this.typingUsers.push({ timeoutId: timeOut, user: account });
      });
    }
  };

  private canShowTyping(data, currentlyTypingUserId): boolean {
    //if i am typing in room, don't show myself as typing
    if (
      (this.type == ResourceType.ROOM && currentlyTypingUserId == this.me.id) ||
      this.resourceId != data['id']
    )
      return false;

    //if typing user is in other dialogue, don't show as typing
    if (this.type == ResourceType.DIALOGUE && currentlyTypingUserId != this.resourceId)
      return false;

    //if typing user is already displayed...
    if (this.typingUsers.some((typingUser) => typingUser.user.id === currentlyTypingUserId))
      return false;
    return true;
  }

  private removeIsTyping(accountId: string, shouldClearTimeout: boolean = false): void {
    if (shouldClearTimeout) {
      let user = this.typingUsers.find((typingUser) => typingUser.user.id == accountId);
      if (user) {
        clearTimeout(user.timeoutId);
      }
    }
    this.typingUsers = this.typingUsers.filter((typingUser) => typingUser.user.id != accountId);
  }

  public getTypingNames(): string {
    if (this.typingUsers.length > 2) {
      let firstTwo = this.typingUsers.slice(0, 2);
      let otherTypingUsersCount = this.typingUsers.length - 2;
      return (
        firstTwo.map((typingUser) => typingUser.user.avatarName).join(', ') +
        this.translate.instant(marker(' and ')) +
        otherTypingUsersCount +
        this.translate.instant(marker(' other people are typing...'))
      );
    }
    return (
      this.typingUsers.map((typingUser) => typingUser.user.avatarName).join(', ') +
      this.translate.instant(marker(' is typing...'))
    );
  }

  constructor(
    private chatService: ChatService,
    private dialogueService: DialogueService,
    private dialogService: DialogService,
    private snackbar: MatSnackBar,
    private accountService: AccountService,
    private routerHandler: RouterHandler,
    private subscriptionService: SubscriptionService,
    private authService: AuthService,
    private roomService: RoomService,
    private clipboardService: ClipboardService,
    private deviceDetectorService: DeviceDetectorService,
    private translateService: TranslateService,
    private translate: TranslateService,
    private cordovaService: CordovaService,
    private snackbarService: SnackBarService,
    private cacheService: CacheService,
    private audioService: AudioService,
    private bottomSheet: MatBottomSheet
  ) {
    this.isOnDesktop;
    this.isOnApp = this.cordovaService.isOnApp() || forceMobileView;

    this.isMessagesLoading = true;

    console.log('router', this.routerHandler.getRoute());

    console.log('init chat');

    let firstLoading = true;
    this.subscriptionService.subscribeWorkspaceLoaded(() => {
      if (firstLoading) {
        firstLoading = false;
      } else {
        // reset now like this, later refactor
        this.resourceId = null;
        this.isMessagesLoading = true;
        /*let route = this.routerHandler.getRoute();

        if (route.params.type == 'private') {
          this.cacheService.deleteCacheDataByRequest({
            query: dialogueMessagesQuery,
            variables: { peerId: route.params.id },
          });
        } else {
          this.cacheService.deleteCacheDataByRequest({
            query: roomMessagesQuery,
            variables: { roomId: route.params.id },
          });
        }*/

        this.onRouterChange(this.routerHandler.getRoute());
      }
    });

    this.chatMessageComparePolicy = this.getSelectedChatMessagePolicy();

    window.addEventListener('popstate', this.popStateCallback);
    this.isOnDesktop = this.deviceDetectorService.isDesktop();

    document.addEventListener('pause', this.offscopeCallback, false);
  }

  private offscopeCallback = () => {
    if (
      this.isMemberOfRoom &&
      (this.lastUnsentMessage.length > 0 || this.lastUnsentMessage === '')
    ) {
      this.chatService.saveMessageInStorage(this.resourceId, this.lastUnsentMessage);
    }
  };

  ngOnInit(): void {
    if (this.routerHandler.isOnPage('room/?/chat/?')) {
      this.onRouterChange(this.routerHandler.getRoute());
    }
    this.routerHandler.subscribe('room/?/chat/?', this.onRouterChange);
    this.subscribeAllWorkspaceChanges();
    this.accountService.getMe().then((me) => {
      this.me = me;
      if (this.type === ResourceType.DIALOGUE) {
        this.myPermissions = {
          owner: false,
          administrator: false,
          moderator: false,
          noReactions: false,
        };
      } else {
        // this.type === ResourceType.ROOM
        this.roomService
          .getMyRoomPermissions(this.resourceId)
          .then((roomPermission) => {
            const noReactions = Object.prototype.hasOwnProperty.call(roomPermission, 'noReactions')
              ? roomPermission.noReactions
              : true;
            this.myPermissions = {
              owner: roomPermission?.ownerAccountId === this.me?.id,
              administrator: roomPermission?.account?.administrator === true,
              moderator: roomPermission?.account?.msgBrdModerate === true,
              noReactions: noReactions,
            };
          })
          .catch((err) => {
            console.error('could not get room perm:3', this.resourceId);
          });
      }
    });
  }

  private subscribeAllWorkspaceChanges() {
    // seen events
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleRoomEventModify
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleDialogueEventModify
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleRoomMessageCreate
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleRoomMessageModify
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleRoomMessageDelete
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleDialogueMessageCreate
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleDialogueMessageModify
    );
    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleDialogueMessageDelete
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.ALL,
      this.onRoomAccountPermissionChange
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.DIALOGUE_TYPING_EVENT,
      SubscriptionServiceEventType.ALL,
      this.handleTypingEvent
    );

    this.subscriptionService.subscribe(
      SubscriptionServiceEvent.ROOM_TYPING_EVENT,
      SubscriptionServiceEventType.ALL,
      this.handleTypingEvent
    );
  }

  private onRoomAccountPermissionChange = (
    event: WorkspaceSubscriptionRoomAccountPermissionEventRecord
  ) => {
    if (this.resourceId == event.id) {
      this.setMyPermissions(event);

      this.checkReadOnly();

      //already subscribing in the WS, remove duplicate
      if (event.cmd == SubscriptionServiceEventType.CREATE) {
        this.chatService.unsubscribeAllChatChanges(this.resourceId);
        this.service.getDetail(this.resourceId);
      }

      if (event.cmd == SubscriptionServiceEventType.DELETE) this.subscribeToAllChatServiceChanges();
    }
  };

  private setMyPermissions = (event: WorkspaceSubscriptionRoomAccountPermissionEventRecord) => {
    if (event.hasOwnProperty('administrator')) {
      this.myPermissions.administrator = event?.administrator;
    }
    if (event.hasOwnProperty('msgBrdModerate')) {
      this.myPermissions.moderator = event?.msgBrdModerate;
    }
  };

  private unsubscribeAllWorkspaceChanges() {
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleRoomEventModify
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleDialogueEventModify
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleRoomMessageCreate
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleRoomMessageModify
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_MESSAGE_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleRoomMessageDelete
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.CREATE,
      this.handleDialogueMessageCreate
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.MODIFY,
      this.handleDialogueMessageModify
    );
    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_MESSAGE_EVENT,
      SubscriptionServiceEventType.DELETE,
      this.handleDialogueMessageDelete
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_ACCOUNT_PERMISSION_EVENT,
      SubscriptionServiceEventType.ALL,
      this.onRoomAccountPermissionChange
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.DIALOGUE_TYPING_EVENT,
      SubscriptionServiceEventType.ALL,
      this.handleTypingEvent
    );

    this.subscriptionService.unsubscribe(
      SubscriptionServiceEvent.ROOM_TYPING_EVENT,
      SubscriptionServiceEventType.ALL,
      this.handleTypingEvent
    );
  }

  ngOnDestroy() {
    if (this.editedMessage) {
      this.editedMessage = null;
    }

    if (
      this.isMemberOfRoom &&
      (this.lastUnsentMessage.length > 0 || this.lastUnsentMessage === '')
    ) {
      this.chatService.saveMessageInStorage(this.resourceId, this.lastUnsentMessage);
    }

    window.removeEventListener('popstate', this.popStateCallback);

    // we dont need all of the callback references, this feature simply close the subscription, and delete the handlers
    this.chatService.unsubscribeAllChatChanges(this.resourceId);

    this.unsubscribeAllWorkspaceChanges();

    this.routerHandler.unsubscribe('room/?/chat/?', this.onRouterChange);
    this.copySnackBarRef?.dismiss();

    document.removeEventListener('pause', this.offscopeCallback);
  }

  onSubmit(ev) {
    if (this.isMessageSending || ev.trim().length == 0) return;
    this.lastUnsentMessage = '';
    this.chatService.removeMessageFromStorage(this.resourceId);

    // we have to trigger the sound after a user interaction
    // or it will be suspended forever
    this.audioService.playSound(Sounds.NOTIFICATION);

    this.isMessageSending = true;

    this.service
      .sendMessage(this.resourceId, ev)
      .then((msgId) => {
        this.slateEditorComponent && this.slateEditorComponent.messageSent(true);
        this.nativeEditorComponent && this.nativeEditorComponent.messageSent(true);
        this.sendSeen();
      })
      .catch((err) => {
        console.error('Send message error', err);
        this.slateEditorComponent && this.slateEditorComponent.messageSent(false);
        this.nativeEditorComponent && this.nativeEditorComponent.messageSent(false);
        this.snackbarService.showSnackbar(
          marker('Could not send message. (max length is 1000 characters)'),
          marker('OK')
        );
      })
      .finally(() => {
        this.isMessageSending = false;
      });
  }

  onEditSubmit(ev: string, editedMessage: ChatData) {
    if (this.isMessageSending || ev.trim().length == 0) return;

    this.isMessageSending = true;

    this.service
      .editMessage(this.resourceId, editedMessage.id, ev)
      .then((msgId) => {
        this.editedMessage = null;
        this.snackbarService.showSnackbar(marker('Message edited.'));
      })
      .catch((err) => {
        console.error('Send message error', err);
        this.snackbarService.showSnackbar(
          marker('Could not send message. (max length is 1000 characters)'),
          marker('OK')
        );
      })
      .finally(() => {
        this.isMessageSending = false;
      });
  }

  /**
   * Message id contains the timestamp in saferadix encoded
   * If you cut the last 5 char from the string, you'll get the encoded time
   * start at 2020.01.01 01:00 GMT+0100
   * every tick (calculated number by decodeSafeRadix) means 0.02 sec
   * @param id
   */
  private getTimeFromId(id: string): Date {
    let ticks = SafeRadix32.decodeSafeRadix32(id.slice(0, -5)); // get the ticks from the encoded number
    let timestamp = ticks * 20; // 0.02 sec for every tick
    return new Date(timestamp + 1577836800000); // add 2020.01.01 01:00 to it (this is the start)
  }

  /**
   * Convert raw server message into ChatData. Use loadMessage to install message into this system
   * @param data
   * @returns
   */
  private convertServerRecordToMessage(data: MessageRecord): ChatData {
    return {
      id: data.id,
      message: data.content,
      date: this.getTimeFromId(data.id),
      account: {
        id: data.posterId,
        avatarImageKey: null,
        avatarName: null,
        deleted: false,
      },
      edited: data.edited,
      seenBy: [],
      error: data.error,
      errorMessage: data.errorMessage,
      messageRef: data,
    };
  }

  /**
   * load, convert, and merge raw server response data into this chatData structure
   * connect user data to messages
   * @param data
   */
  private loadMessage(data: MessageRecord) {
    try {
      let msg = this.convertServerRecordToMessage(data);

      this.accountService.getAccount(msg.account.id).then((account) => {
        msg.account = account;
      });

      if (this.type == ResourceType.ROOM) {
        this.roomService
          .getMyRoomPermissions(this.resourceId)
          .then((roomPermission) => {
            msg.isOwnerOfRoom = roomPermission.ownerAccountId === msg.account.id;
          })
          .catch((err) => {
            console.error('could not get room perm:4', this.resourceId);
          });
      }

      let p = this.service.getSeenIdsByMessage(this.resourceId, msg.id).then((seenUserIdList) => {
        const filteredSeenBy = seenUserIdList.filter(
          (id) => id !== this.me.id && id !== msg.account.id
        );
        msg.seenBy = filteredSeenBy;

        for (const seenUserId of filteredSeenBy) {
          this.userSeenState[seenUserId] = msg.id;
        }
        return;
      });

      Object.assign(msg, this.calcReactionInfo(data.reactions));

      this.mergeMessage(msg);
      return p;
    } catch (e) {
      console.error('loadMessages error', e);
      return Promise.reject(e);
    }
  }

  private calcReactionInfo(reactions: { [userId: ID]: ReactionEnum }) {
    let allReaction = Object.values(reactions);
    let sumReaction = allReaction.length;
    let someReaction = [];
    for (let i = allReaction.length - 1; i >= 0; i--) {
      if (!someReaction.includes(allReaction[i])) {
        someReaction.push(allReaction[i]);
        if (someReaction.length >= MAXIMUM_EMOJI_KIND_NEXT_TO_MESSAGE) {
          break;
        }
      }
    }
    return {
      sumReaction,
      someReaction,
      myReaction: reactions[this.me.id] || null,
    };
  }

  private loadMessages(dataList: MessageRecord[]): Promise<void> {
    let promiseList: Promise<void>[] = [];
    for (let i = dataList.length - 1; i >= 0; i--) {
      promiseList.push(this.loadMessage(dataList[i]));
    }

    return Promise.all(promiseList).then(() => {
      return;
    });
  }

  private getMoreMessages(): Promise<void> {
    if (this.chatMessages.length == 0) return;

    /**
     * cursor in the message list
     */
    const pivot = this.chatMessages[this.chatMessages.length - 1].id;
    const cachedResourceId = this.resourceId;

    return this.service
      .getMoreMessages(this.resourceId, pivot)
      .then((oldMessages: MessageRecord[]) => {
        if (this.resourceId !== cachedResourceId) {
          return;
        }
        if (oldMessages.length) {
          return this.loadMessages(oldMessages).then(() => {
            this.isMessagesLoading = false;
            return;
          });
        } else {
          this.chatEndReached = true;
          this.isMessagesLoading = false;
          return;
        }
      });
  }

  /**
   * push message into the array
   * @param msg
   */
  private mergeMessage(msg: ChatData) {
    // two most common operation (push, unshift)
    let p = performance.now();

    if (this.chatMessages.length == 0) {
      this.chatMessages.push(msg);
      this.setGroupingProperties(0);
    } else if (
      msg.date.getTime() <= this.chatMessages[this.chatMessages.length - 1].date.getTime()
    ) {
      // new message is older than the last message
      this.chatMessages.push(msg);
      this.setGroupingProperties(this.chatMessages.length - 1);
    } else if (msg.date.getTime() >= this.chatMessages[0].date.getTime()) {
      // new message is newer than the "newest" message
      this.chatMessages.unshift(msg);
      // calculate isInGroup, hasLineBreak around the new message
      this.setGroupingProperties(1);
      this.setGroupingProperties(0);
    } else {
      // check for its place
      for (let i = 1; i < this.chatMessages.length; i++) {
        if (msg.date.getTime() >= this.chatMessages[i].date.getTime()) {
          this.chatMessages.splice(i, 0, msg);
          // calculate isInGroup, hasLineBreak around the new message
          this.setGroupingProperties(i);
          this.setGroupingProperties(i - 1);
          break;
        }
      }
    }
  }

  public onMessageResized(messageId) {
    // when the messages are resized, the flex-container's size is not updated and the scroll area is not adjusted
    // automatically because of a bug. We need to manually force the container to render for it to get fixed.
    this.triggerIOSGUIRefresh();
  }

  public onMessageDelete(messageId) {
    this.dialogService
      .openConfirmDialog(
        marker('Delete message'),
        marker('Are you sure you want to delete this message?')
      )
      .subscribe((confirmed: boolean) => {
        if (confirmed) {
          this.service.deleteMessage(this.resourceId, messageId).then(() => {
            this.snackbarService.showSnackbar(marker('Message deleted.'));
          });
        }
      });
  }

  chatBoxBottomVisible(isVisible: boolean) {
    this.isBottomVisible = isVisible;
    if (isVisible && !this.isMessagesLoading && this.chatMessages.length > 0) {
      this.sendSeen();
    }
  }

  private sendSeen() {
    this.isNewMessageAtBottom = false;
    if (this.isMemberOfRoom) {
      this.service.getData(this.resourceId).then((data) => {
        if (data['seenMessageId'] != data['topMessageId']) {
          this.service
            .seenMessage(this.resourceId, data['topMessageId'])
            .then(() => {
              // TODO handle false
            })
            .catch((err) => {
              console.log('seen error', err, data['topMessageId']);
            });
        }
      });
    }
  }

  chatBoxTopVisible(isVisible: boolean) {
    if (
      isVisible &&
      !this.chatEndReached &&
      !this.isMessagesLoading &&
      this.chatMessages.length > 0
    ) {
      this.isMessagesLoading = true;
      this.getMoreMessages();
    }
  }

  public onMouseDown(ev): void {
    this.isSelecting = true;
    this.clearPreviousSelection();
    //window.getSelection().removeAllRanges(); // it ruins the editor in edit mode; can not type character in the input (chrome)
    this.copySnackBarRef?.dismiss();
  }

  private selectChatMessagesBetweenElements(start: Node, end: Node): void {
    var chatParent = start.parentElement;
    var canSelect: boolean = false;

    for (let i = 0; i < chatParent.childNodes.length; i++) {
      const chatMessage = chatParent.childNodes.item(i);
      if (chatMessage.nodeType != 1 && (<HTMLElement>chatMessage).tagName != this.chatTag) continue;

      if (chatMessage == start || chatMessage == end) {
        canSelect = !canSelect;
        if (!canSelect) {
          //still need to add the last message
          (<HTMLElement>chatMessage).classList.add('selected');
          if (this.selectedChatMessages.every((scm) => scm != chatMessage))
            this.selectedChatMessages.push(<HTMLElement>chatMessage);
          continue;
        }
      }

      if (canSelect) {
        if (!this.selectedChatMessages.some((scm) => scm === chatMessage)) {
          (<HTMLElement>chatMessage).classList.add('selected');
          this.selectedChatMessages.push(<HTMLElement>chatMessage);
        }
      } else {
        (<HTMLElement>chatMessage).classList.remove('selected');
        this.selectedChatMessages = this.selectedChatMessages.filter((scm) => scm != chatMessage);
      }
    }
  }

  private selectFirstChatMessageDuringDeselect(start: Node) {
    var chatParent = start.parentElement;
    var canSelect: boolean = false;

    for (let i = 0; i < chatParent.childNodes.length; i++) {
      const chatMessage = chatParent.childNodes.item(i);
      if (chatMessage.nodeType != 1 && (<HTMLElement>chatMessage).tagName != this.chatTag) continue;

      canSelect = start == chatMessage;

      if (canSelect) {
        (<HTMLElement>chatMessage).classList.add('selected');
        if (this.selectedChatMessages.every((scm) => scm != chatMessage))
          this.selectedChatMessages.push(<HTMLElement>chatMessage);
      } else {
        (<HTMLElement>chatMessage).classList.remove('selected');
        this.selectedChatMessages = this.selectedChatMessages.filter((scm) => scm != chatMessage);
      }
    }
  }

  private getChatElementInParents(start: Node): Node {
    if (start.nodeType == 1 && (<HTMLElement>start).tagName === this.chatTag) {
      //node is HTMLElement and is chat element
      return start;
    }

    var el: HTMLElement = start.parentElement;
    while (el?.parentNode) {
      if (el.tagName === this.chatTag) return el;
      if (el.id === 'chat-scroll') return el.querySelector('app-chat-message:last-of-type') || null;
      if (el.id === 'is-typing-container')
        return el.parentElement.querySelector('app-chat-message:first-of-type') || null;

      el = el.parentElement;
    }

    return null;
  }

  private getOneMessageContentInParents(start: Node): Node {
    if (start.nodeType == 1 && (<HTMLElement>start).classList.contains('one-message-content')) {
      return start;
    }

    var el: HTMLElement = start.parentElement;
    while (el?.parentNode) {
      if (el.classList.contains('one-message-content')) {
        return el;
      }

      el = el.parentElement;
    }

    return null;
  }

  private clearPreviousSelection(): void {
    this.selectedChatMessages.forEach((cm) => cm.classList.remove('selected'));
    this.selectedChatMessages = [];
  }

  @HostListener('document:click', ['$event'])
  documentClick(event: MouseEvent) {
    if (this.isOnDesktop) {
      if (
        this.context_trigger.menuOpen &&
        (event.target as Element).innerHTML !== 'expand_circle_down'
      ) {
        this.context_trigger.closeMenu();
      }
    }
  }

  public handleChatMessageContextMenu = (data: ChatMessageContextMenuData) => {
    if (this.isOnDesktop) {
      if (this.context_trigger.menuOpen) {
        this.context_trigger.closeMenu();
        return;
      }
      this.isExpanded = false;
      this.getPermissions(data.data).then((res) => {
        this.contextTargetMessage = data.data;
        this.contextTargetPermissions = res;
        this.openContextMenu(data.event);
      });
    } else {
      // open bottomsheet
      this.isExpanded = false;
      this.getPermissions(data.data).then((res) => {
        this.contextTargetMessage = data.data;
        this.contextTargetPermissions = res;
        this.openMenuSheet();
      });
    }
  };

  public openMenuSheet(): void {
    var sheetRef = this.bottomSheet.open(ChatLayoutBottomSheetMenuComponent, {
      data: {
        contextTargetMessage: this.contextTargetMessage,
        contextTargetPermissions: this.contextTargetPermissions,
        // allowAnonym: this.react,
      },
    });
    sheetRef.afterDismissed().subscribe((ret: { action: ChatLayoutSheetAction; props?: any }) => {
      switch (ret?.action) {
        case ChatLayoutSheetAction.REACT: {
          this.addReaction(this.contextTargetMessage.id, ret.props); //props = emoji
          break;
        }
        case ChatLayoutSheetAction.COPY: {
          const copyText = this.parseCopiedMessages([this.contextTargetMessage]);
          this.clipboardService
            .copy(copyText.trim())
            .then(() => {
              this.snackbarService.showSnackbar(marker('Messages copied to clipboard!'));
            })
            .catch((err) => {
              console.error('clipboard err', err);
              this.dialogService.openAlertDialog(
                marker('Copy Error'),
                marker('Could not copy to the clipboard')
              );
            });
          break;
        }
        case ChatLayoutSheetAction.EDIT: {
          this.setEditedMessage(this.contextTargetMessage); // @todo
          break;
        }
        case ChatLayoutSheetAction.DELETE: {
          this.onMessageDelete(this.contextTargetMessage.id);
          break;
        }
        default:
          return;
      }
    });
  }

  private getPermissions = (data: ChatData): Promise<ChatMessagePermission> => {
    return this.accountService.getMe().then((account) => {
      let modifyPermission = data?.account.id == account.id;
      let deletePermission =
        data?.account.id == account.id ||
        this.myPermissions?.owner ||
        this.myPermissions?.moderator ||
        this.myPermissions?.administrator;
      let reactionPermission = this.myPermissions.noReactions !== true;

      return { modifyPermission, deletePermission, reactionPermission };
    });
  };

  public contextMenuAction = (action: ContextMenuAction, props) => {
    if (action in ContextMenuAction) {
      if (action === ContextMenuAction.edit) {
        this.setEditedMessage(this.contextTargetMessage); // @todo
      }
      if (action === ContextMenuAction.copy) {
        let selection = window.getSelection();
        let copyText = null;
        if (selection?.toString && selection.toString()) {
          copyText = selection.toString();
        } else {
          copyText = this.parseCopiedMessages([this.contextTargetMessage]);
        }
        selection.removeAllRanges();

        // copy
        this.clipboardService
          .copy(copyText.trim())
          .then(() => {
            this.snackbarService.showSnackbar(marker('Messages copied to clipboard!'));
          })
          .catch((err) => {
            console.error('clipboard err', err);
            this.dialogService.openAlertDialog(
              marker('Copy Error'),
              marker('Could not copy to the clipboard')
            );
          });
      }
      if (action === ContextMenuAction.delete) {
        this.onMessageDelete(this.contextTargetMessage.id);
      }
      if (action === ContextMenuAction.select) {
        // selecting targetMessage
      }
      if (action === ContextMenuAction.react) {
        this.addReaction(this.contextTargetMessage.id, props); //props = emoji
      }
    } else {
      console.warn('ContextMenuAction ');
    }
  };

  public expandEmojiPicker = (event: MouseEvent) => {
    event.stopPropagation();

    const EXPANDED_EMOJI_PANEL_HEIGHT = 240;

    // get generated css prop "transform-origin" from parent. It tells us in which way the mat menu opens (left/right top/bottom)
    const menuPosition =
      document.getElementsByClassName('chat-context-menu')?.[0].parentElement.parentElement
        .parentElement.style['transform-origin'];

    if (menuPosition && menuPosition?.split(' ')?.[1] === 'bottom') {
      // empty
    }

    if (menuPosition && menuPosition?.split(' ')?.[1] === 'top') {
      // there is not enough place for the expanded window: change Y coord to be placed higher
      let posY: number = +this.contextMenuPosition.y.split('px')[0];
      if (posY + EXPANDED_EMOJI_PANEL_HEIGHT > document.documentElement.scrollHeight) {
        this.contextMenuPosition.y =
          document.documentElement.scrollHeight - EXPANDED_EMOJI_PANEL_HEIGHT + 'px';
      }
    }
    this.isExpanded = true;
  };

  @HostListener('window:mouseup', ['$event'])
  mouseUp(e) {
    if (this.isSelecting) {
      this.isSelecting = false;
    }
  }

  @HostListener('document:selectionchange', ['$event'])
  selectionChange(e) {
    if (!this.isSelecting) return;

    var selection = window.getSelection();

    if (selection.anchorNode == null || selection.focusNode == null) return;

    var startSelectionNode = this.getChatElementInParents(selection.anchorNode);
    var endSelectionNode = this.getChatElementInParents(selection.focusNode);

    if (startSelectionNode && endSelectionNode) {
      if (startSelectionNode != endSelectionNode)
        this.selectChatMessagesBetweenElements(startSelectionNode, endSelectionNode);
      else if (this.selectedChatMessages.length > 0)
        //only run when not starting selection process
        this.selectFirstChatMessageDuringDeselect(startSelectionNode);
    }

    if (this.selectedChatMessages.length > 0 && !this.copySnackbarShowing) {
      this.copySnackbarShowing = true;
      this.copySnackBarRef = this.snackbar.openFromComponent(CopyChatMessagesSnackbarComponent, {
        horizontalPosition: 'center',
        verticalPosition: 'top',
        panelClass: ['mat-toolbar', 'mat-primary', 'nano-snackbar'],
      });
      this.copySnackBarRef.onAction().subscribe((ret) => {
        this.copySelectedMessages();
      });
      this.copySnackBarRef.afterDismissed().subscribe(() => {
        this.copySnackbarShowing = false;
        this.clearPreviousSelection();
        window.getSelection().removeAllRanges();
      });
    }
  }

  @HostListener('document:copy', ['$event']) onCtrlC(event) {
    if (this.selectedChatMessages.length > 0) {
      event.preventDefault();
      this.copySelectedMessages();
      this.copySnackBarRef?.dismiss();
    }
  }

  @HostListener('document:keydown.arrowup', ['$event']) editFirstOwnMessage(event: KeyboardEvent) {
    var firstOwnMessage = this.chatMessages.find((cm) => cm.account == this.me);
    if (firstOwnMessage && this.canEnterEditOnUpArrow && this.isChatInputFocused) {
      this.setEditedMessage(firstOwnMessage);
    }
  }

  @HostListener('document:keydown.escape', ['$event']) closeEditedMessage(event: KeyboardEvent) {
    if (this.editedMessage) {
      this.editedMessage = null;
    }
  }

  private copySelectedMessages() {
    const foundMsgArray: ChatData[] = [];
    for (var scm of this.selectedChatMessages.reverse()) {
      const messageId = scm.getAttribute('id');
      if (messageId) {
        // var name = scm.querySelector('.message-header .name').innerHTML;
        // var date = scm.querySelector('.message-header .date').innerHTML;
        const foundMsg = this.chatMessages.find((msg) => msg.id === messageId);
        foundMsgArray.push(foundMsg);
      }
    }

    const copyText = this.parseCopiedMessages(foundMsgArray);

    this.clipboardService
      .copy(copyText.trim())
      .then(() => {
        this.snackbarService.showSnackbar(marker('Messages copied to clipboard!'));
      })
      .catch((err) => {
        console.error('clipboard err', err);
        this.dialogService.openAlertDialog(
          marker('Copy Error'),
          marker('Could not copy to the clipboard')
        );
      });
  }

  private parseCopiedMessages = (messageArray: ChatData[]) => {
    const msgArray = [];
    for (let message of messageArray) {
      const name = message.account.avatarName;
      const date = formatDate(message.date, 'short', AppStorage.getItem('lang'));

      if (message?.message?.length === 0) continue;

      msgArray.push({ text: `${name}, [${date}] \n${message.message}`, date: message.date });
    }
    const sortedMsgArray = msgArray.sort((msg1, msg2) => msg1.date - msg2.date);
    const copyText: string = sortedMsgArray.reduce((sum, msg) => (sum += '\n' + msg.text), '');

    return copyText;
  };

  onInputChange(message: string): void {
    if (!this.editedMessage) {
      this.lastUnsentMessage = message;
    }

    if (message && message.length > 0) {
      this.canEnterEditOnUpArrow = false;
    } else {
      this.canEnterEditOnUpArrow = true;
    }

    if (this.isTypingTimer || message['target'] || message.length == 0) {
      //sometimes we get a 'change' HTMLElement event, we must ignore that
      return;
    } else {
      this.isTypingTimer = setTimeout(() => (this.isTypingTimer = null), 5000);
      this.service.sendTyping(this.resourceId);
    }
  }

  onFocusChanged(isFocused: boolean): void {
    this.isChatInputFocused = isFocused;
    if (
      isFocused &&
      this.chatScroll &&
      this.chatScroll.nativeElement.scrollTop != 0 &&
      this.deviceDetectorService.isMobile()
    ) {
      setTimeout(() => {
        this.chatScroll.nativeElement.scrollTo({
          top: 0,
          left: 0,
          behavior: 'smooth',
        });
      }, 500); // wait for keyboard shows up

      this.isNewMessageAtBottom = false;
    }
  }

  /**
   * sets the grouping properties of the "index"-th chatMessage if possible
   *
   * @param index index of message in this.chatMessages
   */
  private setGroupingProperties = (index: number) => {
    // const p = performance.now();
    const msg = this.chatMessages?.[index];
    if (msg) {
      msg.isInGroup = this.chatMessageIsInGroup(index);
      msg.hasLineBreak = !this.isCompactDisplay && !this.chatMessageIsInGroup(index - 1);
      msg.isOwnMessage = msg.account.id === this.me.id;
    }
  };

  getSelectedChatMessagePolicy = () => {
    //number: (d1: any, d2: any) => boolean
    const chatMessageComparePolicies = {
      [ChatMessageGroupingPolicy.sameYear]: (d1, d2) => d1.year === d2.year,
      [ChatMessageGroupingPolicy.sameMonth]: (d1, d2) =>
        d1.year === d2.year && d1.month === d2.month,
      [ChatMessageGroupingPolicy.sameDay]: (d1, d2) =>
        d1.year === d2.year && d1.month === d2.month && d1.day === d2.day,
      [ChatMessageGroupingPolicy.sameHour]: (d1, d2) =>
        d1.year === d2.year && d1.month === d2.month && d1.day === d2.day && d1.hour === d2.hour,
      [ChatMessageGroupingPolicy.sameMinute]: (d1, d2) =>
        d1.year === d2.year &&
        d1.month === d2.month &&
        d1.day === d2.day &&
        d1.hour === d2.hour &&
        d1.minute === d2.minute,
      [ChatMessageGroupingPolicy.in1Day]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 60 * 24,
      [ChatMessageGroupingPolicy.in3Hours]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 60 * 3,
      [ChatMessageGroupingPolicy.in2Hours]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 60 * 2,
      [ChatMessageGroupingPolicy.in1Hour]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 60 * 1,
      [ChatMessageGroupingPolicy.in30Minutes]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 30,
      [ChatMessageGroupingPolicy.in10Minutes]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 10,
      [ChatMessageGroupingPolicy.in5Minutes]: (d1, d2) =>
        Math.abs(d1.time - d2.time) < 1000 * 60 * 5,
    };

    const selectedChatMessageGroupingPolicy =
      AppStorage.getItem('chatGroupingPolicy') ?? ChatMessageGroupingPolicy.sameDay;

    const comparePolicy = chatMessageComparePolicies?.[selectedChatMessageGroupingPolicy];

    return comparePolicy;
  };

  chatMessageIsInGroup = (index) => {
    if (index > -1 && index <= this.chatMessages.length) {
      const msg = this.chatMessages[index];

      const prevMessage = this.chatMessages?.[index + 1];
      if (prevMessage && msg && prevMessage.account?.id === msg.account?.id) {
        const prevMessageTimes = {
          time: prevMessage.date.getTime(),
          year: prevMessage.date.getFullYear(),
          month: prevMessage.date.getMonth(),
          day: prevMessage.date.getDate(),
          hour: prevMessage.date.getHours(),
          minute: prevMessage.date.getMinutes(),
        };
        const msgTimes = {
          time: msg.date.getTime(),
          year: msg.date.getFullYear(),
          month: msg.date.getMonth(),
          day: msg.date.getDate(),
          hour: msg.date.getHours(),
          minute: msg.date.getMinutes(),
        };

        if (this.chatMessageComparePolicy) {
          return this.chatMessageComparePolicy(prevMessageTimes, msgTimes);
        } else {
          console.error(
            'Error while calculating comparePolicy for message',
            msg,
            prevMessage,
            msgTimes,
            prevMessageTimes
          );
          return false;
        }
      } else {
        return false;
      }
    } else {
      return false;
    }
  };

  public instantUploadDialog = () => {
    return null;
  };

  public navigateToDrive = () => {
    this.routerHandler.navigate(['/room/' + this.resourceId + '/drive'], {
      fragment: this.routerHandler.getRoute().rawFragment,
    });
  };

  public addReaction(msgId, reaction: ReactionEnum | null) {
    const foundMsg = this.chatMessages.find((msg) => msg.id === msgId);
    if (foundMsg) {
      if (foundMsg.myReaction === reaction) {
        this.service.reactMessage(this.resourceId, msgId, null);
      } else {
        this.service.reactMessage(this.resourceId, msgId, reaction);
      }
    }
  }

  private openContextMenu(event: MouseEvent) {
    this.contextMenuPosition.x = event.clientX + 'px';
    this.contextMenuPosition.y = event.clientY + 'px';
    console.log('contextMenu Pos: ', this.contextMenuPosition);
    this.context_trigger.openMenu();
  }
}
