import { createSelector } from 'reselect';

import { StateType } from '../reducer';
import {
  ConversationLookupType,
  ConversationsStateType,
  MentionsMembersType,
  MessageModelPropsWithConvoProps,
  MessageModelPropsWithoutConvoProps,
  MessagePropsDetails,
  ReduxConversationType,
  SortedMessageModelProps,
} from '../ducks/conversations';

import { getIntl, getOurNumber } from './user';
import { BlockedNumberController } from '../../util';
import { ConversationNotificationSetting, ConversationTypeEnum } from '../../models/conversation';
import { LocalizerType } from '../../types/Util';
import {
  ConversationHeaderProps,
  ConversationHeaderTitleProps,
} from '../../components/conversation/ConversationHeader';
import { LightBoxOptions } from '../../components/session/conversation/SessionConversation';
import { ReplyingToMessageProps } from '../../components/session/conversation/composition/CompositionBox';
import { getConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils';
import { MessageAvatarSelectorProps } from '../../components/conversation/message/MessageAvatar';
import _ from 'lodash';
import { MessagePreviewSelectorProps } from '../../components/conversation/message/MessagePreview';
import { MessageQuoteSelectorProps } from '../../components/conversation/message/MessageQuote';
import { MessageStatusSelectorProps } from '../../components/conversation/message/MessageStatus';
import { MessageTextSelectorProps } from '../../components/conversation/message/MessageText';
import { MessageContextMenuSelectorProps } from '../../components/conversation/message/MessageContextMenu';
import { MessageAuthorSelectorProps } from '../../components/conversation/message/MessageAuthorText';
import { MessageAttachmentSelectorProps } from '../../components/conversation/message/MessageAttachment';
import { MessageContentSelectorProps } from '../../components/conversation/message/MessageContent';
import { MessageContentWithStatusSelectorProps } from '../../components/conversation/message/MessageContentWithStatus';
import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/GenericReadableMessage';
import { getIsMessageRequestsEnabled } from './userConfig';

export const getConversations = (state: StateType): ConversationsStateType => state.conversations;

export const getConversationLookup = createSelector(
  getConversations,
  (state: ConversationsStateType): ConversationLookupType => {
    return state.conversationLookup;
  }
);

export const getConversationsCount = createSelector(getConversationLookup, (state): number => {
  return Object.values(state).length;
});

export const getBlockedPubkeys = createSelector(
  // make sure to extends this selector to we are rerun on conversation changes
  getConversationLookup,

  (_state): Array<string> => {
    return BlockedNumberController.getBlockedNumbers();
  }
);

export const getSelectedConversationKey = createSelector(
  getConversations,
  (state: ConversationsStateType): string | undefined => {
    return state.selectedConversation;
  }
);

export const getSelectedConversation = createSelector(
  getConversations,
  (state: ConversationsStateType): ReduxConversationType | undefined => {
    return state.selectedConversation
      ? state.conversationLookup[state.selectedConversation]
      : undefined;
  }
);

export const getSelectedConversationIsPublic = createSelector(
  getSelectedConversation,
  (state: ReduxConversationType | undefined): boolean => {
    return state?.isPublic || false;
  }
);

const getConversationId = (_whatever: any, id: string) => id;

export const getConversationById = createSelector(
  getConversations,
  getConversationId,
  (
    state: ConversationsStateType,
    convoId: string | undefined
  ): ReduxConversationType | undefined => {
    return convoId ? state.conversationLookup[convoId] : undefined;
  }
);

export const getIsTypingEnabled = createSelector(
  getConversations,
  getSelectedConversationKey,
  (state: ConversationsStateType, selectedConvoPubkey?: string): boolean => {
    if (!selectedConvoPubkey) {
      return false;
    }
    const selectedConvo = state.conversationLookup[selectedConvoPubkey];
    if (!selectedConvo) {
      return false;
    }
    const { isBlocked, isKickedFromGroup, left } = selectedConvo;

    return !(isBlocked || isKickedFromGroup || left);
  }
);
/**
 * Returns true if the current conversation selected is a group conversation.
 * Returns false if the current conversation selected is not a group conversation, or none are selected
 */
export const isGroupConversation = createSelector(
  getSelectedConversation,
  (state: ReduxConversationType | undefined): boolean => {
    return state?.type === 'group' || false;
  }
);

/**
 * Returns true if the current conversation selected is a closed group and false otherwise.
 */
export const isClosedGroupConversation = createSelector(
  getSelectedConversation,
  (state: ReduxConversationType | undefined): boolean => {
    return (state?.type === 'group' && !state.isPublic) || false;
  }
);

/**
 * Returns true if the current conversation selected is a public group and false otherwise.
 */
export const isPublicGroupConversation = createSelector(
  getSelectedConversation,
  (state: ReduxConversationType | undefined): boolean => {
    return (state?.type === 'group' && state.isPublic) || false;
  }
);

export const getOurPrimaryConversation = createSelector(
  getConversations,
  (state: ConversationsStateType): ReduxConversationType =>
    state.conversationLookup[window.storage.get('primaryDevicePubKey')]
);

const getMessagesOfSelectedConversation = createSelector(
  getConversations,
  (state: ConversationsStateType): Array<MessageModelPropsWithoutConvoProps> => state.messages
);

// Redux recommends to do filtered and deriving state in a selector rather than ourself
export const getSortedMessagesOfSelectedConversation = createSelector(
  getMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): Array<SortedMessageModelProps> => {
    if (messages.length === 0) {
      return [];
    }

    const convoId = messages[0].propsForMessage.convoId;
    const convo = getConversationController().get(convoId);

    if (!convo) {
      return [];
    }

    const isPublic = convo.isPublic() || false;
    const sortedMessage = sortMessages(messages, isPublic);

    return updateFirstMessageOfSeries(sortedMessage);
  }
);

export const getFirstUnreadMessageId = createSelector(
  getConversations,
  (state: ConversationsStateType): string | undefined => {
    return state.firstUnreadMessageId;
  }
);

export type MessagePropsType =
  | 'group-notification'
  | 'group-invitation'
  | 'data-extraction'
  | 'timer-notification'
  | 'regular-message'
  | 'unread-indicator'
  | 'missed-call-notification';

export const getSortedMessagesTypesOfSelectedConversation = createSelector(
  getSortedMessagesOfSelectedConversation,
  getFirstUnreadMessageId,
  (sortedMessages, firstUnreadId) => {
    const maxMessagesBetweenTwoDateBreaks = 5;
    // we want to show the date break if there is a large jump in time
    // remember that messages are sorted from the most recent to the oldest
    return sortedMessages.map((msg, index) => {
      const isFirstUnread = Boolean(firstUnreadId === msg.propsForMessage.id);
      const messageTimestamp = msg.propsForMessage.serverTimestamp || msg.propsForMessage.timestamp;
      const previousMessageTimestamp =
        index + 1 >= sortedMessages.length
          ? 0
          : sortedMessages[index + 1].propsForMessage.serverTimestamp ||
            sortedMessages[index + 1].propsForMessage.timestamp;

      const showDateBreak =
        messageTimestamp - previousMessageTimestamp > maxMessagesBetweenTwoDateBreaks * 60 * 1000
          ? messageTimestamp
          : undefined;

      if (msg.propsForDataExtractionNotification) {
        return {
          showUnreadIndicator: isFirstUnread,
          showDateBreak,
          message: {
            messageType: 'data-extraction',
            props: { ...msg.propsForDataExtractionNotification, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForGroupInvitation) {
        return {
          showUnreadIndicator: isFirstUnread,
          showDateBreak,
          message: {
            messageType: 'group-invitation',
            props: { ...msg.propsForGroupInvitation, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForGroupNotification) {
        return {
          showUnreadIndicator: isFirstUnread,
          showDateBreak,
          message: {
            messageType: 'group-notification',
            props: { ...msg.propsForGroupNotification, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForTimerNotification) {
        return {
          showUnreadIndicator: isFirstUnread,
          showDateBreak,
          message: {
            messageType: 'timer-notification',
            props: { ...msg.propsForTimerNotification, messageId: msg.propsForMessage.id },
          },
        };
      }

      if (msg.propsForMissedCall) {
        return {
          showUnreadIndicator: isFirstUnread,
          showDateBreak,
          message: {
            messageType: 'missed-call-notification',
            props: {
              ...msg.propsForMissedCall,
              messageId: msg.propsForMessage.id,
            },
          },
        };
      }

      return {
        showUnreadIndicator: isFirstUnread,
        showDateBreak,
        message: {
          messageType: 'regular-message',
          props: { messageId: msg.propsForMessage.id },
        },
      };
    });
  }
);

function getConversationTitle(
  conversation: ReduxConversationType,
  testingi18n?: LocalizerType
): string {
  if (conversation.name) {
    return conversation.name;
  }

  if (conversation.type === 'group') {
    return (testingi18n || window.i18n)('unknown');
  }
  return conversation.id;
}

const collator = new Intl.Collator();

export const _getConversationComparator = (testingi18n?: LocalizerType) => {
  return (left: ReduxConversationType, right: ReduxConversationType): number => {
    // Pin is the first criteria to check
    if (left.isPinned && !right.isPinned) {
      return -1;
    }
    if (!left.isPinned && right.isPinned) {
      return 1;
    }
    // Then if none is pinned, check other criteria
    const leftActiveAt = left.activeAt;
    const rightActiveAt = right.activeAt;
    if (leftActiveAt && !rightActiveAt) {
      return -1;
    }
    if (rightActiveAt && !leftActiveAt) {
      return 1;
    }
    if (leftActiveAt && rightActiveAt && leftActiveAt !== rightActiveAt) {
      return rightActiveAt - leftActiveAt;
    }
    const leftTitle = getConversationTitle(left, testingi18n).toLowerCase();
    const rightTitle = getConversationTitle(right, testingi18n).toLowerCase();

    return collator.compare(leftTitle, rightTitle);
  };
};

export const getConversationComparator = createSelector(getIntl, _getConversationComparator);

// export only because we use it in some of our tests
// tslint:disable-next-line: cyclomatic-complexity
export const _getLeftPaneLists = (
  sortedConversations: Array<ReduxConversationType>,
  isMessageRequestEnabled?: boolean
): {
  conversations: Array<ReduxConversationType>;
  contacts: Array<ReduxConversationType>;
  unreadCount: number;
} => {
  const conversations: Array<ReduxConversationType> = [];
  const directConversations: Array<ReduxConversationType> = [];

  let unreadCount = 0;
  for (const conversation of sortedConversations) {
    const excludeUnapproved =
      isMessageRequestEnabled && window.lokiFeatureFlags?.useMessageRequests;

    if (conversation.activeAt !== undefined && conversation.type === ConversationTypeEnum.PRIVATE) {
      directConversations.push(conversation);
    }

    if (excludeUnapproved && !conversation.isApproved && !conversation.isBlocked) {
      // dont increase unread counter, don't push to convo list.
      continue;
    }

    if (
      unreadCount < 9 &&
      conversation.unreadCount &&
      conversation.unreadCount > 0 &&
      conversation.currentNotificationSetting !== 'disabled'
    ) {
      unreadCount += conversation.unreadCount;
    }

    conversations.push(conversation);
  }

  return {
    conversations,
    contacts: directConversations,
    unreadCount,
  };
};

export const _getSortedConversations = (
  lookup: ConversationLookupType,
  comparator: (left: ReduxConversationType, right: ReduxConversationType) => number,
  selectedConversation?: string
): Array<ReduxConversationType> => {
  const values = Object.values(lookup);
  const sorted = values.sort(comparator);

  const sortedConversations: Array<ReduxConversationType> = [];

  for (let conversation of sorted) {
    if (selectedConversation === conversation.id) {
      conversation = {
        ...conversation,
        isSelected: true,
      };
    }

    const isBlocked =
      BlockedNumberController.isBlocked(conversation.id) ||
      BlockedNumberController.isGroupBlocked(conversation.id);

    if (isBlocked) {
      conversation = {
        ...conversation,
        isBlocked: true,
      };
    }

    // Add Open Group to list as soon as the name has been set
    if (conversation.isPublic && (!conversation.name || conversation.name === 'Unknown group')) {
      continue;
    }

    // Remove all invalid conversations and conversatons of devices associated
    //  with cancelled attempted links
    if (!conversation.isPublic && !conversation.activeAt) {
      continue;
    }

    sortedConversations.push(conversation);
  }

  return sortedConversations;
};

export const getSortedConversations = createSelector(
  getConversationLookup,
  getConversationComparator,
  getSelectedConversationKey,
  _getSortedConversations
);

export const _getConversationRequests = (
  sortedConversations: Array<ReduxConversationType>,
  isMessageRequestEnabled?: boolean
): Array<ReduxConversationType> => {
  const pushToMessageRequests =
    isMessageRequestEnabled && window?.lokiFeatureFlags?.useMessageRequests;
  return _.filter(sortedConversations, conversation => {
    return pushToMessageRequests && !conversation.isApproved && !conversation.isBlocked;
  });
};

export const getConversationRequests = createSelector(
  getSortedConversations,
  getIsMessageRequestsEnabled,
  _getConversationRequests
);

export const getLeftPaneLists = createSelector(
  getSortedConversations,
  getIsMessageRequestsEnabled,
  _getLeftPaneLists
);

export const getMe = createSelector(
  [getConversationLookup, getOurNumber],
  (lookup: ConversationLookupType, ourNumber: string): ReduxConversationType => {
    return lookup[ourNumber];
  }
);

export const getDirectContacts = createSelector(
  getLeftPaneLists,
  (state: {
    conversations: Array<ReduxConversationType>;
    contacts: Array<ReduxConversationType>;
    unreadCount: number;
  }) => state.contacts
);

export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): number => {
  return state.unreadCount;
});

export const getConversationHeaderTitleProps = createSelector(getSelectedConversation, (state):
  | ConversationHeaderTitleProps
  | undefined => {
  if (!state) {
    return undefined;
  }
  return {
    isKickedFromGroup: !!state.isKickedFromGroup,
    conversationKey: state.id,
    isMe: !!state.isMe,
    members: state.members || [],
    isPublic: !!state.isPublic,
    profileName: state.profileName,
    name: state.name,
    subscriberCount: state.subscriberCount,
    isGroup: state.type === 'group',
    currentNotificationSetting: state.currentNotificationSetting,
  };
});

/**
 * Returns the formatted text for notification setting.
 */
export const getCurrentNotificationSettingText = createSelector(getSelectedConversation, (state):
  | string
  | undefined => {
  if (!state) {
    return undefined;
  }
  switch (state.currentNotificationSetting) {
    case 'all':
      return window.i18n('notificationForConvo_all');
    case 'mentions_only':
      return window.i18n('notificationForConvo_mentions_only');
    case 'disabled':
      return window.i18n('notificationForConvo_disabled');
    default:
      return window.i18n('notificationForConvo_all');
  }
});

export const getConversationHeaderProps = createSelector(getSelectedConversation, (state):
  | ConversationHeaderProps
  | undefined => {
  if (!state) {
    return undefined;
  }

  const expirationSettingName = state.expireTimer
    ? window.Whisper.ExpirationTimerOptions.getName(state.expireTimer || 0)
    : null;

  return {
    conversationKey: state.id,
    isPrivate: !!state.isPrivate,
    currentNotificationSetting:
      state.currentNotificationSetting || ConversationNotificationSetting[0], // if undefined, it is 'all'
    isBlocked: !!state.isBlocked,
    left: !!state.left,
    avatarPath: state.avatarPath || null,
    expirationSettingName: expirationSettingName,
    hasNickname: !!state.hasNickname,
    weAreAdmin: !!state.weAreAdmin,
    isKickedFromGroup: !!state.isKickedFromGroup,
    isMe: !!state.isMe,
    members: state.members || [],
    isPublic: !!state.isPublic,
    profileName: state.profileName,
    name: state.name,
    subscriberCount: state.subscriberCount,
    isGroup: !!state.isGroup,
  };
});

export const getIsSelectedPrivate = createSelector(
  getConversationHeaderProps,
  (headerProps): boolean => {
    return headerProps?.isPrivate || false;
  }
);

export const getIsSelectedNoteToSelf = createSelector(
  getConversationHeaderProps,
  (headerProps): boolean => {
    return headerProps?.isMe || false;
  }
);

export const getNumberOfPinnedConversations = createSelector(getConversations, (state): number => {
  const values = Object.values(state.conversationLookup);
  return values.filter(conversation => conversation.isPinned).length;
});

export const isMessageDetailView = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.messageDetailProps !== undefined
);

export const getMessageDetailsViewProps = createSelector(
  getConversations,
  (state: ConversationsStateType): MessagePropsDetails | undefined => state.messageDetailProps
);

export const isRightPanelShowing = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.showRightPanel
);

export const isMessageSelectionMode = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.selectedMessageIds.length > 0
);

export const getSelectedMessageIds = createSelector(
  getConversations,
  (state: ConversationsStateType): Array<string> => state.selectedMessageIds
);

export const getLightBoxOptions = createSelector(
  getConversations,
  (state: ConversationsStateType): LightBoxOptions | undefined => state.lightBox
);

export const getQuotedMessage = createSelector(
  getConversations,
  (state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage
);

export const areMoreMessagesBeingFetched = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.areMoreMessagesBeingFetched || false
);

export const getHaveDoneFirstScroll = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.haveDoneFirstScroll
);

export const getShowScrollButton = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => state.showScrollButton || false
);

export const getQuotedMessageToAnimate = createSelector(
  getConversations,
  (state: ConversationsStateType): string | undefined => state.animateQuotedMessageId || undefined
);

export const getNextMessageToPlayId = createSelector(
  getConversations,
  (state: ConversationsStateType): string | undefined => state.nextMessageToPlayId || undefined
);

export const getMentionsInput = createSelector(
  getConversations,
  (state: ConversationsStateType): MentionsMembersType => state.mentionMembers
);

/// Those calls are just related to ordering messages in the redux store.

function updateFirstMessageOfSeries(
  messageModelsProps: Array<MessageModelPropsWithoutConvoProps>
): Array<SortedMessageModelProps> {
  // messages are got from the more recent to the oldest, so we need to check if
  // the next messages in the list is still the same author.
  // The message is the first of the series if the next message is not from the same author
  const sortedMessageProps: Array<SortedMessageModelProps> = [];

  for (let i = 0; i < messageModelsProps.length; i++) {
    const currentSender = messageModelsProps[i].propsForMessage?.authorPhoneNumber;
    // most recent message is at index 0, so the previous message sender is 1+index
    const previousSender =
      i < messageModelsProps.length - 1
        ? messageModelsProps[i + 1].propsForMessage?.authorPhoneNumber
        : undefined;
    const nextSender =
      i > 0 ? messageModelsProps[i - 1].propsForMessage?.authorPhoneNumber : undefined;
    // Handle firstMessageOfSeries for conditional avatar rendering

    sortedMessageProps.push({
      ...messageModelsProps[i],
      firstMessageOfSeries: !(i >= 0 && currentSender === previousSender),
      lastMessageOfSeries: currentSender !== nextSender,
    });
  }
  return sortedMessageProps;
}

function sortMessages(
  messages: Array<MessageModelPropsWithoutConvoProps>,
  isPublic: boolean
): Array<MessageModelPropsWithoutConvoProps> {
  // we order by serverTimestamp for public convos
  // be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
  if (isPublic) {
    return messages.slice().sort((a, b) => {
      return (b.propsForMessage.serverTimestamp || 0) - (a.propsForMessage.serverTimestamp || 0);
    });
  }
  if (messages.some(n => !n.propsForMessage.timestamp && !n.propsForMessage.receivedAt)) {
    throw new Error('Found some messages without any timestamp set');
  }

  // for non public convos, we order by sent_at or received_at timestamp.
  // we assume that a message has either a sent_at or a received_at field set.
  const messagesSorted = messages
    .slice()
    .sort(
      (a, b) =>
        (b.propsForMessage.timestamp || b.propsForMessage.receivedAt || 0) -
        (a.propsForMessage.timestamp || a.propsForMessage.receivedAt || 0)
    );

  return messagesSorted;
}

export const getMostRecentMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
    return messages.length ? messages[0].propsForMessage.id : undefined;
  }
);

export const getOldestMessageId = createSelector(
  getSortedMessagesOfSelectedConversation,
  (messages: Array<MessageModelPropsWithoutConvoProps>): string | undefined => {
    const oldest =
      messages.length > 0 ? messages[messages.length - 1].propsForMessage.id : undefined;

    return oldest;
  }
);

export const getLoadedMessagesLength = createSelector(
  getConversations,
  (state: ConversationsStateType): number => {
    return state.messages.length || 0;
  }
);

export const isFirstUnreadMessageIdAbove = createSelector(
  getConversations,
  (state: ConversationsStateType): boolean => {
    if (!state.firstUnreadMessageId) {
      return false;
    }

    const isNotPresent = !state.messages.some(
      m => m.propsForMessage.id === state.firstUnreadMessageId
    );

    return isNotPresent;
  }
);

const getMessageId = (_whatever: any, id: string) => id;

export const getMessagePropsByMessageId = createSelector(
  getConversations,
  getSortedMessagesOfSelectedConversation,
  getConversationLookup,
  getMessageId,
  (
    _convoState,
    messages: Array<SortedMessageModelProps>,
    conversations,
    id
  ): MessageModelPropsWithConvoProps | undefined => {
    const foundMessageProps: SortedMessageModelProps | undefined = messages?.find(
      m => m?.propsForMessage?.id === id
    );

    if (!foundMessageProps || !foundMessageProps.propsForMessage.convoId) {
      return undefined;
    }
    const authorPhoneNumber = foundMessageProps?.propsForMessage?.authorPhoneNumber;

    const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId];
    if (!foundMessageConversation || !authorPhoneNumber) {
      return undefined;
    }

    const foundSenderConversation = conversations[authorPhoneNumber];
    if (!foundSenderConversation) {
      return undefined;
    }

    const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
    const isGroup = !foundMessageConversation.isPrivate;
    const isPublic = foundMessageConversation.isPublic;

    const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
    const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
    // A message is deletable if
    // either we sent it,
    // or the convo is not a public one (in this case, we will only be able to delete for us)
    // or the convo is public and we are an admin
    const isDeletable = authorPhoneNumber === ourPubkey || !isPublic || (isPublic && !!weAreAdmin);

    // A message is deletable for everyone if
    // either we sent it no matter what the conversation type,
    // or the convo is public and we are an admin
    const isDeletableForEveryone =
      authorPhoneNumber === ourPubkey || (isPublic && !!weAreAdmin) || false;

    const isSenderAdmin = groupAdmins.includes(authorPhoneNumber);
    const senderIsUs = authorPhoneNumber === ourPubkey;

    const authorName = foundSenderConversation.name || null;
    const authorProfileName = senderIsUs ? window.i18n('you') : foundSenderConversation.profileName;

    const messageProps: MessageModelPropsWithConvoProps = {
      ...foundMessageProps,
      propsForMessage: {
        ...foundMessageProps.propsForMessage,
        isBlocked: !!foundMessageConversation.isBlocked,
        isPublic: !!isPublic,
        isOpenGroupV2: !!isPublic,
        isSenderAdmin,
        isDeletable,
        isDeletableForEveryone,
        weAreAdmin,
        conversationType: foundMessageConversation.type,
        authorPhoneNumber,
        authorAvatarPath: foundSenderConversation.avatarPath || null,
        isKickedFromGroup: foundMessageConversation.isKickedFromGroup || false,
        authorProfileName,
        authorName,
      },
    };

    return messageProps;
  }
);

export const getMessageAvatarProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageAvatarSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const {
    authorAvatarPath,
    authorName,
    authorPhoneNumber,
    authorProfileName,
    conversationType,
    direction,
    isPublic,
    isSenderAdmin,
  } = props.propsForMessage;

  const { lastMessageOfSeries } = props;

  const messageAvatarProps: MessageAvatarSelectorProps = {
    authorAvatarPath,
    authorName,
    authorPhoneNumber,
    authorProfileName,
    conversationType,
    direction,
    isPublic,
    isSenderAdmin,
    lastMessageOfSeries,
  };

  return messageAvatarProps;
});

export const getMessagePreviewProps = createSelector(getMessagePropsByMessageId, (props):
  | MessagePreviewSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const { attachments, previews } = props.propsForMessage;

  const msgProps: MessagePreviewSelectorProps = {
    attachments,
    previews,
  };

  return msgProps;
});

export const getMessageQuoteProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageQuoteSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const { direction, quote } = props.propsForMessage;

  const msgProps: MessageQuoteSelectorProps = {
    direction,
    quote,
  };

  return msgProps;
});

export const getMessageStatusProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageStatusSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const { direction, status } = props.propsForMessage;

  const msgProps: MessageStatusSelectorProps = {
    direction,
    status,
  };

  return msgProps;
});

export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageTextSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const { conversationType, convoId, direction, status, text, isDeleted } = props.propsForMessage;

  const msgProps: MessageTextSelectorProps = {
    conversationType,
    convoId,
    direction,
    status,
    text,
    isDeleted,
  };

  return msgProps;
});

export const getMessageContextMenuProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageContextMenuSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const {
    attachments,
    authorPhoneNumber,
    convoId,
    direction,
    status,
    isDeletable,
    isPublic,
    isOpenGroupV2,
    weAreAdmin,
    isSenderAdmin,
    text,
    serverTimestamp,
    timestamp,
    isBlocked,
    isDeletableForEveryone,
  } = props.propsForMessage;

  const msgProps: MessageContextMenuSelectorProps = {
    attachments,
    authorPhoneNumber,
    convoId,
    direction,
    status,
    isDeletable,
    isPublic,
    isOpenGroupV2,
    weAreAdmin,
    isSenderAdmin,
    text,
    serverTimestamp,
    timestamp,
    isBlocked,
    isDeletableForEveryone,
  };

  return msgProps;
});

export const getMessageAuthorProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageAuthorSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const { authorName, authorPhoneNumber, authorProfileName, direction } = props.propsForMessage;
  const { firstMessageOfSeries } = props;

  const msgProps: MessageAuthorSelectorProps = {
    authorName,
    authorPhoneNumber,
    authorProfileName,
    direction,
    firstMessageOfSeries,
  };

  return msgProps;
});

export const getMessageIsDeletable = createSelector(
  getMessagePropsByMessageId,
  (props): boolean => {
    if (!props || _.isEmpty(props)) {
      return false;
    }

    return props.propsForMessage.isDeletable;
  }
);

export const getMessageAttachmentProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageAttachmentSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const {
    attachments,
    direction,
    isTrustedForAttachmentDownload,
    timestamp,
    serverTimestamp,
    authorPhoneNumber,
    convoId,
  } = props.propsForMessage;
  const msgProps: MessageAttachmentSelectorProps = {
    attachments: attachments || [],
    direction,
    isTrustedForAttachmentDownload,
    timestamp,
    serverTimestamp,
    authorPhoneNumber,
    convoId,
  };

  return msgProps;
});

export const getIsMessageSelected = createSelector(
  getMessagePropsByMessageId,
  getSelectedMessageIds,
  (props, selectedIds): boolean => {
    if (!props || _.isEmpty(props)) {
      return false;
    }

    const { id } = props.propsForMessage;

    return selectedIds.includes(id);
  }
);

export const getMessageContentSelectorProps = createSelector(getMessagePropsByMessageId, (props):
  | MessageContentSelectorProps
  | undefined => {
  if (!props || _.isEmpty(props)) {
    return undefined;
  }

  const {
    text,
    direction,
    timestamp,
    serverTimestamp,
    previews,
    attachments,
    quote,
  } = props.propsForMessage;

  const { firstMessageOfSeries, lastMessageOfSeries } = props;
  const msgProps: MessageContentSelectorProps = {
    direction,
    firstMessageOfSeries,
    lastMessageOfSeries,
    serverTimestamp,
    text,
    timestamp,
    previews,
    quote,
    attachments,
  };

  return msgProps;
});

export const getMessageContentWithStatusesSelectorProps = createSelector(
  getMessagePropsByMessageId,
  (props): MessageContentWithStatusSelectorProps | undefined => {
    if (!props || _.isEmpty(props)) {
      return undefined;
    }

    const {
      direction,
      isDeleted,
      attachments,
      isTrustedForAttachmentDownload,
    } = props.propsForMessage;

    const msgProps: MessageContentWithStatusSelectorProps = {
      direction,
      isDeleted,
      hasAttachments: Boolean(attachments?.length) || false,
      isTrustedForAttachmentDownload,
    };

    return msgProps;
  }
);

export const getGenericReadableMessageSelectorProps = createSelector(
  getMessagePropsByMessageId,
  (props): GenericReadableMessageSelectorProps | undefined => {
    if (!props || _.isEmpty(props)) {
      return undefined;
    }

    const {
      direction,
      conversationType,
      expirationLength,
      expirationTimestamp,
      isExpired,
      isUnread,
      receivedAt,
      isKickedFromGroup,
      isDeleted,
    } = props.propsForMessage;

    const msgProps: GenericReadableMessageSelectorProps = {
      direction,
      conversationType,
      expirationLength,
      expirationTimestamp,
      isUnread,
      isExpired,
      convoId: props.propsForMessage.convoId,
      receivedAt,
      isKickedFromGroup,
      isDeleted,
    };

    return msgProps;
  }
);
