package org.thoughtcrime.securesms.mediapreview

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Annotation
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.view.animation.PathInterpolator
import android.widget.Toast
import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.text.getSpans
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.MarginPageTransformer
import androidx.viewpager2.widget.ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.signal.core.models.media.Media
import org.signal.core.ui.logging.LoggingFragment
import org.signal.core.util.Debouncer
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.signal.core.util.requireDrawable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentSaver
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewBinding
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediapreview.caption.ExpandingCaptionView
import org.thoughtcrime.securesms.mediapreview.mediarail.CenterDecoration
import org.thoughtcrime.securesms.mediapreview.mediarail.MediaRailAdapter
import org.thoughtcrime.securesms.mediapreview.mediarail.MediaRailAdapter.ImageLoadingListener
import org.thoughtcrime.securesms.mediasend.MediaSendLauncher
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
import org.thoughtcrime.securesms.util.OffloadedMediaDialogUtil
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import org.signal.core.ui.R as CoreUiR

class MediaPreviewFragment :
  LoggingFragment(R.layout.fragment_media_preview),
  MediaPreviewPageFragment.Events {

  private val lifecycleDisposable = LifecycleDisposable()
  private val binding by ViewBinderDelegate(FragmentMediaPreviewBinding::bind)
  private val viewModel: MediaPreviewViewModel by viewModels(ownerProducer = {
    requireActivity()
  })
  private val debouncer = Debouncer(2, TimeUnit.SECONDS)
  private val args: MediaIntentFactory.MediaPreviewArgs by lazy { MediaIntentFactory.requireArguments(requireArguments()) }

  private lateinit var pagerAdapter: MediaPreviewAdapter
  private lateinit var albumRailAdapter: MediaRailAdapter
  private lateinit var fullscreenHelper: FullscreenHelper

  private var individualItemWidth: Int = 0
  private var dbChangeObserver: DatabaseObserver.Observer? = null

  override fun onAttach(context: Context) {
    super.onAttach(context)
    fullscreenHelper = FullscreenHelper(requireActivity(), true)
    individualItemWidth = context.resources.getDimension(R.dimen.media_rail_item_size).roundToInt()
  }

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    lifecycleDisposable.bindTo(viewLifecycleOwner)
    return super.onCreateView(inflater, container, savedInstanceState)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    initializeViewModel(args)
    initializeToolbar(binding.toolbar)
    bindToolbar(args.fromRecipientId, args.threadRecipientId, args.outgoing, args.showThread, args.date, args.messageId)
    bindInitialPlaybackControls(args)
    bindInitialCaption(args)
    initializeViewPager()
    initializeAlbumRail()
    initializeFullScreenUi()
    anchorPaddingToBottomInsets(binding.mediaPreviewDetailsContainer)
    lifecycleDisposable +=
      viewModel
        .state
        .distinctUntilChanged { t1, t2 ->
          // this is all fields except for [isInSharedAnimation], which is explicitly excluded.
          (
            t1.mediaRecords == t2.mediaRecords &&
              t1.loadState == t2.loadState &&
              t1.position == t2.position &&
              t1.showThread == t2.showThread &&
              t1.allMediaInAlbumRail == t2.allMediaInAlbumRail &&
              t1.leftIsRecent == t2.leftIsRecent &&
              t1.albums == t2.albums &&
              t1.messageBodies == t2.messageBodies
            )
        }
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe {
          bindCurrentState(it)
        }
  }

  private fun initializeViewModel(args: MediaIntentFactory.MediaPreviewArgs) {
    if (!MediaUtil.isImageType(args.initialMediaType) && !MediaUtil.isVideoType(args.initialMediaType)) {
      Log.w(TAG, "Unsupported media type sent to MediaPreviewFragment, finishing.")
      Snackbar.make(binding.root, R.string.MediaPreviewActivity_unssuported_media_type, Snackbar.LENGTH_LONG)
        .setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) {
          activity?.finish()
        }.show()
    }
    viewModel.initialize(args.showThread, args.allMediaInRail, args.leftIsRecent)
    val sorting = MediaTable.Sorting.deserialize(args.sorting.ordinal)
    val startingAttachmentId = PartAuthority.requireAttachmentId(args.initialMediaUri)
    val threadId = args.threadId
    val appContext = requireContext().applicationContext
    viewModel.fetchAttachments(appContext, startingAttachmentId, threadId, sorting)
    val dbObserver = DatabaseObserver.Observer { viewModel.refetchAttachments(appContext, startingAttachmentId, threadId, sorting) }
    AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(dbObserver)
    this.dbChangeObserver = dbObserver
  }

  @SuppressLint("RestrictedApi")
  private fun initializeToolbar(toolbar: MaterialToolbar) {
    toolbar.setNavigationOnClickListener {
      requireActivity().onBackPressedDispatcher.onBackPressed()
    }

    toolbar.setTitleTextAppearance(requireContext(), CoreUiR.style.Signal_Text_TitleMedium)
    toolbar.setSubtitleTextAppearance(requireContext(), CoreUiR.style.Signal_Text_BodyMedium)
    (binding.toolbar.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
    binding.toolbar.inflateMenu(R.menu.media_preview)
  }

  private fun initializeViewPager() {
    binding.mediaPager.offscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT
    binding.mediaPager.setPageTransformer(MarginPageTransformer(ViewUtil.dpToPx(24)))
    pagerAdapter = MediaPreviewAdapter(this)
    binding.mediaPager.adapter = pagerAdapter
    binding.mediaPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
      override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
        if (position != viewModel.currentPosition) {
          debouncer.clear()
        }
        viewModel.setCurrentPage(position)
      }
    })
  }

  private fun initializeAlbumRail() {
    binding.mediaPreviewPlaybackControls.recyclerView.apply {
      layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
      addItemDecoration(CenterDecoration(0))
      albumRailAdapter = MediaRailAdapter(
        Glide.with(this@MediaPreviewFragment),
        { media -> jumpViewPagerToMedia(media) },
        object : ImageLoadingListener() {
          override fun onAllRequestsFinished() {
            val willAnimateIn = crossfadeViewIn(this@apply)
            if (!willAnimateIn) {
              visible = true
            }
          }
        }
      )
      adapter = albumRailAdapter
    }
  }

  private fun initializeFullScreenUi() {
    fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar)
    fullscreenHelper.showAndHideWithSystemUI(requireActivity().window, binding.toolbarLayout, binding.mediaPreviewDetailsContainer)
  }

  private fun bindCurrentState(currentState: MediaPreviewState) {
    if (currentState.position < 0 && currentState.mediaRecords.isEmpty()) {
      onMediaNotAvailable()
      return
    }
    when (currentState.loadState) {
      MediaPreviewState.LoadState.DATA_LOADED -> bindDataLoadedState(currentState)
      MediaPreviewState.LoadState.MEDIA_READY -> bindMediaReadyState(currentState)
      else -> Unit
    }
  }

  private fun bindDataLoadedState(currentState: MediaPreviewState) {
    val currentPosition = currentState.position

    val backingItems = currentState.mediaRecords.mapNotNull { it.attachment }
    if (backingItems.isEmpty() || currentPosition < 0) {
      onMediaNotAvailable()
      return
    }
    pagerAdapter.updateBackingItems(backingItems)

    if (binding.mediaPager.currentItem != currentPosition) {
      binding.mediaPager.setCurrentItem(currentPosition, false)
    }

    val currentItem: MediaTable.MediaRecord = currentState.mediaRecords[currentPosition]
    bindTextViews(currentItem, currentState.showThread, currentState.messageBodies)
    bindMenuItems(currentItem)
  }

  /**
   * These are binding steps that need a reference to the actual fragment within the pager.
   * This is not available until after a page has been chosen by the ViewPager, and we receive the
   * {@link OnPageChangeCallback}.
   */
  private fun bindMediaReadyState(currentState: MediaPreviewState) {
    val currentPosition: Int = currentState.position
    if (currentState.mediaRecords.isEmpty() || currentPosition < 0) {
      onMediaNotAvailable()
      return
    }

    val currentItem: MediaTable.MediaRecord = currentState.mediaRecords[currentPosition]
    val currentItemTag: String? = pagerAdapter.getFragmentTag(currentPosition)

    childFragmentManager.fragments.forEach { fragment ->
      if (fragment.tag != currentItemTag) {
        (fragment as? MediaPreviewPageFragment)?.pause()
      }
    }

    bindTextViews(currentItem, currentState.showThread, currentState.messageBodies)
    bindMenuItems(currentItem)
    tryBindMediaPreviewPlaybackControls(currentItem, currentPosition)

    val albumThumbnailMedia: List<Media> = if (currentState.allMediaInAlbumRail) {
      currentState.mediaRecords.mapNotNull { it.toMedia() }
    } else {
      currentState.albums[currentItem.attachment?.mmsId] ?: emptyList()
    }
    bindAlbumRail(albumThumbnailMedia, currentItem)

    crossfadeViewIn(binding.mediaPreviewDetailsContainer)
  }

  private fun bindTextViews(currentItem: MediaTable.MediaRecord, showThread: Boolean, messageBodies: Map<Long, SpannableString>) {
    bindToolbar(currentItem.recipientId, currentItem.threadRecipientId, currentItem.isOutgoing, showThread, currentItem.date, currentItem.attachment?.mmsId)

    val caption = currentItem.attachment?.caption
    val messageId = currentItem.attachment?.mmsId
    if (caption != null) {
      bindCaptionView(SpannableString(caption))
    } else {
      bindCaptionView(messageBodies[messageId] ?: initialBodyForMessage(messageId))
    }
  }

  /**
   * The body passed in via arguments for the initially-opened message. Used as a fallback until the background
   * body resolution populates [MediaPreviewState.messageBodies], so the caption never blanks out after the
   * instant render.
   */
  private fun initialBodyForMessage(messageId: Long?): SpannableString? {
    return if (messageId != null && messageId == args.messageId) {
      args.initialMessageBody?.let { SpannableString(it) }
    } else {
      null
    }
  }

  private fun bindInitialCaption(args: MediaIntentFactory.MediaPreviewArgs) {
    val caption = args.initialCaption
    if (caption != null) {
      bindCaptionView(SpannableString(caption))
    } else {
      bindCaptionView(args.initialMessageBody?.let { SpannableString(it) })
    }
  }

  private fun bindToolbar(fromRecipientId: RecipientId, threadRecipientId: RecipientId, isOutgoing: Boolean, showThread: Boolean, date: Long, messageId: Long?) {
    val title = getTitleText(fromRecipientId, threadRecipientId, isOutgoing, showThread)
    val (subtitle, subtitleContentDesc) = getSubTitleText(date)
    binding.toolbar.title = title
    binding.toolbar.subtitle = subtitle
    binding.toolbar.contentDescription = "$title $subtitleContentDesc"
    if (messageId != null && messageId > 0) {
      binding.toolbar.setOnClickListener { v ->
        lifecycleDisposable += viewModel.jumpToFragment(v.context, messageId).subscribeBy(
          onSuccess = {
            startActivity(it)
            requireActivity().finish()
          },
          onError = {
            Log.e(TAG, "Could not find message position for message ID: $messageId", it)
            Toast.makeText(v.context, R.string.MediaPreviewActivity_error_finding_message, Toast.LENGTH_LONG).show()
          }
        )
      }
    }
  }

  private fun bindCaptionView(displayBody: SpannableString?) {
    val caption: ExpandingCaptionView = binding.mediaPreviewCaption
    if (displayBody.isNullOrEmpty()) {
      caption.visible = false
    } else {
      caption.expandedHeight = calculateExpandedHeight()
      caption.fullCaptionText = displayBody.removeMentionAnnotations()
      caption.visible = true
    }
  }

  private fun calculateExpandedHeight(): Int {
    val height: Int = view?.height ?: return ViewUtil.dpToPx(requireContext(), EXPANDED_CAPTION_HEIGHT_FALLBACK_DP)
    return ((height - binding.toolbar.height - binding.mediaPreviewPlaybackControls.height) * EXPANDED_CAPTION_HEIGHT_PERCENT).roundToInt()
  }

  private fun bindMenuItems(currentItem: MediaTable.MediaRecord) {
    val menu: Menu = binding.toolbar.menu
    if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) {
      menu.findItem(R.id.delete).isVisible = false
    }

    binding.toolbar.setOnMenuItemClickListener {
      when (it.itemId) {
        R.id.edit -> editMediaItem(currentItem)
        R.id.save -> saveToDisk(currentItem)
        R.id.delete -> deleteMedia(currentItem)
        android.R.id.home -> requireActivity().finish()
        else -> return@setOnMenuItemClickListener false
      }
      return@setOnMenuItemClickListener true
    }
  }

  private fun bindInitialPlaybackControls(args: MediaIntentFactory.MediaPreviewArgs) {
    if (!isContentTypeSupported(args.initialMediaType)) {
      return
    }
    val mediaMode: MediaPreviewPlayerControlView.MediaMode = if (args.isVideoGif) {
      MediaPreviewPlayerControlView.MediaMode.IMAGE
    } else {
      MediaPreviewPlayerControlView.MediaMode.fromString(args.initialMediaType)
    }
    binding.mediaPreviewPlaybackControls.setMediaMode(mediaMode)
    bindShareAndForwardButtons(args.threadId, args.initialMediaDataUri, args.initialMediaType)
    crossfadeViewIn(binding.mediaPreviewDetailsContainer)
  }

  private fun bindMediaPreviewPlaybackControls(currentItem: MediaTable.MediaRecord, currentFragment: MediaPreviewPageFragment?) {
    val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.videoGif == true) {
      MediaPreviewPlayerControlView.MediaMode.IMAGE
    } else {
      MediaPreviewPlayerControlView.MediaMode.fromString(currentItem.contentType)
    }
    binding.mediaPreviewPlaybackControls.setMediaMode(mediaType)
    bindShareAndForwardButtons(currentItem.threadId, currentItem.attachment?.uri, currentItem.contentType)
    currentFragment?.setBottomButtonControls(binding.mediaPreviewPlaybackControls)
    currentFragment?.autoPlayIfNeeded()
  }

  private fun bindShareAndForwardButtons(threadId: Long, uri: Uri?, contentType: String?) {
    if (uri == null) {
      binding.mediaPreviewPlaybackControls.setShareButtonListener(null)
      binding.mediaPreviewPlaybackControls.setForwardButtonListener(null)
      return
    }
    binding.mediaPreviewPlaybackControls.setShareButtonListener {
      pauseCurrentMediaIfVideo()
      share(uri, contentType)
    }
    binding.mediaPreviewPlaybackControls.setForwardButtonListener {
      pauseCurrentMediaIfVideo()
      forward(threadId, uri, contentType)
    }
  }

  private fun pauseCurrentMediaIfVideo() {
    (getMediaPreviewFragmentFromChildFragmentManager(binding.mediaPager.currentItem) as? VideoMediaPreviewPageFragment)?.pause()
  }

  private fun tryBindMediaPreviewPlaybackControls(
    currentItem: MediaTable.MediaRecord,
    currentPosition: Int,
    maxRetries: Int = 5,
    delayMillis: Long = 50L
  ) {
    viewLifecycleOwner.lifecycleScope.launch {
      repeat(maxRetries) { attempt ->
        if (!isActive) return@launch
        val mediaFragment = getMediaPreviewFragmentFromChildFragmentManager(currentPosition)
        bindMediaPreviewPlaybackControls(currentItem, mediaFragment)

        if (mediaFragment != null) return@launch
        delay(delayMillis)
      }
    }
  }

  private fun bindAlbumRail(albumThumbnailMedia: List<Media>, currentItem: MediaTable.MediaRecord) {
    val albumRail: RecyclerView = binding.mediaPreviewPlaybackControls.recyclerView
    if (albumThumbnailMedia.size > 1) {
      val firstRailDisplay = albumRail.visibility == View.GONE
      if (firstRailDisplay) {
        albumRail.visibility = View.INVISIBLE
        albumRail.alpha = 0f
      }
      val railItems = albumThumbnailMedia.map { MediaRailAdapter.MediaRailItem(it, it.uri == currentItem.attachment?.uri) }
      albumRailAdapter.submitList(railItems) {
        albumRail.post {
          scrollAlbumRailToCurrentAdapterPosition(!firstRailDisplay)
          crossfadeViewIn(albumRail)
        }
      }
    } else {
      albumRail.visibility = View.GONE
      albumRailAdapter.submitList(emptyList())
    }
  }

  private fun scrollAlbumRailToCurrentAdapterPosition(smooth: Boolean = true) {
    if (!isResumed) {
      return
    }

    val currentItemPosition = albumRailAdapter.findSelectedItemPosition()
    val albumRail: RecyclerView = binding.mediaPreviewPlaybackControls.recyclerView
    val offsetFromStart = (albumRail.width - individualItemWidth) / 2
    val smoothScroller = OffsetSmoothScroller(requireContext(), offsetFromStart)
    smoothScroller.targetPosition = currentItemPosition
    val layoutManager = albumRail.layoutManager as LinearLayoutManager
    if (smooth) {
      layoutManager.scrollToPosition(currentItemPosition)
      layoutManager.startSmoothScroll(smoothScroller)
    } else {
      layoutManager.scrollToPositionWithOffset(currentItemPosition, offsetFromStart)
    }
  }

  private fun crossfadeViewIn(view: View, duration: Long = 200): Boolean {
    return if (!view.isVisible && fullscreenHelper.isSystemUiVisible) {
      val viewPropertyAnimator = view.animate()
        .alpha(1f)
        .setDuration(duration)
        .withStartAction {
          view.visibility = View.VISIBLE
        }
        .withEndAction {
          if (getView() != null && view == binding.mediaPreviewPlaybackControls.recyclerView) {
            scrollAlbumRailToCurrentAdapterPosition()
          }
        }
      viewPropertyAnimator.interpolator = PathInterpolator(0.17f, 0.17f, 0f, 1f)
      viewPropertyAnimator.start()
      true
    } else {
      false
    }
  }

  private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int): MediaPreviewPageFragment? {
    return childFragmentManager.findFragmentByTag(pagerAdapter.getFragmentTag(currentPosition)) as? MediaPreviewPageFragment
  }

  private fun jumpViewPagerToMedia(media: Media) {
    val position = pagerAdapter.findItemPosition(media)
    binding.mediaPager.setCurrentItem(position, true)
  }

  private fun getTitleText(fromRecipientId: RecipientId, threadRecipientId: RecipientId, isOutgoing: Boolean, showThread: Boolean): String {
    val recipient: Recipient = Recipient.live(fromRecipientId).get()
    val defaultFromString: String = if (isOutgoing) {
      getString(R.string.MediaPreviewActivity_you)
    } else {
      recipient.getDisplayName(requireContext())
    }
    if (!showThread) {
      return defaultFromString
    }

    val threadRecipient = Recipient.live(threadRecipientId).get()
    return if (isOutgoing) {
      if (threadRecipient.isSelf) {
        getString(R.string.note_to_self)
      } else {
        getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(requireContext()))
      }
    } else {
      if (threadRecipient.isGroup) {
        getString(R.string.MediaPreviewActivity_s_to_s, defaultFromString, threadRecipient.getDisplayName(requireContext()))
      } else {
        getString(R.string.MediaPreviewActivity_s_to_you, defaultFromString)
      }
    }
  }

  private fun getSubTitleText(date: Long): Pair<CharSequence, CharSequence> {
    val (text, contentDesc) = if (date > 0) {
      DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), date)
    } else {
      Pair(getString(R.string.MediaPreviewActivity_draft), getString(R.string.MediaPreviewActivity_draft))
    }
    val builder = SpannableStringBuilder(text)

    val onSurfaceColor = ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorOnSurface)
    val chevron = requireContext().requireDrawable(R.drawable.ic_chevron_end_24)
    chevron.colorFilter = PorterDuffColorFilter(onSurfaceColor, PorterDuff.Mode.SRC_IN)

    SpanUtil.appendCenteredImageSpan(builder, chevron, 10, 10)
    return Pair(builder, contentDesc)
  }

  private fun anchorPaddingToBottomInsets(viewToAnchor: View) {
    ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor) { view: View, windowInsetsCompat: WindowInsetsCompat ->
      view.setPadding(
        windowInsetsCompat.systemWindowInsetLeft,
        view.paddingTop,
        windowInsetsCompat.systemWindowInsetRight,
        windowInsetsCompat.systemWindowInsetBottom
      )
      windowInsetsCompat
    }
  }

  override fun singleTapOnMedia(): Boolean {
    fullscreenHelper.toggleUiVisibility()
    return true
  }

  override fun onMediaNotAvailable() {
    val context = context ?: return
    Toast.makeText(context, R.string.MediaPreviewActivity_media_no_longer_available, Toast.LENGTH_LONG).show()
    activity?.finish()
  }

  override fun onMediaReady() {
    viewModel.setMediaReady()
  }

  override fun onPlaying() {
    debouncer.publish { fullscreenHelper.hideSystemUI() }
  }

  override fun onStopped(tag: String?) {
    if (tag == null) {
      return
    }

    if (pagerAdapter.getFragmentTag(viewModel.currentPosition) == tag) {
      debouncer.clear()
    }
  }

  override fun onDestroy() {
    super.onDestroy()
    val observer = dbChangeObserver
    if (observer != null) {
      AppDependencies.databaseObserver.unregisterObserver(observer)
      dbChangeObserver = null
    }
  }

  override fun unableToPlayMedia() {
    val context = context ?: return
    Toast.makeText(context, R.string.MediaPreviewActivity_unable_to_play_media, Toast.LENGTH_LONG).show()
    activity?.finish()
  }

  private fun forward(threadId: Long, uri: Uri, contentType: String?) {
    MultiselectForwardFragmentArgs.create(
      context = requireContext(),
      threadId = threadId,
      mediaUri = uri,
      contentType = contentType
    ) { args: MultiselectForwardFragmentArgs ->
      MultiselectForwardFragment.showBottomSheet(childFragmentManager, args)
    }
  }

  private fun share(uri: Uri, contentType: String?) {
    val publicUri = PartAuthority.getAttachmentPublicUri(uri)
    val mimeType = Intent.normalizeMimeType(contentType)
    val shareIntent = ShareCompat.IntentBuilder(requireActivity())
      .setStream(publicUri)
      .setType(mimeType)
      .createChooserIntent()
      .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

    if (Build.VERSION.SDK_INT < 34) {
      shareIntent.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(ComponentName(requireContext(), ShareActivity::class.java)))
    }

    try {
      startActivity(shareIntent)
    } catch (e: ActivityNotFoundException) {
      Log.w(TAG, "No activity existed to share the media.", e)
      Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
    }
  }

  private fun saveToDisk(mediaItem: MediaTable.MediaRecord) {
    val attachment = mediaItem.attachment
    if (attachment != null && !attachment.hasData && SignalStore.backup.optimizeStorage) {
      OffloadedMediaDialogUtil.showAllOffloaded(requireContext())
      return
    }

    val uri = attachment?.uri
    val contentType = attachment?.contentType
    if (uri == null || contentType == null) {
      Log.w(TAG, "Unable to save attachment with null URI or contentType.")
      return
    }

    val saveAttachment = SaveAttachmentUtil.SaveAttachment(
      uri = uri,
      contentType = contentType,
      date = if (mediaItem.date > 0) mediaItem.date else System.currentTimeMillis(),
      fileName = null
    )

    lifecycleScope.launch {
      AttachmentSaver(this@MediaPreviewFragment).saveAttachments(setOf(saveAttachment))
    }
  }

  private fun deleteMedia(mediaItem: MediaTable.MediaRecord) {
    val attachment: DatabaseAttachment = mediaItem.attachment ?: return

    if (DeleteSyncEducationDialog.shouldShow()) {
      DeleteSyncEducationDialog
        .show(childFragmentManager)
        .subscribe { deleteMedia(mediaItem) }
        .addTo(lifecycleDisposable)

      return
    }

    val messageRecord = SignalDatabase.messages.getMessageRecord(attachment.mmsId)
    val isNoteToSelf = messageRecord.isOutgoing && messageRecord.toRecipient.isSelf

    MaterialAlertDialogBuilder(requireContext()).apply {
      setIcon(R.drawable.symbol_error_triangle_fill_24)
      setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title)
      setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message)
      setCancelable(true)
      setNegativeButton(android.R.string.cancel, null)

      val deleteButtonLabel = if (isNoteToSelf) {
        R.string.ConversationFragment_delete
      } else {
        R.string.ConversationFragment_delete_for_me
      }

      setPositiveButton(deleteButtonLabel) { _, _ ->
        lifecycleDisposable += viewModel.localDelete(requireContext(), attachment)
          .observeOn(AndroidSchedulers.mainThread())
          .subscribeBy(
            onComplete = {
              requireActivity().finish()
            },
            onError = {
              Log.e(TAG, "Delete failed!", it)
              Toast.makeText(requireContext(), R.string.MediaPreviewFragment_media_delete_error, Toast.LENGTH_LONG).show()
              requireActivity().finish()
            }
          )
      }

      if (canRemotelyDelete(attachment, messageRecord) && !isNoteToSelf) {
        setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
          lifecycleDisposable += viewModel.remoteDelete(attachment)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeBy(
              onComplete = {
                requireActivity().finish()
              },
              onError = {
                Log.e(TAG, "Delete failed!", it)
                Toast.makeText(requireContext(), R.string.MediaPreviewFragment_media_delete_error, Toast.LENGTH_LONG).show()
                requireActivity().finish()
              }
            )
        }
      }
    }.show()
  }

  private fun canRemotelyDelete(attachment: DatabaseAttachment, messageRecord: MessageRecord): Boolean {
    val mmsId = attachment.mmsId
    val attachmentCount = SignalDatabase.attachments.getAttachmentsForMessage(mmsId).size
    return attachmentCount <= 1 && MessageConstraintsUtil.isValidRemoteDeleteSend(listOf(messageRecord), System.currentTimeMillis())
  }

  private fun editMediaItem(currentItem: MediaTable.MediaRecord) {
    val media = currentItem.toMedia()
    if (media == null) {
      val rootView = view
      if (rootView != null) {
        Snackbar.make(rootView, R.string.MediaPreviewFragment_edit_media_error, Snackbar.LENGTH_INDEFINITE).show()
      } else {
        Toast.makeText(requireContext(), R.string.MediaPreviewFragment_edit_media_error, Toast.LENGTH_LONG).show()
      }
      return
    }
    startActivity(MediaSendLauncher.editor(context = requireContext(), media = listOf(media)))
  }

  override fun onPause() {
    super.onPause()
    getMediaPreviewFragmentFromChildFragmentManager(binding.mediaPager.currentItem)?.pause()
  }

  override fun onDestroyView() {
    super.onDestroyView()
    viewModel.onDestroyView()
  }

  private class OffsetSmoothScroller(context: Context, val offset: Int) : LinearSmoothScroller(context) {
    override fun getHorizontalSnapPreference(): Int {
      return SNAP_TO_START
    }

    override fun calculateDxToMakeVisible(view: View?, snapPreference: Int): Int {
      return offset + super.calculateDxToMakeVisible(view, snapPreference)
    }
  }

  companion object {
    private const val EXPANDED_CAPTION_HEIGHT_FALLBACK_DP = 400
    private const val EXPANDED_CAPTION_HEIGHT_PERCENT: Float = 0.7F

    private val TAG = Log.tag(MediaPreviewFragment::class.java)

    const val ARGS_KEY: String = "args"

    @JvmStatic
    fun isContentTypeSupported(contentType: String?): Boolean {
      return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)
    }
  }
}

private fun SpannableString.removeMentionAnnotations(): CharSequence {
  val spans: Array<out Annotation> = this.getSpans()
  spans.forEach {
    if (MentionAnnotation.isMentionAnnotation(it)) {
      this.removeSpan(it)
    }
  }
  return this
}
