/* 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.home.toolbar

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.compose.browser.toolbar.concept.Action
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes
import mozilla.components.compose.browser.toolbar.concept.Action.TabCounterAction
import mozilla.components.compose.browser.toolbar.concept.PageOrigin
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.ContextualMenuOption.LoadFromClipboard
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.ContextualMenuOption.PasteFromClipboard
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.PageOriginContextualMenuInteractions.LoadFromClipboardClicked
import mozilla.components.compose.browser.toolbar.concept.PageOrigin.Companion.PageOriginContextualMenuInteractions.PasteFromClipboardClicked
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.BrowserActionsEndUpdated
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.NavigationActionsUpdated
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.PageActionsStartUpdated
import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.PageOriginUpdated
import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchQueryUpdated
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.EnterEditMode
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.ExitEditMode
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.Init
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent.Source
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.CombinedEventAndMenu
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarMenuItem.BrowserToolbarMenuButton
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarMenuItem.BrowserToolbarMenuButton.ContentDescription.StringResContentDescription
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarMenuItem.BrowserToolbarMenuButton.Icon.DrawableResIcon
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarMenuItem.BrowserToolbarMenuButton.Text.StringResText
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarState
import mozilla.components.compose.browser.toolbar.store.Mode
import mozilla.components.compose.browser.toolbar.ui.BrowserToolbarQuery
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.ext.flow
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.ClipboardHandler
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingMode.Normal
import org.mozilla.fenix.browser.browsingmode.BrowsingMode.Private
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.UseCases
import org.mozilla.fenix.components.appstate.AppAction.SearchAction.SearchStarted
import org.mozilla.fenix.components.appstate.SupportedMenuNotifications
import org.mozilla.fenix.components.menu.MenuAccessPoint
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.toolbar.DisplayActions.FakeClicked
import org.mozilla.fenix.home.toolbar.DisplayActions.MenuClicked
import org.mozilla.fenix.home.toolbar.PageOriginInteractions.OriginClicked
import org.mozilla.fenix.home.toolbar.TabCounterInteractions.AddNewPrivateTab
import org.mozilla.fenix.home.toolbar.TabCounterInteractions.AddNewTab
import org.mozilla.fenix.home.toolbar.TabCounterInteractions.TabCounterClicked
import org.mozilla.fenix.home.toolbar.TabCounterInteractions.TabCounterLongClicked
import org.mozilla.fenix.search.BrowserToolbarSearchMiddleware
import org.mozilla.fenix.search.ext.searchEngineShortcuts
import org.mozilla.fenix.settings.ShortcutType
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.utils.Settings
import mozilla.components.lib.state.Action as MVIAction
import mozilla.components.ui.icons.R as iconsR
import mozilla.components.ui.tabcounter.R as tabcounterR

@VisibleForTesting
internal sealed class DisplayActions : BrowserToolbarEvent {
    data class MenuClicked(override val source: Source) : DisplayActions()
    data object FakeClicked : DisplayActions()
}

internal sealed class TabCounterInteractions : BrowserToolbarEvent {
    data class TabCounterClicked(override val source: Source) : TabCounterInteractions()
    data class TabCounterLongClicked(override val source: Source) : TabCounterInteractions()
    data class AddNewTab(override val source: Source) : TabCounterInteractions()
    data class AddNewPrivateTab(override val source: Source) : TabCounterInteractions()
}

internal sealed class PageOriginInteractions : BrowserToolbarEvent {
    data object OriginClicked : PageOriginInteractions()
}

/**
 * [Middleware] responsible for configuring and handling interactions with the composable toolbar.
 *
 * @param uiContext [Context] used for various system interactions.
 * @param appStore [AppStore] to sync from.
 * @param browserStore [BrowserStore] to sync from.
 * @param clipboard [ClipboardHandler] to use for reading from device's clipboard.
 * @param useCases [UseCases] helping this integrate with other features of the applications.
 * @param navController [NavController] to use for navigating to other in-app destinations.
 * @param browsingModeManager [BrowsingModeManager] for querying the current browsing mode.
 * @param settings [Settings] for accessing application settings.
 * @param isWideScreen Callback for checking if the screen is wide.
 * @param isTallScreen Callback for checking if the screen is tall.
 * @param scope [CoroutineScope] used for running long running operations in background.
 */
@Suppress("LongParameterList")
class BrowserToolbarMiddleware(
    private val uiContext: Context,
    private val appStore: AppStore,
    private val browserStore: BrowserStore,
    private val clipboard: ClipboardHandler,
    private val useCases: UseCases,
    private val navController: NavController,
    private val browsingModeManager: BrowsingModeManager,
    private val settings: Settings,
    private val isWideScreen: () -> Boolean,
    private val isTallScreen: () -> Boolean,
    private val scope: CoroutineScope,
) : Middleware<BrowserToolbarState, BrowserToolbarAction> {
    private var syncCurrentSearchEngineJob: Job? = null
    private var observeBrowserSearchStateJob: Job? = null

    @Suppress("LongMethod", "CyclomaticComplexMethod", "CognitiveComplexMethod")
    override fun invoke(
        context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>,
        next: (BrowserToolbarAction) -> Unit,
        action: BrowserToolbarAction,
    ) {
        when (action) {
            is Init -> {
                next(action)

                if (context.state.mode == Mode.DISPLAY) {
                    observeSearchStateUpdates(context)
                }

                updatePageOrigin(context)
                updateEndBrowserActions(context)
                updateNavigationActions(context)
                updateToolbarActionsBasedOnOrientation(context)
                updateTabsCount(context)
                updateMenuHighlight(context)
            }

            is EnterEditMode -> {
                next(action)

                stopSearchStateUpdates()
            }

            is ExitEditMode -> {
                next(action)

                observeSearchStateUpdates(context)
            }

            is MenuClicked -> {
                navController.nav(
                    R.id.homeFragment,
                    HomeFragmentDirections.actionGlobalMenuDialogFragment(
                        accesspoint = MenuAccessPoint.Home,
                    ),
                )
                next(action)
            }

            is TabCounterClicked -> {
                if (settings.tabManagerEnhancementsEnabled) {
                    navController.nav(
                        R.id.homeFragment,
                        NavGraphDirections.actionGlobalTabManagementFragment(
                            page = when (browsingModeManager.mode) {
                                Normal -> Page.NormalTabs
                                Private -> Page.PrivateTabs
                            },
                        ),
                    )
                } else {
                    navController.nav(
                        R.id.homeFragment,
                        NavGraphDirections.actionGlobalTabsTrayFragment(
                            page = when (browsingModeManager.mode) {
                                Normal -> Page.NormalTabs
                                Private -> Page.PrivateTabs
                            },
                        ),
                    )
                }
                next(action)
            }
            is AddNewTab -> {
                openNewTab(context, Normal)
                next(action)
            }
            is AddNewPrivateTab -> {
                openNewTab(context, Private)
                next(action)
            }

            is OriginClicked -> {
                Events.searchBarTapped.record(Events.SearchBarTappedExtra("HOME"))
                appStore.dispatch(SearchStarted())
            }
            is PasteFromClipboardClicked -> {
                openNewTab(context, searchTerms = clipboard.text)
            }
            is LoadFromClipboardClicked -> {
                clipboard.extractURL()?.let {
                    useCases.fenixBrowserUseCases.loadUrlOrSearch(
                        searchTermOrURL = it,
                        newTab = true,
                        private = browsingModeManager.mode == Private,
                        searchEngine = reconcileSelectedEngine(),
                    )
                    navController.navigate(R.id.browserFragment)
                } ?: run {
                    Logger("HomeOriginContextMenu").error("Clipboard contains URL but unable to read text")
                }
            }

            else -> next(action)
        }
    }

    private fun openNewTab(
        context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>,
        browsingMode: BrowsingMode? = null,
        searchTerms: String? = null,
    ) {
        browsingMode?.let { browsingModeManager.mode = it }
        context.dispatch(SearchQueryUpdated(BrowserToolbarQuery(searchTerms ?: "")))
        appStore.dispatch(SearchStarted())
    }

    private fun observeSearchStateUpdates(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) {
        syncCurrentSearchEngineJob?.cancel()
        syncCurrentSearchEngineJob = appStore.observeWhileActive {
            distinctUntilChangedBy { it.searchState.selectedSearchEngine?.searchEngine }
                .collect {
                    it.searchState.selectedSearchEngine?.let {
                        updateStartPageActions(context, it.searchEngine)
                    }
                }
        }

        observeBrowserSearchStateJob?.cancel()
        observeBrowserSearchStateJob = browserStore.observeWhileActive {
            distinctUntilChangedBy { it.search.searchEngineShortcuts }
                .collect {
                    updateStartPageActions(
                        context = context,
                        selectedSearchEngine = reconcileSelectedEngine(),
                    )
                }
        }
    }

    private fun stopSearchStateUpdates() {
        syncCurrentSearchEngineJob?.cancel()
        observeBrowserSearchStateJob?.cancel()
    }

    private fun updateStartPageActions(
        context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>,
        selectedSearchEngine: SearchEngine?,
    ) = context.dispatch(
        PageActionsStartUpdated(
            buildStartPageActions(selectedSearchEngine),
        ),
    )

    private fun updatePageOrigin(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) =
        context.dispatch(
            PageOriginUpdated(
                PageOrigin(
                    hint = R.string.search_hint,
                    title = null,
                    url = null,
                    contextualMenuOptions = listOf(PasteFromClipboard, LoadFromClipboard),
                    onClick = OriginClicked,
                ),
            ),
        )

    private fun updateEndBrowserActions(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) {
        context.dispatch(
            BrowserActionsEndUpdated(
                buildEndBrowserActions(),
            ),
        )
    }

    private fun buildStartPageActions(selectedSearchEngine: SearchEngine?): List<Action> {
        return listOfNotNull(
            BrowserToolbarSearchMiddleware.buildSearchSelector(
                selectedSearchEngine = selectedSearchEngine,
                searchEngineShortcuts = browserStore.state.search.searchEngineShortcuts,
                resources = uiContext.resources,
            ),
        )
    }

    private fun buildEndBrowserActions(): List<Action> {
        val isWideWindow = isWideScreen()
        val isTallWindow = isTallScreen()
        val shouldUseExpandedToolbar = settings.shouldUseExpandedToolbar

        return listOf(
            HomeToolbarActionConfig(HomeToolbarAction.TabCounter) {
                !shouldUseExpandedToolbar || !isTallWindow || isWideWindow
            },
            HomeToolbarActionConfig(HomeToolbarAction.Menu) {
                !shouldUseExpandedToolbar || !isTallWindow || isWideWindow
            },
        ).filter { config ->
            config.isVisible()
        }.map { config ->
            buildHomeAction(config.action, Source.AddressBar.BrowserEnd)
        }
    }

    private fun updateNavigationActions(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) {
        context.dispatch(
            NavigationActionsUpdated(
                buildNavigationActions(),
            ),
        )
    }

    /**
     * - Devices taller than 480dp:
     *   - The navigation bar is always shown (if the user enabled it).
     *
     * - Devices shorter than 480dp:
     *   - The navigation bar is hidden (even if the user enabled it).
     *   - The toolbar redesign customization option is also hidden.
     *
     *   Devices wider than 600dp:
     *   - The navigation bar is hidden. (even If user enabled it)
     *   - The toolbar redesign customization option is also hidden.
     */
    private fun buildNavigationActions(): List<Action> {
        val isWideWindow = isWideScreen()
        val isTallWindow = isTallScreen()
        val shouldUseExpandedToolbar = settings.shouldUseExpandedToolbar
        val primarySlotAction = ShortcutType.fromValue(settings.toolbarExpandedShortcut)
            ?.toHomeToolbarAction() ?: HomeToolbarAction.FakeBookmark

        return listOf(
            HomeToolbarActionConfig(primarySlotAction) {
                shouldUseExpandedToolbar && isTallWindow && !isWideWindow
            },
            HomeToolbarActionConfig(HomeToolbarAction.FakeShare) {
                shouldUseExpandedToolbar && isTallWindow && !isWideWindow
            },
            HomeToolbarActionConfig(HomeToolbarAction.NewTab) {
                shouldUseExpandedToolbar && isTallWindow && !isWideWindow
            },
            HomeToolbarActionConfig(HomeToolbarAction.TabCounter) {
                shouldUseExpandedToolbar && isTallWindow && !isWideWindow
            },
            HomeToolbarActionConfig(HomeToolbarAction.Menu) {
                shouldUseExpandedToolbar && isTallWindow && !isWideWindow
            },
        ).filter { config ->
            config.isVisible()
        }.map { config ->
            buildHomeAction(config.action, Source.NavigationBar)
        }
    }

    private fun buildTabCounterMenu(source: Source): CombinedEventAndMenu? {
        val currentBrowsingMode = browsingModeManager.mode

        return CombinedEventAndMenu(TabCounterLongClicked(source)) {
            when (currentBrowsingMode) {
                Private -> listOf(
                    BrowserToolbarMenuButton(
                        icon = DrawableResIcon(iconsR.drawable.mozac_ic_plus_24),
                        text = StringResText(tabcounterR.string.mozac_browser_menu_new_tab),
                        contentDescription = StringResContentDescription(
                            tabcounterR.string.mozac_browser_menu_new_tab,
                        ),
                        onClick = AddNewTab(source),
                    ),
                )

                else -> listOf(
                    BrowserToolbarMenuButton(
                        icon = DrawableResIcon(iconsR.drawable.mozac_ic_private_mode_24),
                        text = StringResText(tabcounterR.string.mozac_browser_menu_new_private_tab),
                        contentDescription = StringResContentDescription(
                            tabcounterR.string.mozac_browser_menu_new_private_tab,
                        ),
                        onClick = AddNewPrivateTab(source),
                    ),
                )
            }
        }
    }

    private fun updateToolbarActionsBasedOnOrientation(
        context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>,
    ) {
        appStore.observeWhileActive {
            distinctUntilChangedBy { it.orientation }
                .collect {
                    updateEndBrowserActions(context)
                    updateNavigationActions(context)
                }
        }
    }

    private fun updateTabsCount(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) {
        browserStore.observeWhileActive {
            distinctUntilChangedBy { it.tabs.size }
                .collect {
                    updateEndBrowserActions(context)
                    updateNavigationActions(context)
                }
        }
    }

    private fun updateMenuHighlight(context: MiddlewareContext<BrowserToolbarState, BrowserToolbarAction>) {
        appStore.observeWhileActive {
            distinctUntilChangedBy { it.supportedMenuNotifications.isNotEmpty() }
                .collect {
                    updateEndBrowserActions(context)
                    updateNavigationActions(context)
                }
        }
    }

    private inline fun <S : State, A : MVIAction> Store<S, A>.observeWhileActive(
        crossinline observe: suspend (Flow<S>.() -> Unit),
    ): Job = scope.launch { flow().observe() }

    private fun reconcileSelectedEngine(): SearchEngine? =
        appStore.state.searchState.selectedSearchEngine?.searchEngine
            ?: browserStore.state.search.selectedOrDefaultSearchEngine

    @VisibleForTesting
    internal enum class HomeToolbarAction {
        TabCounter,
        Menu,
        FakeBookmark,
        FakeShare,
        NewTab,
        FakeTranslate,
        FakeHomepage,
        FakeBack,
    }

    private data class HomeToolbarActionConfig(
        val action: HomeToolbarAction,
        val isVisible: () -> Boolean = { true },
    )

    @VisibleForTesting
    internal fun buildHomeAction(
        action: HomeToolbarAction,
        source: Source = Source.Unknown,
    ): Action = when (action) {
        HomeToolbarAction.TabCounter -> {
            val isInPrivateMode = browsingModeManager.mode.isPrivate
            val tabsCount = browserStore.state.getNormalOrPrivateTabs(isInPrivateMode).size

            val tabCounterDescription = if (isInPrivateMode) {
                uiContext.getString(tabcounterR.string.mozac_tab_counter_private, tabsCount.toString())
            } else {
                uiContext.getString(tabcounterR.string.mozac_tab_counter_open_tab_tray, tabsCount.toString())
            }

            TabCounterAction(
                count = tabsCount,
                contentDescription = tabCounterDescription,
                showPrivacyMask = isInPrivateMode,
                onClick = TabCounterClicked(source),
                onLongClick = buildTabCounterMenu(source),
            )
        }

        HomeToolbarAction.Menu -> {
            val highlighted = appStore.state.supportedMenuNotifications
                .any { it != SupportedMenuNotifications.OpenInApp }
            ActionButtonRes(
                drawableResId = iconsR.drawable.mozac_ic_ellipsis_vertical_24,
                contentDescription = R.string.content_description_menu,
                highlighted = highlighted,
                onClick = MenuClicked(source),
            )
        }

        HomeToolbarAction.FakeBookmark -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_bookmark_24,
            contentDescription = R.string.browser_menu_bookmark_this_page_2,
            state = ActionButton.State.DISABLED,
            onClick = FakeClicked,
        )

        HomeToolbarAction.FakeShare -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_share_android_24,
            contentDescription = R.string.browser_menu_share,
            state = ActionButton.State.DISABLED,
            onClick = FakeClicked,
        )

        HomeToolbarAction.NewTab -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_plus_24,
            contentDescription = if (browsingModeManager.mode == Private) {
                R.string.home_screen_shortcut_open_new_private_tab_2
            } else {
                R.string.home_screen_shortcut_open_new_tab_2
            },
            onClick = if (browsingModeManager.mode == Private) {
                AddNewPrivateTab(source)
            } else {
                AddNewTab(source)
            },
        )

        HomeToolbarAction.FakeTranslate -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_translate_24,
            contentDescription = R.string.browser_toolbar_translate,
            state = ActionButton.State.DISABLED,
            onClick = FakeClicked,
        )

        HomeToolbarAction.FakeHomepage -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_home_24,
            contentDescription = R.string.browser_menu_homepage,
            state = ActionButton.State.DISABLED,
            onClick = FakeClicked,
        )

        HomeToolbarAction.FakeBack -> ActionButtonRes(
            drawableResId = iconsR.drawable.mozac_ic_back_24,
            contentDescription = R.string.browser_menu_back,
            state = ActionButton.State.DISABLED,
            onClick = FakeClicked,
        )
    }

    companion object {
        @VisibleForTesting
        internal fun ShortcutType.toHomeToolbarAction() = when (this) {
            ShortcutType.NEW_TAB -> HomeToolbarAction.NewTab
            ShortcutType.SHARE -> HomeToolbarAction.FakeShare
            ShortcutType.BOOKMARK -> HomeToolbarAction.FakeBookmark
            ShortcutType.TRANSLATE -> HomeToolbarAction.FakeTranslate
            ShortcutType.HOMEPAGE -> HomeToolbarAction.FakeHomepage
            ShortcutType.BACK -> HomeToolbarAction.FakeBack
        }
    }
}
