import { _decorator, Component, Enum, Event, EventMouse, EventTouch, instantiate, log, Mask, math, Node, NodeEventType, NodePool, Prefab, ScrollView, UIOpacity, UITransform, ValueType } from 'cc';
const { ccclass, property, requireComponent, executionOrder, disallowMultiple, help } = _decorator;

const _vec2Out = new math.Vec2()
const _vec3Out = new math.Vec3()
const _rectOut = new math.Rect()

/**
 * 定义列表的滚动方向  
 */
enum _yx_collection_view_scroll_direction {
    /**
     * 水平滚动
     */
    HORIZONTAL,

    /**
     * 垂直滚动
     */
    VERTICAL,
}
Enum(_yx_collection_view_scroll_direction)

/**
 * 虚拟列表实现模式
 */
enum _yx_collection_view_list_mode {
    /**
     * 根据列表显示范围加载需要的节点，同类型的节点会进行复用  
     * 优点: 控制总节点数量，不会创建大量节点  
     * 缺点: 节点 Label (NONE) 数量多的话，列表滑动时候会抖动  
     */
    RECYCLE,
    /**
     * 跟 RECYCLE 模式差不多，区别是此模式下每条数据都会对应的生成一个节点而不进行复用  
     * 优点: 用户通过滚动行为加载过所有节点后，后续再滑动可以减少卡顿  
     * 缺点: 最终会创建所有显示过的节点，另外在首次滑动过程中列表也可能会有抖动  
     */
    ONCE,
    /**
     * 预加载所有的节点，可以通过 preloadInterval 设置预加载帧间隔  
     * 优点: 配合 `UI_OPACITY` 回收模式优化大量 Label (NONE) 场景下的卡顿问题  
     * 缺点: 会直接创建列表内所有的单元节点，如果列表数据量很大，在刷新阶段会有明显卡死  
     * 
     * 卡顿优化建议:  
     * https://gitee.com/568071718/creator-collection-view-doc/blob/main/md/2.md 
     */
    PRELOAD,
}
Enum(_yx_collection_view_list_mode)

/**
 * 节点池回收策略
 */
enum _yx_collection_view_cell_item_pool_policy {
    /**
     * 什么都不做，仅仅是把目标节点通过池子管理起来，一般来说这个只是调试时候可能用到的模式，可以忽略  
     * @deprecated 标过期是为了提醒此策略不建议使用，不过暂时没有移除计划，
     */
    NONE,
    /**
     * 自动将目标节点从父节点上移除  
     * 优点: 不会修改 cell 节点的任何状态  
     * 缺点: 目标节点会被重复的移除/添加
     */
    REMOVE,
    /**
     * 通过目标节点的 UIOpacity 组件修改透明度  
     * 优点: 避免重复的移除/添加行为
     * 缺点: 内部需要调整 cell 节点的 UIOpacity，对于业务需求本身需要带透明度的 cell 来说并不合适
     */
    UI_OPACITY,
}
Enum(_yx_collection_view_cell_item_pool_policy)

/**
 * 节点池  
 * 跟一般的节点池业务差不多，区别是这个 put 行为不会直接将节点从父节点移除，而是提供一个可选策略  
 * https://github.com/cocos/cocos-engine/blob/v3.8.0/extensions/ccpool/node-pool.ts
 */
class _yx_collection_view_cell_item_pool {
    /**
     * 管理当前所有的可用节点
     */
    private nodes: Set<Node> = new Set()
    /**
     * 同 cc 的 NodePool 组件
     */
    poolHandlerComp: (new (...args: any[]) => YXCollectionViewCell) | string = null
    /**
     * put 回收策略
     */
    policy: _yx_collection_view_cell_item_pool_policy
    constructor(poolHandlerComp: typeof _yx_collection_view_cell_item_pool.prototype.poolHandlerComp, policy: typeof _yx_collection_view_cell_item_pool.prototype.policy = _yx_collection_view_cell_item_pool_policy.REMOVE) {
        this.poolHandlerComp = poolHandlerComp;
        this.policy = policy
    }
    /**
     * 获取当前缓冲池的可用对象数量
     */
    get size(): number {
        return this.nodes.size
    }
    /**
     * 将一个 node 节点添加到节点池
     * @param obj 
     */
    put(obj: Node) {
        if (this.policy == _yx_collection_view_cell_item_pool_policy.REMOVE) {
            obj.removeFromParent()
        } else if (this.policy == _yx_collection_view_cell_item_pool_policy.UI_OPACITY) {
            let comp = obj.getComponent(UIOpacity) || obj.addComponent(UIOpacity)
            comp.opacity = 0
        }
        // @ts-ignore
        const handler = this.poolHandlerComp ? obj.getComponent(this.poolHandlerComp) : null;
        if (handler && handler.unuse) {
            handler.unuse();
        }
        this.nodes.add(obj)
    }
    /**
     * 获取对象池中的对象，如果对象池没有可用对象，则返回空。
     */
    get(...args: any[]): Node | null {
        for (const obj of this.nodes) {
            if (this.policy == _yx_collection_view_cell_item_pool_policy.UI_OPACITY) {
                let comp = obj.getComponent(UIOpacity) || obj.addComponent(UIOpacity)
                comp.opacity = 255
            }
            this.nodes.delete(obj)
            // @ts-ignore
            const handler = this.poolHandlerComp ? obj.getComponent(this.poolHandlerComp) : null;
            if (handler && handler.reuse) {
                handler.reuse(arguments);
            }
            return obj
        }
        return null
    }
    /**
     * 销毁对象池中缓存的所有节点
     */
    clear() {
        for (const obj of this.nodes) { obj.destroy() }
        this.nodes.clear()
    }
}

/**
 * 定义通过编辑器注册节点时的结构体
 */
@ccclass(`_yx_editor_register_cell_info`)
class _yx_editor_register_cell_info {
    @property({ type: Prefab, tooltip: `cell 节点预制体，必须配置` })
    prefab: Prefab = null

    @property({ tooltip: `节点重用标识符\n如果确定此列表仅使用一种节点类型，可以忽略此配置` })
    identifier: string = ``

    @property({ tooltip: `节点挂载的自定义组件\n如果需要监听 NodePool 的重用/回收事件，确保你的自定义组件已经实现了 YXCollectionViewCell 接口并配置此属性为你的自定义组件名\n如果不需要，可以忽略此配置` })
    comp: string = ``
}

/**
 * 表示索引的对象
 */
@ccclass(`YXIndexPath`)
export class YXIndexPath extends ValueType {
    public static ZERO: Readonly<YXIndexPath> = new YXIndexPath(0, 0)
    /**
     * 区索引
     */
    section: number = 0
    /**
     * 单元格在区内的位置
     */
    item: number = 0
    set row(value: number) { this.item = value }
    get row(): number { return this.item }
    constructor(section: number, item: number) {
        super()
        this.section = section
        this.item = item
    }
    clone(): YXIndexPath {
        return new YXIndexPath(this.section, this.item)
    }
    equals(other: YXIndexPath): boolean {
        return (this.section == other.section && this.item == other.item)
    }
    set(other: YXIndexPath): void {
        this.section = other.section
        this.item = other.item
    }
    toString(): string {
        return `${this.section} - ${this.item}`
    }
}

/**
 * 表示边距的对象
 */
@ccclass(`YXEdgeInsets`)
export class YXEdgeInsets extends ValueType {
    public static ZERO: Readonly<YXEdgeInsets> = new YXEdgeInsets(0, 0, 0, 0)
    top: number
    left: number
    bottom: number
    right: number
    constructor(top: number, left: number, bottom: number, right: number) {
        super()
        this.top = top
        this.left = left
        this.bottom = bottom
        this.right = right
    }
    clone(): YXEdgeInsets {
        return new YXEdgeInsets(this.top, this.left, this.bottom, this.right)
    }
    equals(other: YXEdgeInsets): boolean {
        return (this.top == other.top && this.left == other.left && this.bottom == other.bottom && this.right == other.right)
    }
    set(other: YXEdgeInsets): void {
        this.top = other.top
        this.left = other.left
        this.bottom = other.bottom
        this.right = other.right
    }
    toString(): string {
        return `[ ${this.top}, ${this.left}, ${this.bottom}, ${this.right} ]`
    }
}

/**
 * 私有组件
 * cell 节点添加到 YXCollectionView 上时，自动挂载此组件，用来记录一些实时参数
 */
class _cell_ extends Component {
    /**
     * 此节点是通过哪个标识符创建的
     */
    identifier: string

    /**
     * 此节点目前绑定的布局属性
     */
    attributes: YXLayoutAttributes
}

/**
 * 私有组件
 * 内部滚动视图组件
 * https://github.com/cocos/cocos-engine/blob/v3.8.0/cocos/ui/scroll-view.ts
 */
class _scroll_view extends ScrollView {
    /**
     * 禁掉鼠标滚轮
     */
    protected _onMouseWheel(event: EventMouse, captureListeners?: Node[]): void { }

    /**
     * 准备开始惯性滚动
     * @param touchMoveVelocity 手势速度
     */
    protected _startInertiaScroll(touchMoveVelocity: math.Vec3): void {
        super._startInertiaScroll(touchMoveVelocity)
        if (this._yxOnStartInertiaScroll) { this._yxOnStartInertiaScroll(touchMoveVelocity) }
    }
    _yxOnStartInertiaScroll: (touchMoveVelocity: math.Vec3) => void

    protected _onTouchBegan(event: EventTouch, captureListeners?: Node[]): void {
        if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
        let nodes: Node[] = [event.target]
        if (captureListeners) { nodes = nodes.concat(captureListeners) }
        for (let index = 0; index < nodes.length; index++) {
            const element = nodes[index];
            // 清空滚动节点标记
            element[`_yx_scroll_target`] = null
            // 记录开始滚动时的偏移量，如果这是个 YXCollectionView 节点的话
            let collectionView = element.getComponent(YXCollectionView)
            if (collectionView) {
                let offset = collectionView.scrollView.getScrollOffset()
                offset.x = - offset.x
                collectionView[`_scroll_offset_on_touch_start`] = offset
            }
        }
        super._onTouchBegan(event, captureListeners)
    }
    protected _onTouchMoved(event: EventTouch, captureListeners?: Node[]): void {
        if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
        // 处理嵌套冲突，每次只滚动需要滚动的列表
        let scrollTarget = this._yxGetScrollTarget(event, captureListeners)
        if (this.node === scrollTarget) {
            super._onTouchMoved(event, captureListeners)
        }
    }
    protected _onTouchCancelled(event: EventTouch, captureListeners?: Node[]): void {
        super._onTouchCancelled(event, captureListeners)
    }
    protected _onTouchEnded(event: EventTouch, captureListeners?: Node[]): void {
        super._onTouchEnded(event, captureListeners)
    }

    protected _hasNestedViewGroup(event: Event, captureListeners?: Node[]): boolean {
        // 直接把所有的列表都标记为可滑动，具体滑动哪一个，去 _onTouchMoved 判断
        return false
    }

    protected _stopPropagationIfTargetIsMe(event: Event): void {
        if (this._touchMoved) {
            event.propagationStopped = true;
            return
        }
        super._stopPropagationIfTargetIsMe(event)
    }

    /**
     * 获取本次滑动是要滑动哪个列表
     */
    private _yxGetScrollTarget(event: EventTouch, captureListeners?: Node[]): Node {
        // 尝试获取本次已经确定了的滚动节点
        let cache = event.target[`_yx_scroll_target`]
        if (cache) {
            return cache
        }

        let nodes: Node[] = [event.target]
        if (captureListeners) {
            nodes = nodes.concat(captureListeners)
        }
        if (nodes.length == 1) { return nodes[0] } // 无需处理冲突

        let touch = event.touch;
        let deltaMove = touch.getLocation().subtract(touch.getStartLocation());
        let x = Math.abs(deltaMove.x)
        let y = Math.abs(deltaMove.y)
        let distance = Math.abs(x - y)
        if (distance < 5) {
            return null // 不足以计算出方向
        }
        /** @todo 边界检测，滑动到边缘时滑动事件交给其他可滑动列表 */

        let result = null
        for (let index = 0; index < nodes.length; index++) {
            const element = nodes[index];
            let scrollComp = element.getComponent(_scroll_view)
            if (scrollComp) {
                let collectionView = element.getComponent(YXCollectionView)
                if (collectionView && collectionView.scrollEnabled == false) { continue } // 不支持滚动
                if (result == null) { result = element } // 取第一个滚动组件作为默认响应者
                if (scrollComp.horizontal && scrollComp.vertical) { continue } // 全方向滚动暂时不处理
                if (!scrollComp.horizontal && !scrollComp.vertical) { continue } // 不支持滚动的也不处理
                if (scrollComp.horizontal && x > y) {
                    result = element
                    break
                }
                if (scrollComp.vertical && y > x) {
                    result = element
                    break
                }
            }
        }

        // 给所有捕获到的节点都保存一份，方便任意一个节点都可以读到
        if (result) {
            for (let index = 0; index < nodes.length; index++) {
                const element = nodes[index];
                element[`_yx_scroll_target`] = result
            }
        }
        return result
    }
}

/**
 * 节点的布局属性
 */
export class YXLayoutAttributes {
    /**
     * 节点索引
     */
    indexPath: YXIndexPath = null

    /**
     * 节点在滚动视图中的位置和大小属性
     * origin 属性表示节点在父视图坐标系中的左上角的位置，size 属性表示节点的宽度和高度
     */
    frame: math.Rect = null

    /**
     * 节点层级
     * 越小会越早的添加到滚动视图上
     * https://docs.cocos.com/creator/manual/zh/ui-system/components/editor/ui-transform.html?h=uitrans
     * 备注: 内部暂时是通过节点的 siblingIndex 实现的，如果自定义 layout 有修改这个值的需求，需要重写 layout 的 @shouldUpdateAttributesZIndex 方法，默认情况下会忽略这个配置
     */
    zIndex: number = 0

    /**
     * 节点透明度
     * 备注: 内部通过 UIOpacity 组件实现，会修改节点 UIOpacity 组件的 opacity 值，如果自定义 layout 有修改这个值的需求，需要重写 layout 的 @shouldUpdateAttributesOpacity 方法，默认情况下会忽略这个配置
     */
    opacity: number = null

    /**
     * 节点变换 - 缩放
     */
    scale: math.Vec3 = null

    /**
     * 节点变换 - 平移
     */
    offset: math.Vec3 = null

    /**
     * 节点变换 - 旋转
     * 备注: 3D 变换似乎需要透视相机???
     */
    eulerAngles: math.Vec3 = null
}

/**
 * 布局规则
 * 这里只是约定出了一套接口，内部只是一些基础实现，具体布局方案通过子类重载去实现
 */
export abstract class YXLayout {
    constructor() { }

    /**
     * @required
     * 整个滚动区域大小
     * 需要在 @prepare 内初始化
     */
    contentSize: math.Size = math.Size.ZERO

    /**
     * @required
     * 所有元素的布局属性
     * 需要在 @prepare 内初始化
     */
    attributes: YXLayoutAttributes[] = []

    /**
     * @required
     * 子类重写实现布局方案
     * 注意: 必须初始化滚动区域大小并赋值给 @contentSize 属性
     * 注意: 必须初始化所有的元素布局属性，并保存到 @attributes 数组
     * 可选: 根据 collectionView 的 scrollDirection 支持不同的滚动方向
     */
    abstract prepare(collectionView: YXCollectionView): void

    /**
     * @optional
     * 列表在首次更新数据后会执行这个方法
     * 在这个方法里设置滚动视图的初始偏移量
     */
    initOffset(collectionView: YXCollectionView) { }

    /**
     * @optional
     * 当一次手势拖动结束后会立即调用此方法
     * 实现这个方法的话滚动视图会自动滚动到此方法返回的位置
     * @param touchMoveVelocity 本次手势拖动速度
     * @param startOffset 本次手势拖动开始时滚动视图的偏移位置
     * @returns null 不处理
     */
    targetOffset(collectionView: YXCollectionView, touchMoveVelocity: math.Vec3, startOffset: math.Vec2): { offset: math.Vec2, time: number } { return null }

    /**
     * @optional
     * 列表每次滚动结束后会调用此方法
     * @param collectionView 
     */
    onScrollEnded(collectionView: YXCollectionView) { }

    /**
     * @optional
     * 返回区域内可见的节点属性，并实时的调整这些节点变换效果 (如果在这个方法里调整了节点变换属性，需要重写 shouldUpdateAttributesForBoundsChange 以支持实时变换)
     * 根据实际的布局情况，计算出当前屏幕内需要显示的布局属性
     * 这个方法会直接影响到列表的性能，如果在自定义的时候对性能要求不高(比如明确知道数据量不多的情况下)，可以忽略此方法 (默认会检查所有的布局属性并返回所有的处于可见范围内的单元格布局属性)
     * @param rect 当前滚动视图的可见区域
     */
    layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
        let result = []
        for (let index = 0; index < this.attributes.length; index++) {
            let attr = this.attributes[index]
            if (rect.intersects(attr.frame) == true) {
                result.push(attr)
            }
        }
        return result
    }

    /**
     * @optional
     * 通过索引查找布局属性，默认 Array.find()
     * @param indexPath 
     * @param collectionView 
     */
    layoutAttributesForItemAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView): YXLayoutAttributes {
        return this.attributes.find((a) => a.indexPath.equals(indexPath))
    }

    /**
     * @optional
     * YXCollectionView 在调用 @scrollTo 方法时会触发这个方法，如果实现了这个方法，最终的滚动停止位置以这个方法返回的为准
     * @param indexPath 
     * @returns 滚动视图偏移位置
     */
    scrollTo(indexPath: YXIndexPath, collectionView: YXCollectionView): math.Vec2 { return null }

    /**
     * @optional
     * @see YXLayoutAttributes.zIndex
     * @returns 
     */
    shouldUpdateAttributesZIndex(): boolean { return false }

    /**
     * @optional
     * @see YXLayoutAttributes.opacity
     * @returns 
     */
    shouldUpdateAttributesOpacity(): boolean { return false }

    /**
     * @optional
     * 此布局下的节点，是否需要实时更新变换效果
     * @returns 返回 true 会忽略 YXCollectionView 的 frameInterval 设置，强制在滚动过程中实时更新节点
     */
    shouldUpdateAttributesForBoundsChange(): boolean { return false }
}

/**
 * @see NodePool.poolHandlerComp
 * 节点的自定义组件可以通过这个接口跟 @NodePool 的重用业务关联起来
 */
export interface YXCollectionViewCell extends Component {
    unuse(): void;
    reuse(args: any): void;
}

/**
 * 列表组件
 */
@ccclass('YXCollectionView')
@disallowMultiple(true)
@executionOrder(-1)
@help(`https://gitee.com/568071718/creator-collection-view-doc`)
export class YXCollectionView extends Component {

    /**
     * 版本号
     */
    static get VERSION(): string { return `1.1.2` }

    /**
     * 访问定义的私有枚举
     */
    static Mode = _yx_collection_view_list_mode
    static ScrollDirection = _yx_collection_view_scroll_direction
    static RecyclePolicy = _yx_collection_view_cell_item_pool_policy

    /**
     * 滚动视图组件
     */
    get scrollView(): ScrollView {
        let result = this.node.getComponent(_scroll_view)
        if (result == null) {
            result = this.node.addComponent(_scroll_view)
            // 配置 scroll view 默认参数
        }
        if (result.content == null) {
            let content = new Node(`com.yx.scroll.content`)
            content.parent = result.node
            content.layer = content.parent.layer

            let transform = content.getComponent(UITransform) || content.addComponent(UITransform)
            transform.contentSize = this.node.getComponent(UITransform).contentSize

            result.content = content
        }

        if (this.mask) {
            let mask = result.node.getComponent(Mask)
            if (mask == null) {
                mask = result.node.addComponent(Mask)
                mask.type = Mask.Type.GRAPHICS_RECT
            }
        }

        return result
    }
    private get _scrollView(): _scroll_view { return this.scrollView as _scroll_view }

    /**
     * 自动给挂载节点添加 mask 组件
     */
    @property({ tooltip: `自动给挂载节点添加 mask 组件` })
    mask: boolean = true

    /**
     * 允许手势滚动
     */
    @property({ tooltip: `允许手势滚动` })
    scrollEnabled: boolean = true

    /**
     * 虚拟列表模式
     */
    @property({ type: _yx_collection_view_list_mode, tooltip: `虚拟列表模式 (详细区别查看枚举注释)\nRECYCLE: 根据列表显示范围加载需要的节点\nONCE: 跟 RECYCLE 模式差不多，区别是此模式下每条数据都会对应的生成一个节点而不进行复用\nPRELOAD: 直接加载所有的节点` })
    mode: YXCollectionView.Mode = YXCollectionView.Mode.RECYCLE

    /**
     * 预加载模式专用，每多少帧执行一次预加载，1 表示每帧都执行
     */
    @property({
        tooltip: `预加载模式专用，每多少帧执行一次预加载，1 表示每帧都执行`,
        visible: function (this) {
            return (this.mode == _yx_collection_view_list_mode.PRELOAD)
        }
    })
    preloadInterval: number = 1

    /**
     * 列表滚动方向，默认垂直方向滚动  
     * 自定义 YXLayout 应该尽量根据这个配置来实现不同方向的布局业务  
     * 注意: 如果使用的 YXLayout 未支持对应的滚动方向，则此配置不会生效
     */
    @property({ type: _yx_collection_view_scroll_direction, tooltip: `列表滚动方向` })
    scrollDirection: YXCollectionView.ScrollDirection = YXCollectionView.ScrollDirection.VERTICAL

    /**
     * 回收节点策略  
     * 必须在 register() 之前设置
     */
    @property({ type: _yx_collection_view_cell_item_pool_policy, tooltip: `回收节点策略\n具体区别查看枚举注释` })
    recyclePolicy: YXCollectionView.RecyclePolicy = YXCollectionView.RecyclePolicy.REMOVE

    /**
     * 每多少帧刷新一次可见节点，1 表示每帧都刷
     */
    @property({ tooltip: `每多少帧刷新一次可见节点，1 表示每帧都刷` })
    frameInterval: number = 1

    /**
     * 滚动过程中，每多少帧回收一次不可见节点，1表示每帧都回收，0表示不在滚动过程中回收不可见节点
     * @bug 滚动过程中如果实时的回收不可见节点，有时候会收不到 scroll view 的 cancel 事件，导致 scroll view 的滚动状态不会更新 (且收不到滚动结束事件)
     * @fix 当这个属性设置为 0 时，只会在 `touch-up` 和 `scroll-ended` 里面回收不可见节点  
     * @fix recyclePolicy 设置为 UI_OPACITY 也可以避免这个问题  
     */
    @property({ tooltip: `滚动过程中，每多少帧回收一次不可见节点，1表示每帧都回收，0表示不在滚动过程中回收不可见节点` })
    recycleInterval: number = 1

    /**
     * 此列表上的 cell 节点，是否是 3d cell 节点  
     * 3d 节点的点击事件，需要通过射线检测来做，外部自行配置碰撞体实现节点的点击事件，列表只做个排列展示  
     * https://docs.cocos.com/creator/manual/zh/engine/event/event-input.html#3d-%E7%89%A9%E4%BD%93%E7%9A%84%E8%A7%A6%E6%91%B8%E6%A3%80%E6%B5%8B  
     * 备注: 暂时不支持 2d/3d 节点同时出现在同一个列表内，正确配置此属性避免页面节点事件错乱
     */
    @property({ tooltip: `此列表上的 cell 节点，是否是 3d cell 节点` })
    is3DCell: boolean = false

    /**
     * 通过编辑器注册节点类型
     */
    @property({ type: [_yx_editor_register_cell_info], visible: true, displayName: `Register Cells`, tooltip: `配置此列表内需要用到的 cell 节点类型` })
    private registerCellForEditor: _yx_editor_register_cell_info[] = []

    /**
     * 注册 cell
     * 可多次注册不同种类的 cell，只要确保 @identifier 的唯一性就好
     * @param identifier cell 标识符，通过 @dequeueReusableCell 获取重用 cell 时，会根据这个值匹配
     * @param maker 生成节点，当重用池里没有可用的节点时，会通过这个回调获取节点，需要在这个回调里面生成节点
     * @param poolComp (可选) 节点自定义组件，可以通过这个组件跟 @NodePool 的重用业务关联起来
     */
    register(identifier: string, maker: () => Node, poolComp: (new (...args: any[]) => YXCollectionViewCell) | string | null = null) {
        let pool = new _yx_collection_view_cell_item_pool(poolComp, this.recyclePolicy)
        this.pools.set(identifier, pool)
        this.makers.set(identifier, maker)
    }

    /**
     * 每个注册的标识符对应一个节点池
     */
    private pools: Map<string, _yx_collection_view_cell_item_pool> = new Map()

    /**
     * 每个注册的标识符对应一个生成节点回调
     */
    private makers: Map<string, () => Node> = new Map()

    /**
     * 通过标识符从重用池里取出一个可用的 cell 节点
     * @param identifier 注册时候的标识符
     * @returns 
     */
    dequeueReusableCell(identifier: string): Node {
        let pool = this.pools.get(identifier)
        if (pool == null) {
            throw new Error(`YXCollectionView: 未注册标识符为 \`${identifier}\` 的 cell，请先调用 YXCollectionView 的 register() 方法注册 cell 节点`);
        }
        let result: Node = null
        if (this.mode == _yx_collection_view_list_mode.RECYCLE) {
            result = pool.get()
        }
        if (result == null) {
            // 如果对应的重用池里没有的话，新生成一个
            let maker = this.makers.get(identifier)
            if (maker instanceof Function == false) {
                throw new Error(`YXCollectionView: register() 参数错误，请正确配置 maker 回调函数以生成标识对应的节点`);
            }
            result = maker()
            let cell = result.getComponent(_cell_) || result.addComponent(_cell_)
            cell.identifier = identifier

            if (this.is3DCell == false) {
                result.on(NodeEventType.TOUCH_END, this.onTouchItem, this)
            }
        }
        return result
    }

    /**
     * 内容要分几个区展示，默认 1
     * 没有分区展示的需求可以不管这个配置
     */
    numberOfSections: number | ((collectionView: YXCollectionView) => number) = 1

    /**
     * 每个区里要展示多少条内容
     */
    numberOfItems: number | ((section: number, collectionView: YXCollectionView) => number) = 0

    /**
     * 配置每块内容对应的 UI 节点
     * 在这个方法里，只需要关心 indexPath 这个位置对应的节点应该是用注册过的哪个类型的 Node 节点，然后通过 dequeueReusableCell 生成对应的 Node
     * @example
     * yourList.cellForItemAt = (indexPath, collectionView) => {
     *      return collectionView.dequeueReusableCell(`your identifier`)
     * }
     * 
     * 另外如果节点大小是确定好了的话，也可以直接在这里更新节点的 UI 内容
     * @example
     * yourList.cellForItemAt = (indexPath ,collectionView) => {
     *      let cell = collectionView.dequeueReusableCell(`your identifier`)
     *      let comp = cell.getComponent(YourCellComp)
     *      comp.label.string = `${indexPath}`
     *      return cell
     * }
     * 
     * @returns 注意: 不要在这个方法里创建新的节点对象，这个方法返回的 Node，必须是通过 dequeueReusableCell 匹配到的 Node
     */
    cellForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => Node = null

    /**
     * cell 触发复用之后执行，在这个方法里更新 cell 显示的 UI 内容
     * 如果确定节点大小没什么变化，可以直接在 cellForItemAt 里更新数据，省去这一步，这里主要是为了应付节点大小变化的情况
     * 可以通过 @indexPath 区分 cell 的种类
     * 重要: 如果 cell 的大小不是固定的，需要在这里重新调整子节点的位置，避免布局错乱
     */
    onCellDisplay: (cell: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null

    /**
     * 当 cell 移出当前可见范围后执行
     */
    onCellEndDisplay: (cell: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null

    /**
     * 点击到节点后调用
     * 仅支持 2D 节点
     */
    onTouchItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
    private onTouchItem(ev: EventTouch) {
        if (this.onTouchItemAt) {
            let cell = ev.target.getComponent(_cell_)
            this.onTouchItemAt(cell.attributes.indexPath, this)
        }
    }

    /**
     * 布局规则
     */
    layout: YXLayout = null

    /**
     * 记录当前正在显示的所有节点
     */
    private allVisibleNodes: any = {}

    /**
     * 非回收模式专用，记录已经实例化的所有节点  
     */
    private allNodes: any = {}

    /**
     * 获取当前正在显示的所有节点
     */
    get visibleNodes(): Node[] {
        let result = []
        for (const key in this.allVisibleNodes) {
            result.push(this.allVisibleNodes[key])
        }
        return result
    }

    /**
     * 获取所有正在显示节点的索引
     */
    get visibleIndexPaths(): YXIndexPath[] {
        let result = []
        for (const key in this.allVisibleNodes) {
            const element = this.allVisibleNodes[key]
            let cell = element.getComponent(_cell_)
            result.push(cell.attributes.indexPath.clone())
        }
        return result
    }

    /**
     * 获取当前正在显示的某个节点
     * @param indexPath 
     */
    getVisibleNode(indexPath: YXIndexPath): Node | null {
        return this.allVisibleNodes[indexPath.toString()]
    }

    /**
     * 获取指定节点的索引
     * @param node 
     * @returns 
     */
    getVisibleNodeIndexPath(node: Node): YXIndexPath {
        let cell = node.getComponent(_cell_)
        if (cell) {
            return cell.attributes.indexPath.clone()
        }
        return null
    }

    /**
     * 刷新列表数据
     */
    reloadData() {
        // 校验 layout 参数
        if (this.layout == null) {
            throw new Error("YXCollectionView: 参数错误，请正确配置 layout 以确定布局方案");
        }
        // 立即停止当前滚动
        this.scrollView.stopAutoScroll()

        // 移除掉当前所有节点，准备刷新
        for (const key in this.allVisibleNodes) {
            const element = this.allVisibleNodes[key];
            let cell = element.getComponent(_cell_)
            this.pools.get(cell.identifier).put(element)
            delete this.allVisibleNodes[key]
            if (this.onCellEndDisplay) {
                this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
            }
        }
        this.allVisibleNodes = {}
        this.allNodes = {}

        // 记录一下当前的偏移量，保证数据更新之后位置也不会太偏
        let offset = this.scrollView.getScrollOffset()
        offset.x = -offset.x

        // 重新计算一遍布局属性
        this.layout.prepare(this)

        // 更新 content size
        let contentTransform = this.scrollView.content.getComponent(UITransform) || this.scrollView.content.addComponent(UITransform)
        contentTransform.contentSize = this.layout.contentSize

        // 默认偏移量 或者 恢复偏移量
        if (this.reloadDataCounter <= 0) {
            this.layout.initOffset(this)
        } else {
            let maxOffset = this.scrollView.getMaxScrollOffset()
            math.Vec2.min(offset, offset, maxOffset)
            this.scrollView.scrollToOffset(offset, 0)
        }

        // 更新 cell 节点
        if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
            this._late_preload_offset = new math.Vec2(0, 0)
        }
        this.markForUpdateVisibleData(true)
        this.reloadDataCounter++
    }

    /**
     * 记录 @reloadData 执行了多少次了，用来区分刷新列表的时候是否是首次刷新列表
     */
    private reloadDataCounter: number = 0

    /**
     * 根据当前的可见区域调整需要显示的节点
     */
    private reloadVisibleCells(rect: math.Rect = null, isPreload: boolean = false) {
        // 获取当前可见区域
        if (rect == null) {
            _rectOut.origin = this.scrollView.getScrollOffset()
            _rectOut.x = - _rectOut.x
            _rectOut.size = this.scrollView.view.contentSize
            rect = _rectOut
        }

        // 根据可见区域，找出对应的布局属性
        let layoutAttributes = this.layout.layoutAttributesForElementsInRect(rect, this)

        // 是否需要实时更新节点
        let shouldUpdateAttributesForBoundsChange = this.layout.shouldUpdateAttributesForBoundsChange()

        // 按 zIndex 排序
        let shouldUpdateAttributesZIndex = this.layout.shouldUpdateAttributesZIndex()
        if (shouldUpdateAttributesZIndex) {
            if (layoutAttributes == null || layoutAttributes == this.layout.attributes) {
                layoutAttributes = this.layout.attributes.slice()
            }
            layoutAttributes.sort((a, b) => a.zIndex - b.zIndex)
        }

        /*
        let poolsCounter = 0
        this.pools.forEach((a) => {
            poolsCounter = poolsCounter + a.size()
        })
        log(`需要显示的节点数量: ${layoutAttributes.length}  当前显示的节点数量: ${this.visibleNodes.length}  缓存池里的节点数量: ${poolsCounter}`)
        */

        // 添加需要显示的节点
        for (let index = 0; index < layoutAttributes.length; index++) {
            const element = layoutAttributes[index];

            if (isPreload == false) {
                // 需要显示并且正在显示的
                let visibleNode = this.getVisibleNode(element.indexPath) || this.allNodes[element.indexPath.toString()]
                if (visibleNode && isPreload == false) {
                    this.restoreCellNodeIfNeeds(visibleNode) // 恢复节点状态
                    this.applyLayoutAttributes(visibleNode, element) // 更新节点变换
                    if (shouldUpdateAttributesZIndex) { visibleNode.setSiblingIndex(-1) } // 调整节点层级
                    this.allVisibleNodes[element.indexPath.toString()] = visibleNode // 标记此节点正在显示
                    continue
                }
            } else {
                let node = this.getVisibleNode(element.indexPath) || this.allNodes[element.indexPath.toString()]
                if (node) {
                    continue // 去重
                }
            }

            /**
             * 需要显示但是当前未显示出来的
             * @version 1.1.0 开始，单一 cell 类型的列表支持不实现 cellForItemAt 方法来获取重用节点，这里默认自动执行
             */
            let node = (this.pools.size > 1 || this.cellForItemAt) ? this.cellForItemAt(element.indexPath, this) : this.dequeueReusableCell(this.pools.keys().next().value)
            if (node.parent != this.scrollView.content) {
                node.parent = this.scrollView.content
            }
            this.applyLayoutAttributes(node, element)
            let key = element.indexPath.toString()
            this.allVisibleNodes[key] = node // 标记此节点正在显示
            if (this.mode == _yx_collection_view_list_mode.PRELOAD || this.mode == _yx_collection_view_list_mode.ONCE) {
                this.allNodes[key] = node // 保存节点
            }
            if (this.onCellDisplay) {
                this.onCellDisplay(node, element.indexPath, this)
            }
        }

        layoutAttributes = []
    }

    /**
     * 节点被回收后需要重新使用时，根据当前回收模式恢复节点的状态，保证节点可见
     */
    private restoreCellNodeIfNeeds(node: Node) {
        if (this.recyclePolicy == _yx_collection_view_cell_item_pool_policy.REMOVE) {
            if (node.parent != this.scrollView.content) {
                node.parent = this.scrollView.content
            }
        } else if (this.recyclePolicy == _yx_collection_view_cell_item_pool_policy.UI_OPACITY) {
            let comp = node.getComponent(UIOpacity) || node.addComponent(UIOpacity)
            comp.opacity = 255
        }
    }

    /**
     * 回收不可见节点
     */
    private recycleInvisibleNodes(visibleRect: math.Rect = null) {
        if (visibleRect == null) {
            visibleRect = new math.Rect()
            visibleRect.origin = this.scrollView.getScrollOffset()
            visibleRect.x = - visibleRect.x
            visibleRect.size = this.scrollView.view.contentSize
        }

        const _realFrame = new math.Rect()
        const _contentSize = this.scrollView.content.getComponent(UITransform).contentSize

        for (const key in this.allVisibleNodes) {
            let element = this.allVisibleNodes[key]
            let cell = element.getComponent(_cell_)
            /**
             * @version 1.0.2
             * 检查节点是否可见应该是通过变换后的位置来检查
             * 通过 boundingBox 获取实际变换后的大小
             * 把实际的 position 转换为 origin
             */
            let boundingBox = element.getComponent(UITransform).getBoundingBox()
            _realFrame.size = boundingBox.size
            _realFrame.x = (_contentSize.width - _realFrame.width) * 0.5 + element.position.x
            _realFrame.y = (_contentSize.height - _realFrame.height) * 0.5 - element.position.y
            if (visibleRect.intersects(_realFrame) == false) {
                this.pools.get(cell.identifier).put(element)
                delete this.allVisibleNodes[key]
                if (this.onCellEndDisplay) {
                    this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
                }
            }
        }
    }

    /**
     * 调整节点的位置/变换
     */
    private applyLayoutAttributes(node: Node, attributes: YXLayoutAttributes) {
        let cell = node.getComponent(_cell_)
        cell.attributes = attributes

        let transform = node.getComponent(UITransform) || node.addComponent(UITransform)
        transform.setContentSize(attributes.frame.size)

        _vec3Out.x = - (this.layout.contentSize.width - attributes.frame.width) * 0.5 + attributes.frame.x
        _vec3Out.y = + (this.layout.contentSize.height - attributes.frame.height) * 0.5 - attributes.frame.y
        _vec3Out.z = node.position.z
        if (attributes.offset) {
            math.Vec3.add(_vec3Out, _vec3Out, attributes.offset)
        }
        node.position = _vec3Out

        if (attributes.scale) {
            node.scale = attributes.scale
        }
        if (attributes.eulerAngles) {
            node.eulerAngles = attributes.eulerAngles
        }
        if (this.layout.shouldUpdateAttributesOpacity() && attributes.opacity) {
            let opacity = node.getComponent(UIOpacity) || node.addComponent(UIOpacity)
            opacity.opacity = attributes.opacity
        }
    }

    /**
     * 滚动到指定节点的位置
     * @returns 
     */
    scrollTo(indexPath: YXIndexPath, timeInSecond: number = 0, attenuated: boolean = true) {
        let toOffSet: math.Vec2 = this.layout.scrollTo(indexPath, this)
        if (toOffSet == null) {
            toOffSet = this.layout.layoutAttributesForItemAtIndexPath(indexPath, this)?.frame.origin
        }
        if (toOffSet) {
            this.scrollView.stopAutoScroll()
            this.scrollView.scrollToOffset(toOffSet, timeInSecond, attenuated)
            this.markForUpdateVisibleData()
        }
    }

    /**
     * 生命周期方法
     */
    protected onLoad(): void {
        for (let index = 0; index < this.registerCellForEditor.length; index++) {
            const element = this.registerCellForEditor[index];
            this.register(element.identifier, () => instantiate(element.prefab), element.comp)
        }
        this.node.on(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
        this.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this)
        this.node.on(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
        this.node.on(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)
        this._scrollView._yxOnStartInertiaScroll = this.onStartInertiaScroll.bind(this)
    }

    protected onDestroy(): void {
        this.node.off(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
        this.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this)
        this.node.off(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
        this.node.off(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)

        this.allVisibleNodes = {}
        delete this.allVisibleNodes;

        this.pools.forEach((element) => {
            element.clear()
        })
        this.pools.clear()
        this.makers.clear()

        if (this.layout) {
            this.layout.attributes = []
        }
    }

    private _frameIdx = 0
    protected update(dt: number): void {
        this._frameIdx++
        this.update_reloadVisibleCellsIfNeeds(dt)
        this.update_recycleInvisibleNodesIfNeeds(dt)
        this.update_preloadItemsIfNeeds(dt)
    }

    /**
     * 刷新当前可见节点
     * @param force true: 立即刷新  false: 下帧刷新
     */
    private _late_update_visible_data: boolean = false
    markForUpdateVisibleData(force: boolean = false) {
        if (force) {
            this.reloadVisibleCells()
            return
        }
        this._late_update_visible_data = true
        this._late_recycle_invisible_node = true
    }

    /**
     * 更新可见区域节点逻辑
     */
    private update_reloadVisibleCellsIfNeeds(dt: number) {
        if ((this.frameInterval <= 1) || (this._frameIdx % this.frameInterval == 0)) {
            if (this._late_update_visible_data) {
                this._late_update_visible_data = false
                this.reloadVisibleCells()
            }
        }
    }

    /**
     * 回收不可见节点逻辑
     */
    private _late_recycle_invisible_node = false
    private update_recycleInvisibleNodesIfNeeds(dt: number) {
        if ((this.recycleInterval >= 1) && (this._frameIdx % this.recycleInterval == 0)) {
            if (this._late_recycle_invisible_node) {
                this._late_recycle_invisible_node = false
                this.recycleInvisibleNodes()
            }
        }
    }

    /**
     * 预加载节点
     */
    private _late_preload_offset: math.Vec2 = null
    private update_preloadItemsIfNeeds(dt: number) {

        /**
         * 预加载已完成
         */
        if (this._late_preload_offset == null) {
            return
        }

        /**
         * 分帧加载
         */
        let preloadInterval = this.preloadInterval
        if (preloadInterval < 1) { preloadInterval = 1 }
        if (this._frameIdx % preloadInterval != 0) {
            return
        }

        /**
         * 只有在列表静止的时候才去执行预加载行为
         */
        if (this.scrollView.isScrolling()) {
            return
        }

        if (this.scrollView.isAutoScrolling()) {
            return
        }

        /**
         * 预加载逻辑，暂定为每次加载一页的节点
         */
        let rect = _rectOut
        rect.size = this.scrollView.view.contentSize
        if (this.scrollDirection == YXCollectionView.ScrollDirection.HORIZONTAL) {
            rect.x = this._late_preload_offset.x + rect.size.width
        }
        if (this.scrollDirection == YXCollectionView.ScrollDirection.VERTICAL) {
            rect.y = this._late_preload_offset.y + rect.size.height
        }

        this.reloadVisibleCells(rect, true)
        this._late_recycle_invisible_node = true
        this._late_preload_offset.x = rect.x
        this._late_preload_offset.y = rect.y

        if (this.scrollDirection == YXCollectionView.ScrollDirection.HORIZONTAL) {
            if (rect.xMax > this.layout.contentSize.width) {
                this._late_preload_offset = null
            }
        }
        if (this.scrollDirection == YXCollectionView.ScrollDirection.VERTICAL) {
            if (rect.yMax > this.layout.contentSize.height) {
                this._late_preload_offset = null
            }
        }
    }

    private onScrollBegan() {
    }

    private onScrolling() {
        this.markForUpdateVisibleData(this.layout.shouldUpdateAttributesForBoundsChange())
        this._late_recycle_invisible_node = true
    }

    private onScrollTouchUp() {
        this.recycleInvisibleNodes()
    }

    private onScrollEnded() {
        this.markForUpdateVisibleData()
        this.recycleInvisibleNodes()
        this.layout.onScrollEnded(this)
    }

    private _scroll_offset_on_touch_start: math.Vec2 = null
    private onStartInertiaScroll(touchMoveVelocity: math.Vec3) {
        let endValue = this.layout.targetOffset(this, touchMoveVelocity, this._scroll_offset_on_touch_start)
        if (endValue) {
            this.scrollView.scrollToOffset(endValue.offset, endValue.time)
            this.markForUpdateVisibleData()
        }
    }
}

export namespace YXCollectionView {
    /**
     * 重定义私有枚举类型
     */
    export type Mode = _yx_collection_view_list_mode
    export type ScrollDirection = _yx_collection_view_scroll_direction
    export type RecyclePolicy = _yx_collection_view_cell_item_pool_policy
}