import { Element, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type PaddingStyle, type Style, type UINodeChild } from "./UINode"

export type UIVirtualizedListStyle = DrawableStyle & ElementStyle

export interface VListConfig<T> {
    /** Стабильный уникальный ключ айтема */
    keyOf: (item: T) => string
    /** Фабрика поддерева. Вызывается при входе айтема в окно; поддерево живёт до выхода из окна */
    render: (item: T) => UINodeChild
    /**
     * Оценка высоты (px) до первого измерения. Число — или функция от айтема,
     * например для чата: msg => 40 + Math.ceil(msg.text.length / 35) * 20
     * Вычисляется один раз при добавлении айтема, в нативку уходит готовое число.
     */
    estimatedHeight: number | ((item: T) => number)
    /** px вокруг вьюпорта, которые держим смонтированными. Default: высота вьюпорта */
    overscan?: number
    /** Чат-режим: первый layout проскроллен в конец, append при положении внизу автоскроллит */
    inverted?: boolean
}

export interface UIVirtualizedList<T = unknown> {
    readonly type: "vlist"
    style: Style<this, UIVirtualizedListStyle>

    /** Полная замена данных. Нативка сама размонтирует исчезнувшие ключи через _syncWindow */
    setData(items: T[]): this
    append(...items: T[]): this
    /** Вставка в начало БЕЗ прыжка скролла (компенсация на стороне нативки) */
    prepend(...items: T[]): this
    /** Обновить айтемы с теми же ключами: re-render, если смонтированы. Высота в кэше сохраняется до повторного измерения */
    update(...items: T[]): this
    removeByKey(...keys: string[]): this

    readonly itemCount: number
    getItem(key: string): T | undefined

    scrollTo(offset: number, animated?: boolean): void
    scrollToKey(key: string, animated?: boolean): void
    scrollToEnd(animated?: boolean): void

    onScroll(callback: (scrollPosition: number) => void): this
    /** Порог в px от нижнего края контента. Срабатывает один раз до ухода от края (защёлка в нативке) */
    onEndReached(thresholdPx: number, callback: () => void): this
    /** Порог от верхнего края — подгрузка истории в чате */
    onStartReached(thresholdPx: number, callback: () => void): this
    onLayout(onLayout: OnLayoutCallback): this

    setClass(name: string, enabled: boolean): this
    toggleClass(name: string): this
    hasClass(name: string): boolean
    bindClass(name: string, fn: () => boolean): this
}

export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {

    // --- читается нативкой в createUINode (copyProperty / copyNodePtr) ---
    _overscan: number               // -1 = "высота вьюпорта", резолвится в нативке
    _inverted: boolean
    _keys: string[] = []            // начальное состояние до маунта ноды
    _estimates: number[] = []       // параллельно _keys
    _endThreshold = 0
    _startThreshold = 0

    // --- списки слушателей, нативка забирает через copyPropertyListPtr ---
    protected sl: ((scrollPosition: number) => void)[] = []     // scroll
    protected erl: (() => void)[] = []                          // endReached
    protected strl: (() => void)[] = []                         // startReached

    // --- приватное состояние JS-стороны ---
    private items = new Map<string, T>()
    private mounted = new Map<string, UINodeChild>()
    private keyOf: (item: T) => string
    private renderItem: (item: T) => UINodeChild
    private estimate: (item: T) => number

    constructor(config: VListConfig<T>) {
        super("vlist", {})
        this.keyOf = config.keyOf
        this.renderItem = config.render
        this.estimate = typeof config.estimatedHeight === "function"
            ? config.estimatedHeight
            : () => config.estimatedHeight as number
        this._overscan = config.overscan ?? -1
        this._inverted = config.inverted ?? false
    }

    private get isAttached(): boolean {
        return !!this._id && this._id !== 0
    }

    private register(items: T[]): [string[], number[]] {
        const keys: string[] = []
        const estimates: number[] = []
        for (const item of items) {
            const k = this.keyOf(item)
            this.items.set(k, item)
            keys.push(k)
            estimates.push(this.estimate(item))
        }
        return [keys, estimates]
    }

    setData(items: T[]): this {
        const prevItems = new Map(this.items)
        const prevMounted = [...this.mounted.keys()]
        this.items.clear()
        const [keys, estimates] = this.register(items)
        this._keys = keys
        this._estimates = estimates
        if (this.isAttached) {
            // нативка диффает по ключам: исчезнувшие смонтированные ключи придут в _syncWindow как unmount
            _creatorUI.vlistSetKeys(this, keys, estimates)
            // setKeys диффает ТОЛЬКО по ключам — пережившие replace смонтированные айтемы сохраняют
            // старое поддерево. Если объект данных изменился, перерисовываем их (как update), иначе
            // строки остаются со старым содержимым. На вебе экран в истории держит айтемы
            // смонтированными, поэтому без этого повторный setData их не обновлял.
            for (const k of prevMounted) {
                if (this.mounted.has(k) && this.items.get(k) !== prevItems.get(k)) {
                    _creatorUI.vlistInvalidate(this, k)
                }
            }
        }
        return this
    }

    append(...items: T[]): this {
        const [keys, estimates] = this.register(items)
        this._keys.push(...keys)
        this._estimates.push(...estimates)
        if (this.isAttached) {
            // inverted + позиция внизу → нативка автоскроллит к концу
            _creatorUI.vlistInsertKeys(this, -1, keys, estimates)
        }
        return this
    }

    prepend(...items: T[]): this {
        const [keys, estimates] = this.register(items)
        this._keys.unshift(...keys)
        this._estimates.unshift(...estimates)
        if (this.isAttached) {
            // нативка компенсирует scrollY на сумму estimates — позиция не прыгает
            _creatorUI.vlistInsertKeys(this, 0, keys, estimates)
        }
        return this
    }

    update(...items: T[]): this {
        for (const item of items) {
            const k = this.keyOf(item)
            if (!this.items.has(k)) continue
            this.items.set(k, item)
            if (this.isAttached && this.mounted.has(k)) {
                // unmount+mount, кэш высоты сохраняется до повторного измерения.
                // Несмонтированный айтем перерендерится сам при входе в окно
                _creatorUI.vlistInvalidate(this, k)
            }
        }
        return this
    }

    removeByKey(...keys: string[]): this {
        for (const k of keys) {
            if (!this.items.delete(k)) continue
            const i = this._keys.indexOf(k)
            if (i >= 0) { this._keys.splice(i, 1); this._estimates.splice(i, 1) }
        }
        if (this.isAttached) {
            _creatorUI.vlistRemoveKeys(this, keys)
        }
        return this
    }

    get itemCount(): number { return this.items.size }
    getItem(key: string): T | undefined { return this.items.get(key) }

    // --- команды: офсеты и позиции знает только нативка (префикс-суммы) ---
    scrollTo(offset: number, animated = true): void {
        _creatorUI.command(this, "scrollTo", offset, animated)
    }
    scrollToKey(key: string, animated = true): void {
        _creatorUI.command(this, "scrollToKey", key, animated)
    }
    scrollToEnd(animated = true): void {
        _creatorUI.command(this, "scrollToEnd", animated)
    }

    onScroll(callback: (scrollPosition: number) => void): this {
        this.sl.push(callback)
        return this
    }
    onEndReached(thresholdPx: number, callback: () => void): this {
        this._endThreshold = thresholdPx
        this.erl.push(callback)
        return this
    }
    onStartReached(thresholdPx: number, callback: () => void): this {
        this._startThreshold = thresholdPx
        this.strl.push(callback)
        return this
    }

    /**
     * Единственный колбэк из нативки (через jsPtr, как у button).
     * Вызывается ТОЛЬКО когда окно пересекло границу — не на каждый кадр скролла.
     * Порядок: сначала unmount (нативка уже снесла поддеревья и обнулила _id,
     * здесь только чистим JS-ссылки), потом mount.
     */
    _syncWindow(unmountKeys: string[], mountKeys: string[]): void {
        for (const k of unmountKeys) {
            this.mounted.delete(k)
        }
        for (const k of mountKeys) {
            const item = this.items.get(k)
            if (item === undefined) continue
            const node = this.renderItem(item)
            this.mounted.set(k, node)
            // createUINode в detached yoga root + измерение (width, NaN) + кэш высоты
            _creatorUI.vlistMount(this, k, node)
        }
    }
}

export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
    return new VListElement<T>(config)
}