import React, { createRef, useEffect } from 'react';
import classNames from 'classnames';

import { Avatar, AvatarSize } from '../Avatar';
import { Spinner } from '../basic/Spinner';
import { MessageBody } from './MessageBody';
import { ImageGrid } from './ImageGrid';
import { Image } from './Image';
import { ContactName } from './ContactName';
import { Quote } from './Quote';

// Audio Player
import H5AudioPlayer from 'react-h5-audio-player';
// import 'react-h5-audio-player/lib/styles.css';

const AudioPlayerWithEncryptedFile = (props: {
  src: string;
  contentType: string;
  playbackSpeed: number;
}) => {
  const theme = useTheme();
  const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType);
  const { playbackSpeed } = props;
  const player = createRef<H5AudioPlayer>();

  useEffect(() => {
    // updates playback speed to value selected in context menu
    if (player.current?.audio.current?.playbackRate) {
      player.current.audio.current.playbackRate = playbackSpeed;
    }
  }, [playbackSpeed]);

  return (
    <H5AudioPlayer
      src={urlToLoad}
      layout="horizontal-reverse"
      showSkipControls={false}
      showJumpControls={false}
      showDownloadProgress={false}
      listenInterval={100}
      ref={player}
      customIcons={{
        play: (
          <SessionIcon
            iconType={SessionIconType.Play}
            iconSize={SessionIconSize.Small}
            iconColor={theme.colors.textColorSubtle}
            theme={theme}
          />
        ),
        pause: (
          <SessionIcon
            iconType={SessionIconType.Pause}
            iconSize={SessionIconSize.Small}
            iconColor={theme.colors.textColorSubtle}
            theme={theme}
          />
        ),
      }}
    />
  );
};

import {
  canDisplayImage,
  getExtensionForDisplay,
  getGridDimensions,
  getImageDimensions,
  hasImage,
  hasVideoScreenshot,
  isAudio,
  isImage,
  isImageAttachment,
  isVideo,
} from '../../../ts/types/Attachment';
import { AttachmentType } from '../../types/Attachment';

import { getIncrement } from '../../util/timer';
import { isFileDangerous } from '../../util/isFileDangerous';
import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import _ from 'lodash';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import uuid from 'uuid';
import { InView } from 'react-intersection-observer';
import { useTheme, withTheme } from 'styled-components';
import { MessageMetadata } from './message/MessageMetadata';
import { PubKey } from '../../session/types';
import { MessageRegularProps } from '../../models/messageType';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import {
  addSenderAsModerator,
  removeSenderFromModerator,
} from '../../interactions/messageInteractions';
import { updateUserDetailsModal } from '../../state/ducks/modalDialog';
import { MessageInteraction } from '../../interactions';
import { useState } from 'react';

// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;

interface State {
  expiring: boolean;
  expired: boolean;
  imageBroken: boolean;
  playbackSpeed: number;
}

const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600;

const MessageInner = (props: MessageRegularProps) => {

  let handleImageErrorBound: () => void;

  let expirationCheckInterval: any;
  let expiredTimeout: any;
  let ctxMenuID: string;

  // public constructor(props: MessageRegularProps) {
  //   super(props);

  //   this.handleImageErrorBound = this.handleImageError.bind(this);
  //   this.onReplyPrivate = this.onReplyPrivate.bind(this);
  //   this.handleContextMenu = this.handleContextMenu.bind(this);
  //   this.onAddModerator = this.onAddModerator.bind(this);
  //   this.onRemoveFromModerator = this.onRemoveFromModerator.bind(this);
  //   this.updatePlaybackSpeed = this.updatePlaybackSpeed.bind(this);

  //   this.state = {
  //     expiring: false,
  //     expired: false,
  //     imageBroken: false,
  //     playbackSpeed: 1,
  //   };
  //   this.ctxMenuID = `ctx-menu-message-${uuid()}`;
  // }

  const [expiring, setExpiring] = useState(false);
  const [expired, setExpired] = useState(false);
  const [image, setImageBroken] = useState(false);
  const [playbackSpeed, setPlaybackSpeed] = useState(1);

  ctxMenuID = `ctx-menu-message-${uuid()}`;


  useEffect(() => {
    if (!props.expirationLength) {
      return;
    }

    const { expirationLength } = props;

    const increment = getIncrement(expirationLength);
    const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);

    checkExpired();

    expirationCheckInterval = setInterval(() => {
      checkExpired();
    }, checkFrequency);

  }, [])


  // equivalent to componentWillUpdate
  useEffect(() => {
    checkExpired();

    // return occurs on unmount equivalent to componentWillUnmount
    return () => {
      if (expirationCheckInterval) {
        clearInterval(expirationCheckInterval);
      }
      if (expiredTimeout) {
        clearTimeout(expiredTimeout);
      }
    }
  })

  const checkExpired = () => {
    const now = Date.now();
    const { isExpired, expirationTimestamp, expirationLength } = props;

    if (!expirationTimestamp || !expirationLength) {
      return;
    }
    if (expiredTimeout) {
      return;
    }

    if (isExpired || now >= expirationTimestamp) {
      setExpiring(true);

      const triggerSetExpired = () => {
        setExpired(true)
      };
      expiredTimeout = setTimeout(triggerSetExpired(), EXPIRED_DELAY);
    }
  }

  const handleImageError = () => {
  setImageBroken(true);
}

  // tslint:disable-next-line max-func-body-length cyclomatic-complexity
  public renderAttachment() {
  const {
    id,
    attachments,
    text,
    collapseMetadata,
    conversationType,
    direction,
    quote,
    onClickAttachment,
    multiSelectMode,
    onSelectMessage,
  } = this.props;
  const { imageBroken } = this.state;

  if (!attachments || !attachments[0]) {
    return null;
  }
  const firstAttachment = attachments[0];

  // For attachments which aren't full-frame
  const withContentBelow = Boolean(text);
  const withContentAbove =
    Boolean(quote) || (conversationType === 'group' && direction === 'incoming');
  const displayImage = canDisplayImage(attachments);

  if (
    displayImage &&
    !imageBroken &&
    ((isImage(attachments) && hasImage(attachments)) ||
      (isVideo(attachments) && hasVideoScreenshot(attachments)))
  ) {
    return (
      <div
        className={classNames(
          'module-message__attachment-container',
          withContentAbove ? 'module-message__attachment-container--with-content-above' : null,
          withContentBelow ? 'module-message__attachment-container--with-content-below' : null
        )}
      >
        <ImageGrid
          attachments={attachments}
          withContentAbove={withContentAbove}
          withContentBelow={withContentBelow}
          bottomOverlay={!collapseMetadata}
          onError={this.handleImageErrorBound}
          onClickAttachment={(attachment: AttachmentType) => {
            if (multiSelectMode) {
              onSelectMessage(id);
            } else if (onClickAttachment) {
              onClickAttachment(attachment);
            }
          }}
        />
      </div>
    );
  } else if (!firstAttachment.pending && isAudio(attachments)) {
    return (
      <div
        role="main"
        onClick={(e: any) => {
          e.stopPropagation();
        }}
      >
        <AudioPlayerWithEncryptedFile
          playbackSpeed={this.state.playbackSpeed}
          src={firstAttachment.url}
          contentType={firstAttachment.contentType}
        />
      </div>
    );
  } else {
    const { pending, fileName, fileSize, contentType } = firstAttachment;
    const extension = getExtensionForDisplay({ contentType, fileName });
    const isDangerous = isFileDangerous(fileName || '');

    return (
      <div
        className={classNames(
          'module-message__generic-attachment',
          withContentBelow ? 'module-message__generic-attachment--with-content-below' : null,
          withContentAbove ? 'module-message__generic-attachment--with-content-above' : null
        )}
      >
        {pending ? (
          <div className="module-message__generic-attachment__spinner-container">
            <Spinner size="small" direction={direction} />
          </div>
        ) : (
          <div className="module-message__generic-attachment__icon-container">
            <div
              role="button"
              className="module-message__generic-attachment__icon"
              onClick={(e: any) => {
                if (this.props?.onDownload) {
                  e.stopPropagation();
                  this.props.onDownload(firstAttachment);
                }
              }}
            >
              {extension ? (
                <div className="module-message__generic-attachment__icon__extension">
                  {extension}
                </div>
              ) : null}
            </div>
            {isDangerous ? (
              <div className="module-message__generic-attachment__icon-dangerous-container">
                <div className="module-message__generic-attachment__icon-dangerous" />
              </div>
            ) : null}
          </div>
        )}
        <div className="module-message__generic-attachment__text">
          <div
            className={classNames(
              'module-message__generic-attachment__file-name',
              `module-message__generic-attachment__file-name--${direction}`
            )}
          >
            {fileName}
          </div>
          <div
            className={classNames(
              'module-message__generic-attachment__file-size',
              `module-message__generic-attachment__file-size--${direction}`
            )}
          >
            {fileSize}
          </div>
        </div>
      </div>
    );
  }
}

  // tslint:disable-next-line cyclomatic-complexity
  public renderPreview() {
  const {
    attachments,
    conversationType,
    direction,
    onClickLinkPreview,
    previews,
    quote,
  } = this.props;

  // Attachments take precedence over Link Previews
  if (attachments && attachments.length) {
    return null;
  }

  if (!previews || previews.length < 1) {
    return null;
  }

  const first = previews[0];
  if (!first) {
    return null;
  }

  const withContentAbove =
    Boolean(quote) || (conversationType === 'group' && direction === 'incoming');

  const previewHasImage = first.image && isImageAttachment(first.image);
  const width = first.image && first.image.width;
  const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;

  return (
    <div
      role="button"
      className={classNames(
        'module-message__link-preview',
        withContentAbove ? 'module-message__link-preview--with-content-above' : null
      )}
      onClick={() => {
        if (onClickLinkPreview) {
          onClickLinkPreview(first.url);
        }
      }}
    >
      {first.image && previewHasImage && isFullSizeImage ? (
        <ImageGrid
          attachments={[first.image]}
          withContentAbove={withContentAbove}
          withContentBelow={true}
          onError={this.handleImageErrorBound}
        />
      ) : null}
      <div
        className={classNames(
          'module-message__link-preview__content',
          withContentAbove || isFullSizeImage
            ? 'module-message__link-preview__content--with-content-above'
            : null
        )}
      >
        {first.image && previewHasImage && !isFullSizeImage ? (
          <div className="module-message__link-preview__icon_container">
            <Image
              smallCurveTopLeft={!withContentAbove}
              softCorners={true}
              alt={window.i18n('previewThumbnail', [first.domain])}
              height={72}
              width={72}
              url={first.image.url}
              attachment={first.image}
              onError={this.handleImageErrorBound}
            />
          </div>
        ) : null}
        <div
          className={classNames(
            'module-message__link-preview__text',
            previewHasImage && !isFullSizeImage
              ? 'module-message__link-preview__text--with-icon'
              : null
          )}
        >
          <div className="module-message__link-preview__title">{first.title}</div>
          <div className="module-message__link-preview__location">{first.domain}</div>
        </div>
      </div>
    </div>
  );
}

  public renderQuote() {
  const {
    conversationType,
    direction,
    quote,
    isPublic,
    convoId,
    id,
    multiSelectMode,
  } = this.props;

  if (!quote || !quote.authorPhoneNumber) {
    return null;
  }

  const withContentAbove = conversationType === 'group' && direction === 'incoming';

  const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber);

  const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber;

  return (
    <Quote
      onClick={(e: any) => {
        e.preventDefault();
        e.stopPropagation();
        if (multiSelectMode && id) {
          this.props.onSelectMessage(id);
          return;
        }
        const { authorPhoneNumber, messageId: quoteId, referencedMessageNotFound } = quote;
        quote?.onClick({
          quoteAuthor: authorPhoneNumber,
          quoteId,
          referencedMessageNotFound,
        });
      }}
      text={quote.text}
      attachment={quote.attachment}
      isIncoming={direction === 'incoming'}
      conversationType={conversationType}
      convoId={convoId}
      isPublic={isPublic}
      authorPhoneNumber={displayedPubkey}
      authorProfileName={quote.authorProfileName}
      authorName={quote.authorName}
      referencedMessageNotFound={quote.referencedMessageNotFound}
      isFromMe={quote.isFromMe}
      withContentAbove={withContentAbove}
    />
  );
}

  public renderAvatar() {
  const {
    authorAvatarPath,
    authorName,
    authorPhoneNumber,
    authorProfileName,
    collapseMetadata,
    isAdmin,
    conversationType,
    direction,
    isPublic,
    firstMessageOfSeries,
  } = this.props;

  if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') {
    return;
  }
  const userName = authorName || authorProfileName || authorPhoneNumber;

  if (!firstMessageOfSeries) {
    return <div style={{ marginInlineEnd: '60px' }} />;
  }

  return (
    <div className="module-message__author-avatar">
      <Avatar
        avatarPath={authorAvatarPath}
        name={userName}
        size={AvatarSize.S}
        onAvatarClick={() => {
          window.inboxStore?.dispatch(
            updateUserDetailsModal({
              conversationId: this.props.convoId,
              userName,
              authorAvatarPath,
            })
          );
        }}
        pubkey={authorPhoneNumber}
      />
      {isPublic && isAdmin && (
        <div className="module-avatar__icon--crown-wrapper">
          <div className="module-avatar__icon--crown" />
        </div>
      )}
    </div>
  );
}

  public renderText() {
  const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props;

  const contents =
    direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text;

  if (!contents) {
    return null;
  }

  return (
    <div
      dir="auto"
      className={classNames(
        'module-message__text',
        `module-message__text--${direction}`,
        status === 'error' && direction === 'incoming' ? 'module-message__text--error' : null
      )}
    >
      <MessageBody
        text={contents || ''}
        isGroup={conversationType === 'group'}
        convoId={convoId}
        disableLinks={multiSelectMode}
      />
    </div>
  );
}

  public renderError(isCorrectSide: boolean) {
  const { status, direction } = this.props;

  if (!isCorrectSide || status !== 'error') {
    return null;
  }

  return (
    <div className="module-message__error-container">
      <div
        className={classNames('module-message__error', `module-message__error--${direction}`)}
      />
    </div>
  );
}

  public renderContextMenu() {
  const {
    attachments,
    authorPhoneNumber,
    convoId,
    onCopyText,
    direction,
    status,
    isDeletable,
    id,
    onSelectMessage,
    onDeleteMessage,
    onDownload,
    onRetrySend,
    onShowDetail,
    isPublic,
    isOpenGroupV2,
    weAreAdmin,
    isAdmin,
  } = this.props;

  const showRetry = status === 'error' && direction === 'outgoing';
  const multipleAttachments = attachments && attachments.length > 1;

  const onContextMenuShown = () => {
    window.contextMenuShown = true;
  };

  const onContextMenuHidden = () => {
    // This function will called before the click event
    // on the message would trigger (and I was unable to
    // prevent propagation in this case), so use a short timeout
    setTimeout(() => {
      window.contextMenuShown = false;
    }, 100);
  };

  const selectMessageText = window.i18n('selectMessage');
  const deleteMessageText = window.i18n('deleteMessage');

  return (
    <Menu
      id={this.ctxMenuID}
      onShown={onContextMenuShown}
      onHidden={onContextMenuHidden}
      animation={animation.fade}
    >
      {!multipleAttachments && attachments && attachments[0] ? (
        <Item
          onClick={(e: any) => {
            if (onDownload) {
              onDownload(attachments[0]);
            }
          }}
        >
          {window.i18n('downloadAttachment')}
        </Item>
      ) : null}

      {isAudio(attachments) ? (
        <Item onClick={this.updatePlaybackSpeed}>
          {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)}
        </Item>
      ) : null}
      <Item onClick={onCopyText}>{window.i18n('copyMessage')}</Item>
      <Item onClick={this.onReplyPrivate}>{window.i18n('replyToMessage')}</Item>
      <Item onClick={onShowDetail}>{window.i18n('moreInformation')}</Item>
      {showRetry ? <Item onClick={onRetrySend}>{window.i18n('resend')}</Item> : null}
      {isDeletable ? (
        <>
          <Item
            onClick={() => {
              onSelectMessage(id);
            }}
          >
            {selectMessageText}
          </Item>
          <Item
            onClick={() => {
              onDeleteMessage(id);
            }}
          >
            {deleteMessageText}
          </Item>
        </>
      ) : null}
      {weAreAdmin && isPublic ? (
        <Item
          onClick={() => {
            MessageInteraction.banUser(authorPhoneNumber, convoId);
          }}
        >
          {window.i18n('banUser')}
        </Item>
      ) : null}
      {weAreAdmin && isOpenGroupV2 ? (
        <Item
          onClick={() => {
            MessageInteraction.unbanUser(authorPhoneNumber, convoId);
          }}
        >
          {window.i18n('unbanUser')}
        </Item>
      ) : null}
      {weAreAdmin && isPublic && !isAdmin ? (
        <Item onClick={this.onAddModerator}>{window.i18n('addAsModerator')}</Item>
      ) : null}
      {weAreAdmin && isPublic && isAdmin ? (
        <Item onClick={this.onRemoveFromModerator}>{window.i18n('removeFromModerators')}</Item>
      ) : null}
    </Menu>
  );
}

  public getWidth(): number | undefined {
  const { attachments, previews } = this.props;

  if (attachments && attachments.length) {
    const dimensions = getGridDimensions(attachments);
    if (dimensions) {
      return dimensions.width;
    }
  }

  if (previews && previews.length) {
    const first = previews[0];

    if (!first || !first.image) {
      return;
    }
    const { width } = first.image;

    if (isImageAttachment(first.image) && width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH) {
      const dimensions = getImageDimensions(first.image);
      if (dimensions) {
        return dimensions.width;
      }
    }
  }

  return;
}

  public isShowingImage(): boolean {
  const { attachments, previews } = this.props;
  const { imageBroken } = this.state;

  if (imageBroken) {
    return false;
  }

  if (attachments && attachments.length) {
    const displayImage = canDisplayImage(attachments);

    return Boolean(
      displayImage &&
      ((isImage(attachments) && hasImage(attachments)) ||
        (isVideo(attachments) && hasVideoScreenshot(attachments)))
    );
  }

  if (previews && previews.length) {
    const first = previews[0];
    const { image } = first;

    if (!image) {
      return false;
    }

    return isImageAttachment(image);
  }

  return false;
}

  // tslint:disable-next-line: cyclomatic-complexity
  public render() {
  const {
    direction,
    id,
    selected,
    multiSelectMode,
    conversationType,
    isPublic,
    text,
    isUnread,
    markRead,
  } = this.props;
  const { expired, expiring } = this.state;

  if (expired) {
    return null;
  }

  const width = this.getWidth();
  const isShowingImage = this.isShowingImage();

  const isIncoming = direction === 'incoming';
  const shouldMarkReadWhenVisible = isIncoming && isUnread;
  const divClasses = ['session-message-wrapper'];

  if (selected) {
    divClasses.push('message-selected');
  }

  if (conversationType === 'group') {
    divClasses.push('public-chat-message-wrapper');
  }

  if (this.props.isQuotedMessageToAnimate) {
    divClasses.push('flash-green-once');
  }

  const onVisible = (inView: boolean) => {
    if (inView && shouldMarkReadWhenVisible) {
      // mark the message as read.
      // this will trigger the expire timer.
      void markRead(Date.now());
    }
  };

  return (
    <InView
      id={id}
      as="div"
      className={classNames(divClasses)}
      onChange={onVisible}
      threshold={1}
      delay={200}
      triggerOnce={true}
      onContextMenu={this.handleContextMenu}
    >
      {this.renderAvatar()}
      <div
        className={classNames(
          'module-message',
          `module-message--${direction}`,
          expiring ? 'module-message--expired' : null
        )}
        role="button"
        onClick={event => {
          const selection = window.getSelection();
          // Text is being selected
          if (selection && selection.type === 'Range') {
            return;
          }

          // User clicked on message body
          const target = event.target as HTMLDivElement;
          if (
            (!multiSelectMode && target.className === 'text-selectable') ||
            window.contextMenuShown
          ) {
            return;
          }

          if (id) {
            this.props.onSelectMessage(id);
          }
        }}
      >
        {this.renderError(isIncoming)}

        <div
          className={classNames(
            'module-message__container',
            `module-message__container--${direction}`
          )}
          style={{
            width: isShowingImage ? width : undefined,
          }}
          role="button"
          onClick={event => {
            const selection = window.getSelection();
            // Text is being selected
            if (selection && selection.type === 'Range') {
              return;
            }

            // User clicked on message body
            const target = event.target as HTMLDivElement;
            if (target.className === 'text-selectable' || window.contextMenuShown) {
              return;
            }

            if (id) {
              this.props.onSelectMessage(id);
            }
          }}
        >
          {this.renderAuthor()}
          {this.renderQuote()}
          {this.renderAttachment()}
          {this.renderPreview()}
          {this.renderText()}
          <MessageMetadata {...this.props} isShowingImage={this.isShowingImage()} />
        </div>
        {this.renderError(!isIncoming)}
        {this.renderContextMenu()}
      </div>
    </InView>
  );
}

  /**
   * Doubles / halves the playback speed based on the current playback speed.
   */
  private updatePlaybackSpeed() {
  this.setState({
    ...this.state,
    playbackSpeed: this.state.playbackSpeed === 1 ? 2 : 1,
  });
}

  private handleContextMenu(e: any) {
  e.preventDefault();
  e.stopPropagation();
  const { multiSelectMode, isKickedFromGroup } = this.props;
  const enableContextMenu = !multiSelectMode && !isKickedFromGroup;

  if (enableContextMenu) {
    // Don't forget to pass the id and the event and voila!
    contextMenu.hideAll();
    contextMenu.show({
      id: this.ctxMenuID,
      event: e,
    });
  }
}

  private renderAuthor() {
  const {
    authorName,
    authorPhoneNumber,
    authorProfileName,
    conversationType,
    direction,
    isPublic,
  } = this.props;

  const title = authorName ? authorName : authorPhoneNumber;

  if (direction !== 'incoming' || conversationType !== 'group' || !title) {
    return null;
  }

  const shortenedPubkey = PubKey.shorten(authorPhoneNumber);

  const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber;

  return (
    <div className="module-message__author">
      <ContactName
        phoneNumber={displayedPubkey}
        name={authorName}
        profileName={authorProfileName}
        module="module-message__author"
        boldProfileName={true}
        shouldShowPubkey={Boolean(isPublic)}
      />
    </div>
  );
}

  private onReplyPrivate(e: any) {
  if (this.props && this.props.onReply) {
    this.props.onReply(this.props.timestamp);
  }
}

  private async onAddModerator() {
  await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId);
}

  private async onRemoveFromModerator() {
  await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId);
}
}

export const Message = withTheme(MessageInner);
