/* Copyright 2017 Andrew Dawson
 *
 * This file is a part of Tusky.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Tusky; if not,
 * see <http://www.gnu.org/licenses>. */

package com.keylesspalace.tusky

import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.transition.Transition
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.copyToClipboard
import com.keylesspalace.tusky.util.getParcelableArrayListExtraCompat
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.submitAsync
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch

typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit

@AndroidEntryPoint
class ViewMediaActivity :
    BaseActivity(),
    ViewImageFragment.PhotoActionsListener,
    ViewVideoFragment.VideoActionsListener {

    private val binding by viewBinding(ActivityViewMediaBinding::inflate)

    val toolbar: View
        get() = binding.toolbar

    var isToolbarVisible = true
        private set

    private var attachments: ArrayList<AttachmentViewData>? = null
    private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
    private var imageUrl: String? = null

    private val requestDownloadMediaPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                downloadMedia()
            } else {
                showErrorDialog(
                    binding.toolbar,
                    R.string.error_media_download_permission,
                    R.string.action_retry
                ) { requestDownloadMedia() }
            }
        }

    fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
        this.toolbarVisibilityListeners.add(listener)
        listener(isToolbarVisible)
        return { toolbarVisibilityListeners.remove(listener) }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        supportPostponeEnterTransition()

        // Gather the parameters.
        attachments = intent.getParcelableArrayListExtraCompat(EXTRA_ATTACHMENTS)
        val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)

        // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
        // but it cannot be expressed and if I don't specify type explicitly compilation fails
        // (probably a bug in compiler)
        val adapter: ViewMediaAdapter = if (attachments != null) {
            val realAttachs = attachments!!.map(AttachmentViewData::attachment)
            // Setup the view pager.
            ImagePagerAdapter(this, realAttachs, initialPosition)
        } else {
            imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
                ?: throw IllegalArgumentException("attachment list or image url has to be set")

            SingleImagePagerAdapter(this, imageUrl!!)
        }

        binding.viewPager.adapter = adapter
        binding.viewPager.setCurrentItem(initialPosition, false)
        binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                binding.toolbar.title = getPageTitle(position)
                adjustScreenWakefulness()
            }
        })

        // Setup the toolbar.
        setSupportActionBar(binding.toolbar)
        val actionBar = supportActionBar
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true)
            actionBar.setDisplayShowHomeEnabled(true)
            actionBar.title = getPageTitle(initialPosition)
        }
        binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
        binding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
            when (item.itemId) {
                R.id.action_download -> requestDownloadMedia()
                R.id.action_open_status -> onOpenStatus()
                R.id.action_share_media -> shareMedia()
                R.id.action_copy_media_link -> copyLink()
            }
            true
        }

        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE

        window.statusBarColor = Color.BLACK
        window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
            override fun onTransitionEnd(transition: Transition) {
                adapter.onTransitionEnd(binding.viewPager.currentItem)
                window.sharedElementEnterTransition.removeListener(this)
            }
        })

        adjustScreenWakefulness()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.view_media_toolbar, menu)
        // We don't support 'open status' from single image views
        menu.findItem(R.id.action_open_status)?.isVisible = (attachments != null)
        return true
    }

    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
        menu?.findItem(R.id.action_share_media)?.isEnabled = !isCreating
        return true
    }

    override fun onBringUp() {
        supportStartPostponedEnterTransition()
    }

    override fun onDismiss() {
        supportFinishAfterTransition()
    }

    override fun onPhotoTap() {
        isToolbarVisible = !isToolbarVisible
        for (listener in toolbarVisibilityListeners) {
            listener(isToolbarVisible)
        }

        val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE
        val alpha = if (isToolbarVisible) 1.0f else 0.0f
        if (isToolbarVisible) {
            // If to be visible, need to make visible immediately and animate alpha
            binding.toolbar.alpha = 0.0f
            binding.toolbar.visibility = visibility
        }

        binding.toolbar.animate().alpha(alpha)
            .setListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    binding.toolbar.visibility = visibility
                    animation.removeListener(this)
                }
            })
            .start()
    }

    private fun getPageTitle(position: Int): CharSequence {
        if (attachments == null) {
            return ""
        }
        return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
    }

    private fun downloadMedia() {
        val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
        val filename = Uri.parse(url).lastPathSegment
        Toast.makeText(
            applicationContext,
            resources.getString(R.string.download_image, filename),
            Toast.LENGTH_SHORT
        ).show()

        val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(Uri.parse(url))
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
        downloadManager.enqueue(request)
    }

    private fun requestDownloadMedia() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            requestDownloadMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        } else {
            downloadMedia()
        }
    }

    private fun onOpenStatus() {
        val attach = attachments!![binding.viewPager.currentItem]
        startActivityWithSlideInAnimation(
            ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)
        )
    }

    private fun copyLink() {
        copyToClipboard(
            imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url,
            getString(R.string.url_copied),
        )
    }

    private fun shareMedia() {
        val directory = applicationContext.getExternalFilesDir("Tusky")
        if (directory == null || !(directory.exists())) {
            Log.e(TAG, "Error obtaining directory to save temporary media.")
            return
        }

        if (imageUrl != null) {
            shareImage(directory, imageUrl!!)
        } else {
            val attachment = attachments!![binding.viewPager.currentItem].attachment
            when (attachment.type) {
                Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
                Attachment.Type.AUDIO,
                Attachment.Type.VIDEO,
                Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
                else -> Log.e(TAG, "Unknown media format for sharing.")
            }
        }
    }

    private fun shareFile(file: File, mimeType: String?) {
        ShareCompat.IntentBuilder(this)
            .setType(mimeType)
            .addStream(
                FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
            )
            .setChooserTitle(R.string.send_media_to)
            .startChooser()
    }

    private var isCreating: Boolean = false

    private fun shareImage(directory: File, url: String) {
        isCreating = true
        binding.progressBarShare.visibility = View.VISIBLE
        invalidateOptionsMenu()

        lifecycleScope.launch {
            val file = File(directory, getTemporaryMediaFilename("png"))
            val result = try {
                val bitmap =
                    Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submitAsync()
                try {
                    FileOutputStream(file).use { stream ->
                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
                    }
                    true
                } catch (ioe: IOException) {
                    // FileNotFoundException is covered by IOException
                    Log.e(TAG, "Error writing temporary media.")
                    false
                }.also { result -> Log.d(TAG, "Download image result: $result") }
            } catch (error: Throwable) {
                if (error is CancellationException) {
                    throw error
                }
                Log.e(TAG, "Failed to download image", error)
                false
            }

            isCreating = false
            invalidateOptionsMenu()
            binding.progressBarShare.visibility = View.GONE
            if (result) {
                shareFile(file, "image/png")
            }
        }
    }

    private fun shareMediaFile(directory: File, url: String) {
        val uri = Uri.parse(url)
        val mimeTypeMap = MimeTypeMap.getSingleton()
        val extension = MimeTypeMap.getFileExtensionFromUrl(url)
        val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension)
        val filename = getTemporaryMediaFilename(extension)
        val file = File(directory, filename)

        val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(uri)
        request.setDestinationUri(Uri.fromFile(file))
        request.setVisibleInDownloadsUi(false)
        downloadManager.enqueue(request)

        shareFile(file, mimeType)
    }

    // Prevent this activity from dimming or sleeping the screen if, and only if, it is playing video or audio
    private fun adjustScreenWakefulness() {
        attachments?.run {
            if (get(binding.viewPager.currentItem).attachment.type == Attachment.Type.IMAGE) {
                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            } else {
                window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            }
        }
    }

    companion object {
        private const val EXTRA_ATTACHMENTS = "attachments"
        private const val EXTRA_ATTACHMENT_INDEX = "index"
        private const val EXTRA_SINGLE_IMAGE_URL = "single_image"
        private const val TAG = "ViewMediaActivity"

        @JvmStatic
        fun newIntent(
            context: Context?,
            attachments: List<AttachmentViewData>,
            index: Int
        ): Intent {
            val intent = Intent(context, ViewMediaActivity::class.java)
            intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
            intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)
            return intent
        }

        @JvmStatic
        fun newSingleImageIntent(context: Context, url: String): Intent {
            val intent = Intent(context, ViewMediaActivity::class.java)
            intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url)
            return intent
        }
    }
}

abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    abstract fun onTransitionEnd(position: Int)
}

interface NoopTransitionListener : Transition.TransitionListener {
    override fun onTransitionEnd(transition: Transition) {
    }

    override fun onTransitionResume(transition: Transition) {
    }

    override fun onTransitionPause(transition: Transition) {
    }

    override fun onTransitionCancel(transition: Transition) {
    }

    override fun onTransitionStart(transition: Transition) {
    }
}
