<script lang="tsx">
import Bubble from './xt-bubble.vue';
import BubbleContextProvider from './context';
import useDisplayData from './hooks/useDisplayData';
import useListData from './hooks/useListData';
import useState from '@/hooks/useState';
import pickAttrs from '@/utils/pick-attrs';
import { useEventCallback } from '@/hooks/useEventCallback';
import type { BubbleListProps, BubbleRef, RolesType } from './types';
import {
  computed,
  defineComponent,
  mergeProps,
  nextTick,
  onWatcherCleanup,
  ref,
  unref,
  useAttrs,
  watch,
  watchPostEffect,
} from 'vue';

export default defineComponent({
  name: 'BubbleList',
  inheritAttrs: false,
  props: {},
  setup(_, { attrs, slots, expose }) {
    const props = attrs as unknown as BubbleListProps & { class?: any; style?: any };
    const passThroughAttrs = useAttrs();

    const TOLERANCE = 1;

    const domProps = computed(() => pickAttrs(mergeProps(props as any, passThroughAttrs)));

    const items = ref(props.items);
    const roles = ref(props.roles as RolesType);
    watch(
      () => props.items,
      (val) => (items.value = val),
      { immediate: true },
    );
    watch(
      () => props.roles,
      (val) => (roles.value = val as RolesType),
      { immediate: true },
    );

    const listRef = ref<HTMLDivElement | null>(null);
    const bubbleRefs = ref<Record<string | number, BubbleRef>>({});

    const listPrefixCls = 'xt-bubble-list';

    const [initialized, setInitialized] = useState(false);
    watchPostEffect(() => {
      setInitialized(true);
      onWatcherCleanup(() => setInitialized(false));
    });

    // data
    const mergedData = useListData(items as any, roles as any);
    const [displayData, onTypingComplete] = useDisplayData(mergedData);

    // scroll
    const [scrollReachEnd, setScrollReachEnd] = useState(true);
    const [updateCount, setUpdateCount] = useState(0);

    const onInternalScroll = (e: Event) => {
      const target = e.target as HTMLElement;
      setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
    };

    watch(updateCount, () => {
      if ((props.autoScroll ?? true) && unref(listRef) && unref(scrollReachEnd)) {
        nextTick(() => {
          unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
        });
      }
    });

    watch(
      () => unref(displayData).length,
      () => {
        if (props.autoScroll ?? true) {
          const lastItemKey = unref(displayData)[unref(displayData).length - 2]?.key;
          const bubbleInst = unref(bubbleRefs)[lastItemKey!];
          if (bubbleInst) {
            const { nativeElement } = bubbleInst;
            const { top = 0, bottom = 0 } = nativeElement?.getBoundingClientRect() ?? {};
            const { top: listTop, bottom: listBottom } = unref(listRef)!.getBoundingClientRect();
            const isVisible = top < listBottom && bottom > listTop;
            if (isVisible) {
              setUpdateCount(unref(updateCount) + 1);
              setScrollReachEnd(true);
            }
          }
        }
      },
    );

    const onBubbleUpdate = useEventCallback<void>(() => {
      if (props.autoScroll ?? true) setUpdateCount(unref(updateCount) + 1);
    });
    const context = computed(() => ({ onUpdate: onBubbleUpdate }));

    // 暴露控制方法
    const abortTypingByKey = (key: string | number) => {
      bubbleRefs.value[key]?.abortTyping?.();
    };
    // 对外暴露能力
    expose({
      nativeElement: listRef,
      abortTypingByKey,
      scrollTo: (info: any) => {
        unref(listRef)?.scrollTo?.(info);
      },
    });

    return () => (
      <BubbleContextProvider value={context.value}>
        <div
          {...domProps.value}
          class={[
            listPrefixCls,
            props.rootClassName,
            props.class,
            { [`${listPrefixCls}-reach-end`]: unref(scrollReachEnd) },
          ]}
          style={props.style}
          ref={listRef}
          onScroll={onInternalScroll}
        >
          {unref(displayData).map(({ key, onTypingComplete: onTypingCompleteBubble, ...bubble }) => (
            <Bubble
              {...bubble}
              avatar={slots.avatar ? () => slots.avatar?.({ item: { key, ...bubble } }) : bubble.avatar}
              header={slots.header?.({ item: { key, ...bubble } }) ?? bubble.header}
              footer={slots.footer?.({ item: { key, ...bubble } }) ?? bubble.footer}
              loadingRender={slots.loading ? () => slots.loading({ item: { key, ...bubble } }) : bubble.loadingRender}
              content={slots.message?.({ item: { key, ...bubble } }) ?? bubble.content}
              key={key}
              ref={(node: any) => {
                if (node) {
                  bubbleRefs.value[key] = node as BubbleRef;
                } else {
                  delete bubbleRefs.value[key];
                }
              }}
              typing={unref(initialized) ? bubble.typing : false}
              onTypingComplete={() => {
                onTypingCompleteBubble?.();
                onTypingComplete(key);
              }}
            />
          ))}
        </div>
      </BubbleContextProvider>
    );
  },
});
</script>

<style scoped lang="scss">
@import './style.scss';
</style>
