import React, { forwardRef, useContext } from "react";
import warning from "warning";
import classNames from "classnames";
import { TableAddon, TableProps, RowRenderContext } from "../TableProps";
import { ChangeContext, ControlledProps } from "../../form/controlled";
import {
  CheckTreeRelation,
  CheckTree,
  completeDisabledNames,
} from "../../checktree";
import { Checkbox } from "../../checkbox";
import { getRowKeyFromRecordKey } from "../util/get-row-key-from-record-key";
import { CheckContext } from "../../check";
import { injectPropsIfTargetNotExisted } from "../util/inject-props-if-target-not-existed";
import { useConfig } from "../../_util/config-context";

export interface SelectableChangeContext<Record = any> extends ChangeContext {
  /**
   * 当前被改变项 Key
   *
   * **多层展开表格下子行只能获取到 `recordKey`, 无法获取行数据**
   *
   * @since 2.7.2
   */
  recordKey: Record;

  /**
   * 当前被改变项是否选中
   * @since 2.7.2
   */
  checked: boolean;

  /**
   * 当前被改变项数据
   */
  record: Record;

  /**
   * 被改变数据
   */
  selectedRecords: Record[];

  /**
   * 发生变化的选项的更多信息
   */
  check: { name?: string; value?: boolean; all?: boolean };
}

/**
 * `selectable` 插件用于支持表格可选择行的样式及操作。
 */
export interface SelectableOptions<Record = any>
  extends ControlledProps<
    string[],
    React.SyntheticEvent,
    SelectableChangeContext<Record>
  > {
  /**
   * 不支持非受控模式
   */
  defaultValue?: never;

  /**
   * 提供 `relations` 属性，则会按照树状选择逻辑进行
   */
  relations?: CheckTreeRelation;

  /**
   * 提供 `all` 属性来支持全选
   * 给定字符串可以用作全选项的 `key`
   */
  all?: boolean | string;

  /**
   * 如果开启了全选支持，则可以指定哪些记录从全选的范围内排除
   *
   * - 默认为 `disabled` 可以排除禁用的记录
   * - 提供回调则自定义哪些记录应该排除，对于应该排除的记录，应该返回 `true`
   *
   * @default "disabled"
   */
  shouldRecordExcludeFromAll?: "disabled" | ((record: any) => boolean);

  /**
   * 提供一个列的 `key`，将选择组件插入到一个目标列
   *
   * 默认在最前新建一列插入
   */
  targetColumnKey?: string;

  /**
   * **\[Deprecated\]** 请使用 `indentable` 插件代替
   *
   * @deprecated
   */
  indent?: number;

  /**
   * 是否整行可选
   */
  rowSelect?: boolean;

  /**
   * 列宽度，可以指定 CSS 属性或数字 (单位：px)
   * @default 26
   */
  width?: string | number;

  /**
   * **高级用法**
   * 更改该插件的在每行的渲染内容，`element` 为默认渲染内容，`context` 中包含该行数据相关信息
   * @default x => x
   */
  render?: (
    element: JSX.Element,
    context: RowRenderContext<Record>
  ) => React.ReactNode;

  /**
   * 指定行的选择是否可用
   *
   * - 使用 Table 中的 `rowDisabled` 可将整行置灰并禁用选择
   *
   * - `2.5.0` 新增 `record` 返回，但展开行无法获取该值
   *
   * @default () => true
   */
  rowSelectable?: (
    rowKey: string,
    context: { parent: string; children: string[]; record?: Record }
  ) => boolean;
}

const fallbackColumnKey = "__selectable_addon__";
const fallbackAllKey = "__selectable_all__";

export function selectable(options: SelectableOptions): TableAddon {
  const {
    relations = {},
    value,
    onChange,
    all = true,
    targetColumnKey,
    indent,
    rowSelect,
    width = 26,
    render = x => x,
    shouldRecordExcludeFromAll = "disabled",
    rowSelectable = () => true,
  } = options;

  if (typeof indent !== "undefined") {
    warning(
      false,
      "Property `indent` is deprecated. Please use `indentable` addon instead."
    );
  }

  const allKey = typeof all === "string" ? all : fallbackAllKey;

  let rowDisabled: TableProps["rowDisabled"] = null;

  const childrenMap = new Map<string, Set<string>>();

  for (const [child, parent] of Object.entries(relations)) {
    let childrenSet = childrenMap.get(parent);
    if (!childrenSet) {
      childrenSet = new Set<string>();
      childrenMap.set(parent, childrenSet);
    }
    childrenSet.add(child);
  }

  const relationsKeys = Object.keys(relations);

  return {
    getInfo: () => ({ name: "selectable" }),
    onInjectProps: props => {
      let { columns } = props;

      rowDisabled = props.rowDisabled || (() => false);

      columns = injectPropsIfTargetNotExisted(columns, targetColumnKey, {
        key: fallbackColumnKey,
        width,
        header: null,
        render: () => null,
      });

      return { ...props, columns };
    },
    onInjectColumn: previous => (
      record,
      rowKey,
      recordIndex,
      column,
      columnIndex
    ) => {
      // 不是目标列
      if (column.key !== targetColumnKey && column.key !== fallbackColumnKey) {
        return previous(record, rowKey, recordIndex, column, columnIndex);
      }

      const { children: preChildren, props, ...result } = previous(
        record,
        rowKey,
        recordIndex,
        column,
        columnIndex
      );

      let children = preChildren;

      // 表头
      if (recordIndex === -1) {
        children = (
          <>
            <Checkbox
              style={all ? undefined : { display: "none" }}
              name={allKey}
            >
              {children || <>&nbsp;</>}
            </Checkbox>
            {!all ? children : null}
          </>
        );
      }
      // 记录行
      else {
        let paddingLeft = 0;
        let depth = 0;
        if (indent > 0) {
          let node = rowKey;
          while (relations && relations[node]) {
            depth += 1;
            node = relations[node];
          }
          paddingLeft = indent * depth;
        }

        const checkbox = (
          <TableCheckbox name={rowKey}>{children || <>&nbsp;</>}</TableCheckbox>
        );

        const element = paddingLeft ? (
          <div style={{ paddingLeft }}>{checkbox}</div>
        ) : (
          checkbox
        );

        children = render(element, {
          children: preChildren,
          record,
          depth,
          rowKey,
          recordIndex,
          disabled: rowDisabled(record),
        });
      }
      return { ...result, props, children };
    },
    onInjectTable: renderBody => props => {
      const { recordKey, records, rowDisabled = () => false } = props;
      const rowKey = getRowKeyFromRecordKey(recordKey);

      // 哪些应该从 all 里面排除
      let shouldRecordExcludeFromAllFinal: (record: any) => boolean = null;
      if (typeof shouldRecordExcludeFromAll === "function") {
        shouldRecordExcludeFromAllFinal = shouldRecordExcludeFromAll;
      } else if (shouldRecordExcludeFromAll === "disabled") {
        shouldRecordExcludeFromAllFinal = rowDisabled;
      }
      shouldRecordExcludeFromAllFinal =
        shouldRecordExcludeFromAllFinal || (() => false);

      const recordRowKeys: string[] = [];
      const recordMap = new Map<string, any>();
      records.forEach((record, index) => {
        const key = rowKey(record, index);
        recordRowKeys.push(key);
        recordMap.set(key, record);
      });

      const disabledKeys = Array.from([
        ...relationsKeys,
        ...recordRowKeys,
      ]).filter(
        key =>
          !rowSelectable(key, {
            parent: relations[key],
            children: childrenMap.has(relations[key])
              ? [...childrenMap.get(relations[key])]
              : [],
            record: recordMap.get(key),
          })
      );

      const disabledKeySet = new Set(disabledKeys);

      // all keys
      const keys = Array.from(
        new Set([
          ...relationsKeys,
          // 这里 records 不包括扩展行
          ...records
            .filter(x => !shouldRecordExcludeFromAllFinal(x))
            .map(rowKey)
            .filter(x =>
              shouldRecordExcludeFromAll === "disabled"
                ? !disabledKeySet.has(x)
                : true
            ),
        ])
      );

      // keys without children
      const rootKeys = relations ? keys.filter(key => !relations[key]) : keys;

      const relationWithLeafs = {
        ...(relations || {}),
      };

      for (const key of rootKeys) {
        relationWithLeafs[key] = allKey;
      }

      // 只有全选 Check 可用时
      const hasRelations = !!Object.keys(relationWithLeafs).length;

      const disabledNames = [
        ...records.filter(x => rowDisabled(x)).map(rowKey),
        ...disabledKeys,
      ];
      const disabledSet = completeDisabledNames(disabledNames, childrenMap);

      return (
        <CheckTree
          shiftSelect="virtualized"
          relations={relationWithLeafs}
          value={value}
          onChange={
            hasRelations
              ? (value, context) => {
                  let newValue = value;
                  // 处理连选
                  if (context.shiftSelect) {
                    const { startName, endName } = context.shiftSelect;
                    const section = new Set(value);
                    let inSection;
                    records.forEach((record, index) => {
                      const key = rowKey(record, index);
                      if (inSection && !disabledSet.has(key)) {
                        section.add(key);
                      }
                      if (key === startName || key === endName) {
                        inSection = !inSection;
                        if (inSection && !disabledSet.has(key)) {
                          section.add(key);
                        }
                      }
                    });
                    newValue = [...section];
                  }
                  const newValueSet = new Set(newValue);
                  const name =
                    context.check.name || context.shiftSelect?.endName;
                  onChange(newValue, {
                    ...context,
                    check: {
                      all: context.check.name === allKey,
                      ...context.check,
                    },
                    checked: context.check.value,
                    recordKey: name,
                    record: records.find(
                      (record, index) => name === rowKey(record, index)
                    ),
                    selectedRecords: records.filter((record, index) =>
                      newValueSet.has(rowKey(record, index))
                    ),
                  });
                }
              : null
          }
          disabledNames={disabledNames}
        >
          {renderBody(props)}
        </CheckTree>
      );
    },
    onInjectRow: renderRow => (record, rowKey, recordIndex, columns) => {
      const { prepends, appends, row: preRow } = renderRow(
        record,
        rowKey,
        recordIndex,
        columns
      );

      let row = preRow;

      // 支持整行选择
      row = (
        <SelectWrapper
          key={rowKey}
          name={rowKey}
          rowSelect={rowSelect && !rowDisabled(record)}
        >
          {row}
        </SelectWrapper>
      );

      return { prepends, row, appends };
    },
  };
}

const SelectWrapper = forwardRef(function SelectWrapper(
  {
    name,
    rowSelect,
    children,
    ...props
  }: {
    name: string;
    rowSelect: boolean;
    children: React.ReactElement<React.HTMLAttributes<HTMLTableRowElement>>;
  } & React.HTMLAttributes<HTMLTableRowElement>,
  ref
): JSX.Element {
  const context = useContext(CheckContext);
  if (context) {
    const checkProps = context.inject({ type: "checkbox", name });
    const { onChange, value } = checkProps;

    const rowSelectProps = {
      onClick: (event: React.MouseEvent<HTMLTableRowElement>) => {
        // 事件合并
        if (typeof props.onClick === "function") {
          props.onClick(event);
        }
        if (typeof children.props.onClick === "function") {
          children.props.onClick(event);
        }
        return onChange(!value, {
          event,
          check: { type: "checkbox", name, value: !value, all: false } as any,
        });
      },
    };

    return React.cloneElement(children, {
      ...props,
      ref,
      className: classNames(props.className, children.props.className, {
        "is-selected": !!value,
      }),
      ...(rowSelect ? rowSelectProps : {}),
    });
  }

  return children;
});

function TableCheckbox({ children, ...props }) {
  const { classPrefix } = useConfig();
  return (
    <Checkbox className={`${classPrefix}-form-check--table-select`} {...props}>
      {children}
    </Checkbox>
  );
}
