import Header from './Header/Header';
import type {
  GetRowKey,
  ColumnsType,
  TableComponents,
  Key,
  TriggerEventHandler,
  GetComponentProps,
  PanelRender,
  TableLayout,
  RowClassName,
  CustomizeComponent,
  ColumnType,
  CustomizeScrollBody,
  TableSticky,
  ExpandedRowRender,
  RenderExpandIcon,
  TransformCellText,
  DefaultRecordType,
} from './interface';
import Body from './Body';
import useColumns from './hooks/useColumns';
import { useLayoutState, useTimeoutLock } from './hooks/useFrame';
import { getPathValue, mergeObject, validateValue, getColumnsKey } from './utils/valueUtil';
import useStickyOffsets from './hooks/useStickyOffsets';
import ColGroup from './ColGroup';
import Panel from './Panel';
import Footer from './Footer';
import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil';
import { getCellFixedInfo } from './utils/fixUtil';
import StickyScrollBar from './stickyScrollBar';
import useSticky from './hooks/useSticky';
import FixedHolder from './FixedHolder';
import type { CSSProperties } from 'vue';
import {
  computed,
  defineComponent,
  nextTick,
  onMounted,
  reactive,
  ref,
  shallowRef,
  toRef,
  toRefs,
  watch,
  watchEffect,
} from 'vue';
import { warning } from '../vc-util/warning';
import { reactivePick } from '../_util/reactivePick';
import useState from '../_util/hooks/useState';
import { toPx } from '../_util/util';
import isVisible from '../vc-util/Dom/isVisible';
import { getTargetScrollBarSize } from '../_util/getScrollBarSize';
import classNames from '../_util/classNames';
import type { EventHandler } from '../_util/EventInterface';
import VCResizeObserver from '../vc-resize-observer';
import { useProvideTable } from './context/TableContext';
import { useProvideBody } from './context/BodyContext';
import { useProvideResize } from './context/ResizeContext';
import { getDataAndAriaProps } from './utils/legacyUtil';

// Used for conditions cache
const EMPTY_DATA = [];

// Used for customize scroll
const EMPTY_SCROLL_TARGET = {};

export const INTERNAL_HOOKS = 'rc-table-internal-hook';

export interface TableProps<RecordType = DefaultRecordType> {
  prefixCls?: string;
  data?: RecordType[];
  columns?: ColumnsType<RecordType>;
  rowKey?: string | GetRowKey<RecordType>;
  tableLayout?: TableLayout;

  // Fixed Columns
  scroll?: { x?: number | true | string; y?: number | string };

  rowClassName?: string | RowClassName<RecordType>;

  // Additional Part
  title?: PanelRender<RecordType>;
  footer?: PanelRender<RecordType>;
  // summary?: (data: readonly RecordType[]) => any;

  // Customize
  id?: string;
  showHeader?: boolean;
  components?: TableComponents<RecordType>;
  customRow?: GetComponentProps<RecordType>;
  customHeaderRow?: GetComponentProps<ColumnType<RecordType>[]>;
  // emptyText?: any;

  direction?: 'ltr' | 'rtl';

  // Expandable
  expandFixed?: boolean;
  expandColumnWidth?: number;
  expandedRowKeys?: Key[];
  defaultExpandedRowKeys?: Key[];
  expandedRowRender?: ExpandedRowRender<RecordType>;
  expandRowByClick?: boolean;
  expandIcon?: RenderExpandIcon<RecordType>;
  onExpand?: (expanded: boolean, record: RecordType) => void;
  onExpandedRowsChange?: (expandedKeys: Key[]) => void;
  defaultExpandAllRows?: boolean;
  indentSize?: number;
  expandIconColumnIndex?: number;
  expandedRowClassName?: RowClassName<RecordType>;
  childrenColumnName?: string;
  rowExpandable?: (record: RecordType) => boolean;

  // =================================== Internal ===================================
  /**
   * @private Internal usage, may remove by refactor. Should always use `columns` instead.
   *
   * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
   */
  internalHooks?: string;

  /**
   * @private Internal usage, may remove by refactor. Should always use `columns` instead.
   *
   * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
   */
  // Used for antd table transform column with additional column
  transformColumns?: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>;

  /**
   * @private Internal usage, may remove by refactor.
   *
   * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
   */
  internalRefs?: {
    body: HTMLDivElement;
  };

  sticky?: boolean | TableSticky;

  canExpandable?: boolean;

  onUpdateInternalRefs?: (refs: Record<string, any>) => void;

  transformCellText?: TransformCellText<RecordType>;
}

export default defineComponent<TableProps<DefaultRecordType>>({
  name: 'Table',
  inheritAttrs: false,
  props: [
    'prefixCls',
    'data',
    'columns',
    'rowKey',
    'tableLayout',
    'scroll',
    'rowClassName',
    'title',
    'footer',
    'id',
    'showHeader',
    'components',
    'customRow',
    'customHeaderRow',
    'direction',
    'expandFixed',
    'expandColumnWidth',
    'expandedRowKeys',
    'defaultExpandedRowKeys',
    'expandedRowRender',
    'expandRowByClick',
    'expandIcon',
    'onExpand',
    'onExpandedRowsChange',
    'defaultExpandAllRows',
    'indentSize',
    'expandIconColumnIndex',
    'expandedRowClassName',
    'childrenColumnName',
    'rowExpandable',
    'sticky',
    'transformColumns',
    'internalHooks',
    'internalRefs',
    'canExpandable',
    'onUpdateInternalRefs',
    'transformCellText',
  ] as any,
  slots: ['title', 'footer', 'summary', 'emptyText'],
  emits: ['expand', 'expandedRowsChange', 'updateInternalRefs'],
  setup(props, { attrs, slots, emit }) {
    const mergedData = computed(() => props.data || EMPTY_DATA);
    const hasData = computed(() => !!mergedData.value.length);
    // ==================== Customize =====================
    const mergedComponents = computed(() =>
      mergeObject<TableComponents<any>>(props.components, {}),
    );

    const getComponent = (path, defaultComponent?: string) =>
      getPathValue<CustomizeComponent, TableComponents<any>>(mergedComponents.value, path) ||
      defaultComponent;

    const getRowKey = computed(() => {
      const rowKey = props.rowKey;
      if (typeof rowKey === 'function') {
        return rowKey;
      }
      return record => {
        const key = record && record[rowKey];

        if (process.env.NODE_ENV !== 'production') {
          warning(
            key !== undefined,
            'Each record in table should have a unique `key` prop, or set `rowKey` to an unique primary key.',
          );
        }

        return key;
      };
    });

    // ====================== Expand ======================

    const mergedExpandIcon = computed(() => props.expandIcon || renderExpandIcon);

    const mergedChildrenColumnName = computed(() => props.childrenColumnName || 'children');

    const expandableType = computed(() => {
      if (props.expandedRowRender) {
        return 'row';
      }
      /* eslint-disable no-underscore-dangle */
      /**
       * Fix https://github.com/ant-design/ant-design/issues/21154
       * This is a workaround to not to break current behavior.
       * We can remove follow code after final release.
       *
       * To other developer:
       *  Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor
       */
      if (
        props.canExpandable ||
        mergedData.value.some(
          record => record && typeof record === 'object' && record[mergedChildrenColumnName.value],
        )
      ) {
        return 'nest';
      }
      /* eslint-enable */
      return false;
    });

    const innerExpandedKeys = shallowRef([]);
    const stop = watchEffect(() => {
      if (props.defaultExpandedRowKeys) {
        innerExpandedKeys.value = props.defaultExpandedRowKeys;
      }
      if (props.defaultExpandAllRows) {
        innerExpandedKeys.value = findAllChildrenKeys(
          mergedData.value,
          getRowKey.value,
          mergedChildrenColumnName.value,
        );
      }
    });
    // defalutXxxx 仅仅第一次生效
    stop();

    const mergedExpandedKeys = computed(
      () => new Set(props.expandedRowKeys || innerExpandedKeys.value || []),
    );

    const onTriggerExpand: TriggerEventHandler<any> = record => {
      const key = getRowKey.value(record, mergedData.value.indexOf(record));

      let newExpandedKeys: Key[];
      const hasKey = mergedExpandedKeys.value.has(key);
      if (hasKey) {
        mergedExpandedKeys.value.delete(key);
        newExpandedKeys = [...mergedExpandedKeys.value];
      } else {
        newExpandedKeys = [...mergedExpandedKeys.value, key];
      }
      innerExpandedKeys.value = newExpandedKeys;

      emit('expand', !hasKey, record);
      emit('expandedRowsChange', newExpandedKeys);
    };

    const componentWidth = ref(0);

    const [columns, flattenColumns] = useColumns(
      {
        ...toRefs(props),

        // children,
        expandable: computed(() => !!props.expandedRowRender),
        expandedKeys: mergedExpandedKeys,
        getRowKey,
        onTriggerExpand,
        expandIcon: mergedExpandIcon,
      },
      computed(() => (props.internalHooks === INTERNAL_HOOKS ? props.transformColumns : null)),
    );

    const columnContext = computed(() => ({
      columns: columns.value,
      flattenColumns: flattenColumns.value,
    }));

    // ====================== Scroll ======================
    const fullTableRef = ref<HTMLDivElement>();
    const scrollHeaderRef = ref<HTMLDivElement>();
    const scrollBodyRef = ref<HTMLDivElement>();
    const scrollSummaryRef = ref<HTMLDivElement>();
    const [pingedLeft, setPingedLeft] = useState(false);
    const [pingedRight, setPingedRight] = useState(false);
    const [colsWidths, updateColsWidths] = useLayoutState(new Map<Key, number>());

    // Convert map to number width
    const colsKeys = computed(() => getColumnsKey(flattenColumns.value));
    const colWidths = computed(() =>
      colsKeys.value.map(columnKey => colsWidths.value.get(columnKey)),
    );
    const columnCount = computed(() => flattenColumns.value.length);
    const stickyOffsets = useStickyOffsets(colWidths, columnCount, toRef(props, 'direction'));
    const fixHeader = computed(() => props.scroll && validateValue(props.scroll.y));
    const horizonScroll = computed(
      () => (props.scroll && validateValue(props.scroll.x)) || Boolean(props.expandFixed),
    );
    const fixColumn = computed(
      () => horizonScroll.value && flattenColumns.value.some(({ fixed }) => fixed),
    );

    // Sticky
    const stickyRef = ref<{ setScrollLeft: (left: number) => void }>();
    const stickyState = useSticky(toRef(props, 'sticky'), toRef(props, 'prefixCls'));

    const summaryFixedInfos = reactive<Record<string, boolean | string>>({});
    const fixFooter = computed(() => {
      const info = Object.values(summaryFixedInfos)[0];
      return (fixHeader.value || stickyState.value.isSticky) && info;
    });

    const summaryCollect = (uniKey: string, fixed: boolean | string) => {
      if (fixed) {
        summaryFixedInfos[uniKey] = fixed;
      } else {
        delete summaryFixedInfos[uniKey];
      }
    };

    // Scroll
    const scrollXStyle = ref<CSSProperties>({});
    const scrollYStyle = ref<CSSProperties>({});
    const scrollTableStyle = ref<CSSProperties>({});

    watchEffect(() => {
      if (fixHeader.value) {
        scrollYStyle.value = {
          overflowY: 'scroll',
          maxHeight: toPx(props.scroll.y),
        };
      }

      if (horizonScroll.value) {
        scrollXStyle.value = { overflowX: 'auto' };
        // When no vertical scrollbar, should hide it
        // https://github.com/ant-design/ant-design/pull/20705
        // https://github.com/ant-design/ant-design/issues/21879
        if (!fixHeader.value) {
          scrollYStyle.value = { overflowY: 'hidden' };
        }
        scrollTableStyle.value = {
          width: props.scroll.x === true ? 'auto' : toPx(props.scroll.x),
          minWidth: '100%',
        };
      }
    });

    const onColumnResize = (columnKey: Key, width: number) => {
      if (isVisible(fullTableRef.value)) {
        updateColsWidths(widths => {
          if (widths.get(columnKey) !== width) {
            const newWidths = new Map(widths);
            newWidths.set(columnKey, width);
            return newWidths;
          }
          return widths;
        });
      }
    };

    const [setScrollTarget, getScrollTarget] = useTimeoutLock(null);

    function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) {
      if (!target) {
        return;
      }

      if (typeof target === 'function') {
        target(scrollLeft);
        return;
      }
      const domTarget = (target as any).$el || target;
      if (domTarget.scrollLeft !== scrollLeft) {
        // eslint-disable-next-line no-param-reassign
        domTarget.scrollLeft = scrollLeft;
      }
    }

    const onScroll: EventHandler = ({
      currentTarget,
      scrollLeft,
    }: {
      currentTarget: HTMLElement;
      scrollLeft?: number;
    }) => {
      const isRTL = props.direction === 'rtl';
      const mergedScrollLeft =
        typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft;

      const compareTarget = currentTarget || EMPTY_SCROLL_TARGET;
      if (!getScrollTarget() || getScrollTarget() === compareTarget) {
        setScrollTarget(compareTarget);

        forceScroll(mergedScrollLeft, scrollHeaderRef.value);
        forceScroll(mergedScrollLeft, scrollBodyRef.value);
        forceScroll(mergedScrollLeft, scrollSummaryRef.value);
        forceScroll(mergedScrollLeft, stickyRef.value?.setScrollLeft);
      }

      if (currentTarget) {
        const { scrollWidth, clientWidth } = currentTarget;
        if (isRTL) {
          setPingedLeft(-mergedScrollLeft < scrollWidth - clientWidth);
          setPingedRight(-mergedScrollLeft > 0);
        } else {
          setPingedLeft(mergedScrollLeft > 0);
          setPingedRight(mergedScrollLeft < scrollWidth - clientWidth);
        }
      }
    };

    const triggerOnScroll = () => {
      if (scrollBodyRef.value) {
        onScroll({ currentTarget: scrollBodyRef.value });
      }
    };
    let timtout;
    const onFullTableResize = ({ width }) => {
      clearTimeout(timtout);
      timtout = setTimeout(() => {
        if (width !== componentWidth.value) {
          triggerOnScroll();
          componentWidth.value = fullTableRef.value ? fullTableRef.value.offsetWidth : width;
        }
      }, 100);
    };

    watch([horizonScroll, () => props.data, () => props.columns], () => {
      if (horizonScroll.value) {
        triggerOnScroll();
      }
    });

    const [scrollbarSize, setScrollbarSize] = useState(0);

    onMounted(() => {
      nextTick(() => {
        triggerOnScroll();
        setScrollbarSize(getTargetScrollBarSize(scrollBodyRef.value).width);
      });
    });

    watchEffect(
      () => {
        if (props.internalHooks === INTERNAL_HOOKS && props.internalRefs) {
          props.onUpdateInternalRefs({
            body: scrollBodyRef.value
              ? (scrollBodyRef.value as any).$el || scrollBodyRef.value
              : null,
          });
        }
      },
      { flush: 'post' },
    );

    // Table layout
    const mergedTableLayout = computed(() => {
      if (props.tableLayout) {
        return props.tableLayout;
      }
      // https://github.com/ant-design/ant-design/issues/25227
      // When scroll.x is max-content, no need to fix table layout
      // it's width should stretch out to fit content
      if (fixColumn.value) {
        return props.scroll.x === 'max-content' ? 'auto' : 'fixed';
      }
      if (
        fixHeader.value ||
        stickyState.value.isSticky ||
        flattenColumns.value.some(({ ellipsis }) => ellipsis)
      ) {
        return 'fixed';
      }
      return 'auto';
    });

    const emptyNode = () => {
      return hasData.value ? null : slots.emptyText?.() || 'No Data';
    };
    useProvideTable(
      reactive({
        ...toRefs(reactivePick(props, 'prefixCls', 'direction', 'transformCellText')),
        getComponent,
        scrollbarSize,
        fixedInfoList: computed(() =>
          flattenColumns.value.map((_, colIndex) =>
            getCellFixedInfo(
              colIndex,
              colIndex,
              flattenColumns.value,
              stickyOffsets.value,
              props.direction,
            ),
          ),
        ),
        isSticky: computed(() => stickyState.value.isSticky),
        summaryCollect,
      }),
    );
    useProvideBody(
      reactive({
        ...toRefs(
          reactivePick(
            props,
            'rowClassName',
            'expandedRowClassName',
            'expandRowByClick',
            'expandedRowRender',
            'expandIconColumnIndex',
            'indentSize',
          ),
        ),
        columns,
        flattenColumns,
        tableLayout: mergedTableLayout,
        componentWidth,
        fixHeader,
        fixColumn,
        horizonScroll,
        expandIcon: mergedExpandIcon,
        expandableType,
        onTriggerExpand,
      }),
    );

    useProvideResize({
      onColumnResize,
    });

    // Body
    const bodyTable = () => (
      <Body
        data={mergedData.value}
        measureColumnWidth={fixHeader.value || horizonScroll.value || stickyState.value.isSticky}
        expandedKeys={mergedExpandedKeys.value}
        rowExpandable={props.rowExpandable}
        getRowKey={getRowKey.value}
        customRow={props.customRow}
        childrenColumnName={mergedChildrenColumnName.value}
        v-slots={{ emptyNode }}
      />
    );

    const bodyColGroup = () => (
      <ColGroup
        colWidths={flattenColumns.value.map(({ width }) => width)}
        columns={flattenColumns.value}
      />
    );
    return () => {
      const {
        prefixCls,
        scroll,
        tableLayout,
        direction,

        // Additional Part
        title = slots.title,
        footer = slots.footer,

        // Customize
        id,
        showHeader,
        customHeaderRow,
      } = props;
      const { isSticky, offsetHeader, offsetSummary, offsetScroll, stickyClassName, container } =
        stickyState.value;
      const TableComponent = getComponent(['table'], 'table');
      const customizeScrollBody = getComponent(['body']) as unknown as CustomizeScrollBody<any>;
      const summaryNode = slots.summary?.({ pageData: mergedData.value });

      let groupTableNode = () => null;

      // Header props
      const headerProps = {
        colWidths: colWidths.value,
        columCount: flattenColumns.value.length,
        stickyOffsets: stickyOffsets.value,
        customHeaderRow,
        fixHeader: fixHeader.value,
        scroll,
      };

      if (
        process.env.NODE_ENV !== 'production' &&
        typeof customizeScrollBody === 'function' &&
        hasData.value &&
        !fixHeader.value
      ) {
        warning(false, '`components.body` with render props is only work on `scroll.y`.');
      }
      if (fixHeader.value || isSticky) {
        // >>>>>> Fixed Header
        let bodyContent = () => null;

        if (typeof customizeScrollBody === 'function') {
          bodyContent = () =>
            customizeScrollBody(mergedData.value, {
              scrollbarSize: scrollbarSize.value,
              ref: scrollBodyRef,
              onScroll,
            });

          headerProps.colWidths = flattenColumns.value.map(({ width }, index) => {
            const colWidth =
              index === columns.value.length - 1 ? (width as number) - scrollbarSize.value : width;
            if (typeof colWidth === 'number' && !Number.isNaN(colWidth)) {
              return colWidth;
            }
            warning(
              false,
              'When use `components.body` with render props. Each column should have a fixed `width` value.',
            );

            return 0;
          }) as number[];
        } else {
          bodyContent = () => (
            <div
              style={{
                ...scrollXStyle.value,
                ...scrollYStyle.value,
              }}
              onScroll={onScroll}
              ref={scrollBodyRef}
              class={classNames(`${prefixCls}-body`)}
            >
              <TableComponent
                style={{
                  ...scrollTableStyle.value,
                  tableLayout: mergedTableLayout.value,
                }}
              >
                {bodyColGroup()}
                {bodyTable()}
                {!fixFooter.value && summaryNode && (
                  <Footer stickyOffsets={stickyOffsets.value} flattenColumns={flattenColumns.value}>
                    {summaryNode}
                  </Footer>
                )}
              </TableComponent>
            </div>
          );
        }

        // Fixed holder share the props
        const fixedHolderProps = {
          noData: !mergedData.value.length,
          maxContentScroll: horizonScroll.value && scroll.x === 'max-content',
          ...headerProps,
          ...columnContext.value,
          direction,
          stickyClassName,
          onScroll,
        };

        groupTableNode = () => (
          <>
            {/* Header Table */}
            {showHeader !== false && (
              <FixedHolder
                {...fixedHolderProps}
                stickyTopOffset={offsetHeader}
                class={`${prefixCls}-header`}
                ref={scrollHeaderRef}
                v-slots={{
                  default: fixedHolderPassProps => (
                    <>
                      <Header {...fixedHolderPassProps} />
                      {fixFooter.value === 'top' && (
                        <Footer {...fixedHolderPassProps}>{summaryNode}</Footer>
                      )}
                    </>
                  ),
                }}
              ></FixedHolder>
            )}

            {/* Body Table */}
            {bodyContent()}

            {/* Summary Table */}
            {fixFooter.value && fixFooter.value !== 'top' && (
              <FixedHolder
                {...fixedHolderProps}
                stickyBottomOffset={offsetSummary}
                class={`${prefixCls}-summary`}
                ref={scrollSummaryRef}
                v-slots={{
                  default: fixedHolderPassProps => (
                    <Footer {...fixedHolderPassProps}>{summaryNode}</Footer>
                  ),
                }}
              ></FixedHolder>
            )}

            {isSticky && scrollBodyRef.value && (
              <StickyScrollBar
                ref={stickyRef}
                offsetScroll={offsetScroll}
                scrollBodyRef={scrollBodyRef}
                onScroll={onScroll}
                container={container}
              />
            )}
          </>
        );
      } else {
        // >>>>>> Unique table
        groupTableNode = () => (
          <div
            style={{
              ...scrollXStyle.value,
              ...scrollYStyle.value,
            }}
            class={classNames(`${prefixCls}-content`)}
            onScroll={onScroll}
            ref={scrollBodyRef}
          >
            <TableComponent
              style={{ ...scrollTableStyle.value, tableLayout: mergedTableLayout.value }}
            >
              {bodyColGroup()}
              {showHeader !== false && <Header {...headerProps} {...columnContext.value} />}
              {bodyTable()}
              {summaryNode && (
                <Footer stickyOffsets={stickyOffsets.value} flattenColumns={flattenColumns.value}>
                  {summaryNode}
                </Footer>
              )}
            </TableComponent>
          </div>
        );
      }
      const ariaProps = getDataAndAriaProps(attrs);
      const fullTable = () => (
        <div
          {...ariaProps}
          class={classNames(prefixCls, {
            [`${prefixCls}-rtl`]: direction === 'rtl',
            [`${prefixCls}-ping-left`]: pingedLeft.value,
            [`${prefixCls}-ping-right`]: pingedRight.value,
            [`${prefixCls}-layout-fixed`]: tableLayout === 'fixed',
            [`${prefixCls}-fixed-header`]: fixHeader.value,
            /** No used but for compatible */
            [`${prefixCls}-fixed-column`]: fixColumn.value,
            [`${prefixCls}-scroll-horizontal`]: horizonScroll.value,
            [`${prefixCls}-has-fix-left`]: flattenColumns.value[0] && flattenColumns.value[0].fixed,
            [`${prefixCls}-has-fix-right`]:
              flattenColumns.value[columnCount.value - 1] &&
              flattenColumns.value[columnCount.value - 1].fixed === 'right',
            [attrs.class as string]: attrs.class,
          })}
          style={attrs.style}
          id={id}
          ref={fullTableRef}
        >
          {title && <Panel class={`${prefixCls}-title`}>{title(mergedData.value)}</Panel>}
          <div class={`${prefixCls}-container`}>{groupTableNode()}</div>
          {footer && <Panel class={`${prefixCls}-footer`}>{footer(mergedData.value)}</Panel>}
        </div>
      );

      if (horizonScroll.value) {
        return (
          <VCResizeObserver
            onResize={onFullTableResize}
            v-slots={{ default: fullTable }}
          ></VCResizeObserver>
        );
      }
      return fullTable();
    };
  },
});
