/* 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.fragment

import android.Manifest
import android.app.DownloadManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityOptionsCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.PostLookupFallbackBehavior
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity.Companion.newHashtagIntent
import com.keylesspalace.tusky.ViewMediaActivity.Companion.newIntent
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.startIntent
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.components.report.ReportActivity.Companion.getIntent
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Translation
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.launch

/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
 * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
 * of that is complicated by how they're coupled with Status and Notification and the corresponding
 * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
 * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
 * up what needs to be where. */
abstract class SFragment : Fragment(), Injectable {
    protected abstract fun removeItem(position: Int)
    protected abstract fun onReblog(reblog: Boolean, position: Int)

    /** `null` if translation is not supported on this screen */
    protected abstract val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit)?

    private lateinit var bottomSheetActivity: BottomSheetActivity

    @Inject
    lateinit var mastodonApi: MastodonApi

    @Inject
    lateinit var accountManager: AccountManager

    @Inject
    lateinit var timelineCases: TimelineCases

    @Inject
    lateinit var instanceInfoRepository: InstanceInfoRepository

    override fun startActivity(intent: Intent) {
        requireActivity().startActivityWithSlideInAnimation(intent)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        bottomSheetActivity = if (context is BottomSheetActivity) {
            context
        } else {
            throw IllegalStateException("Fragment must be attached to a BottomSheetActivity!")
        }
    }

    override fun onResume() {
        super.onResume()

        // make sure we have instance info for when we'll need it
        instanceInfoRepository.precache()
    }

    protected fun openReblog(status: Status?) {
        if (status == null) return
        bottomSheetActivity.viewAccount(status.account.id)
    }

    protected fun viewThread(statusId: String?, statusUrl: String?) {
        bottomSheetActivity.viewThread(statusId!!, statusUrl)
    }

    protected fun viewAccount(accountId: String?) {
        bottomSheetActivity.viewAccount(accountId!!)
    }

    open fun onViewUrl(url: String) {
        bottomSheetActivity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
    }

    protected fun reply(status: Status) {
        val actionableStatus = status.actionableStatus
        val account = actionableStatus.account
        var loggedInUsername: String? = null
        val activeAccount = accountManager.activeAccount
        if (activeAccount != null) {
            loggedInUsername = activeAccount.username
        }
        val mentionedUsernames = LinkedHashSet(
            listOf(account.username) + actionableStatus.mentions.map { it.username }
        ).apply { remove(loggedInUsername) }

        val composeOptions = ComposeOptions(
            inReplyToId = status.actionableId,
            replyVisibility = actionableStatus.visibility,
            contentWarning = actionableStatus.spoilerText,
            mentionedUsernames = mentionedUsernames,
            replyingStatusAuthor = account.localUsername,
            replyingStatusContent = actionableStatus.content.parseAsMastodonHtml().toString(),
            language = actionableStatus.language,
            kind = ComposeActivity.ComposeKind.NEW
        )

        val intent = startIntent(requireContext(), composeOptions)
        requireActivity().startActivity(intent)
    }

    protected fun more(status: Status, view: View, position: Int, translation: Translation?) {
        val id = status.actionableId
        val accountId = status.actionableStatus.account.id
        val accountUsername = status.actionableStatus.account.username
        val statusUrl = status.actionableStatus.url
        var loggedInAccountId: String? = null
        val activeAccount = accountManager.activeAccount
        if (activeAccount != null) {
            loggedInAccountId = activeAccount.accountId
        }
        val popup = PopupMenu(requireContext(), view)
        // Give a different menu depending on whether this is the user's own toot or not.
        val statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId == accountId
        if (statusIsByCurrentUser) {
            popup.inflate(R.menu.status_more_for_user)
            val menu = popup.menu
            when (status.visibility) {
                Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> {
                    menu.add(
                        0,
                        R.id.pin,
                        1,
                        getString(
                            if (status.pinned) R.string.unpin_action else R.string.pin_action
                        )
                    )
                }

                Status.Visibility.PRIVATE -> {
                    val reblogged = status.reblog?.reblogged ?: status.reblogged
                    menu.findItem(R.id.status_reblog_private).isVisible = !reblogged
                    menu.findItem(R.id.status_unreblog_private).isVisible = reblogged
                }

                else -> {}
            }
        } else {
            popup.inflate(R.menu.status_more)
            popup.menu.findItem(R.id.status_download_media).isVisible =
                status.attachments.isNotEmpty()
        }
        val menu = popup.menu
        val openAsItem = menu.findItem(R.id.status_open_as)
        val openAsText = (activity as BaseActivity?)?.openAsText
        if (openAsText == null) {
            openAsItem.isVisible = false
        } else {
            openAsItem.title = openAsText
        }
        val muteConversationItem = menu.findItem(R.id.status_mute_conversation)
        val mutable =
            statusIsByCurrentUser || accountIsInMentions(activeAccount, status.mentions)
        muteConversationItem.isVisible = mutable
        if (mutable) {
            muteConversationItem.setTitle(
                if (!status.muted) {
                    R.string.action_mute_conversation
                } else {
                    R.string.action_unmute_conversation
                }
            )
        }

        // translation not there for your own posts
        menu.findItem(R.id.status_translate)?.let { translateItem ->
            translateItem.isVisible = onMoreTranslate != null &&
                !status.language.equals(Locale.getDefault().language, ignoreCase = true) &&
                instanceInfoRepository.cachedInstanceInfoOrFallback.translationEnabled == true
            translateItem.setTitle(if (translation != null) R.string.action_show_original else R.string.action_translate)
        }

        popup.setOnMenuItemClickListener { item: MenuItem ->
            when (item.itemId) {
                R.id.post_share_content -> {
                    val statusToShare = status.reblog ?: status
                    val sendIntent = Intent().apply {
                        action = Intent.ACTION_SEND
                        type = "text/plain"
                        putExtra(
                            Intent.EXTRA_TEXT,
                            "${statusToShare.account.username} - ${statusToShare.content.parseAsMastodonHtml()}"
                        )
                        putExtra(Intent.EXTRA_SUBJECT, statusUrl)
                    }
                    startActivity(
                        Intent.createChooser(
                            sendIntent,
                            resources.getText(R.string.send_post_content_to)
                        )
                    )
                    return@setOnMenuItemClickListener true
                }

                R.id.post_share_link -> {
                    val sendIntent = Intent().apply {
                        action = Intent.ACTION_SEND
                        putExtra(Intent.EXTRA_TEXT, statusUrl)
                        type = "text/plain"
                    }
                    startActivity(
                        Intent.createChooser(
                            sendIntent,
                            resources.getText(R.string.send_post_link_to)
                        )
                    )
                    return@setOnMenuItemClickListener true
                }

                R.id.status_copy_link -> {
                    (
                        requireActivity().getSystemService(
                            Context.CLIPBOARD_SERVICE
                        ) as ClipboardManager
                        ).apply {
                        setPrimaryClip(ClipData.newPlainText(null, statusUrl))
                    }
                    return@setOnMenuItemClickListener true
                }

                R.id.status_open_as -> {
                    showOpenAsDialog(statusUrl, item.title)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_download_media -> {
                    requestDownloadAllMedia(status)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_mute -> {
                    onMute(accountId, accountUsername)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_block -> {
                    onBlock(accountId, accountUsername)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_report -> {
                    openReportPage(accountId, accountUsername, id)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_unreblog_private -> {
                    onReblog(false, position)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_reblog_private -> {
                    onReblog(true, position)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_delete -> {
                    showConfirmDeleteDialog(id, position)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_delete_and_redraft -> {
                    showConfirmEditDialog(id, position, status)
                    return@setOnMenuItemClickListener true
                }

                R.id.status_edit -> {
                    editStatus(id, status)
                    return@setOnMenuItemClickListener true
                }

                R.id.pin -> {
                    lifecycleScope.launch {
                        timelineCases.pin(status.id, !status.pinned)
                            .onFailure { e: Throwable ->
                                val message = e.message
                                    ?: getString(if (status.pinned) R.string.failed_to_unpin else R.string.failed_to_pin)
                                Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG)
                                    .show()
                            }
                    }
                    return@setOnMenuItemClickListener true
                }

                R.id.status_mute_conversation -> {
                    lifecycleScope.launch {
                        timelineCases.muteConversation(status.id, !status.muted)
                    }
                    return@setOnMenuItemClickListener true
                }

                R.id.status_translate -> {
                    onMoreTranslate?.invoke(translation == null, position)
                }
            }
            false
        }
        popup.show()
    }

    private fun onMute(accountId: String, accountUsername: String) {
        showMuteAccountDialog(
            this.requireActivity(),
            accountUsername
        ) { notifications: Boolean?, duration: Int? ->
            lifecycleScope.launch {
                timelineCases.mute(accountId, notifications == true, duration)
            }
        }
    }

    private fun onBlock(accountId: String, accountUsername: String) {
        AlertDialog.Builder(requireContext())
            .setMessage(getString(R.string.dialog_block_warning, accountUsername))
            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
                lifecycleScope.launch {
                    timelineCases.block(accountId)
                }
            }
            .setNegativeButton(android.R.string.cancel, null)
            .show()
    }

    protected fun viewMedia(urlIndex: Int, attachments: List<AttachmentViewData>, view: View?) {
        val (attachment) = attachments[urlIndex]
        when (attachment.type) {
            Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
                val intent = newIntent(context, attachments, urlIndex)
                if (view != null) {
                    val url = attachment.url
                    view.transitionName = url
                    val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                        requireActivity(),
                        view,
                        url
                    )
                    startActivity(intent, options.toBundle())
                } else {
                    startActivity(intent)
                }
            }

            Attachment.Type.UNKNOWN -> {
                requireContext().openLink(attachment.url)
            }
        }
    }

    protected fun viewTag(tag: String) {
        startActivity(newHashtagIntent(requireContext(), tag))
    }

    private fun openReportPage(accountId: String, accountUsername: String, statusId: String) {
        startActivity(getIntent(requireContext(), accountId, accountUsername, statusId))
    }

    private fun showConfirmDeleteDialog(id: String, position: Int) {
        AlertDialog.Builder(requireActivity())
            .setMessage(R.string.dialog_delete_post_warning)
            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
                lifecycleScope.launch {
                    val result = timelineCases.delete(id).exceptionOrNull()
                    if (result != null) {
                        Log.w("SFragment", "error deleting status", result)
                        Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
                    }
                    // XXX: Removes the item even if there was an error. This is probably not
                    // correct (see similar code in showConfirmEditDialog() which only
                    // removes the item if the timelineCases.delete() call succeeded.
                    //
                    // Either way, this logic should be in the view model.
                    removeItem(position)
                }
            }
            .setNegativeButton(android.R.string.cancel, null)
            .show()
    }

    private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
        if (activity == null) {
            return
        }
        AlertDialog.Builder(requireActivity())
            .setMessage(R.string.dialog_redraft_post_warning)
            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
                lifecycleScope.launch {
                    timelineCases.delete(id).fold(
                        { deletedStatus ->
                            removeItem(position)
                            val sourceStatus = if (deletedStatus.isEmpty) {
                                status.toDeletedStatus()
                            } else {
                                deletedStatus
                            }
                            val composeOptions = ComposeOptions(
                                content = sourceStatus.text,
                                inReplyToId = sourceStatus.inReplyToId,
                                visibility = sourceStatus.visibility,
                                contentWarning = sourceStatus.spoilerText,
                                mediaAttachments = sourceStatus.attachments,
                                sensitive = sourceStatus.sensitive,
                                modifiedInitialState = true,
                                language = sourceStatus.language,
                                poll = sourceStatus.poll?.toNewPoll(sourceStatus.createdAt),
                                kind = ComposeActivity.ComposeKind.NEW
                            )
                            startActivity(startIntent(requireContext(), composeOptions))
                        },
                        { error: Throwable? ->
                            Log.w("SFragment", "error deleting status", error)
                            Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT)
                                .show()
                        }
                    )
                }
            }
            .setNegativeButton(android.R.string.cancel, null)
            .show()
    }

    private fun editStatus(id: String, status: Status) {
        lifecycleScope.launch {
            mastodonApi.statusSource(id).fold(
                { source ->
                    val composeOptions = ComposeOptions(
                        content = source.text,
                        inReplyToId = status.inReplyToId,
                        visibility = status.visibility,
                        contentWarning = source.spoilerText,
                        mediaAttachments = status.attachments,
                        sensitive = status.sensitive,
                        language = status.language,
                        statusId = source.id,
                        poll = status.poll?.toNewPoll(status.createdAt),
                        kind = ComposeActivity.ComposeKind.EDIT_POSTED
                    )
                    startActivity(startIntent(requireContext(), composeOptions))
                },
                {
                    Snackbar.make(
                        requireView(),
                        getString(R.string.error_status_source_load),
                        Snackbar.LENGTH_SHORT
                    ).show()
                }
            )
        }
    }

    private fun showOpenAsDialog(statusUrl: String?, dialogTitle: CharSequence?) {
        if (statusUrl == null) {
            return
        }

        (activity as BaseActivity).apply {
            showAccountChooserDialog(
                dialogTitle,
                false,
                object : AccountSelectionListener {
                    override fun onAccountSelected(account: AccountEntity) {
                        openAsAccount(statusUrl, account)
                    }
                }
            )
        }
    }

    private fun downloadAllMedia(status: Status) {
        Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
        val downloadManager = requireActivity().getSystemService(
            Context.DOWNLOAD_SERVICE
        ) as DownloadManager

        for ((_, url) in status.attachments) {
            val uri = Uri.parse(url)
            downloadManager.enqueue(
                DownloadManager.Request(uri).apply {
                    setDestinationInExternalPublicDir(
                        Environment.DIRECTORY_DOWNLOADS,
                        uri.lastPathSegment
                    )
                }
            )
        }
    }

    private fun requestDownloadAllMedia(status: Status) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            (activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    downloadAllMedia(status)
                } else {
                    Toast.makeText(
                        context,
                        R.string.error_media_download_permission,
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        } else {
            downloadAllMedia(status)
        }
    }

    companion object {
        private const val TAG = "SFragment"
        private fun accountIsInMentions(
            account: AccountEntity?,
            mentions: List<Status.Mention>
        ): Boolean {
            return mentions.any { mention ->
                account?.username == mention.username && account.domain == Uri.parse(mention.url)?.host
            }
        }
    }
}
