import { Element, type Color, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"

export type UIVirtualizedListStyle = DrawableStyle & ElementStyle & {
  /** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
  refreshControlColor?: Color,
  /** Same key and semantics as UIScrollable's; `"none"` keeps a chat composer's keyboard up no
   *  matter how the history is scrolled. */
  keyboardDismissMode?: "interactive" | "scroll" | "none",
}

export interface VListConfig<T> {
    /** Stable unique key of an item. */
    keyOf: (item: T) => string
    /** Subtree factory — called when the item enters the window; the subtree lives until it
     *  leaves. */
    render: (item: T) => UINodeChild
    /** Height estimate (px) used before the first measurement — a number, or a per-item function
     *  (chat: `msg => 40 + Math.ceil(msg.text.length / 35) * 20`). Computed once when the item is
     *  added. */
    estimatedHeight: number | ((item: T) => number)
    /** Extra px around the viewport kept mounted. Default: one viewport height. */
    overscan?: number
    /** Chat mode: the first layout starts scrolled to the end, and appends auto-scroll while the
     *  list sits at the bottom. */
    inverted?: boolean
}

/** A windowed (virtualized) list: only the items near the viewport are mounted — for long feeds
 *  and chats. Items are keyed; feed it via `setData`/`append`/`prepend`. */
export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
    readonly type: "vlist"

    /** Replace the data wholesale — vanished keys are unmounted by the host's window sync. */
    setData(items: T[]): this
    /** Append items to the end (auto-scrolls when `inverted` and resting at the bottom). */
    append(...items: T[]): this
    /** Insert at the start WITHOUT a scroll jump (the host compensates the offset). */
    prepend(...items: T[]): this
    /** Update items with the same keys: mounted ones re-render; the cached height persists until
     *  re-measured. */
    update(...items: T[]): this
    /** Remove items by key. */
    removeByKey(...keys: string[]): this

    /** Number of data items (mounted or not). */
    readonly itemCount: number
    /** The data item under `key`, if any. */
    getItem(key: string): T | undefined

    /** Scroll to `offset` px. */
    scrollTo(offset: number, animated?: boolean): void
    /** Scroll to the item with `key`. */
    scrollToKey(key: string, animated?: boolean): void
    /** Scroll to the end of the content. */
    scrollToEnd(animated?: boolean): void

    /** Scroll position changed (px from the start edge). */
    onScroll(callback: (scrollPosition: number) => void): this
    /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
     *  node mounts (hosts read it at creation). Native hosts only — no-op on web. */
    onRefresh(callback: () => void | Promise<void>): this
    /** Fires within `thresholdPx` of the content's bottom edge — once, re-armed after leaving the
     *  edge (host-side latch). Infinite-scroll loading. */
    onEndReached(thresholdPx: number, callback: () => void): this
    /** Same, for the top edge — chat history loading. */
    onStartReached(thresholdPx: number, callback: () => void): this
}

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

    // --- read by native side in createUINode (copyProperty / copyNodePtr) ---
    _overscan: number
    _inverted: boolean
    _keys: string[] = [] 
    _estimates: number[] = [] 
    _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>) {
        // flexShrink: 1 default (SDK-owned, same as UIScrollable): an explicit height/flexBasis
        // larger than the available space shrinks to fit instead of overflowing. The vlist's own
        // yoga basis is 0 (items live in detached roots), so this is inert until a size is set.
        super("vlist", { flexShrink: 1 })
        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) {
            _creatorUI.vlistSetKeys(this, keys, estimates)

            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) {
            _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) {
            _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)) {
                _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
    }
    // Читается нативкой при маунте (тот же контракт, что у UIScrollable).
    declare _refreshCallback: any
    onRefresh(callback: () => void | Promise<void>): this {
        this._refreshCallback = 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
    }

    /** @internal The one host→JS callback (via jsPtr, like button). Fires ONLY when the window
     *  crossed a boundary — not per scroll frame. Order: unmounts first (the host already tore the
     *  subtrees down and zeroed _id; only JS refs are cleaned here), then mounts. */
    _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)
        }
    }
}

/** Create a virtualized list from a config (`keyOf` + `render` + `estimatedHeight` required),
 *  then feed it via `setData`/`append`/`prepend`. Give it a size (`flexGrow: 1` as a screen body). */
export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
    return new VListElement<T>(config)
}