SearchReducer.kt

/* 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 mozilla.components.browser.state.reducer

import mozilla.components.browser.state.action.SearchAction
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SearchState

internal object SearchReducer {
    /**
     * [SearchAction] Reducer function for modifying [SearchState].
     */
    fun reduce(state: BrowserState, action: SearchAction): BrowserState {
        return when (action) {
            is SearchAction.RefreshSearchEnginesAction -> state // This is handled in [RegionMiddleware].
            is SearchAction.ApplicationSearchEnginesLoaded -> state.setApplicationEngines(action)
            is SearchAction.SetSearchEnginesAction -> state.setSearchEngines(action)
            is SearchAction.SetRegionAction -> state.setRegion(action)
            is SearchAction.UpdateCustomSearchEngineAction -> state.updateCustomSearchEngine(action)
            is SearchAction.RemoveCustomSearchEngineAction -> state.removeSearchEngine(action)
            is SearchAction.SelectSearchEngineAction -> state.selectSearchEngine(action)
            is SearchAction.ShowSearchEngineAction -> state.maybeShowSearchEngine(action)
            is SearchAction.HideSearchEngineAction -> state.hideSearchEngine(action)
            is SearchAction.AddAdditionalSearchEngineAction -> state.addAdditionalSearchEngine(action)
            is SearchAction.RemoveAdditionalSearchEngineAction -> state.removeAdditionalSearchEngine(action)
            is SearchAction.UpdateDisabledSearchEngineIdsAction ->
                state.updateDisabledSearchEngineIds(action)
            is SearchAction.RestoreHiddenSearchEnginesAction -> state.restoreHiddenSearchEngines()
        }
    }
}

private fun BrowserState.setApplicationEngines(
    action: SearchAction.ApplicationSearchEnginesLoaded,
): BrowserState {
    return copy(
        search = search.copy(
            applicationSearchEngines = action.applicationSearchEngines,
        ),
    )
}

private fun BrowserState.setSearchEngines(
    action: SearchAction.SetSearchEnginesAction,
): BrowserState {
    return copy(
        search = search.copy(
            regionSearchEngines = action.regionSearchEngines,
            customSearchEngines = action.customSearchEngines,
            userSelectedSearchEngineId = action.userSelectedSearchEngineId,
            userSelectedSearchEngineName = action.userSelectedSearchEngineName,
            regionDefaultSearchEngineId = action.regionDefaultSearchEngineId,
            hiddenSearchEngines = action.hiddenSearchEngines,
            disabledSearchEngineIds = action.disabledSearchEngineIds,
            additionalSearchEngines = action.additionalSearchEngines,
            additionalAvailableSearchEngines = action.additionalAvailableSearchEngines,
            regionSearchEnginesOrder = action.regionSearchEnginesOrder,
            complete = true,
        ),
    )
}

private fun BrowserState.setRegion(
    action: SearchAction.SetRegionAction,
): BrowserState {
    return copy(
        search = search.copy(
            region = action.regionState,
        ),
    )
}

private fun BrowserState.updateCustomSearchEngine(
    action: SearchAction.UpdateCustomSearchEngineAction,
): BrowserState {
    val searchEngines = search.customSearchEngines
    val index = searchEngines.indexOfFirst { searchEngine -> searchEngine.id == action.searchEngine.id }

    val updatedSearchEngines = if (index != -1) {
        searchEngines.subList(0, index) + action.searchEngine + searchEngines.subList(index + 1, searchEngines.size)
    } else {
        searchEngines + action.searchEngine
    }

    return copy(
        search = search.copy(
            customSearchEngines = updatedSearchEngines,
        ),
    )
}

private fun BrowserState.removeSearchEngine(
    action: SearchAction.RemoveCustomSearchEngineAction,
): BrowserState {
    return copy(
        search = search.copy(
            customSearchEngines = search.customSearchEngines.filter { it.id != action.searchEngineId },
        ),
    )
}

private fun BrowserState.selectSearchEngine(
    action: SearchAction.SelectSearchEngineAction,
): BrowserState {
    // We allow setting an ID of a search engine that is not in the state since loading the search
    // engines may happen asynchronously and the search engine may not be loaded yet at this point.
    return copy(
        search = search.copy(
            userSelectedSearchEngineId = action.searchEngineId,
            userSelectedSearchEngineName = action.searchEngineName,
        ),
    )
}

private fun BrowserState.maybeShowSearchEngine(
    action: SearchAction.ShowSearchEngineAction,
): BrowserState {
    val searchEngine = search.hiddenSearchEngines.find { searchEngine -> searchEngine.id == action.searchEngineId }
    return if (searchEngine != null) {
        return showSearchEngine(searchEngine)
    } else {
        this
    }
}

private fun BrowserState.showSearchEngine(
    searchEngine: SearchEngine,
): BrowserState {
    return copy(
        search = search.copy(
            hiddenSearchEngines = search.hiddenSearchEngines - searchEngine,
            regionSearchEngines = (search.regionSearchEngines + searchEngine).sortedBy {
                search.regionSearchEnginesOrder.indexOf(it.id)
            },
        ),
    )
}

private fun BrowserState.hideSearchEngine(
    action: SearchAction.HideSearchEngineAction,
): BrowserState {
    val searchEngine = search.regionSearchEngines.find { searchEngine -> searchEngine.id == action.searchEngineId }

    return if (searchEngine != null) {
        copy(
            search = search.copy(
                regionSearchEngines = search.regionSearchEngines - searchEngine,
                hiddenSearchEngines = search.hiddenSearchEngines + searchEngine,
            ),
        )
    } else {
        this
    }
}

private fun BrowserState.addAdditionalSearchEngine(
    action: SearchAction.AddAdditionalSearchEngineAction,
): BrowserState {
    val searchEngine = search.additionalAvailableSearchEngines.find { searchEngine ->
        searchEngine.id == action.searchEngineId
    }

    return if (searchEngine != null) {
        copy(
            search = search.copy(
                additionalSearchEngines = search.additionalSearchEngines + searchEngine,
                additionalAvailableSearchEngines = search.additionalAvailableSearchEngines - searchEngine,
            ),
        )
    } else {
        this
    }
}

private fun BrowserState.removeAdditionalSearchEngine(
    action: SearchAction.RemoveAdditionalSearchEngineAction,
): BrowserState {
    val searchEngine = search.additionalSearchEngines.find { searchEngine ->
        searchEngine.id == action.searchEngineId
    }

    return if (searchEngine != null) {
        copy(
            search = search.copy(
                additionalAvailableSearchEngines = search.additionalAvailableSearchEngines + searchEngine,
                additionalSearchEngines = search.additionalSearchEngines - searchEngine,
            ),
        )
    } else {
        this
    }
}

private fun BrowserState.updateDisabledSearchEngineIds(
    action: SearchAction.UpdateDisabledSearchEngineIdsAction,
): BrowserState {
    val updatedDisabledSearchEngineShortcutIds = if (action.isEnabled) {
        search.disabledSearchEngineIds - action.searchEngineId
    } else {
        search.disabledSearchEngineIds + action.searchEngineId
    }

    return copy(
        search = search.copy(
            disabledSearchEngineIds = updatedDisabledSearchEngineShortcutIds,
        ),
    )
}

private fun BrowserState.restoreHiddenSearchEngines(): BrowserState {
    return search.hiddenSearchEngines.fold(this) { state, engine ->
        state.showSearchEngine(engine)
    }
}