package org.thoughtcrime.securesms.mediasend.v2.gallery

import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.marginEnd
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import org.signal.core.models.media.Media
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.getWindowBreakpoint
import org.signal.core.ui.permissions.Permissions
import org.signal.core.ui.util.StorageUtil
import org.signal.core.util.Stopwatch
import org.signal.core.util.permissions.PermissionCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.conversation.ManageContextMenu
import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding
import org.thoughtcrime.securesms.mediasend.MediaRepository
import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.visible
import org.signal.core.ui.R as CoreUiR

/**
 * Displays a collection of files and folders to the user to allow them to select
 * media to send.
 */
class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {

  companion object {
    private const val SPAN_COUNT = 24
  }

  private val viewModel: MediaGalleryViewModel by viewModels(
    factoryProducer = { MediaGalleryViewModel.Factory(null, null, MediaGalleryRepository(requireContext(), MediaRepository())) }
  )

  private lateinit var callbacks: Callbacks

  private var selectedMediaTouchHelper: ItemTouchHelper? = null
  private var shouldEnableScrolling: Boolean = true

  private val galleryAdapter = MappingAdapter()
  private val selectedAdapter = MappingAdapter()
  private val mediaGalleryGridItemTouchListener = MediaGalleryGridItemTouchListener()

  private lateinit var viewStateLiveData: MutableLiveData<ViewState>

  private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(false) {
    override fun handleOnBackPressed() {
      onBack()
    }
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    viewStateLiveData = MutableLiveData(ViewState(chatColor = ContextCompat.getColor(requireContext(), CoreUiR.color.signal_light_colorPrimary)))
    callbacks = requireListener()
    val binding = V2MediaGalleryFragmentBinding.bind(view)

    applyWindowInsets(view, binding)

    binding.mediaGalleryGrid.layoutManager = object : GridLayoutManager(requireContext(), SPAN_COUNT) {
      override fun canScrollVertically() = shouldEnableScrolling
    }

    val breakpoint = resources.getWindowBreakpoint()
    val folderSpans = when (breakpoint) {
      is WindowBreakpoint.Large -> SPAN_COUNT / 4
      is WindowBreakpoint.Medium -> SPAN_COUNT / 3
      is WindowBreakpoint.Small -> SPAN_COUNT / 2
    }

    val fileSpans = when (breakpoint) {
      is WindowBreakpoint.Large -> SPAN_COUNT / 8
      is WindowBreakpoint.Medium -> SPAN_COUNT / 6
      is WindowBreakpoint.Small -> SPAN_COUNT / 4
    }

    (binding.mediaGalleryGrid.layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
      override fun getSpanSize(position: Int): Int {
        val isFolder: Boolean = (binding.mediaGalleryGrid.adapter as MappingAdapter).getModel(position).map { it is MediaGallerySelectableItem.FolderModel }.orElse(false)

        return if (isFolder) folderSpans else fileSpans
      }
    }

    binding.mediaGalleryToolbar.setNavigationOnClickListener {
      onBack()
    }

    Material3OnScrollHelper(
      activity = requireActivity(),
      views = listOf(binding.mediaGalleryToolbar, binding.mediaGalleryStatusBarBackground),
      lifecycleOwner = viewLifecycleOwner
    ).attach(binding.mediaGalleryGrid)

    if (callbacks.isCameraEnabled()) {
      binding.mediaGalleryToolbar.setOnMenuItemClickListener { item ->
        if (item.itemId == R.id.action_camera) {
          callbacks.onNavigateToCamera()
          true
        } else {
          false
        }
      }
    } else {
      binding.mediaGalleryToolbar.menu.findItem(R.id.action_camera).isVisible = false
    }

    binding.mediaGalleryCountButton.setOnClickListener {
      callbacks.onSubmit()
    }

    MediaGallerySelectedItem.register(selectedAdapter) { media ->
      callbacks.onSelectedMediaClicked(media)
    }
    binding.mediaGallerySelected.adapter = selectedAdapter
    selectedMediaTouchHelper?.attachToRecyclerView(binding.mediaGallerySelected)

    val onDragSelectListener = object : MediaGalleryGridItemTouchListener.OnDragSelectListener {
      override fun onSelectionStarted(start: Int) {
        galleryAdapter.getModel(start).ifPresent {
          val fileModel = it as MediaGallerySelectableItem.FileModel
          val media = fileModel.media
          if (fileModel.isSelected) {
            callbacks.onMediaUnselected(media)
            mediaGalleryGridItemTouchListener.setIsActive(false)
          } else {
            callbacks.onMediaSelected(media)
          }
        }
      }

      override fun onSelectChange(start: Int, end: Int, shouldSelect: Boolean) {
        val mediaSet = (start..end)
          .mapNotNull { i ->
            galleryAdapter.getModel(i).orElse(null) as? MediaGallerySelectableItem.FileModel
          }
          .map { fileModel ->
            fileModel.media
          }
          .toSet()

        if (mediaSet.isNotEmpty()) {
          if (shouldSelect) {
            callbacks.onMediaSelected(mediaSet)
          } else {
            callbacks.onMediaUnselected(mediaSet)
          }
        }
      }
    }
    mediaGalleryGridItemTouchListener.withSelectListener(onDragSelectListener)

    MediaGallerySelectableItem.registerAdapter(
      mappingAdapter = galleryAdapter,
      mediaGalleryGridItemTouchListener = mediaGalleryGridItemTouchListener,
      onMediaFolderClicked = {
        onBackPressedCallback.isEnabled = true
        viewModel.setMediaFolder(it)
      },
      onMediaClicked = { media, selected ->
        if (selected) {
          callbacks.onMediaUnselected(media)
        } else {
          callbacks.onMediaSelected(media)
        }
      },
      callbacks.isMultiselectEnabled()
    )

    binding.mediaGalleryGrid.adapter = galleryAdapter
    binding.mediaGalleryGrid.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(2)))
    binding.mediaGalleryGrid.addOnItemTouchListener(mediaGalleryGridItemTouchListener)

    viewStateLiveData.observe(viewLifecycleOwner) { state ->
      binding.mediaGalleryBottomBarGroup.visible = state.selectedMedia.isNotEmpty()
      binding.mediaGalleryCountButton.setCount(state.selectedMedia.size)
      state.chatColor?.let { binding.mediaGalleryCountButton.setChatColor(it) }

      val stopwatch = Stopwatch("mediaSubmit")
      selectedAdapter.submitList(state.selectedMedia.map { MediaGallerySelectedItem.Model(it) }) {
        stopwatch.split("after-submit")
        stopwatch.stop("MediaGalleryFragment")
        if (state.selectedMedia.isNotEmpty()) {
          binding.mediaGallerySelected.smoothScrollToPosition(state.selectedMedia.size - 1)
        }
      }
    }

    viewModel.state.observe(viewLifecycleOwner) { state ->
      binding.mediaGalleryToolbar.title = state.bucketTitle ?: requireContext().getString(R.string.AttachmentKeyboard_gallery)
    }

    val galleryItemsWithSelection = LiveDataUtil.combineLatest(
      viewModel.state.map { it.items },
      viewStateLiveData.map { it.selectedMedia },
      viewStateLiveData.map { it.chatColor }
    ) { galleryItems, selectedMedia, chatColor ->
      galleryItems.map {
        if (it is MediaGallerySelectableItem.FileModel) {
          val selectedIndex = selectedMedia.indexOfFirst { selected -> selected.uri == it.media.uri }
          it.copy(
            isSelected = selectedIndex >= 0,
            selectionOneBasedIndex = selectedIndex + 1,
            chatColor = chatColor
          )
        } else {
          it
        }
      }
    }

    galleryItemsWithSelection.observe(viewLifecycleOwner) {
      if (StorageUtil.canOnlyReadSelectedMediaStore() && it.isEmpty()) {
        binding.mediaGalleryMissingPermissions.visible = true
        binding.mediaGalleryManageContainer.visible = false
        binding.mediaGalleryPermissionText.text = getString(R.string.MediaGalleryFragment__no_photos_found)
        binding.mediaGalleryAllowAccess.text = getString(R.string.AttachmentKeyboard_manage)
        binding.mediaGalleryAllowAccess.setOnClickListener { v -> showManageContextMenu(v, v.parent as ViewGroup, false, true) }
        shouldEnableScrolling = false
        galleryAdapter.submitList((1..100).map { MediaGallerySelectableItem.PlaceholderModel() })
      } else if (StorageUtil.canOnlyReadSelectedMediaStore()) {
        binding.mediaGalleryMissingPermissions.visible = false
        binding.mediaGalleryManageContainer.visible = true
        binding.mediaGalleryManageButton.setOnClickListener { v -> showManageContextMenu(v, v.rootView as ViewGroup, false, false) }
        shouldEnableScrolling = true
        galleryAdapter.submitList(it)
      } else if (StorageUtil.canReadAnyFromMediaStore()) {
        binding.mediaGalleryMissingPermissions.visible = false
        binding.mediaGalleryManageContainer.visible = false
        shouldEnableScrolling = true
        galleryAdapter.submitList(it)
      } else {
        binding.mediaGalleryMissingPermissions.visible = true
        binding.mediaGalleryManageContainer.visible = false
        binding.mediaGalleryPermissionText.text = getString(R.string.AttachmentKeyboard_Signal_needs_permission_to_show_your_photos_and_videos)
        binding.mediaGalleryAllowAccess.text = getString(R.string.AttachmentKeyboard_allow_access)
        binding.mediaGalleryAllowAccess.setOnClickListener { requestRequiredPermissions() }
        shouldEnableScrolling = false
        galleryAdapter.submitList((1..100).map { MediaGallerySelectableItem.PlaceholderModel() })
      }
    }

    requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
  }

  /**
   * The top bar spans the full width so its background covers the cutout, while everything that holds
   * content is inset horizontally. Driven off the inset dispatch rather than read once, since our hosts
   * declare `configChanges="orientation"` and never recreate this view on rotation.
   */
  private fun applyWindowInsets(view: View, binding: V2MediaGalleryFragmentBinding) {
    val rootPaddingBottom = view.paddingBottom
    val toolbarPaddingLeft = binding.mediaGalleryToolbar.paddingLeft
    val toolbarPaddingRight = binding.mediaGalleryToolbar.paddingRight
    val managePaddingLeft = binding.mediaGalleryManageContainer.paddingLeft
    val managePaddingRight = binding.mediaGalleryManageContainer.paddingRight
    val selectedPaddingStart = binding.mediaGallerySelected.paddingStart
    val countButtonMarginEnd = binding.mediaGalleryCountButton.marginEnd

    ViewCompat.setOnApplyWindowInsetsListener(view) { root, windowInsets ->
      val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
      val isLtr = ViewUtil.isLtr(root)
      val startInset = if (isLtr) insets.left else insets.right
      val endInset = if (isLtr) insets.right else insets.left

      root.updatePadding(bottom = rootPaddingBottom + insets.bottom)

      binding.mediaGalleryStatusBarBackground.updateLayoutParams {
        height = insets.top
      }

      binding.mediaGalleryToolbar.updateLayoutParams<ConstraintLayout.LayoutParams> {
        topMargin = insets.top
      }

      binding.mediaGalleryToolbar.updatePadding(left = toolbarPaddingLeft + insets.left, right = toolbarPaddingRight + insets.right)
      binding.mediaGalleryManageContainer.updatePadding(left = managePaddingLeft + insets.left, right = managePaddingRight + insets.right)
      binding.mediaGalleryGrid.updatePadding(left = insets.left, right = insets.right)
      binding.mediaGalleryMissingPermissions.updatePadding(left = insets.left, right = insets.right)
      binding.mediaGallerySelected.updatePaddingRelative(start = selectedPaddingStart + startInset)

      binding.mediaGalleryCountButton.updateLayoutParams<ConstraintLayout.LayoutParams> {
        marginEnd = countButtonMarginEnd + endInset
      }

      windowInsets
    }

    view.post { ViewCompat.requestApplyInsets(view) }
  }

  override fun onResume() {
    super.onResume()
    refreshMediaGallery()
  }

  private fun refreshMediaGallery() {
    viewModel.refreshMediaGallery()
  }

  @Deprecated("Deprecated in Java")
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
    Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
  }

  private fun showManageContextMenu(view: View, rootView: ViewGroup, showAbove: Boolean, showAtStart: Boolean) {
    ManageContextMenu.show(
      context = requireContext(),
      anchorView = view,
      rootView = rootView,
      showAbove = showAbove,
      showAtStart = showAtStart,
      onSelectMore = { selectMorePhotos() },
      onSettings = { requireContext().startActivity(Permissions.getApplicationSettingsIntent(requireContext())) }
    )
  }

  private fun selectMorePhotos() {
    Permissions.with(this)
      .request(*PermissionCompat.forImagesAndVideos())
      .onAnyResult { refreshMediaGallery() }
      .execute()
  }

  private fun requestRequiredPermissions() {
    Permissions.with(this)
      .request(*PermissionCompat.forImagesAndVideos())
      .ifNecessary()
      .onAnyResult { refreshMediaGallery() }
      .withPermanentDenialDialog(
        getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio),
        null,
        R.string.AttachmentManager_signal_allow_storage,
        R.string.AttachmentManager_signal_to_show_photos,
        true,
        parentFragmentManager
      )
      .onSomeDenied {
        val deniedPermission = PermissionCompat.getRequiredPermissionsForDenial()
        if (it.containsAll(deniedPermission.toList())) {
          Toast.makeText(requireContext(), R.string.AttachmentManager_signal_needs_storage_access, Toast.LENGTH_LONG).show()
        }
      }
      .execute()
  }

  fun onBack() {
    if (viewModel.pop()) {
      onBackPressedCallback.isEnabled = false
      callbacks.onToolbarNavigationClicked()
    }
  }

  fun onViewStateUpdated(state: ViewState) {
    viewStateLiveData.value = state
  }

  fun onMediaErrorOccurred() {
    mediaGalleryGridItemTouchListener.stopAutoScroll()
    mediaGalleryGridItemTouchListener.setIsActive(false)
  }

  fun bindSelectedMediaItemDragHelper(helper: ItemTouchHelper) {
    selectedMediaTouchHelper = helper
  }

  data class ViewState(
    val selectedMedia: List<Media> = listOf(),
    val chatColor: Int
  )

  interface Callbacks {
    fun isCameraEnabled(): Boolean = true
    fun isMultiselectEnabled(): Boolean = false
    fun onMediaSelected(media: Media)
    fun onMediaSelected(media: Set<Media>) = Unit
    fun onMediaUnselected(media: Set<Media>) = Unit
    fun onMediaUnselected(media: Media): Unit = throw UnsupportedOperationException()
    fun onSelectedMediaClicked(media: Media): Unit = throw UnsupportedOperationException()
    fun onNavigateToCamera(): Unit = throw UnsupportedOperationException()
    fun onSubmit(): Unit = throw UnsupportedOperationException()
    fun onToolbarNavigationClicked(): Unit = throw UnsupportedOperationException()
  }
}
