import { closestByEvent, debounce, html } from '../../utils';
import {
    type BaseAttrs,
    type BaseEmits,
    type Emit2EventMap,
    UiBase
} from '../web-component-base';
import styleStr from './index.css';

export interface Attrs extends BaseAttrs {
    /**
     * The current number in the list. The component will scroll to this number when rendered.
     * @type {number}
     */
    'current-num': number;
    /**
     * The minimum number in the list (include). If not set, there is no minimum limit.
     * @type {number}
     * @default -Infinity
     */
    'min-num'?: number;
    /**
     * The maximum number in the list (include). If not set, there is no maximum limit.
     * @type {number}
     * @default Infinity
     */
    'max-num'?: number;
    // /**
    //  * The position to scroll the current number into view.
    //  * @type {`"center" | "end" | "nearest" | "start"`}
    //  * @default "start"
    //  */
    // position?: ScrollLogicalPosition;
}

export interface Emits extends BaseEmits {
    'select-num': {
        oldNum: number;
        newNum: number;
    };
}
export type EventMap = Emit2EventMap<Emits>;

/**
 * 基础的数字列表组件。允许无限滚动。点击后可以滚动定位到当前数字。
 *
 * 存在一个 formatter 方法，可以重写该方法以自定义数字的显示格式。
 */
export class Ele extends UiBase<Attrs, Emits> {
    public static tagName = 'dt-num-list' as const;
    protected static _style = styleStr;
    protected static _template =
        html`<div class="container" part="container"></div>`;

    static get observedAttributes(): string[] {
        return [
            ...(super.observedAttributes as (keyof BaseAttrs)[]),
            'current-num',
            'min-num',
            'max-num'
        ] satisfies (keyof Attrs)[];
    }

    get _staticEls() {
        return {
            ...super._staticEls,
            container: this.$0`.container`!
        } as const;
    }
    get _dynamicEls() {
        return {
            ...super._dynamicEls,
            currentItem: this.$0`.item-current`,
            items: this.$`.item`
        } as const;
    }

    public get currentNum() {
        return Number(this._getAttr('current-num'));
    }
    public set currentNum(val: number) {
        this.setAttribute('current-num', String(val));
    }
    public get minNum() {
        return Number(this._getAttr('min-num', '-Infinity'));
    }
    public set minNum(val: number) {
        let min = +val;
        if (Number.isNaN(min)) min = Number.NEGATIVE_INFINITY;
        if (min > this.maxNum) [this.maxNum, min] = [min, this.maxNum];
        this.setAttribute('min-num', String(val));
    }
    public get maxNum() {
        return Number(this._getAttr('max-num', 'Infinity'));
    }
    public set maxNum(val: number) {
        let max = +val;
        if (Number.isNaN(max)) max = Number.POSITIVE_INFINITY;
        if (max < this.minNum) [this.minNum, max] = [max, this.minNum];
        this.setAttribute('max-num', String(val));
    }

    private _createItem = (num: number, currentNum = this.currentNum) => {
        const ele = document.createElement('div');
        ele.setAttribute('part', (ele.className = 'item'));
        ele.classList.toggle('item-current', num === currentNum);
        ele.part.toggle('item-current', num === currentNum);
        ele.dataset.number = num + '';
        ele.textContent = this.formatter(num);
        return ele;
    };

    private _intersectionOb: IntersectionObserver | null = null;
    private _destroyOb() {
        this._intersectionOb?.disconnect();
        this._intersectionOb = null;
    }
    private _initOb() {
        this._destroyOb();
        if (!this.shadowRoot) return;
        this._intersectionOb = new IntersectionObserver(
            (
                entries: IntersectionObserverEntry[],
                observer: IntersectionObserver
            ) => {
                const container = this._els.container;
                const firstItem = container.firstElementChild as HTMLElement;
                const lastItem = container.lastElementChild as HTMLElement;
                for (const {
                    target,
                    isIntersecting,
                    intersectionRatio,
                    rootBounds,
                    boundingClientRect
                } of entries) {
                    if (!isIntersecting) continue;
                    observer.unobserve(target);
                    // 只有在滚动的时候才会观察当前元素
                    if (target === this._els.currentItem) {
                        observer.observe(firstItem!);
                        observer.observe(lastItem!);
                        // 继续观察：如果元素没有完全进入可视区域 || 滚动还没有结束
                        if (
                            intersectionRatio !== 1 ||
                            (rootBounds &&
                                boundingClientRect &&
                                Math.abs(
                                    rootBounds.top - boundingClientRect.top
                                ) > 2)
                        ) {
                            observer.observe(target);
                        } else {
                            this._isScrolling = false;
                        }
                    }
                    if (target === firstItem) {
                        this._loadBefore();
                    } else if (target === lastItem) {
                        this._loadAfter();
                    }
                }
            },
            { root: this }
        );
    }

    private _loadBefore = debounce(() => {
        const container = this._els.container;
        const firstItem = container.firstElementChild as HTMLElement;
        const lastItem = container.lastElementChild as HTMLElement;
        const firstNum = Number(firstItem.dataset.number);
        const curNum = this.currentNum;
        const pageSize = this._pageSize;
        const itemHeight = this._itemHeight;
        const items = [...Array(pageSize * 2)].map((_, i) =>
            this._createItem(firstNum - pageSize * 2 + i, curNum)
        );
        this._intersectionOb?.unobserve(firstItem);
        this._intersectionOb?.unobserve(lastItem);
        const scrollTop = this.scrollTop;
        container.prepend(...items);
        for (let i = 0; i < items.length; ++i) {
            container.removeChild(container.lastElementChild!);
        }
        const addedHeight = items.length * itemHeight;
        this.scrollTo({
            top: scrollTop + addedHeight,
            behavior: 'instant'
        });
        this._intersectionOb?.observe(items[0]);
        this._intersectionOb?.observe(container.lastElementChild!);
        if (this._isScrolling) this.scrollToCurrent();
    });
    private _loadAfter = debounce(() => {
        const container = this._els.container;
        const firstItem = container.firstElementChild as HTMLElement;
        const lastItem = container.lastElementChild as HTMLElement;
        const curNum = this.currentNum;
        const pageSize = this._pageSize;
        const itemHeight = this._itemHeight;
        const lastNum = Number(lastItem.textContent);
        const items = [...Array(pageSize * 2)].map((_, i) =>
            this._createItem(lastNum + i + 1, curNum)
        );
        this._intersectionOb?.unobserve(firstItem);
        this._intersectionOb?.unobserve(lastItem);
        const scrollTop = this.scrollTop;
        container.append(...items);
        for (let i = 0; i < items.length; ++i) {
            container.removeChild(container.firstElementChild!);
        }
        const addedHeight = items.length * itemHeight;
        this.scrollTo({
            top: scrollTop - addedHeight,
            behavior: 'instant'
        });
        this._intersectionOb?.observe(container.firstElementChild!);
        this._intersectionOb?.observe(items[items.length - 1]);
        if (this._isScrolling) this.scrollToCurrent();
    });

    public connectedCallback() {
        if (!super.connectedCallback()) return;
        this._render();
        this.addEventListener('click', this._onClick);
    }
    public disconnectedCallback() {
        this.removeEventListener('click', this._onClick);
        this._destroyOb();
        return super.disconnectedCallback();
    }

    protected _onAttrChanged(
        name: string,
        oldValue: string | null,
        newValue: string | null
    ) {
        super._onAttrChanged(name, oldValue, newValue);
        // 选中选项后，会更新 dom class，此时触发的更新不需要重新渲染。
        // 这里是针对无限滚动时重新渲染会导致元素滚动异常。
        if (
            name === 'current-num' &&
            newValue === this._els.currentItem?.dataset.number
        ) {
            return;
        }
        this._render();
    }

    private get _itemHeight() {
        const container = this._els.container;
        const items = Array.from(
            container.querySelectorAll<HTMLElement>('.item')
        );
        const len = items.length;
        if (len === 1) {
            container.append(this._createItem(0, 1));
        } else if (len === 0) {
            container.append(this._createItem(0, 1), this._createItem(0, 1));
        }
        const h = (container.firstElementChild as HTMLElement).offsetHeight;
        const itemNum = Math.max(2, len);
        const gap = Math.max(
            0,
            (container.clientHeight - h * itemNum) / (itemNum - 1)
        );
        if (len === 0) {
            container.removeChild(container.lastElementChild!);
            container.removeChild(container.lastElementChild!);
        } else if (len === 1) {
            container.removeChild(container.lastElementChild!);
        }
        return h + gap;
    }
    private get _pageSize() {
        const thisHeight = this.clientHeight;
        if (thisHeight === 0) return 10;
        return Math.min(10, Math.ceil(thisHeight / this._itemHeight));
    }

    private _isScrolling = false;
    public scrollToCurrent = () => {
        const ele = this._els.currentItem;
        if (!ele) return;
        const thisRect = this.getBoundingClientRect();
        // 如果当前元素不可见，则不执行滚动
        if (thisRect.height === 0) return;
        this._intersectionOb?.observe(ele);
        this._isScrolling = true;
        const eleRect = ele.getBoundingClientRect();
        const offsetTop = eleRect.top - thisRect.top + this.scrollTop;
        this.scrollTo({
            top: offsetTop
        });
    };

    private _render = super._genRenderFn(() => {
        this._destroyOb();
        const container = this._els.container;
        container.innerHTML = '';
        if (!this.hasAttribute('current-num')) return;

        const currentNum = this.currentNum;
        const minNum = this.minNum;
        const maxNum = this.maxNum;

        if (
            minNum === Number.NEGATIVE_INFINITY &&
            maxNum === Number.POSITIVE_INFINITY
        ) {
            this._initOb();
            const pageSize = this._pageSize;
            for (let i = -pageSize * 2; i <= pageSize * 2; ++i) {
                container.appendChild(
                    this._createItem(currentNum + i, currentNum)
                );
            }
        } else
            for (let i = minNum; i <= maxNum; ++i) {
                container.appendChild(this._createItem(i, currentNum));
            }
        setTimeout(this.scrollToCurrent, 0);
    });

    private _onClick = (e: MouseEvent) => {
        if (!this.isConnected) return;
        const container = this._els.container;
        const oldCurrent =
            container.querySelector<HTMLElement>('.item-current');
        const item = closestByEvent(e, '.item', this);
        if (!item || item === oldCurrent) return;
        oldCurrent?.classList.remove('item-current');
        oldCurrent?.part.remove('item-current');
        item.classList.add('item-current');
        item.part.add('item-current');
        this.scrollToCurrent();
        super.dispatchEvent(
            'select-num',
            {
                oldNum: +(oldCurrent?.dataset.number ?? this.currentNum),
                newNum: +item.dataset.number!
            },
            true
        );
    };

    public formatter = (num: number) => '' + num;
}

Ele.define();
