/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.customtabs

import android.content.Context
import android.content.Intent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.navigation.fragment.navArgs
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.concept.engine.manifest.getOrNull
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.customtabs.CustomTabWindowFeature
import mozilla.components.feature.pwa.feature.ManifestUpdateFeature
import mozilla.components.feature.pwa.feature.WebAppActivityFeature
import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature
import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings

/**
 * Fragment used for browsing the web within external apps.
 */
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {

    private val args by navArgs<ExternalAppBrowserFragmentArgs>()

    private val customTabsIntegration = ViewBoundFeatureWrapper<CustomTabsIntegration>()
    private val windowFeature = ViewBoundFeatureWrapper<CustomTabWindowFeature>()
    private val hideToolbarFeature = ViewBoundFeatureWrapper<WebAppHideToolbarFeature>()

    @Suppress("LongMethod", "ComplexMethod")
    override fun initializeUI(view: View, tab: SessionState) {
        super.initializeUI(view, tab)

        val customTabSessionId = customTabSessionId ?: return
        val activity = requireActivity()
        val components = activity.components
        val toolbar = binding.root.findViewById<BrowserToolbar>(R.id.toolbar)

        val manifest = args.webAppManifest?.let { json -> WebAppManifestParser().parse(json).getOrNull() }

        customTabsIntegration.set(
            feature = CustomTabsIntegration(
                store = requireComponents.core.store,
                useCases = requireComponents.useCases.customTabsUseCases,
                toolbar = toolbar,
                sessionId = customTabSessionId,
                activity = activity,
                onItemTapped = { browserToolbarInteractor.onBrowserToolbarMenuItemTapped(it) },
                isPrivate = tab.content.private,
                shouldReverseItems = !activity.settings().shouldUseBottomToolbar
            ),
            owner = this,
            view = view
        )

        windowFeature.set(
            feature = CustomTabWindowFeature(
                activity,
                components.core.store,
                customTabSessionId
            ) { uri ->
                val intent =
                    Intent.parseUri("${BuildConfig.DEEP_LINK_SCHEME}://open?url=$uri", 0)
                if (intent.action == Intent.ACTION_VIEW) {
                    intent.addCategory(Intent.CATEGORY_BROWSABLE)
                    intent.component = null
                    intent.selector = null
                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                }
                activity.startActivity(intent)
            },
            owner = this,
            view = view
        )

        hideToolbarFeature.set(
            feature = WebAppHideToolbarFeature(
                store = requireComponents.core.store,
                customTabsStore = requireComponents.core.customTabsStore,
                tabId = customTabSessionId,
                manifest = manifest
            ) { toolbarVisible ->
                browserToolbarView.view.isVisible = toolbarVisible
                webAppToolbarShouldBeVisible = toolbarVisible
                if (!toolbarVisible) {
                    binding.engineView.setDynamicToolbarMaxHeight(0)
                    val browserEngine =
                        binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
                    browserEngine.bottomMargin = 0
                }
            },
            owner = this,
            view = toolbar
        )

        if (manifest != null) {
            activity.lifecycle.addObservers(
                WebAppActivityFeature(
                    activity,
                    components.core.icons,
                    manifest
                ),
                ManifestUpdateFeature(
                    activity.applicationContext,
                    requireComponents.core.store,
                    requireComponents.core.webAppShortcutManager,
                    requireComponents.core.webAppManifestStorage,
                    customTabSessionId,
                    manifest
                )
            )
            viewLifecycleOwner.lifecycle.addObserver(
                WebAppSiteControlsFeature(
                    activity.applicationContext,
                    requireComponents.core.store,
                    requireComponents.useCases.sessionUseCases.reload,
                    customTabSessionId,
                    manifest,
                    WebAppSiteControlsBuilder(
                        requireComponents.core.store,
                        requireComponents.useCases.sessionUseCases.reload,
                        customTabSessionId,
                        manifest
                    )
                )
            )
        } else {
            viewLifecycleOwner.lifecycle.addObserver(
                PoweredByNotification(
                    activity.applicationContext,
                    requireComponents.core.store,
                    customTabSessionId
                )
            )
        }
    }

    override fun removeSessionIfNeeded(): Boolean {
        return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
    }

    override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
        requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
            runIfFragmentIsAttached {
                val directions = ExternalAppBrowserFragmentDirections
                    .actionGlobalQuickSettingsSheetDialogFragment(
                        sessionId = tab.id,
                        url = tab.content.url,
                        title = tab.content.title,
                        isSecured = tab.content.securityInfo.secure,
                        sitePermissions = sitePermissions,
                        gravity = getAppropriateLayoutGravity(),
                        certificateName = tab.content.securityInfo.issuer,
                        permissionHighlights = tab.content.permissionHighlights,
                        isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
                    )
                nav(R.id.externalAppBrowserFragment, directions)
            }
        }
    }

    override fun getContextMenuCandidates(
        context: Context,
        view: View
    ): List<ContextMenuCandidate> = CustomTabContextMenuCandidate.defaultCandidates(
        context,
        context.components.useCases.contextMenuUseCases,
        view,
        FenixSnackbarDelegate(view)
    )

    companion object {
        // We only care about millisecond precision for telemetry events
        internal const val MS_PRECISION = 1_000_000L
    }
}
