package com.gh.gamecenter.feature.exposure

import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.gh.gamecenter.common.utils.isVisibleOnScreenLargerThanPercentage
import com.gh.gamecenter.common.view.stacklayoutmanager.StackLayoutManager
import com.gh.gamecenter.common.view.stacklayoutmanager2.StackLayoutManagerV2
import com.gh.gamecenter.feature.exposure.model.InExposureData
import com.gh.gamecenter.feature.exposure.model.VisibleItemPositionRange
import com.lightgame.utils.Utils
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.BehaviorSubject
import java.util.concurrent.TimeUnit

/**
 * ViewHolder (或者 View) 需实现 [IExposureProvider] 接口来提供曝光实体 [ExposureEvent]，曝光实体的曝光时间会在实际成为有效曝光的时候进行刷新
 *
 * 曝光实体的收集 (见方法 collectExposureData())：
 * > 前提： 内存中记录当前正在曝光中的曝光实体集合 [inExposureDataList]
 * 1. 通过 RecyclerView.layoutManager 获取当前列表可见的第一个和最后一个 item 的有效可见 position 数组
 * 2. 计算列表中第一个和最后一个 item 在屏幕上显示的比例，若比例小于指定的比例可不进行曝光收集，根据结果修正有效可见 position 数组
 * 3. 遍历有效可见 position 数组对应的 item，对其所有子 View 进行递归查找，尝试找到第一个满足一定可见比例且实现了 IExposureProvider 的对象
 * 4. 将步骤 3 获取到的所有 IExposureProvider 对象的曝光实体收集到集合中
 * 5. 若步骤 4 获取到的曝光实体集合不为空，则将其与 [inExposureDataList] 进行比较，若不在 [inExposureDataList] 中，更改状态为曝光中，记录开始曝光时间，并添加至 [inExposureDataList]
 * 6. 将 [inExposureDataList] 与步骤 4 的曝光实体集合进行比较，若不在步骤 4 的曝光实体集合中，更改状态为结束曝光，记录结束曝光时间，并从 [inExposureDataList] 中移除
 * 7. 收集步骤 6 中从 [inExposureDataList] 中移除且满足 `结束曝光时间 - 开始曝光时间 > 指定的曝光时长` 的曝光实体
 *
 * 曝光实体的收集时机：
 * 1. lifecycleOwner 生命周期感知，当 lifecycleOwner 生命周期变为可见时，自动触发曝光收集；当 lifecycleOwner 生命周期变为不可见时，自动触发结束曝光
 * 2. RecyclerView 滚动时，触发曝光收集的信号
 * 3. 内嵌 RecyclerView 滚动时调用 onScroll 方法，触发曝光收集的信号
 *
 * 曝光实体的收集频率：
 * 通过 BehaviorSubject.create<Any>().throttleLatest(SAMPLE_RATE, TimeUnit.MILLISECONDS) 控制曝光收集的频率
 *
 * 曝光实体的比较：
 * 存在一个处于曝光中的 item 集合
 * 当新的曝光实体被收集时，会与处于曝光中的 item 集合进行比较，若新的曝光实体不在处于曝光中的 item 集合中，则代表是新的曝光实体，需要触发开始曝光
 * 当处于曝光中的 item 集合与新的曝光实体集合进行比较，若处于曝光中的 item 集合中的 item 不在新的曝光实体集合中，则代表是结束曝光
 *
 * 曝光实体的处理：
 * 当结束曝光时间 - 开始曝光时间 > 指定的曝光时长，更新曝光实体的状态为已使用，并上报曝光事件
 *
 * @constructor 创建一个 RecyclerView 曝光收集实例，必传三个参数
 * @param boundedRecyclerView 需要收集曝光的 RecyclerView
 * @param lifecycleOwner RecyclerView 感知此生命周期组件,根据生命周期感知RV可见性，以便自动处理开始曝光和a结束曝光
 * @param exposureStateChangeListener 曝光状态改变监听器
 *
 * @author 原始作者 wp529 https://github.com/wp529/Exposure
 */
class RecyclerViewExposureHelper(
    private val boundedRecyclerView: RecyclerView,
    private val lifecycleOwner: LifecycleOwner?,
    private val exposureStateChangeListener: IExposureStateChangeListener,
) : RecyclerView.OnScrollListener() {
    // 处于曝光中的 Item 数据集合
    private val inExposureDataList = ArrayList<InExposureData<ExposureEvent>>()

    // 是否可见,不可见的状态就不触发收集了。主要是为了避免 RecyclerView 处于滚动惯性中然后退出后台导致收集异常
    private var visible = true

    private val collectExposureSubject = BehaviorSubject.create<Any>()
    private val compositeDisposable = CompositeDisposable()

    init {
        val disposable = collectExposureSubject
            .throttleLatest(SAMPLE_RATE, TimeUnit.MILLISECONDS)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                collectExposureData()
            }, {
                Utils.log(TAG, "曝光收集异常: $it")
            })
        compositeDisposable.add(disposable)

        boundedRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                if (visible) {
                    recyclerView.post {
                        sendCollectExposureDataSignal()
                    }
                }
            }
        })
        val adapter = boundedRecyclerView.adapter
            ?: throw IllegalStateException("在初始化 RecyclerViewExposureHelper 之前，RecyclerView 必须已经设置好了 adapter ")
        adapter.registerAdapterDataObserver(
            object : RecyclerView.AdapterDataObserver() {
                override fun onChanged() {
                    Utils.log(TAG, "adapter 的 item 有改变")
                    if (adapter.itemCount == 0) {
                        endExposure()
                        return
                    }
                    // item 改变，触发重新收集
                    boundedRecyclerView.viewTreeObserver.addOnGlobalLayoutListener(
                        object : ViewTreeObserver.OnGlobalLayoutListener {
                            override fun onGlobalLayout() {
                                // 绑定的曝光数据刷新完毕后才重新收集
                                boundedRecyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                                val itemAnimator = boundedRecyclerView.itemAnimator
                                if (itemAnimator == null) {
                                    boundedRecyclerView.post {
                                        sendCollectExposureDataSignal()
                                    }
                                } else {
                                    itemAnimator.isRunning {
                                        boundedRecyclerView.post {
                                            sendCollectExposureDataSignal()
                                        }
                                    }
                                }
                            }
                        }
                    )
                }

                override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
                    onChanged()
                }

                override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
                    onChanged()
                }

                override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
                    onChanged()
                }

                override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
                    onChanged()
                }
            }
        )

        // 感知生命周期
        lifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                onVisible()
            }

            override fun onPause(owner: LifecycleOwner) {
                onInvisible()
            }

            override fun onDestroy(owner: LifecycleOwner) {
                compositeDisposable.clear()
            }
        })
    }

    /**
     * 若传递了生命周期组件,那么常规场景下 RecyclerViewExposureHelper 会自动感知RV变更为可见状态并调用此方法。
     * 但可能实际的业务场景会出现非常规情况，所以仍可让外部告知 RV 处于可见状态以便触发曝光
     */
    fun onVisible() {
        Utils.log(TAG, "外部告知 RecyclerView 可见了")
        visible = true
        boundedRecyclerView.post {
            collectExposureData()
        }
    }

    /**
     * 若传递了生命周期组件,那么常规场景下 RecyclerViewExposureHelper 会自动感知RV变更为可见状态并调用此方法。
     * 但可能实际的业务场景会出现非常规情况，所以仍可让外部告知RV处于不可见状态以便触发结束曝光
     */
    fun onInvisible() {
        Utils.log(TAG, "外部告知 RecyclerView 不可见了")
        visible = false
        endExposure()
    }

    /**
     * 一般用于 RecyclerView 被嵌套在可滚动布局中(eg:ScrollView，NestedScrollView，RecyclerView 等)，导致 RecyclerViewExposureHelper 持有的 RV 不能响应滑动的情况，
     * 就必须由外部告知 RV 被滚动了触发曝光收集
     * 虽然 Google 强烈建议不能把 RecyclerView 嵌套在滚动布局中，但在实际开发中仍然存在复杂的业务逻辑导致难以避免这样的用法，故提供此方法由外部调用
     */
    fun onScroll() {
        if (visible) {
            boundedRecyclerView.post {
                sendCollectExposureDataSignal()
            }
        }
    }

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        onScroll()
    }

    private fun sendCollectExposureDataSignal() {
        collectExposureSubject.onNext(Any())
    }

    /**
     * 收集开始曝光和结束曝光的数据
     */
    private fun collectExposureData() {
        val startTime = System.currentTimeMillis()
        val layoutManager = boundedRecyclerView.layoutManager ?: return
        val visibleItemPositionRange = getVisibleItemPositionRange(layoutManager, 0.2F) ?: return
        val visiblePositions = IntRange(
            visibleItemPositionRange.firstVisibleItemPosition,
            visibleItemPositionRange.lastVisibleItemPosition
        )
        Utils.log(TAG, "当前可见的 position 范围: $visiblePositions")
        // 当前所有可见的曝光中的数据
        val currentVisibleExposureEventList = ArrayList<InExposureData<ExposureEvent>>()
        for (visiblePosition in visiblePositions) {
            val visiblePositionExposureEventList = getExposureDataListByPosition(visiblePosition) ?: continue
            currentVisibleExposureEventList.addAll(visiblePositionExposureEventList)
            visiblePositionExposureEventList.forEach { visiblePositionExposureEvent ->
                if (visiblePositionExposureEvent !in inExposureDataList) {
                    // 刷新曝光时间
                    visiblePositionExposureEvent.data.refreshTime()
                    // 当前可见位置不在曝光中集合，代表是从不可见变为可见，那么此 position 开始曝光
                    inExposureDataList.add(visiblePositionExposureEvent)
                    invokeExposureStateChange(visiblePositionExposureEvent.data, visiblePosition, true)
                }
            }
        }

        inExposureDataList.filter { inExposureData ->
            // 过滤出状态为曝光中，但实际不在当前可见的曝光数据集合中的数据
            inExposureData !in currentVisibleExposureEventList
        }.also {
            // 更改为结束曝光状态
            it.forEach { inExposureData ->
                invokeExposureStateChange(inExposureData.data, inExposureData.position, false)
            }
            // 移除出正在曝光中的集合
            inExposureDataList.removeAll(it)
        }

        Utils.log(TAG, "曝光收集耗时: ${System.currentTimeMillis() - startTime}毫秒")
    }

    /**
     * 获取 RecyclerView 第一个有效可见 item position 和最后一个有效可见 item position
     * @param minimumVisiblePercentage 最小有效可见比例
     */
    private fun getVisibleItemPositionRange(
        layoutManager: RecyclerView.LayoutManager,
        minimumVisiblePercentage: Float,
    ): VisibleItemPositionRange? {
        val visibleItemPositionRange = when (layoutManager) {
            is LinearLayoutManager -> {
                var firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
                var lastVisiblePosition = layoutManager.findLastVisibleItemPosition()

                // 校验首个和最后一个可见的 item 是否真的可见 （大于 minimumVisiblePercentage 的区域显示在屏幕上），不可见则修正
                if (lastVisiblePosition - firstVisiblePosition > 1) {
                    val firstVisibleItemView = layoutManager.findViewByPosition(firstVisiblePosition)
                    if (firstVisibleItemView != null
                        && !firstVisibleItemView.isVisibleOnScreenLargerThanPercentage(minimumVisiblePercentage)
                    ) {
                        firstVisiblePosition += 1
                    }

                    val lastVisibleItemView = layoutManager.findViewByPosition(lastVisiblePosition)
                    if (lastVisibleItemView != null
                        && !lastVisibleItemView.isVisibleOnScreenLargerThanPercentage(minimumVisiblePercentage)
                    ) {
                        lastVisiblePosition -= 1
                    }
                }
                VisibleItemPositionRange(firstVisiblePosition, lastVisiblePosition)
            }

            is StaggeredGridLayoutManager -> {
                val firstVisibleItemPosition = layoutManager.let {
                    val visiblePositionArray = IntArray(it.spanCount)
                    it.findFirstVisibleItemPositions(visiblePositionArray)
                    visiblePositionArray.min()
                }
                val lastVisibleItemPosition = layoutManager.let {
                    val visiblePositionArray = IntArray(it.spanCount)
                    it.findLastVisibleItemPositions(visiblePositionArray)
                    visiblePositionArray.max()
                }
                VisibleItemPositionRange(firstVisibleItemPosition, lastVisibleItemPosition)
            }

            else -> null
        }

        return if (visibleItemPositionRange == null
            || visibleItemPositionRange.firstVisibleItemPosition < 0
            || visibleItemPositionRange.lastVisibleItemPosition < 0
        ) {
            null
        } else {
            visibleItemPositionRange
        }
    }

    /**
     * 获取 position 绑定的曝光数据
     **/
    private fun getExposureDataListByPosition(position: Int): List<InExposureData<ExposureEvent>>? {
        val provideExposureDataViewList =
            findAllExposureProviderAndAddOnScrollListener(boundedRecyclerView.layoutManager?.findViewByPosition(position))
        if (provideExposureDataViewList.isEmpty()) {
            Utils.log(
                TAG,
                "position 为 $position 的 ItemView 没有提供有效的曝光数据"
            )
            return null
        }
        val inExposureDataListResult = ArrayList<InExposureData<ExposureEvent>>()
        @Suppress("UNCHECKED_CAST")
        provideExposureDataViewList.forEach {
            // 若实现了 IExposureListProvider 接口,则调用 provideExposureDataList 方法获取曝光数据集合
            if (it is IExposureListProvider) {
                val exposureEventList = it.provideExposureDataList()
                if (exposureEventList != null) {
                    for (exposureEvent in exposureEventList) {
                        // 添加到队列里
                        inExposureDataListResult.add(InExposureData(exposureEvent, position))
                    }
                }
            } else {
                // 若实现了 IExposureProvider 接口,则调用 provideExposureData 方法获取曝光数据
                val exposureEvent = it.provideExposureData()
                if (exposureEvent != null) {
                    // 添加到队列里
                    inExposureDataListResult.add(InExposureData(exposureEvent, position))
                }
            }
        }
        return inExposureDataListResult
    }

    /**
     * 递归获取 rootView 下所有绑定了曝光数据的集合并为嵌套 RecyclerView 添加滚动监听
     * 当该层 View 有支持曝光的实现，会停止继续递归
     **/
    private fun findAllExposureProviderAndAddOnScrollListener(rootView: View?): List<IExposureProvider> {
        val currentVisibleExposureProviderList = ArrayList<IExposureProvider>()
        rootView ?: return currentVisibleExposureProviderList

        val parentRecyclerView = rootView.parent as? RecyclerView
        val isUsingStackLayoutManager = if (parentRecyclerView != null) {
            parentRecyclerView.layoutManager is StackLayoutManagerV2
                || parentRecyclerView.layoutManager is StackLayoutManager
        } else {
            false
        }

        val containingViewHolder = if (parentRecyclerView != null) {
            // 仅为子 recyclerView 添加 scrollListener
            if (rootView.parent != boundedRecyclerView
                && parentRecyclerView.getTag(TAG_SCROLL_LISTENER) == null) {
                parentRecyclerView.addOnScrollListener(this)
                parentRecyclerView.setTag(TAG_SCROLL_LISTENER, true)
            }
            parentRecyclerView.findContainingViewHolder(rootView)
        } else {
            null
        }

        if (containingViewHolder is IExposureProvider
            && rootView.isVisibleOnScreenLargerThanPercentage(VALID_VISIBLE_RECT_PERCENTAGE)
        ) {
            // 当 layoutManager 为 StackLayoutManager 的时候，仅 scale 大于 0.95 的 item 才曝光
            if (isUsingStackLayoutManager && rootView.scaleX < 0.95) {
                return currentVisibleExposureProviderList
            }

            currentVisibleExposureProviderList.add(containingViewHolder)
            // 当前 ViewGroup 节点已经支持统计曝光了,那么就不需要再向下找了
            return currentVisibleExposureProviderList
        }

        if (rootView is IExposureProvider
            && rootView.isVisibleOnScreenLargerThanPercentage(VALID_VISIBLE_RECT_PERCENTAGE)
        ) {
            currentVisibleExposureProviderList.add(rootView)
            // 当前 ViewGroup 节点已经支持统计曝光了,那么就不需要再向下找了
            return currentVisibleExposureProviderList
        }

        if (rootView !is ViewGroup) {
            return currentVisibleExposureProviderList
        }

        repeat(rootView.childCount) {
            currentVisibleExposureProviderList.addAll(
                findAllExposureProviderAndAddOnScrollListener(
                    rootView.getChildAt(it)
                )
            )
        }
        return currentVisibleExposureProviderList
    }

    // 将处于曝光的 item 全部结束曝光
    private fun endExposure() {
        inExposureDataList.onEach { inExposureData ->
            // 当前 position 绑定了曝光数据,回调结束曝光
            invokeExposureStateChange(inExposureData.data, inExposureData.position, false)
        }.also {
            // 清空曝光中 position 集合
            it.clear()
        }
    }

    // 回调曝光状态改变
    private fun invokeExposureStateChange(
        data: ExposureEvent,
        position: Int,
        inExposure: Boolean
    ) {
        exposureStateChangeListener.onExposureStateChange(
            exposureEvent = data,
            position = position,
            inExposure = inExposure
        )
    }

    companion object {
        const val TAG = "RecyclerViewExposureHelper"

        var TAG_SCROLL_LISTENER = com.gh.gamecenter.common.R.id.recyclerview_exposure

        const val VALID_VISIBLE_RECT_PERCENTAGE = 0.7F
        const val SAMPLE_RATE = 100L
    }
}

fun RecyclerView.addExposureHelper(lifecycleOwner: LifecycleOwner?, exposureStateChangeListener: IExposureStateChangeListener) {
    RecyclerViewExposureHelper(this, lifecycleOwner, exposureStateChangeListener)
}