import React, {
  createContext,
  useRef,
  useState,
  forwardRef,
  useContext,
} from "react";
import classNames from "classnames";
import { TableColumn } from "../../TableProps";
import { getElementOffset } from "../../../_util/get-element-offset";
import { mergeRefs } from "../../../_util/merge-refs";
/**
 * 拖拽 Context
 */
interface DragContextValue {
  /**
   * 供表格内容区设置其 DOM Ref
   */
  setBodyRef?: (element: HTMLDivElement) => void;

  /**
   * 表格body区Ref
   */
  bodyRef?: React.RefObject<HTMLDivElement>;

  /**
   * 供表格body区遮罩设置其 DOM Ref
   */
  setBodyMaskRef?: (element: HTMLDivElement) => void;

  /**
   * 表格body区遮罩Ref
   */
  bodyMaskRef?: React.RefObject<HTMLDivElement>;

  /**
   * 供表格body区高亮插入线设置其 DOM Ref
   */
  setBodyLineRef?: (element: HTMLDivElement) => void;

  /**
   * 高亮插入线Ref
   */
  bodyLineRef?: React.RefObject<HTMLDivElement>;

  /**
   * 拖拽的列名称
   */
  draggingColumn?: string;

  /**
   * 设置拖拽列名称
   */
  setDraggingColumn?: React.Dispatch<React.SetStateAction<string>>;

  /**
   * 列的类名称
   */
  columnClassNames?: React.ComponentState;

  /**
   * 列的类名称
   */
  columns?: TableColumn<any>[];

  /**
   * 设置列的类名称
   */
  setColumns?: React.Dispatch<React.SetStateAction<TableColumn[]>>;

  /**
   * 事件句柄对象
   */
  dragEventHandlers?: React.DOMAttributes<HTMLTableDataCellElement>;

  /**
   * 可拖拽的列, 默认所有列
   * @default all
   */
  draggableColumns?: string[] | "all";
}

/**
 * 拖拽 Context 实例
 */
export const DragContext = createContext<DragContextValue>({});
DragContext.displayName = "DragContext";

/**
 * 注入到外层容器
 */
interface DraggableTableProps {
  /**
   * 表格
   */
  table: JSX.Element;

  /**
   * 可拖拽的列, 默认所有列
   * @default all
   */
  draggableColumns?: string[] | "all";

  /**
   * 拖拽完成回调
   */
  onDragEnd?: (columns: TableColumn[]) => void;

  /**
   * 列配置
   */
  columns: TableColumn<any>[];

  /**
   * className
   */
  className?: string;
}
export function DraggableTable({
  table,
  draggableColumns,
  onDragEnd,
  columns,
  ...props
}: DraggableTableProps) {
  const [draggingColumn, setDraggingColumn] = useState("");
  const [columnClassNames, setColumnClassNames] = useState({});
  const [mouseStartX, setMouseStartX] = useState(0);
  const bodyRef = useRef<HTMLDivElement>(null);
  const bodyMaskRef = useRef<HTMLDivElement>(null);
  const bodyLineRef = useRef<HTMLDivElement>(null);
  function addColumnClassNames(column: string, className: string) {
    setColumnClassNames(preState => {
      if (preState[column] && Array.isArray(preState[column])) {
        const classNameSet = new Set(preState[column]);
        classNameSet.add(className);
        preState[column] = [...classNameSet];
      } else {
        preState[column] = [className];
      }
      return { ...preState };
    });
  }

  function deleteColumnClassNames(className: string) {
    setColumnClassNames(preState => {
      for (const column in preState) {
        if (preState[column] && Array.isArray(preState[column])) {
          const classNameSet = new Set(preState[column]);
          classNameSet.delete(className);
          preState[column] = [...classNameSet];
        } else {
          preState[column] = [];
        }
      }
      return { ...preState };
    });
  }

  const dragEventHandlers = {
    onDragStart: (event: React.DragEvent) => {
      event.stopPropagation();
      // QQ 浏览器设置 text 不触发 drop
      // event.dataTransfer.setData("text", "text");
      event.dataTransfer.setData("image/png", "img");
      event.dataTransfer.effectAllowed = "move";
      const dragImage = document.createElement("div");
      dragImage.style.width = "1px";
      dragImage.style.height = "1px";
      dragImage.style.backgroundColor = "#ebeef2";
      dragImage.style.position = "absolute";
      dragImage.style.top = "-1000px";
      document.body.appendChild(dragImage);
      if ("setDragImage" in event.dataTransfer) {
        event.dataTransfer.setDragImage(dragImage, 0, 0);
      }

      // 添加拖拽选择样式
      const draggingColumn = event.currentTarget.attributes.getNamedItem(
        "data-column"
      ).value;
      setDraggingColumn(draggingColumn);
      addColumnClassNames(draggingColumn, "is-dragging");

      // 展示遮罩层
      const maskWith = event.currentTarget.clientWidth;
      const currentElementOffset = getElementOffset(
        event.currentTarget as HTMLElement
      );
      const tableElementOffset = getElementOffset(
        bodyRef.current as HTMLElement
      );
      requestAnimationFrame(() => {
        bodyMaskRef.current.style.display = "block";
        bodyMaskRef.current.style.transform = `translate3d(${currentElementOffset.left -
          tableElementOffset.left}px, 0px, 0px)`;
        bodyMaskRef.current.style.width = `${maskWith}px`;
      });
      setMouseStartX(event.pageX);
    },
    onDrag: (event: React.DragEvent) => {
      event.stopPropagation();
      const tableElementOffset = getElementOffset(
        bodyRef.current as HTMLElement
      );
      const currentElementOffset = getElementOffset(
        event.currentTarget as HTMLElement
      );
      const left =
        currentElementOffset.left -
        tableElementOffset.left +
        (event.pageX - mouseStartX);
      requestAnimationFrame(() => {
        bodyMaskRef.current.style.transform = `translate3d(${left}px, 0px, 0px)`;
      });
    },
    onDragEnd: (event: React.DragEvent) => {
      requestAnimationFrame(() => {
        bodyMaskRef.current.style.display = "none";
        bodyMaskRef.current.style.transform = "translate3d(0px, 0px, 0px)";
        bodyLineRef.current.style.display = "none";
        bodyLineRef.current.style.transform = "translate3d(0px, 0px, 0px)";
        setMouseStartX(0);
        deleteColumnClassNames("is-dragging");
        deleteColumnClassNames("is-draggable");
        setDraggingColumn("");
      });
    },
    onDragEnter: (event: React.DragEvent) => {
      event.preventDefault();
    },
    onDragOver: (event: React.DragEvent) => {
      event.preventDefault();
      event.dataTransfer.dropEffect = "move";
      const dropColumn = event.currentTarget.attributes.getNamedItem(
        "data-column"
      ).value;
      const dropIndex = columns.findIndex(item => item.key === dropColumn);
      const dragIndex = columns.findIndex(item => item.key === draggingColumn);
      const dropElementOffset = getElementOffset(
        event.currentTarget as HTMLElement
      );
      const tableElementOffset = getElementOffset(
        bodyRef.current as HTMLElement
      );

      // 固定列/ignore列不作排序
      if (
        !columns[dropIndex] ||
        (columns[dropIndex] && columns[dropIndex].fixed)
      ) {
        return;
      }

      if (dropIndex <= dragIndex) {
        requestAnimationFrame(() => {
          bodyLineRef.current.style.display = "block";
          bodyLineRef.current.style.transform = `translate3d(${dropElementOffset.left -
            tableElementOffset.left}px, 0px, 0px)`;
        });

        return;
      }

      bodyLineRef.current.style.display = "block";
      let leftValue =
        dropElementOffset.left -
        tableElementOffset.left +
        event.currentTarget.clientWidth;
      if (leftValue > bodyRef.current.clientWidth - 2) {
        leftValue = bodyRef.current.clientWidth - 2;
      }
      requestAnimationFrame(() => {
        bodyLineRef.current.style.transform = `translate3d(${leftValue}px, 0px, 0px)`;
      });
    },
    onDragLeave: (event: React.DragEvent) => {
      event.preventDefault();
    },
    onDrop: (event: React.DragEvent) => {
      event.preventDefault();
      if (!draggingColumn) {
        return;
      }

      const dropColumn = event.currentTarget.attributes.getNamedItem(
        "data-column"
      ).value;
      const dropIndex = columns.findIndex(item => item.key === dropColumn);
      const dragIndex = columns.findIndex(item => item.key === draggingColumn);
      if (dropIndex === dragIndex) {
        return;
      }

      // 固定列/ignore列不作排序
      if (
        !columns[dropIndex] ||
        (columns[dropIndex] && columns[dropIndex].fixed)
      ) {
        return;
      }

      if (columns) {
        const newColumns = [...columns];
        newColumns.splice(dropIndex, 0, newColumns.splice(dragIndex, 1)[0]);
        onDragEnd(newColumns);
      }
    },
    onMouseEnter: (event: React.MouseEvent) => {
      const targetColumn = event.currentTarget.attributes.getNamedItem(
        "data-column"
      ).value;
      addColumnClassNames(targetColumn, "is-hover");
    },
    onMouseLeave: (event: React.MouseEvent) => {
      deleteColumnClassNames("is-hover");
    },
  };

  const dragContext: DragContextValue = {
    bodyRef,
    setBodyRef: body => {
      bodyRef.current = body;
    },
    bodyMaskRef,
    setBodyMaskRef: bodyMask => {
      bodyMaskRef.current = bodyMask;
    },
    bodyLineRef,
    setBodyLineRef: bodyLine => {
      bodyLineRef.current = bodyLine;
    },
    dragEventHandlers,
    draggableColumns,
    columns,
    draggingColumn,
    setDraggingColumn,
    columnClassNames,
  };

  return (
    <DragContext.Provider value={dragContext}>
      {React.cloneElement(table, {
        className: classNames(table.props.className, props.className), // eslint-disable-line
      })}
    </DragContext.Provider>
  );
}

/**
 * 包装表格内容区
 */
interface DraggableTableBodyProps {
  body: React.FunctionComponentElement<{
    ref: React.Ref<HTMLDivElement>;
    style: React.CSSProperties;
  }>;
  onDragEnd?: (columns: TableColumn[]) => void;
  draggableColumns?: string[] | "all";
}
export const DraggableTableBody = forwardRef(
  (
    { body, onDragEnd }: DraggableTableBodyProps,
    ref: React.Ref<HTMLDivElement>
  ) => {
    const bodyRef = useRef<HTMLDivElement>(null);
    const {
      setBodyRef,
      setBodyMaskRef,
      setBodyLineRef,
      bodyLineRef,
      columns,
      draggingColumn,
    } = useContext(DragContext);
    const getHoverTdNode = (event: React.MouseEvent) => {
      let currentTd = null;
      const trs: HTMLElement[] = Array.prototype.slice.call(
        bodyRef.current.getElementsByTagName("tr"),
        0
      );
      const tds: HTMLElement[] =
        trs && trs.length > 0
          ? Array.prototype.slice.call(trs[0].childNodes, 0)
          : [];

      if (tds && tds.length > 0) {
        const len = tds.length;
        for (let i = 0; i < len; i++) {
          const currentElementOffset = getElementOffset(tds[i]);
          if (
            currentElementOffset.left < event.pageX &&
            event.pageX < currentElementOffset.left + tds[i].clientWidth
          ) {
            currentTd = tds[i];
            break;
          }
        }
      }

      return currentTd;
    };

    /**
     * 挂在遮罩层的事件处理器
     * 当鼠标移动到table的body区域的遮罩层时，需重新计算插入位置和样式
     */
    const maskDragEventHandlers = {
      onDragEnter: (event: React.DragEvent) => {
        event.preventDefault();
      },
      onDragOver: (event: React.DragEvent) => {
        event.preventDefault();
        const currentTd = getHoverTdNode(event);
        const dataColumnNode = currentTd && currentTd.attributes.getNamedItem("data-column");
        if (dataColumnNode) {
          const dropColumn = dataColumnNode.value;
          const dropIndex = columns.findIndex(item => item.key === dropColumn);
          const dropElementOffset = getElementOffset(currentTd as HTMLElement);
          const tableElementOffset = getElementOffset(
            bodyRef.current as HTMLElement
          );
          const dragIndex = columns.findIndex(
            item => item.key === draggingColumn
          );

          // 固定列/ignore列不作排序
          if (
            !columns[dropIndex] ||
            (columns[dropIndex] && columns[dropIndex].fixed)
          ) {
            return;
          }

          if (dropIndex <= dragIndex) {
            requestAnimationFrame(() => {
              bodyLineRef.current.style.display = "block";
              bodyLineRef.current.style.transform = `translate3d(${dropElementOffset.left -
                tableElementOffset.left}px, 0px, 0px)`;
            });

            return;
          }

          bodyLineRef.current.style.display = "block";
          let leftValue =
            dropElementOffset.left -
            tableElementOffset.left +
            currentTd.clientWidth;
          if (leftValue > bodyRef.current.clientWidth - 2) {
            leftValue = bodyRef.current.clientWidth - 2;
          }
          requestAnimationFrame(() => {
            bodyLineRef.current.style.transform = `translate3d(${leftValue}px, 0px, 0px)`;
          });
        }
      },
      onDrop: (event: React.DragEvent) => {
        event.preventDefault();
        const currentTd = getHoverTdNode(event);
        const dataColumnNode = currentTd && currentTd.attributes.getNamedItem("data-column");
        if (dataColumnNode) {
          const dropColumn = dataColumnNode.value;
          const dropIndex = columns.findIndex(item => item.key === dropColumn);
          const dragIndex = columns.findIndex(
            item => item.key === draggingColumn
          );
          if (dropIndex === dragIndex) {
            return;
          }

          // 固定列/ignore列不作排序
          if (
            !columns[dropIndex] ||
            (columns[dropIndex] && columns[dropIndex].fixed)
          ) {
            return;
          }

          if (columns) {
            const newColumns = [...columns];
            newColumns.splice(dropIndex, 0, newColumns.splice(dragIndex, 1)[0]);
            onDragEnd(newColumns);
          }
        }
      },
    };
    return (
      <div style={{ position: "relative", overflow: "auto" }}>
        <div
          ref={setBodyMaskRef}
          {...maskDragEventHandlers}
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            height: "100%",
            backgroundColor: "rgba(0, 0, 0, 0.2)",
            width: "100px",
            zIndex: 1000,
            display: "none",
          }}
        ></div>
        <div
          ref={setBodyLineRef}
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            height: "100%",
            backgroundColor: "rgba(0, 0, 0, 0.4)",
            width: "2px",
            zIndex: 500,
            display: "none",
          }}
        ></div>
        {React.cloneElement(body, {
          ref: mergeRefs(setBodyRef, body.ref, ref, bodyRef),
        })}
      </div>
    );
  }
);

DraggableTable.displayName = "DraggableTable";
