Загрузка данных


package ru.ozon.seller.composer.screen

import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.compose.runtime.Composable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import ru.ozon.analytics.storage.connectToParent
import ru.ozon.composer.ComposerInterceptor
import ru.ozon.composer.compose.ComposerComposeScreen
import ru.ozon.composer.event.ComposerEvent
import ru.ozon.composer.state.ComposerRequestState
import ru.ozon.composer.state.Loader
import ru.ozon.composer.ui.ComposerScreen
import ru.ozon.composer.ui.ComposerScreenUiContainer
import ru.ozon.composer.ui.FragmentViewModelOwnerProvider
import ru.ozon.composer.ui.OwnerContainer
import ru.ozon.composer.ui.ViewModelOwnerProvider
import ru.ozon.composer.ui.ext.composerAppbar
import ru.ozon.composer.widget.item.ComposerViewItem
import ru.ozon.composer.widget.store.WidgetFactory
import ru.ozon.designsystem.theme.DsTheme
import ru.ozon.seller.composer.configuration.ConfiguratorComposerDependencies
import ru.ozon.seller.composer.configuration.PageConfigurator
import ru.ozon.seller.composer.di.composer.component.ComposerFragmentComponentDependencies
import ru.ozon.seller.composer.di.composer.component.DaggerComposerFragmentComponent
import ru.ozon.seller.composer.navigation.COMPOSER_ARGS
import ru.ozon.seller.composer.navigation.COMPOSER_PAGE
import ru.ozon.seller.composer.navigation.LoadableScreen
import ru.ozon.seller.composer.navigation.TopScrollable
import ru.ozon.seller.composer.navigation.config.ComposerPage
import ru.ozon.seller.composer.utils.SoftInputChangeDelegate
import ru.ozon.seller.composer.utils.serializable
import ru.ozon.seller.composer.utils.whenStarted
import ru.ozon.seller.composer.utils.withArgs
import ru.ozon.seller.composer.viewmodel.ComposerHolderViewModel
import ru.ozon.seller.composer.viewmodel.sharedViewModelStore
import ru.ozon.seller.composer.wrapper.ComposerErrorStateFactoryWrapper
import ru.ozon.seller.composer.wrapper.InitialWidgetsProviderWrapper
import ru.ozon.seller.core.api.abTesting.isDisabled
import ru.ozon.seller.core.api.abTesting.isEnabled
import ru.ozon.seller.growthStream.api.core.MonetizationFeatureFlags
import ru.ozon.seller_app.core.android.api.di.findFeatureDependencies
import ru.ozon.seller_app.core.android.api.system.parcelable
import ru.ozon.seller_app.core.navigation.routers.ScreenRouter
import ru.ozon.uikit.pool.ViewPool
import ru.ozon.uikit.pool.ViewPoolHolder
import ru.ozon.uikit.pool.precreation.PreCreationViewPool
import ru.ozon.uikit.theme.OzonTheme
import ru.ozon.uni.android.uikit.extensions.exhaustive
import ru.ozon.uni.ozi.theme.OziTheme
import javax.inject.Inject
import javax.inject.Provider

class ComposerFragment :
    Fragment(),
    ComposerComposeScreen,
    TopScrollable,
    ViewPoolHolder,
    LoadableScreen {

    companion object {
        internal const val CONFIG = "ru.ozon.composer.SCREEN_CONFIG"
        internal const val ARG_DISPLAY_MODE = "ARG_DISPLAY_MODE"

        fun newInstance(
            config: ComposerScreenConfig,
            displayMode: DisplayMode? = null,
        ): ComposerFragment {
            return ComposerFragment().withArgs {
                putParcelable(CONFIG, config)
                putSerializable(ARG_DISPLAY_MODE, displayMode)
            }
        }
    }

    @Inject
    lateinit var widgets: Set<@JvmSuppressWildcards WidgetFactory>

    @Inject
    lateinit var configurators: List<@JvmSuppressWildcards PageConfigurator>

    @Inject
    lateinit var interceptors: Set<@JvmSuppressWildcards ComposerInterceptor>

    @Inject
    lateinit var router: ScreenRouter

    @Inject
    lateinit var errorStateFactoryWrapper: ComposerErrorStateFactoryWrapper

    @Inject
    lateinit var initialWidgetsProviderWrapper: InitialWidgetsProviderWrapper

    @Inject
    lateinit var viewPoolProvider: Provider<ViewPool>

    private val handler by lazy { Handler(Looper.getMainLooper()) }

    private val fragmentController: ComposerFragmentController by lazy {
        ComposerFragmentController(
            fragment = this,
            config = config,
            ownerContainer = OwnerContainer(this),
            viewModelOwnerProvider = FragmentViewModelOwnerProvider(this, sharedViewModelStore()),
            widgets = widgets,
            displayMode = displayMode(),
            interceptors = interceptors,
            errorStateFactory = errorStateFactoryWrapper.factory,
            initialWidgetsProvider = initialWidgetsProviderWrapper.provider,
        )
    }

    private val ownerContainer: OwnerContainer
        get() = fragmentController.ownerContainer

    private val viewModelOwnerProvider: ViewModelOwnerProvider
        get() = fragmentController.viewModelOwnerProvider

    private val composerStore
        get() = fragmentController.composerStore

    private val controller
        get() = fragmentController.controller

    lateinit var config: ComposerScreenConfig
    lateinit var displayMode: DisplayMode

    private var softInputInputChangeDelegate: SoftInputChangeDelegate? = null

    private var isFirstLoaded = false
    private var host: Host? = null

    private var composerScreenUiContainer: ComposerScreenUiContainer? = null

    private var prefetchViewPool: ViewPool? = null

    private val loading = Runnable {
        // если фрагмент не добавлен к родителю, то выполнение runnable нужно игнорировать
        if (!isAdded) return@Runnable

        // Запускаем инициализационную загрузку данных, только если передана информация для загрузки данных.
        // Например при использовании общего ComposerRepository на два ComposerFragment,
        // для зависимого экрана ссылки не будет, ComposerRepository проинициализируется основным экраном.
        if (!isFirstLoaded) {
            isFirstLoaded = true
            fragmentController.pagePerformanceTracker.onComposerFirstLoadInitiated()
            composerStore.emit(ComposerEvent.ReloadEvent.FirstLoad)
        }
    }

    override fun onAttach(context: Context) {
        val composerInitTimeStart = System.nanoTime()
        val args = arguments
        val composerDeps = requireActivity().findFeatureDependencies<ComposerFragmentComponentDependencies>()

        config = args?.parcelable<ComposerScreenConfig>(CONFIG)
            ?: composerDeps
                .getComposerScreenConfigStore()
                .config(
                    ComposerPage(
                        value = requireNotNull(args?.getString(COMPOSER_PAGE)),
                        args = args.getString(COMPOSER_ARGS),
                    ),
                )
        displayMode = args?.serializable(ARG_DISPLAY_MODE)
            ?: config.bottomSheetConfig?.displayMode
            ?: DisplayMode.REGULAR

        DaggerComposerFragmentComponent
            .factory()
            .create(
                config,
                composerDeps,
            )
            .inject(this)

        fragmentController.pagePerformanceTracker.onComposerViewInitStarted(composerInitTimeStart)
        super.onAttach(context)

        host = (parentFragment ?: activity) as? Host
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val softInputMode = config.softInputMode
        if (softInputMode != null) {
            softInputInputChangeDelegate = SoftInputChangeDelegate(softInputMode)
                .also { it.register(requireActivity(), this) }
        }

        val references = ConfiguratorComposerDependencies(fragmentController)

        configurators.forEach {
            it.composerInitialized(references)
            it.onRestoreInstanceState(savedInstanceState, viewModelOwnerProvider)
            lifecycle.addObserver(it)
        }
        fragmentController.pagePerformanceTracker.onComposerViewInitEnded()
        prefetchViewPool = viewPoolProvider.get()
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        fragmentController.pagePerformanceTracker.onComposerViewCreationStarted()
        val screenContainer = fragmentController.createComposerScreenUiContainer(inflater, container)
        composerScreenUiContainer = screenContainer
        return screenContainer.view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val viewModel = ComposerHolderViewModel
            .getFromViewModelStoreOwner(router.currentBackStackEntry.value?.viewModelStoreOwner)

        viewModel?.setComposer(fragmentController.composer)

        host?.onComposerCreated(this, fragmentController)

        fragmentController.analyticsScreenStorage.connectToParent(this)

        handler.post(loading)
        activity?.onBackPressedDispatcher?.addCallback(
            viewLifecycleOwner,
            object : OnBackPressedCallback(true) {
                override fun handleOnBackPressed() {
                    router.back()
                }
            },
        )
        fragmentController.pagePerformanceTracker.onComposerViewCreationEnded()
        if (MonetizationFeatureFlags.CONSUME_INSETS_BEFORE_COMPOSER.isDisabled()) {
            ViewCompat.setOnApplyWindowInsetsListener(view) { view, insets ->

                val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())

                view.setPadding(0, 0, 0, imeInsets.bottom)

                composerStore.emit(ComposerEvent.ApplyWindowInsets(insets))

                ViewCompat.onApplyWindowInsets(view, insets)
            }
        }
        configurators.forEach { it.onViewCreated(view, savedInstanceState) }
    }

    @Composable
    override fun ComposeWidgetWrapper(item: ComposerViewItem, content: @Composable (() -> Unit)) {
        if (MonetizationFeatureFlags.COMPOSER_CONFIG_INIT.isEnabled()) {
            OziTheme {
                DsTheme {
                    OzonTheme {
                        content()
                    }
                }
            }
        } else {
            super.ComposeWidgetWrapper(item, content)
        }
    }

    override fun onResume() {
        super.onResume()
        handler.post(loading)
    }

    override fun onPause() {
        super.onPause()
        handler.removeCallbacks(loading)
    }

    override fun onStop() {
        super.onStop()
        PreCreationViewPool.clear(this.toString())
    }

    override fun onDestroyView() {
        super.onDestroyView()
        composerScreenUiContainer = null
        prefetchViewPool = null
        configurators.forEach { it.onDestroyView() }
    }

    override fun onDetach() {
        host = null
        super.onDetach()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        whenStarted {
            configurators.forEach { it.onActivityResult(requestCode, resultCode, data) }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        configurators.forEach { it.onSaveInstanceState(outState, viewModelOwnerProvider) }
        super.onSaveInstanceState(outState)
    }

    override fun scrollToTop() {
        controller.scrollToPosition(0)
        ownerContainer.fragment?.view?.composerAppbar()?.setExpanded(true)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        configurators.forEach { it.onConfigurationChanged(newConfig) }
    }

    override fun showLoader(type: LoadableScreen.LoaderType) {
        val loaderType = when (type) {
            LoadableScreen.LoaderType.OVERLAY -> Loader.Type.Overlay()
            LoadableScreen.LoaderType.TRANSPARENT -> Loader.Type.Transparent()
        }
        controller.showLoader(loaderType)
    }

    override fun hideLoader() {
        controller.hideLoader()
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        whenStarted {
            configurators.forEach { it.onRequestPermissionsResult(requestCode, permissions, grantResults) }
        }
    }

    fun startLoading(deeplink: String, oneTimePostProcessing: ComposerRequestState.OneTimePostProcessing? = null) {
        composerStore.emit(
            ComposerEvent.ReloadEvent.Refresh(url = deeplink, postProcessingInfo = oneTimePostProcessing),
        )
    }

    fun startLoading(pageRef: ComposerRequestState.PageRef) {
        when (pageRef) {
            is ComposerRequestState.PageRef.Deeplink -> composerStore.emit(
                ComposerEvent.ReloadEvent.Refresh(url = pageRef.rootUrl),
            )

            is ComposerRequestState.PageRef.Json -> composerStore.emit(
                ComposerEvent.ReloadEvent.Refresh(jsonBody = pageRef.value),
            )
        }.exhaustive
    }

    fun scrollToWidget(
        widgetComponent: String,
        includeToolbarHeight: Boolean = true,
        offset: Int = 0,
    ) {
        controller.scrollToWidget(
            widgetComponent = widgetComponent,
            includeToolbarHeight = includeToolbarHeight,
            offset = offset,
        )
    }

    fun scrollToWidgetKey(
        widgetKey: Int,
        offset: Int = 0,
        smooth: Boolean = false,
    ) {
        controller.scrollToWidget(
            widgetKey = widgetKey,
            offset = offset,
            smooth = smooth,
        )
    }

    private fun displayMode() = when (displayMode.ordinal) {
        DisplayMode.BOTTOM_SHEET_FULL.ordinal -> ComposerScreen.DisplayMode.BOTTOM_SHEET_FULL
        DisplayMode.BOTTOM_SHEET_WRAP.ordinal -> ComposerScreen.DisplayMode.BOTTOM_SHEET_WRAP
        else -> ComposerScreen.DisplayMode.REGULAR
    }

    override fun getViewPool(): ViewPool? {
        return prefetchViewPool
    }

    enum class DisplayMode {
        // Это фулл скрин, который поддерживает match_parent у холдера, но перекрывает bottomContainer
        REGULAR,

        // Шторка отображается по контенту
        BOTTOM_SHEET_WRAP,

        // Это фулл скрин, который не поддерживает match_parent у холдера, но учитывает bottomContainer
        BOTTOM_SHEET_FULL,
    }

    /**
     * Если родительский фрагмент (host) ComposerFragment-а реализует этот метод,
     * то он сможет гибче контролировать композер.
     */
    interface Host {

        /**
         * Вызывается когда композер создан.
         */
        fun onComposerCreated(fragment: Fragment, composer: ComposerFragmentController) {
        }
    }
}