import type { ElementRef, TeactNode } from '../../lib/teact/teact';
import { getIsHeavyAnimating, memo } from '../../lib/teact/teact';
import { getActions, getGlobal } from '../../global';

import type { ApiMessage } from '../../api/types';
import type { IAlbum, IDocumentGroup, MessageListType, ThreadId } from '../../types';
import type { Signal } from '../../util/signals';
import type { MessageDateGroup } from './helpers/groupMessages';
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types';

import { SCHEDULED_WHEN_ONLINE } from '../../config';
import {
  getMessageHtmlId,
  getMessageOriginalId,
  getSuggestedChangesActionText,
  getSuggestedChangesInfo,
  isActionMessage,
  isOwnMessage,
  isServiceNotificationMessage,
} from '../../global/helpers';
import { getPeerTitle } from '../../global/helpers/peers';
import { selectChatMessage, selectSender } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { formatHumanDate, formatScheduledDateTime } from '../../util/dates/oldDateFormat';
import { isUserId } from '../../util/entities/ids';
import { convertTonFromNanos } from '../../util/formatCurrency';
import { compact } from '../../util/iteratees';
import { formatMessageListDate } from '../../util/localization/dateFormat';
import { formatStarsAsText, formatTonAsText } from '../../util/localization/format';
import { isAlbum, isDocumentGroup } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import { renderPeerLink } from './message/helpers/messageActions';

import useDerivedSignal from '../../hooks/useDerivedSignal';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useMessageObservers from './hooks/useMessageObservers';
import useScrollHooks from './hooks/useScrollHooks';

import Icon from '../common/icons/Icon';
import MiniTable, { type TableEntry } from '../common/MiniTable';
import ActionMessage from './message/ActionMessage';
import Message from './message/Message';
import SenderGroupContainer from './message/SenderGroupContainer';
import SponsoredMessage from './message/SponsoredMessage';
import MessageListAccountInfo from './MessageListAccountInfo';
import MessageListBottomMarker from './MessageListBottomMarker';

import actionMessageStyles from './message/ActionMessage.module.scss';

interface OwnProps {
  canShowAds?: boolean;
  chatId: string;
  threadId: ThreadId;
  messageIds: number[];
  messageGroups: MessageDateGroup[];
  getContainerHeight: Signal<number | undefined>;
  isViewportNewest: boolean;
  isUnread: boolean;
  withUsers: boolean;
  isChannelChat: boolean | undefined;
  isChatMonoforum?: boolean;
  canManageBotForumTopics?: boolean;
  isEmptyThread?: boolean;
  isComments?: boolean;
  noAvatars: boolean;
  containerRef: ElementRef<HTMLDivElement>;
  anchorIdRef: { current: string | undefined };
  memoUnreadDividerBeforeIdRef: { current: number | undefined };
  memoFirstUnreadIdRef: { current: number | undefined };
  liveTailStartOriginalId?: number;
  isReplacingHistoryRef: { current: boolean };
  type: MessageListType;
  isReady: boolean;
  hasLinkedChat: boolean | undefined;
  isSchedule: boolean;
  shouldRenderAccountInfo?: boolean;
  nameChangeDate?: number;
  photoChangeDate?: number;
  noAppearanceAnimation: boolean;
  isSavedDialog?: boolean;
  isQuickPreview?: boolean;
  canPost?: boolean;
  shouldScrollToBottom?: boolean;
  onScrollDownToggle?: BooleanToVoidFunction;
  onNotchToggle?: AnyToVoidFunction;
  onIntersectPinnedMessage?: OnIntersectPinnedMessage;
}

const UNREAD_DIVIDER_CLASS = 'unread-divider';

function senderGroupContainsOriginalId(
  senderGroup: (ApiMessage | IAlbum | IDocumentGroup)[],
  originalId: number,
) {
  return senderGroup.some((messageOrAlbum) => {
    if (isAlbum(messageOrAlbum) || isDocumentGroup(messageOrAlbum)) {
      return messageOrAlbum.messages.some((message) => getMessageOriginalId(message) === originalId);
    }

    return getMessageOriginalId(messageOrAlbum) === originalId;
  });
}

const MessageListContent = ({
  canShowAds,
  chatId,
  threadId,
  messageIds,
  messageGroups,
  getContainerHeight,
  isViewportNewest,
  isUnread,
  isComments,
  isEmptyThread,
  withUsers,
  isChannelChat,
  isChatMonoforum,
  canManageBotForumTopics,
  noAvatars,
  containerRef,
  anchorIdRef,
  memoUnreadDividerBeforeIdRef,
  memoFirstUnreadIdRef,
  liveTailStartOriginalId,
  isReplacingHistoryRef,
  type,
  isReady,
  hasLinkedChat,
  isSchedule,
  shouldRenderAccountInfo,
  nameChangeDate,
  photoChangeDate,
  noAppearanceAnimation,
  isSavedDialog,
  isQuickPreview,
  shouldScrollToBottom,
  canPost,
  onScrollDownToggle,
  onNotchToggle,
  onIntersectPinnedMessage,
}: OwnProps) => {
  const { openHistoryCalendar } = getActions();

  const getIsHeavyAnimating2 = getIsHeavyAnimating;
  const getIsReady = useDerivedSignal(() => isReady && !getIsHeavyAnimating2(), [isReady, getIsHeavyAnimating2]);

  const areDatesClickable = !isSavedDialog && !isSchedule;
  const isPrivate = isUserId(chatId);
  const shouldRenderSponsoredMessage = canShowAds && isViewportNewest;
  const shouldHideComments = hasLinkedChat === false || !isChannelChat || Boolean(isChatMonoforum);

  const {
    observeIntersectionForReading,
    observeIntersectionForLoading,
    observeIntersectionForPlaying,
    onMessageUnmount,
  } = useMessageObservers({
    type,
    containerRef,
    memoFirstUnreadIdRef,
    chatId,
    threadId,
    isQuickPreview,
    onIntersectPinnedMessage,
  });

  const {
    withHistoryTriggers,
    backwardsTriggerRef,
    forwardsTriggerRef,
    fabTriggerRef,
  } = useScrollHooks({
    type,
    containerRef,
    messageIds,
    getContainerHeight,
    isViewportNewest,
    isUnread,
    isReady,
    isReplacingHistoryRef,
    onScrollDownToggle,
    onNotchToggle,
  });

  const oldLang = useOldLang();
  const lang = useLang();

  const unreadDivider = (
    <div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
      <span>{oldLang('UnreadMessages')}</span>
    </div>
  );
  const renderPaidMessageAction = (message: ApiMessage, album?: IAlbum) => {
    if (message.paidMessageStars) {
      const messagesLength = album?.messages?.length || 1;
      const amount = message.paidMessageStars * messagesLength;
      return (
        <div
          className={buildClassName('local-action-message')}
          key={`paid-messages-action-${message.id}`}
        >
          <span>
            {
              message.isOutgoing
                ? lang('ActionPaidOneMessageOutgoing', {
                  amount: formatStarsAsText(lang, amount),
                })
                : (() => {
                  const sender = selectSender(getGlobal(), message);
                  const userTitle = sender ? getPeerTitle(lang, sender) : '';
                  return lang('ActionPaidOneMessageIncoming', {
                    user: userTitle,
                    amount: formatStarsAsText(lang, amount),
                  });
                })()
            }
          </span>
        </div>
      );
    }
    return undefined;
  };

  const renderSuggestedPostInfoAction = (message: ApiMessage) => {
    if (message.suggestedPostInfo) {
      const { price, scheduleDate } = message.suggestedPostInfo;
      const sender = selectSender(getGlobal(), message);
      const userTitle = sender ? getPeerTitle(lang, sender) : '';
      const userLink = renderPeerLink(sender?.id, userTitle || lang('ActionFallbackUser'));

      const originalMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
        ? selectChatMessage(getGlobal(), message.chatId, message.replyInfo.replyToMsgId)
        : undefined;
      const changesInfo = getSuggestedChangesInfo(message, originalMessage);

      const titleText = changesInfo
        ? getSuggestedChangesActionText(lang, message, originalMessage, message.isOutgoing, userLink)
        : message.isOutgoing
          ? lang('ActionSuggestedPostOutgoing', undefined, { withNodes: true, withMarkdown: true })
          : lang('ActionSuggestedPostIncoming', { user: userLink }, { withNodes: true, withMarkdown: true });

      const tableData: TableEntry[] = compact([
        [lang('TitlePrice'), price ? (price.currency === 'TON'
          ? formatTonAsText(lang, convertTonFromNanos(price.amount))
          : formatStarsAsText(lang, price.amount)) : lang('SuggestMessageNoPrice')],
        [lang('TitleTime'),
          scheduleDate
            ? formatScheduledDateTime(scheduleDate, lang, oldLang)
            : lang('SuggestMessageAnytime'),
        ],
      ]);

      return (
        <div
          className={buildClassName('local-action-message')}
          key={`suggested-post-action-${message.id}`}
        >
          <span className={actionMessageStyles.suggestedPostContainer}>
            <div
              className={actionMessageStyles.suggestedPostTitle}
            >
              {titleText}
            </div>
            {Boolean(tableData.length) && (
              <MiniTable
                className={actionMessageStyles.suggestedPostInfo}
                data={tableData}
              />
            )}
          </span>
        </div>
      );
    }
    return undefined;
  };

  const renderBotForumTopicAction = () => {
    if (!canManageBotForumTopics || threadId !== MAIN_THREAD_ID) return undefined;
    return (
      <div className={buildClassName('local-action-message', actionMessageStyles.root)} key="botforum-new-topic">
        <div className={actionMessageStyles.contentBox}>
          <Icon className={actionMessageStyles.botForumTopicIcon} name="topic-new" />
          <h3 className={actionMessageStyles.botForumTopicTitle}>{lang('BotForumActionNew')}</h3>
          <span className={actionMessageStyles.botForumTopicDescription}>{lang('BotForumActionNewDescription')}</span>
          <Icon className={actionMessageStyles.botForumTopicArrow} name="down" />
        </div>
      </div>
    );
  };

  const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
    return acc + messageGroup.senderGroups.flat().reduce((innerAcc, messageOrAlbum) => {
      return innerAcc + (isDocumentGroup(messageOrAlbum) ? messageOrAlbum.messages.length : 1);
    }, 0);
  }, 0);
  let appearanceIndex = 0;

  const prevMessageIds = usePreviousDeprecated(messageIds);
  const isNewMessage = Boolean(
    messageIds && prevMessageIds && messageIds[messageIds.length - 2] === prevMessageIds[prevMessageIds.length - 1],
  );

  function calculateSenderGroups(
    dateGroup: MessageDateGroup, dateGroupIndex: number, dateGroupsArray: MessageDateGroup[],
  ) {
    return dateGroup.senderGroups.map((
      senderGroup,
      senderGroupIndex,
      senderGroupsArray,
    ) => {
      if (
        senderGroup.length === 1
        && !isAlbum(senderGroup[0])
        && !isDocumentGroup(senderGroup[0])
        && isActionMessage(senderGroup[0])
        && senderGroup[0].content.action?.type !== 'phoneCall'
      ) {
        const message = senderGroup[0];
        const isLastInList = (
          senderGroupIndex === senderGroupsArray.length - 1
          && dateGroupIndex === dateGroupsArray.length - 1
        );

        return compact([
          message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
          <ActionMessage
            key={message.id}
            message={message}
            threadId={threadId}
            observeIntersectionForBottom={observeIntersectionForReading}
            observeIntersectionForLoading={observeIntersectionForLoading}
            observeIntersectionForPlaying={observeIntersectionForPlaying}
            memoFirstUnreadIdRef={memoFirstUnreadIdRef}
            appearanceOrder={messageCountToAnimate - ++appearanceIndex}
            isJustAdded={isLastInList && isNewMessage}
            isLastInList={isLastInList}
            getIsMessageListReady={getIsReady}
            onMessageUnmount={onMessageUnmount}
          />,
        ]);
      }

      let currentDocumentGroupHasThreadTop = false;

      const senderGroupElements = senderGroup.map((
        messageOrAlbum,
        messageIndex,
      ) => {
        function renderMessageElement(
          message: ApiMessage,
          position: {
            isFirstInGroup: boolean;
            isLastInGroup: boolean;
            isFirstInDocumentGroup: boolean;
            isLastInDocumentGroup: boolean;
            isLastInList: boolean;
          },
          isThreadTopMessage: boolean,
          album?: IAlbum,
          documentGroup?: IDocumentGroup,
        ) {
          const isOwn = isOwnMessage(message);
          const originalId = getMessageOriginalId(message);
          const isInLiveTail = liveTailStartOriginalId !== undefined && originalId >= liveTailStartOriginalId;
          const key = isServiceNotificationMessage(message)
            ? `${message.date}_${originalId}` : originalId;
          const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(message.guestChatViaId);

          return compact([
            message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
            message.paidMessageStars && !withUsers && renderPaidMessageAction(message, album),
            message.suggestedPostInfo && renderSuggestedPostInfoAction(message),
            <Message
              key={key}
              message={message}
              observeIntersectionForBottom={observeIntersectionForReading}
              observeIntersectionForLoading={observeIntersectionForLoading}
              observeIntersectionForPlaying={observeIntersectionForPlaying}
              album={album}
              documentGroup={documentGroup}
              noAvatars={noAvatars && !shouldShowGuestAvatar}
              withAvatar={position.isLastInGroup && (withUsers || shouldShowGuestAvatar)
                && !isOwn && (!isThreadTopMessage || !isComments)}
              withSenderName={position.isFirstInGroup && (withUsers || shouldShowGuestAvatar) && !isOwn}
              threadId={threadId}
              messageListType={type}
              noComments={shouldHideComments}
              noReplies={!shouldHideComments || threadId !== MAIN_THREAD_ID || type === 'scheduled'}
              appearanceOrder={messageCountToAnimate - ++appearanceIndex}
              isJustAdded={position.isLastInList && isNewMessage}
              isThreadTop={isThreadTopMessage}
              isFirstInGroup={position.isFirstInGroup}
              isLastInGroup={position.isLastInGroup}
              isFirstInDocumentGroup={position.isFirstInDocumentGroup}
              isLastInDocumentGroup={position.isLastInDocumentGroup}
              isLastInList={position.isLastInList}
              shouldIgnoreSendFocus={isInLiveTail && isOwn}
              isQuickPreview={isQuickPreview}
              memoFirstUnreadIdRef={memoFirstUnreadIdRef}
              getIsMessageListReady={getIsReady}
              onMessageUnmount={onMessageUnmount}
            />,
          ]);
        }

        if (isDocumentGroup(messageOrAlbum)) {
          const documentGroup = messageOrAlbum;
          return documentGroup.messages.map((docMessage, docIndex) => {
            const isFirstInDocGroup = docIndex === 0;
            const isLastInDocGroup = docIndex === documentGroup.messages.length - 1;

            if (docMessage.previousLocalId && anchorIdRef.current === getMessageHtmlId(docMessage.previousLocalId)) {
              anchorIdRef.current = getMessageHtmlId(docMessage.id);
            }

            if (isFirstInDocGroup && docMessage.id === threadId) {
              currentDocumentGroupHasThreadTop = true;
            }

            const isThreadTopMessage = docMessage.id === threadId || currentDocumentGroupHasThreadTop;

            if (isLastInDocGroup) {
              currentDocumentGroupHasThreadTop = false;
            }

            const position = {
              isFirstInGroup: messageIndex === 0 && isFirstInDocGroup,
              isLastInGroup: messageIndex === senderGroup.length - 1 && isLastInDocGroup,
              isFirstInDocumentGroup: isFirstInDocGroup,
              isLastInDocumentGroup: isLastInDocGroup,
              isLastInList: (
                messageIndex === senderGroup.length - 1
                && isLastInDocGroup
                && senderGroupIndex === senderGroupsArray.length - 1
                && dateGroupIndex === dateGroupsArray.length - 1
              ),
            };

            return renderMessageElement(docMessage, position, isThreadTopMessage, undefined, documentGroup);
          }).flat();
        }

        const message = isAlbum(messageOrAlbum) ? messageOrAlbum.mainMessage : messageOrAlbum;
        const album = isAlbum(messageOrAlbum) ? messageOrAlbum : undefined;

        if (message.previousLocalId && anchorIdRef.current === getMessageHtmlId(message.previousLocalId)) {
          anchorIdRef.current = getMessageHtmlId(message.id);
        }

        const isThreadTopMessage = message.id === threadId;

        const position = {
          isFirstInGroup: messageIndex === 0,
          isLastInGroup: messageIndex === senderGroup.length - 1,
          isFirstInDocumentGroup: false,
          isLastInDocumentGroup: false,
          isLastInList: (
            messageIndex === senderGroup.length - 1
            && senderGroupIndex === senderGroupsArray.length - 1
            && dateGroupIndex === dateGroupsArray.length - 1
          ),
        };

        return renderMessageElement(message, position, isThreadTopMessage, album);
      }).flat();

      const lastItem = senderGroup[senderGroup.length - 1];
      const lastMessage = isAlbum(lastItem)
        ? lastItem.mainMessage
        : isDocumentGroup(lastItem)
          ? lastItem.messages[lastItem.messages.length - 1]
          : lastItem;
      const lastMessageId = getMessageOriginalId(lastMessage);
      const lastAppearanceOrder = messageCountToAnimate - appearanceIndex;

      const isOwn = isOwnMessage(lastMessage);

      const firstItem = senderGroup[0];
      const firstMessage = isAlbum(firstItem)
        ? firstItem.mainMessage
        : isDocumentGroup(firstItem)
          ? firstItem.messages[0]
          : firstItem;
      const firstMessageId = getMessageOriginalId(firstMessage);

      const isThreadTopMessage = lastMessage.id === threadId
        || (firstMessage.id === threadId && Boolean(firstMessage.groupedId));

      const shouldShowGuestAvatar = isPrivate && !withUsers && Boolean(lastMessage.guestChatViaId);
      if (!withUsers && !shouldShowGuestAvatar) return senderGroupElements;

      const key = `${firstMessageId}-${lastMessageId}`;
      const id = (firstMessageId === lastMessageId) ? `message-group-${firstMessageId}`
        : `message-group-${firstMessageId}-${lastMessageId}`;

      const withAvatar = (withUsers || shouldShowGuestAvatar) && !isOwn && (!isThreadTopMessage || !isComments);
      return compact([
        <SenderGroupContainer
          key={key}
          id={id}
          message={lastMessage}
          withAvatar={withAvatar}
          appearanceOrder={lastAppearanceOrder}
          canPost={canPost}
        >
          {senderGroupElements}
        </SenderGroupContainer>,
        isThreadTopMessage && (
          <div className="local-action-message" key={`discussion-started-${lastMessageId}`}>
            <span>
              {oldLang(isEmptyThread
                ? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')}
            </span>
          </div>
        ),
      ]);
    });
  }

  function renderDateHeader(dateGroup: MessageDateGroup) {
    return (
      <div
        className={buildClassName('sticky-date', areDatesClickable && 'interactive')}
        key="date-header"
        onMouseDown={preventMessageInputBlur}
        onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
      >
        <span dir="auto">
          {isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
            oldLang('MessageScheduledUntilOnline')
          )}
          {isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && (
            oldLang('MessageScheduledOn', formatHumanDate(oldLang, dateGroup.datetime, undefined, true))
          )}
          {!isSchedule && formatMessageListDate(lang, new Date(dateGroup.datetime))}
        </span>
      </div>
    );
  }

  function renderDateGroup(
    dateGroup: MessageDateGroup,
    children: TeactNode[],
    keySuffix: string,
    shouldAddFirstClass: boolean,
  ) {
    return (
      <div
        className={buildClassName('message-date-group', shouldAddFirstClass && 'first-message-date-group')}
        key={`${dateGroup.datetime}-${keySuffix}`}
        onMouseDown={preventMessageInputBlur}
        teactFastList
      >
        {children}
      </div>
    );
  }

  let isRenderingLiveTail = false;
  const dateGroups: TeactNode[] = [];
  const liveTailDateGroups: TeactNode[] = [];

  messageGroups.forEach((
    dateGroup: MessageDateGroup,
    dateGroupIndex: number,
    dateGroupsArray: MessageDateGroup[],
  ) => {
    const senderGroups = calculateSenderGroups(dateGroup, dateGroupIndex, dateGroupsArray);
    const beforeTailChildren: TeactNode[] = [];
    const liveTailChildren: TeactNode[] = [];

    if (isRenderingLiveTail) {
      liveTailChildren.push(renderDateHeader(dateGroup));
    } else {
      beforeTailChildren.push(renderDateHeader(dateGroup));
    }

    senderGroups.forEach((senderGroupElements, senderGroupIndex) => {
      const isLiveTailStart = (
        !isRenderingLiveTail
        && liveTailStartOriginalId !== undefined
        && senderGroupContainsOriginalId(
          dateGroup.senderGroups[senderGroupIndex],
          liveTailStartOriginalId,
        )
      );

      if (isLiveTailStart) {
        isRenderingLiveTail = true;
      }

      const target = isRenderingLiveTail ? liveTailChildren : beforeTailChildren;
      target.push(...senderGroupElements);
    });

    const shouldAddFirstClass = !(nameChangeDate || photoChangeDate) && dateGroupIndex === 0;
    if (beforeTailChildren.length) {
      dateGroups.push(renderDateGroup(
        dateGroup, beforeTailChildren, 'before-tail', shouldAddFirstClass,
      ));
    }

    if (liveTailChildren.length) {
      liveTailDateGroups.push(renderDateGroup(
        dateGroup, liveTailChildren, 'live-tail', shouldAddFirstClass && !beforeTailChildren.length,
      ));
    }
  });

  return (
    <div className="messages-container" teactFastList>
      {withHistoryTriggers && <div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />}
      {shouldRenderAccountInfo
        && <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
      {dateGroups}
      {Boolean(liveTailDateGroups.length) && (
        <div className="live-tail" key="live-tail" teactFastList>
          {liveTailDateGroups}
        </div>
      )}
      {isViewportNewest && renderBotForumTopicAction()}
      {withHistoryTriggers && (
        <div
          ref={forwardsTriggerRef}
          key="forwards-trigger"
          className="forwards-trigger"
        />
      )}
      <div
        ref={fabTriggerRef}
        key="fab-trigger"
        className="fab-trigger"
      />
      {isViewportNewest && (
        <MessageListBottomMarker
          key="bottom-marker"
          isFocused={shouldScrollToBottom}
          className={shouldRenderSponsoredMessage ? 'with-sponsored' : undefined}
        />
      )}
      {shouldRenderSponsoredMessage && (
        <SponsoredMessage
          key={chatId}
          chatId={chatId}
          containerRef={containerRef}
          observeIntersectionForLoading={observeIntersectionForLoading}
          observeIntersectionForPlaying={observeIntersectionForPlaying}
        />
      )}
    </div>
  );
};

export default memo(MessageListContent);
