<!--
  @component Renders a message that can be used as part of a conversation.
-->
<script lang="ts">
  import {globals} from '~/app/globals';
  import OverlayProvider from '~/app/ui/components/hocs/overlay-provider/OverlayProvider.svelte';
  import Message from '~/app/ui/components/molecules/message/Message.svelte';
  import type {MessageProps} from '~/app/ui/components/molecules/message/props';
  import MessageAvatarProvider from '~/app/ui/components/partials/conversation/internal/message-list/internal/message-avatar-provider/MessageAvatarProvider.svelte';
  import MessageContextMenuProvider from '~/app/ui/components/partials/conversation/internal/message-list/internal/message-context-menu-provider/MessageContextMenuProvider.svelte';
  import {
    getTextContent,
    getTranslatedSyncButtonTitle,
    isUnsyncedOrSyncingFile,
    shouldShowReactionButtons,
  } from '~/app/ui/components/partials/conversation/internal/message-list/internal/regular-message/helpers';
  import EmojiReactionsStrip from '~/app/ui/components/partials/conversation/internal/message-list/internal/regular-message/internal/emoji-reactions-strip/EmojiReactionsStrip.svelte';
  import type {RegularMessageProps} from '~/app/ui/components/partials/conversation/internal/message-list/internal/regular-message/props';
  import {transformMessageFileProps} from '~/app/ui/components/partials/conversation/internal/message-list/internal/regular-message/transformers';
  import {i18n} from '~/app/ui/i18n';
  import {toast} from '~/app/ui/snackbar';
  import IconButtonProgressBarOverlay from '~/app/ui/svelte-components/blocks/Button/IconButtonProgressBarOverlay.svelte';
  import MdIcon from '~/app/ui/svelte-components/blocks/Icon/MdIcon.svelte';
  import {handleCopyImage, handleSaveAsFile} from '~/app/ui/utils/file-sync/handlers';
  import {syncAndGetPayload} from '~/app/ui/utils/file-sync/helpers';
  import {reactive} from '~/app/ui/utils/svelte';
  import {escapeHtmlUnsafeChars} from '~/app/ui/utils/text';
  import {getDisplayTimestampForMessage} from '~/app/ui/utils/timestamp';
  import {extractErrorMessage} from '~/common/error';
  import {EDIT_MESSAGE_GRACE_PERIOD_IN_MINUTES} from '~/common/network/protocol/constants';
  import {assertUnreachable, unreachable} from '~/common/utils/assert';

  const {uiLogging, systemTime} = globals.unwrap();
  const log = uiLogging.logger('ui.component.message');

  const {
    boundary,
    conversation,
    direction,
    emojiReactions,
    file,
    highlighted,
    id,
    onclickcontextmenufavoriteemoji,
    onclickdeleteoption,
    onclickeditoption,
    onclickemojireactionstripbucket,
    onclickforwardoption,
    onclickopendetailsoption,
    onclickopenemojipicker,
    onclickquote,
    onclickquoteoption,
    onclickthumbnail,
    oncompletehighlightanimation,
    options = {},
    pollData,
    quote,
    sender,
    services,
    status,
    text,
  }: RegularMessageProps = $props();

  const {
    settings: {
      views: {appearance},
    },
  } = services;

  let quoteProps = $state<MessageProps['quote']>();

  function handleClickCopyOption(): void {
    if (text !== undefined) {
      navigator.clipboard
        .writeText(text.raw)
        .then(() =>
          toast.addSimpleSuccess(
            i18n
              .get()
              .t('messaging.success--copy-message-content', 'Message content copied to clipboard'),
          ),
        )
        .catch((error: unknown) => {
          log.error('Could not copy message content to clipboard', error);

          toast.addSimpleFailure(
            i18n
              .get()
              .t(
                'messaging.error--copy-message-content',
                'Could not copy message content to clipboard',
              ),
          );
        });
    } else {
      log.warn('Attempting to copy undefined message content');

      toast.addSimpleFailure(
        i18n.get().t('messaging.error--copy-undefined-message-content', 'Nothing to copy'),
      );
    }
  }

  function handleClickCopyImageOption(): void {
    handleCopyImage(file, log, $i18n.t, toast.addSimpleSuccess, toast.addSimpleFailure).catch(
      assertUnreachable,
    );
  }

  function handleClickSaveAsFileOption(): void {
    handleSaveAsFile(file, log, $i18n.t, toast.addSimpleFailure).catch(assertUnreachable);
  }

  async function handleClickSync(): Promise<void> {
    if (file === undefined) {
      return;
    }

    switch (file.sync.state) {
      case 'unsynced': {
        // Start down- or upload.
        // TODO(DESK-961): Handle upload resumption for local unsynced files.
        const result = await syncAndGetPayload(file.fetchFileBytes, $i18n.t);
        switch (result.status) {
          case 'ok':
            break;

          case 'error':
            log.error(
              `File payload sync for message ${id} failed: ${extractErrorMessage(
                result.error,
                'short',
              )}`,
            );

            toast.addSimpleFailure(result.message);
            break;

          default:
            unreachable(result);
        }
        break;
      }
      case 'syncing':
        /* TODO(DESK-948): Implement cancellation. */
        break;
      case 'synced':
      case 'failed':
        // Nothing to do.
        break;
      default:
        unreachable(file.sync.state);
    }
  }

  function handleClickFileInfo(): void {
    handleSaveAsFile(file, log, $i18n.t, toast.addSimpleFailure).catch(assertUnreachable);
  }

  function updateQuoteProps(rawQuote: RegularMessageProps['quote']): void {
    if (rawQuote === undefined) {
      quoteProps = undefined;
    } else if (rawQuote === 'not-found') {
      quoteProps = {
        type: 'not-found',
        fallbackText: $i18n.t(
          'messaging.error--quoted-message-not-found',
          'The quoted message could not be found',
        ),
      };
    } else if (rawQuote.type === 'deleted-message') {
      quoteProps = {
        type: 'deleted',
        fallbackText: $i18n.t(
          'messaging.error--quoted-message-deleted',
          'The quoted message was deleted',
        ),
      };
    } else {
      const sanitizedHtml = getTextContent(
        rawQuote.text?.raw,
        rawQuote.text?.mentions,
        true,
        $i18n.t,
        250,
      );

      quoteProps = {
        type: 'default',
        alt: $i18n.t('messaging.hint--media-thumbnail', 'Media preview'),
        content:
          sanitizedHtml === undefined
            ? undefined
            : {
                sanitizedHtml,
              },
        file: transformMessageFileProps(
          rawQuote.file,
          rawQuote.id,
          conversation.receiver.lookup,
          services,
        ),
        onerror: (error) =>
          log.error(
            `An error occurred in a child component: ${extractErrorMessage(error, 'short')}`,
          ),
        poll: rawQuote.pollData,
        sender: rawQuote.sender,
      };
    }
  }

  const htmlContent = $derived(
    status.deleted !== undefined
      ? escapeHtmlUnsafeChars(
          $i18n.t('messaging.prose--message-deleted', 'This message was deleted'),
        )
      : getTextContent(text?.raw, text?.mentions, false, $i18n.t),
  );

  const shouldAllowReactions = $derived(
    shouldShowReactionButtons(
      conversation.receiver,
      direction,
      conversation.emojiReactionsFeatureSupport.supported,
      status.deleted?.at,
    ),
  );

  const showEditButton = $derived.by<boolean>(() => {
    // Trigger reactivity of `showEditButton` when the system time changes.
    //
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    $systemTime.current;

    return (
      direction === 'outbound' &&
      status.deleted === undefined &&
      status.sent !== undefined &&
      // In left group chats, we do not show the edit button
      !(conversation.receiver.type === 'group' && conversation.receiver.isLeft) &&
      // For audio we don't support edits yet.
      !(file !== undefined && file.type === 'audio') &&
      // Polls also don't have an edit functionality.
      pollData === undefined &&
      Date.now() - status.sent.at.getTime() < EDIT_MESSAGE_GRACE_PERIOD_IN_MINUTES * 60000
    );
  });

  const timestamp = $derived(
    reactive(
      () => getDisplayTimestampForMessage($i18n, direction, status, $appearance.use24hTime),
      [$systemTime.current],
    ),
  );

  $effect(() => {
    updateQuoteProps(quote);
  });
</script>

<div class="container">
  <MessageAvatarProvider {conversation} {direction} {services} {sender}>
    <div class="content" data-alignment={direction === 'inbound' ? 'start' : 'end'}>
      <MessageContextMenuProvider
        {boundary}
        caretAnchorName={`--message-context-menu-caret-${id}`}
        enabledOptions={{
          copyLink: true,
          copySelection: true,
          copyImage: file !== undefined && file.type === 'image',
          copy: text !== undefined,
          edit: showEditButton
            ? {disabled: !conversation.editMessageFeatureSupport.supported}
            : false,
          saveAsFile: file !== undefined,
          quote:
            (conversation.receiver.type === 'contact'
              ? !conversation.receiver.isBlocked
              : !conversation.receiver.isDisabled) && status.deleted === undefined,
          // TODO(DESK-1400)
          forward:
            pollData === undefined &&
            status.deleted === undefined &&
            (file === undefined || file.sync.state === 'synced'),
          openDetails: true,
          deleteMessage: true,
        }}
        emojiReactions={{
          enabled: shouldAllowReactions,
          fullSupport: conversation.emojiReactionsFeatureSupport.supported,
          ownReactions: emojiReactions.filter((reaction) => reaction.direction === 'outbound'),
        }}
        onclickcopyimageoption={handleClickCopyImageOption}
        onclickcopymessageoption={handleClickCopyOption}
        {onclickdeleteoption}
        {onclickeditoption}
        onclickfavoriteemoji={(event) =>
          onclickcontextmenufavoriteemoji?.(event.rawEvent, event.emoji)}
        {onclickforwardoption}
        {onclickopendetailsoption}
        onclickopenemojipicker={(event) => {
          onclickopenemojipicker?.(event, `--message-context-menu-caret-${id}`);
        }}
        {onclickquoteoption}
        onclicksaveasfileoption={handleClickSaveAsFileOption}
        options={{
          alwaysShowCaret: options.alwaysShowCaret,
        }}
        placement={direction === 'inbound' ? 'right' : 'left'}
        {services}
      >
        {#snippet snippetMessage()}
          <div class="message">
            <OverlayProvider show={isUnsyncedOrSyncingFile(file)}>
              {#snippet snippetAbove()}
                {#if isUnsyncedOrSyncingFile(file)}
                  {@const {sync} = file}

                  <button class="sync-button" onclick={handleClickSync}>
                    {#if sync.state === 'unsynced'}
                      <MdIcon theme="Filled" title={getTranslatedSyncButtonTitle(file, $i18n.t)}>
                        {#if sync.direction === 'download'}
                          file_download
                        {:else if sync.direction === 'upload'}
                          file_upload
                        {:else if sync.direction === undefined}
                          help
                        {:else}
                          {unreachable(sync.direction)}
                        {/if}
                      </MdIcon>
                    {:else if sync.state === 'syncing'}
                      <!-- TODO(DESK-948): Cancellation <MdIcon theme="Filled">close</MdIcon>. -->
                      <IconButtonProgressBarOverlay />
                    {:else}
                      {unreachable(sync.state)}
                    {/if}
                  </button>
                {/if}
              {/snippet}
              {#snippet snippetBelow()}
                <!--TODO(DESK-771): Handle distribution list conversation type. -->
                <Message
                  alt={$i18n.t('messaging.hint--media-thumbnail')}
                  content={htmlContent === undefined
                    ? undefined
                    : {
                        sanitizedHtml: htmlContent,
                      }}
                  {direction}
                  file={transformMessageFileProps(file, id, conversation.receiver.lookup, services)}
                  footerHint={status.edited === undefined
                    ? undefined
                    : $i18n.t('messaging.prose--message-edited', 'Edited')}
                  {highlighted}
                  onerror={(error) =>
                    log.error(
                      `An error occurred in a child component: ${extractErrorMessage(error, 'short')}`,
                    )}
                  onclickfileinfo={handleClickFileInfo}
                  {onclickquote}
                  {onclickthumbnail}
                  {oncompletehighlightanimation}
                  options={{
                    showSender: conversation.receiver.type !== 'contact',
                    indicatorOptions: {
                      hideStatus:
                        conversation.receiver.type !== 'contact' && status.sent !== undefined,
                      fillReactions: conversation.receiver.type === 'contact',
                      alwaysShowNumber: conversation.receiver.type === 'group',
                    },
                    hideVideoPlayButton: isUnsyncedOrSyncingFile(file),
                  }}
                  {pollData}
                  quote={quoteProps}
                  receiver={conversation.receiver}
                  {sender}
                  {services}
                  {status}
                  {timestamp}
                />
              {/snippet}
            </OverlayProvider>
          </div>
        {/snippet}
      </MessageContextMenuProvider>
      <div class="reactions">
        <EmojiReactionsStrip
          id={`emoji-reactions-strip-${id}`}
          {conversation}
          {direction}
          onclickbucket={(event, emoji) => onclickemojireactionstripbucket?.(event, emoji)}
          onclickopenemojipicker={(event) =>
            onclickopenemojipicker?.(event, `--emoji-reactions-strip-open-button-${id}`)}
          openEmojiPickerButtonAnchorName={`--emoji-reactions-strip-open-button-${id}`}
          options={{
            showAddEmojiReactionButton: conversation.emojiReactionsFeatureSupport.supported,
          }}
          reactions={emojiReactions}
        />
      </div>
    </div>
  </MessageAvatarProvider>
</div>

<style lang="scss">
  @use 'component' as *;

  .container {
    display: flex;
    align-items: start;
    justify-content: start;
    gap: rem(8px);

    .content {
      display: flex;
      flex-direction: column;
      align-items: start;
      justify-content: start;

      // Balance the negative margin of `.reactions`.
      padding-bottom: rem(5px);

      .message {
        border-radius: rem(10px);
        overflow: hidden;

        .sync-button {
          --c-icon-button-progress-bar-overlay-color: var(--mc-message-overlay-button-color);

          --c-icon-button-naked-outer-background-color--hover: var(
            --mc-message-overlay-button-background-color--hover
          );
          --c-icon-button-naked-outer-background-color--focus: var(
            --mc-message-overlay-button-background-color--focus
          );
          --c-icon-button-naked-outer-background-color--active: var(
            --mc-message-overlay-button-background-color--active
          );

          @include clicktarget-button-circle;

          & {
            display: flex;
            align-items: center;
            justify-content: center;
            color: var(--mc-message-overlay-button-color);
            background-color: var(--mc-message-overlay-button-background-color);
            width: rem(44px);
            height: rem(44px);
            font-size: rem(22px);
          }
        }
      }

      .reactions {
        z-index: 1;
        margin-top: rem(-5px);

        max-width: 100%;
      }

      &[data-alignment='end'] {
        align-items: end;

        .reactions {
          align-self: end;
          padding-left: rem(28px);
        }
      }
    }
  }
</style>
