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

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import mozilla.components.browser.domains.autocomplete.CustomDomainsProvider
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.util.URLStringUtils
import mozilla.components.support.utils.StatusBarUtils
import mozilla.components.support.utils.ThreadUtils
import org.mozilla.focus.GleanMetrics.BrowserSearch
import org.mozilla.focus.GleanMetrics.SearchBar
import org.mozilla.focus.GleanMetrics.SearchWidget
import org.mozilla.focus.R
import org.mozilla.focus.activity.MainActivity
import org.mozilla.focus.databinding.FragmentUrlinputBinding
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.defaultSearchEngineName
import org.mozilla.focus.ext.hasSearchTerms
import org.mozilla.focus.ext.requireComponents
import org.mozilla.focus.ext.settings
import org.mozilla.focus.input.InputToolbarIntegration
import org.mozilla.focus.menu.home.HomeMenu
import org.mozilla.focus.menu.home.HomeMenuItem
import org.mozilla.focus.searchsuggestions.SearchSuggestionsViewModel
import org.mozilla.focus.searchsuggestions.ui.SearchSuggestionsFragment
import org.mozilla.focus.state.AppAction
import org.mozilla.focus.state.Screen
import org.mozilla.focus.telemetry.TelemetryWrapper
import org.mozilla.focus.topsites.DefaultTopSitesStorage.Companion.TOP_SITES_MAX_LIMIT
import org.mozilla.focus.topsites.DefaultTopSitesView
import org.mozilla.focus.topsites.TopSitesOverlay
import org.mozilla.focus.ui.theme.FocusTheme
import org.mozilla.focus.utils.OneShotOnPreDrawListener
import org.mozilla.focus.utils.SupportUtils
import org.mozilla.focus.utils.ViewUtils
import kotlin.coroutines.CoroutineContext

class FocusCrashException : Exception()

/**
 * Fragment for displaying the URL input controls.
 */
// Refactoring the size and function count of this fragment is non-trivial at this point.
// Therefore we ignore those violations for now.
@Suppress("LargeClass", "TooManyFunctions")
class UrlInputFragment :
    BaseFragment(),
    View.OnClickListener,
    CoroutineScope {
    companion object {
        const val FRAGMENT_TAG = "url_input"

        private const val ARGUMENT_ANIMATION = "animation"
        private const val ARGUMENT_SESSION_UUID = "sesssion_uuid"

        private const val ANIMATION_BROWSER_SCREEN = "browser_screen"

        private const val ANIMATION_DURATION = 200

        @JvmStatic
        fun createWithoutSession(): UrlInputFragment {
            val arguments = Bundle()

            val fragment = UrlInputFragment()
            fragment.arguments = arguments

            return fragment
        }

        @JvmStatic
        fun createWithTab(
            tabId: String,
        ): UrlInputFragment {
            val arguments = Bundle()

            arguments.putString(ARGUMENT_SESSION_UUID, tabId)
            arguments.putString(ARGUMENT_ANIMATION, ANIMATION_BROWSER_SCREEN)

            val fragment = UrlInputFragment()
            fragment.arguments = arguments

            return fragment
        }
    }

    private var job = Job()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main
    private val shippedDomainsProvider = ShippedDomainsProvider()
    private val customDomainsProvider = CustomDomainsProvider()
    private var _binding: FragmentUrlinputBinding? = null
    private val binding get() = _binding!!
    private lateinit var searchSuggestionsViewModel: SearchSuggestionsViewModel

    @Volatile
    private var isAnimating: Boolean = false

    var tab: TabSessionState? = null
        private set

    private val isOverlay: Boolean
        get() = tab != null

    private var isInitialized = false

    private val toolbarIntegration = ViewBoundFeatureWrapper<InputToolbarIntegration>()
    private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()

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

        // Get session from session manager if there's a session UUID in the fragment's arguments
        arguments?.getString(ARGUMENT_SESSION_UUID)?.let { id ->
            tab = requireComponents.store.state.findTab(id)
        }
    }

    @Suppress("DEPRECATION") // https://github.com/mozilla-mobile/focus-android/issues/4958
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        searchSuggestionsViewModel =
            ViewModelProvider(this).get(SearchSuggestionsViewModel::class.java)

        childFragmentManager.beginTransaction()
            .replace(binding.searchViewContainer.id, SearchSuggestionsFragment.create())
            .commit()

        searchSuggestionsViewModel.selectedSearchSuggestion.observe(
            viewLifecycleOwner,
        ) {
            val isSuggestion = searchSuggestionsViewModel.searchQuery.value != it
            it?.let {
                if (searchSuggestionsViewModel.alwaysSearch) {
                    onSearch(it, isSuggestion = false, alwaysSearch = true)
                } else {
                    onSearch(it, isSuggestion)
                }
                searchSuggestionsViewModel.clearSearchSuggestion()
            }
        }

        searchSuggestionsViewModel.autocompleteSuggestion.observe(viewLifecycleOwner) { text ->
            if (text != null) {
                searchSuggestionsViewModel.clearAutocompleteSuggestion()
                binding.browserToolbar.setSearchTerms(text)
            }
        }
    }

    override fun onResume() {
        super.onResume()

        if (job.isCancelled) {
            job = Job()
        }

        activity?.let {
            shippedDomainsProvider.initialize(it.applicationContext)
            customDomainsProvider.initialize(it.applicationContext)
        }

        // Hide status bar background if the parent activity can be casted to MainActivity
        (requireActivity() as? MainActivity)?.hideStatusBarBackground()
        StatusBarUtils.getStatusBarHeight(binding.landingLayout) {
            adjustViewToStatusBarHeight(it)
        }

        if (!isInitialized) {
            // Explicitly switching to "edit mode" here in order to focus the toolbar and select
            // all text in it. We only want to do this once per fragment.
            binding.browserToolbar.editMode()
            isInitialized = true
        }

        if (
            requireComponents.settings.searchWidgetInstalled &&
            requireComponents.appStore.state.showSearchWidgetSnackbar
        ) {
            ViewUtils.showBrandedSnackbar(view, R.string.promote_search_widget_snackbar_message, 0)
            SearchWidget.widgetWasAdded.record(mozilla.telemetry.glean.private.NoExtras())
            requireComponents.appStore.dispatch(AppAction.ShowSearchWidgetSnackBar(false))
        }
    }

    override fun onPause() {
        job.cancel()
        super.onPause()
        view?.hideKeyboard()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    private fun adjustViewToStatusBarHeight(statusBarHeight: Int) {
        val inputHeight = resources.getDimension(R.dimen.urlinput_height)

        binding.urlInputLayout.layoutParams.height = (inputHeight + statusBarHeight).toInt()

        val topMargin = (inputHeight + statusBarHeight).toInt()

        if (binding.searchViewContainer.layoutParams is ViewGroup.MarginLayoutParams) {
            val marginParams =
                binding.searchViewContainer.layoutParams as ViewGroup.MarginLayoutParams
            marginParams.topMargin = topMargin
        }

        if (binding.landingLayout.layoutParams is ViewGroup.MarginLayoutParams) {
            val marginParams = binding.landingLayout.layoutParams as ViewGroup.MarginLayoutParams
            marginParams.topMargin = topMargin
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = FragmentUrlinputBinding.inflate(inflater, container, false)

        binding.topSites.setContent {
            FocusTheme {
                TopSitesOverlay()
            }
        }
        return binding.root
    }

    @Suppress("LongMethod")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.browserToolbar.private = true

        toolbarIntegration.set(
            InputToolbarIntegration(
                binding.browserToolbar,
                fragment = this,
                shippedDomainsProvider = shippedDomainsProvider,
                customDomainsProvider = customDomainsProvider,
            ),
            owner = this,
            view = binding.browserToolbar,
        )

        topSitesFeature.set(
            feature = TopSitesFeature(
                view = DefaultTopSitesView(requireComponents.appStore),
                storage = requireComponents.topSitesStorage,
                config = {
                    TopSitesConfig(
                        totalSites = TOP_SITES_MAX_LIMIT,
                        frecencyConfig = null,
                        providerConfig = null,
                    )
                },
            ),
            owner = this,
            view = view,
        )

        binding.dismissView.setOnClickListener(this)

        OneShotOnPreDrawListener(binding.urlInputContainerView) {
            animateFirstDraw()
            true
        }

        if (isOverlay) {
            binding.landingLayout.isVisible = false
        } else {
            binding.backgroundView.background = AppCompatResources.getDrawable(
                requireContext(),
                R.drawable.home_background,
            )

            binding.dismissView.isVisible = false

            binding.menuView.isVisible = true
        }

        tab?.let { tab ->
            binding.browserToolbar.url = if (tab.content.hasSearchTerms) {
                tab.content.searchTerms
            } else {
                tab.content.url
            }

            binding.searchViewContainer.isVisible = false
            binding.menuView.isVisible = false
        }

        binding.browserToolbar.editMode()
        setHomeMenu()
    }

    private fun setHomeMenu() {
        binding.menuView.menuBuilder = HomeMenu(requireContext()) { menuItem ->
            when (menuItem) {
                is HomeMenuItem.Help -> openHelpPage()
                is HomeMenuItem.Settings -> openSettingsPage()
            }
        }.getMenuBuilder()
    }

    private fun openHelpPage() {
        requireComponents.tabsUseCases.addTab(
            SupportUtils.HELP_URL,
            source = SessionState.Source.Internal.Menu,
            private = true,
        )
    }

    private fun openSettingsPage() {
        requireComponents.appStore.dispatch(
            AppAction.OpenSettings(page = Screen.Settings.Page.Start),
        )
    }

    fun onBackPressed(): Boolean {
        if (isOverlay) {
            animateAndDismiss()
            return true
        }

        return false
    }

    override fun onStart() {
        super.onStart()

        activity?.let {
            if (requireContext().settings.isFirstRun) return@onStart
        }
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)

        if (newConfig.orientation != Configuration.ORIENTATION_UNDEFINED) {
            // Make sure we update the background for landscape / portrait orientations.
            binding.backgroundView.background = AppCompatResources.getDrawable(
                requireContext(),
                R.drawable.home_background,
            )
        }
    }

    // This method triggers the complexity warning. However it's actually not that hard to understand.
    @Suppress("ComplexMethod")
    override fun onClick(view: View) {
        if (view.id == R.id.dismissView) {
            handleDismiss()
        } else {
            throw IllegalStateException("Unhandled view in onClick()")
        }
    }

    private fun handleDismiss() {
        if (isOverlay) {
            animateAndDismiss()
        } else {
            // This is a bit hacky, but emulates what the legacy toolbar did before we replaced
            // it with `browser-toolbar`. First we clear the text and then we invoke the "text
            // changed" callback manually for this change.
            binding.browserToolbar.edit.updateUrl("", false)
            onTextChange("")
        }
    }

    private fun animateFirstDraw() {
        if (ANIMATION_BROWSER_SCREEN == arguments?.getString(ARGUMENT_ANIMATION)) {
            playVisibilityAnimation(false)
        }
    }

    private fun animateAndDismiss() {
        ThreadUtils.assertOnUiThread()

        if (isAnimating) {
            // We are already animating some state change. Ignore all other requests.
            return
        }

        // Don't allow any more clicks: dismissView is still visible until the animation ends,
        // but we don't want to restart animations and/or trigger hiding again (which could potentially
        // cause crashes since we don't know what state we're in). Ignoring further clicks is the simplest
        // solution, since dismissView is about to disappear anyway.
        binding.dismissView.isClickable = false

        if (ANIMATION_BROWSER_SCREEN == arguments?.getString(ARGUMENT_ANIMATION)) {
            playVisibilityAnimation(true)
        } else {
            dismiss()
        }
    }

    /**
     * This animation is quite complex. The 'reverse' flag controls whether we want to show the UI
     * (false) or whether we are going to hide it (true). Additionally the animation is slightly
     * different depending on whether this fragment is shown as an overlay on top of other fragments
     * or if it draws its own background.
     */
    // This method correctly triggers a complexity warning. This method is indeed very and too complex.
    // However refactoring it is not trivial at this point so we ignore the warning for now.
    @Suppress("ComplexMethod")
    private fun playVisibilityAnimation(reverse: Boolean) {
        if (isAnimating) {
            // We are already animating, let's ignore another request.
            return
        }

        isAnimating = true

        val xyOffset = (
            if (isOverlay) {
                (binding.urlInputContainerView.layoutParams as FrameLayout.LayoutParams).bottomMargin
            } else {
                0
            }
            ).toFloat()

        val width = binding.urlInputBackgroundView.width.toFloat()
        val height = binding.urlInputBackgroundView.height.toFloat()

        val widthScale = if (isOverlay) {
            (width + 2 * xyOffset) / width
        } else {
            1f
        }

        val heightScale = if (isOverlay) {
            (height + 2 * xyOffset) / height
        } else {
            1f
        }

        if (!reverse) {
            binding.urlInputBackgroundView.apply {
                pivotX = 0f
                pivotY = 0f
                scaleX = widthScale
                scaleY = heightScale
                translationX = -xyOffset
                translationY = -xyOffset
            }
        }

        // Let the URL input use the full width/height and then shrink to the actual size
        binding.urlInputBackgroundView.animate()
            .setDuration(ANIMATION_DURATION.toLong())
            .scaleX(if (reverse) widthScale else 1f)
            .scaleY(if (reverse) heightScale else 1f)
            .alpha((if (reverse && isOverlay) 0 else 1).toFloat())
            .translationX(if (reverse) -xyOffset else 0f)
            .translationY(if (reverse) -xyOffset else 0f)
            .setListener(
                object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) {
                        if (reverse) {
                            if (isOverlay) {
                                dismiss()
                            }
                        }

                        isAnimating = false
                    }
                },
            )

        // We only need to animate the toolbar if we are an overlay.
        /*
        if (isOverlay) {
            val screenLocation = IntArray(2)
            //urlView?.getLocationOnScreen(screenLocation)

            val leftDelta = requireArguments().getInt(ARGUMENT_X) - screenLocation[0] - urlView.paddingLeft

            if (!reverse) {
                //urlView?.pivotX = 0f
                //urlView?.pivotY = 0f
                //urlView?.translationX = leftDelta.toFloat()
            }

            if (urlView != null) {
                // The URL moves from the right (at least if the lock is visible) to it's actual position
                urlView.animate()
                    .setDuration(ANIMATION_DURATION.toLong())
                    .translationX((if (reverse) leftDelta else 0).toFloat())
            }
        }
        */

        if (reverse) {
            binding.toolbarBottomBorder.isVisible = true

            if (!isOverlay) {
                binding.dismissView.isVisible = false
                binding.menuView.isVisible = true
            }
        } else {
            binding.toolbarBottomBorder.isVisible = false
        }
    }

    private fun dismiss() {
        // This method is called from animation callbacks. In the short time frame between the animation
        // starting and ending the activity can be paused. In this case this code can throw an
        // IllegalStateException because we already saved the state (of the activity / fragment) before
        // this transaction is committed. To avoid this we commit while allowing a state loss here.
        // We do not save any state in this fragment (It's getting destroyed) so this should not be a problem.

        context?.components?.appStore?.dispatch(AppAction.FinishEdit(tab!!.id))
    }

    internal fun onCommit(input: String) {
        if (input.trim { it <= ' ' }.isNotEmpty()) {
            handleCrashTrigger(input)

            ViewUtils.hideKeyboard(binding.browserToolbar)

            val isUrl = URLStringUtils.isURLLike(input)
            if (isUrl) {
                openUrl(URLStringUtils.toNormalizedURL(input))
            } else {
                search(input)
            }

            TelemetryWrapper.urlBarEvent(isUrl)

            if (isUrl) {
                SearchBar.enteredUrl.record(NoExtras())
            } else {
                val defaultSearchEngineName =
                    requireComponents.store.defaultSearchEngineName().lowercase()
                SearchBar.performedSearch.record(
                    SearchBar.PerformedSearchExtra(defaultSearchEngineName),
                )
                TelemetryWrapper.searchEnterEvent()
                BrowserSearch.searchCount["$defaultSearchEngineName.action"].add()
            }
        }
    }

    private fun handleCrashTrigger(input: String) {
        if (input == "focus:crash") {
            throw FocusCrashException()
        }
    }

    private fun onSearch(
        query: String,
        isSuggestion: Boolean = false,
        alwaysSearch: Boolean = false,
    ) {
        if (alwaysSearch) {
            search(query)
        } else {
            if (URLStringUtils.isURLLike(query)) {
                openUrl(URLStringUtils.toNormalizedURL(query))
            } else {
                search(query)
            }
        }

        TelemetryWrapper.searchSelectEvent(isSuggestion)

        val defaultSearchEngineName = requireComponents.store.defaultSearchEngineName().lowercase()
        BrowserSearch.searchCount["$defaultSearchEngineName.suggestion"].add()
    }

    private fun search(searchTerms: String) {
        val tab = tab
        if (tab != null) {
            requireComponents.searchUseCases.defaultSearch.invoke(searchTerms, tab.id)
            requireComponents.appStore.dispatch(AppAction.FinishEdit(tab.id))
        } else {
            requireComponents.searchUseCases.newPrivateTabSearch.invoke(
                searchTerms = searchTerms,
                source = SessionState.Source.Internal.UserEntered,
            )
        }
    }

    private fun openUrl(url: String) {
        when (url) {
            "focus:about" -> {
                requireComponents.appStore.dispatch(
                    AppAction.OpenSettings(Screen.Settings.Page.About),
                )
                return
            }
        }

        val tab = tab
        if (tab != null) {
            requireComponents.sessionUseCases.loadUrl(url, tab.id)

            requireComponents.appStore.dispatch(AppAction.FinishEdit(tab.id))
        } else {
            requireComponents.tabsUseCases.addTab(
                url,
                source = SessionState.Source.Internal.UserEntered,
                selectTab = true,
                private = true,
            )
        }
    }

    internal fun onStartEditing() {
        if (tab != null) {
            binding.searchViewContainer.isVisible = true
        }
    }

    internal fun onCancelEditing() {
        handleDismiss()
    }

    internal fun onTextChange(text: String) {
        searchSuggestionsViewModel.setSearchQuery(text)

        if (text.trim { it <= ' ' }.isEmpty()) {
            binding.searchViewContainer.isVisible = false

            if (!isOverlay) {
                playVisibilityAnimation(true)
            }
        } else {
            binding.menuView.isVisible = false

            if (!isOverlay && binding.dismissView.isVisible != true) {
                playVisibilityAnimation(false)
                binding.dismissView.isVisible = true
            }

            binding.searchViewContainer.isVisible = true
        }
    }
}
