import React, { useRef, forwardRef, useState, useEffect, useMemo } from "react";
import classNames from "classnames";
import { ReferenceObject } from "popper.js";
import { BubbleContent } from "../bubble";
import { Popover } from "../popover";
import { getPrecision } from "../_util/get-precision";
import { useConfig } from "../_util/config-context";
import { SliderTrackProps } from "./SliderProps";
import { noop } from "../_util/noop";
import { flatten } from "../_util/flatten";
import { SliderHandle } from "./SliderHandle";
import { StyledProps } from "../_type";

const defaultFormatter = number => number;

const sort = (arr: [number, number]) =>
  arr.sort((a, b) => a - b) as [number, number];

const getEvent = (
  event: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent
) => {
  if ("targetTouches" in event) {
    return event.targetTouches[0];
  }
  return event;
};

export const SliderTrack = React.forwardRef(function SliderTrack(
  {
    value,
    min = 0,
    max,
    step = 1,
    range,
    multiRangeMode = false,
    dotValues = [],
    rangeMode,
    marks = [],
    disabled,
    markValueOnly,
    enableTrackTip,
    after,
    vertical,
    onUpdate = noop,
    onChange = noop,
    tipFormatter = defaultFormatter,
    className,
    ...props
  }: SliderTrackProps,
  ref: React.Ref<HTMLDivElement>
) {
  const { classPrefix } = useConfig();

  const offsetKey = vertical ? "bottom" : "left";
  const boundingKey = vertical ? "height" : "width";

  const [startValue, endValue] = value;

  const precision = useMemo(() => getPrecision(step), [step]);

  // 定位相关 hooks
  const startRef = useRef<HTMLElement>(null);
  const endRef = useRef<HTMLElement>(null);

  // 滑轨相关 hooks
  const scheduleRef = useRef(null);
  const mouseElement = useRef<
    ReferenceObject & { scrollTop: number; scrollLeft: number }
  >(null);
  const [mousePosition, setMousePosition] = useState<number>(null);

  // 滑块相关 hooks
  const positionRef = useRef<number>(0);
  const dragOffsetRef = useRef<number>(0);
  const innerScheduleRef = useRef(null);
  const [dragging, setDragging] = useState(false);
  // const [hovering, setHovering] = useState(false);

  useEffect(() => removeDocumentEvents, []); // eslint-disable-line react-hooks/exhaustive-deps

  // 拖动过程中值
  const [interactiveValue, setInteractiveValue] = useState(null);
  const interactivePosRef = useRef<"start" | "end">(null);
  const displayDotValues = getValidDotAndRanges(dotValues, range).filter(
    value => typeof value === "number"
  );

  const total = Math.abs(max - min);
  const proportions = !rangeMode
    ? (getProportions() as any)
    : [sort(getProportions() as any)];

  function calcProportion(num: number) {
    return ((num - min) / total) * 100;
  }
  function getProportions(): SliderTrackProps["range"] {
    if (!rangeMode && !multiRangeMode) {
      return [
        [
          0,
          calcProportion(
            interactiveValue === null ? endValue : interactiveValue
          ),
        ],
      ];
    }

    // 多段模式
    if (!rangeMode && multiRangeMode) {
      const realRanges = getValidDotAndRanges(dotValues, range)
        .filter(item => Array.isArray(item))
        .map(item => {
          return [calcProportion(item[0]), calcProportion(item[1])] as any;
        });

      return realRanges;
    }

    // rangeMode
    if (interactiveValue === null || interactivePosRef.current === null) {
      return [calcProportion(startValue), calcProportion(endValue)];
    }
    return interactivePosRef.current === "start"
      ? [calcProportion(interactiveValue), calcProportion(endValue)]
      : [calcProportion(startValue), calcProportion(interactiveValue)];
  }

  function addDocumentMouseEvents() {
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("touchmove", handleMouseMove, { passive: false });
    document.addEventListener("mouseup", handleMouseUp);
    document.addEventListener("touchend", handleMouseUp, { passive: false });
  }

  function removeDocumentEvents() {
    document.removeEventListener("mousemove", handleMouseMove);
    document.removeEventListener("touchmove", handleMouseMove);
    document.removeEventListener("mouseup", handleMouseUp);
    document.removeEventListener("touchend", handleMouseUp);
  }

  function handleMouseUp(event) {
    event.preventDefault();
    setDragging(false);
    removeDocumentEvents();
    const value = getValueByPosition(positionRef.current);
    if (!rangeMode) {
      onChange([0, value], { event });
    } else {
      onChange(
        sort(
          interactivePosRef.current === "start"
            ? [value, endValue]
            : [startValue, value]
        ),
        { event }
      );
    }
    if (rangeMode) {
      interactivePosRef.current = null;
    }
    setInteractiveValue(null);
  }

  function handleMouseMove(event: MouseEvent | TouchEvent) {
    if (event.cancelable) {
      event.preventDefault();
    }
    if (disabled) {
      return;
    }
    const position = vertical ? getEvent(event).pageY : getEvent(event).pageX;
    const value = getValueByPosition(position - dragOffsetRef.current);

    if (getValueByPosition(positionRef.current) !== value) {
      positionRef.current = position;
      setInteractiveValue(value);
      onUpdate(
        sort(
          interactivePosRef.current === "start"
            ? [value, endValue]
            : [startValue, value]
        )
      );
    }

    if (innerScheduleRef.current) {
      innerScheduleRef.current();
    }
  }

  function handleMouseDown(event: React.MouseEvent | React.TouchEvent) {
    if (disabled) {
      return;
    }
    const position = vertical ? getEvent(event).pageY : getEvent(event).pageX;
    const value = getValueByPosition(position);
    if (rangeMode) {
      interactivePosRef.current =
        Math.abs(value - startValue) < Math.abs(value - endValue)
          ? "start"
          : "end";
    }
    setInteractiveValue(value);
    onUpdate(
      sort(
        interactivePosRef.current === "start"
          ? [value, endValue]
          : [startValue, value]
      )
    );

    // 滑块拖动相关事件
    setDragging(true);
    positionRef.current = position;
    dragOffsetRef.current = 0;
    removeDocumentEvents();
    addDocumentMouseEvents();
  }

  function handleMouseLeave(event: React.MouseEvent | React.TouchEvent) {
    event.preventDefault();
    setMousePosition(null);
  }

  function getStartPos(): number {
    if (!startRef.current) {
      return 0;
    }
    const coords = startRef.current.getBoundingClientRect();
    return (
      (vertical ? window.pageYOffset : window.pageXOffset) + coords[offsetKey]
    );
  }

  function getEndPos(): number {
    if (!endRef.current) {
      return 0;
    }
    const coords = endRef.current.getBoundingClientRect();
    return (
      (vertical ? window.pageYOffset : window.pageXOffset) + coords[offsetKey]
    );
  }

  function getValidDotAndRanges(
    dotValues: SliderTrackProps["dotValues"],
    range: SliderTrackProps["range"]
  ) {
    return [...dotValues, ...range]
      .sort((a, b) => {
        if (Array.isArray(a) && Array.isArray(b)) {
          return a[0] > b[0] || (a[0] === b[0] && a[1] < b[1]) ? 1 : -1;
        }

        if (Array.isArray(a) && typeof b === "number") {
          return a[0] > b ? 1 : -1;
        }

        if (typeof a === "number" && Array.isArray(b)) {
          return a > b[0] ? 1 : -1;
        }

        return a > b ? 1 : -1;
      })
      .reduce((pre, current) => {
        const len = pre.length;
        if (len === 0) {
          return [current];
        }

        const tailItem = pre[len - 1];

        // 最后元素和遍历元素均为数组
        if (Array.isArray(tailItem) && Array.isArray(current)) {
          if (tailItem[1] >= current[0] && tailItem[1] <= current[1]) {
            pre[len - 1] = [tailItem[0], current[1]];
            return pre;
          }

          if (tailItem[1] >= current[0] && tailItem[1] > current[1]) {
            return pre;
          }

          pre.push(current);
        }

        if (Array.isArray(tailItem) && typeof current === "number") {
          if (tailItem[1] >= current) {
            return pre;
          }

          pre.push(current);
        }

        if (typeof tailItem === "number" && Array.isArray(current)) {
          if (tailItem === current[0]) {
            pre[len - 1] = current;
            return pre;
          }

          pre.push(current);
        }

        if (typeof tailItem === "number" && typeof current === "number") {
          if (tailItem !== current) {
            pre.push(current);
          }
        }

        return pre;
      }, []);
  }

  function getValueByPosition(position: number) {
    // 垂直时上下相反
    const [start, end] = sort([getStartPos(), getEndPos()]);
    const posRange = end - start;
    if (position < start) {
      position = start; // eslint-disable-line no-param-reassign
    }
    if (position > end) {
      position = end; // eslint-disable-line no-param-reassign
    }
    const proportion =
      (vertical ? end - position : position - start) / posRange;
    return getValueByProportion(proportion);
  }

  function getValueByProportion(proportion: number): number {
    const pow = 10 ** precision;
    let value = proportion * total;
    value -= ((value * pow) % (step * pow)) / pow;
    value += min;

    if (markValueOnly && marks.length) {
      // eslint-disable-next-line prefer-destructuring
      value = marks.reduce(
        (prev, cur) =>
          Math.abs(cur.value - value) < Math.abs(prev.value - value)
            ? cur
            : prev,
        marks[0]
      ).value;
    }

    // 点模式的range可为多段
    if (!rangeMode) {
      const valueRanges = getValidDotAndRanges(dotValues, range);

      const isInRangeOrDotValues = valueRanges.reduce((pre, current) => {
        return (
          pre ||
          (Array.isArray(current) &&
            value >= current[0] &&
            value <= current[1]) ||
          (typeof current === "number" && current === value)
        );
      }, false);
      if (!isInRangeOrDotValues && valueRanges.length) {
        value = flatten(valueRanges).reduce((pre, current) => {
          return Math.abs(value - pre) < Math.abs(value - current)
            ? pre
            : current;
        });
      }
    } else {
      const [allowedStart, allowedEnd] = range as [number, number];
      if (value < allowedStart) {
        value = allowedStart;
      }

      if (value > allowedEnd) {
        value = allowedEnd;
      }
    }

    return parseFloat(value.toFixed(precision));
  }

  function handleMouseMoveForPopover(
    event: React.MouseEvent | React.TouchEvent
  ) {
    if (disabled) {
      return;
    }
    const position = vertical ? getEvent(event).pageY : getEvent(event).pageX;
    setMousePosition(position);
    setMouseElement(event.nativeEvent);
    if (scheduleRef.current) {
      scheduleRef.current();
    }
  }

  function setMouseElement(event: MouseEvent | TouchEvent) {
    const clientX = Math.round(getEvent(event).clientX);
    const clientY = startRef.current.getBoundingClientRect().top;
    mouseElement.current = {
      clientWidth: 0,
      clientHeight: 0,
      getBoundingClientRect: () =>
        ({
          left: clientX,
          top: clientY,
          right: clientX,
          bottom: clientY,
          width: 0,
          height: 0,
        } as any),
      // IE10 下 Popper 读取这两个值计算
      scrollTop: 0,
      scrollLeft: 0,
    };
  }

  return (
    <div
      ref={ref}
      className={classNames(`${classPrefix}-slider`, className, {
        [`${classPrefix}-slider--vertical`]: vertical,
        "is-disabled": disabled,
        [`${classPrefix}-slider--time`]: !rangeMode && multiRangeMode,
      })}
      {...props}
    >
      <Popover
        visible={!!enableTrackTip && !disabled && !!mousePosition}
        referenceElement={mouseElement.current}
        placement={vertical ? "left" : "top"}
        placementOffset={16}
        transitionTimeout={0}
        overlayStyle={{
          pointerEvents: "none",
        }}
        overlay={({ placement, scheduleUpdate }) => {
          scheduleRef.current = scheduleUpdate;
          const tips = tipFormatter(getValueByPosition(mousePosition));
          return (
            tips !== null &&
            !!mousePosition && (
              <BubbleContent placement={placement}>{tips}</BubbleContent>
            )
          );
        }}
      >
        <div
          className={`${classPrefix}-slider__content`}
          onMouseDown={handleMouseDown}
          onMouseMove={handleMouseMoveForPopover}
          onMouseLeave={handleMouseLeave}
          onTouchStart={handleMouseDown}
          onTouchMove={handleMouseMoveForPopover}
          onTouchEnd={handleMouseLeave}
          onDragStart={e => e.preventDefault()}
        >
          <StartFlag style={{ [offsetKey]: 0 }} ref={startRef} />

          {/* 滑轨 */}
          <div className={`${classPrefix}-slider__bar`}>
            {proportions &&
              proportions.map((proportion, index) => {
                return (
                  <div
                    key={index}
                    className={`${classPrefix}-slider__track`}
                    style={{
                      [boundingKey]: `${proportion[1] - proportion[0]}%`,
                      [offsetKey]: `${proportion[0]}%`,
                    }}
                  />
                );
              })}
          </div>

          {/* 滑块 */}
          {rangeMode && (
            <SliderHandle
              vertical={vertical}
              classPrefix={classPrefix}
              value={
                // eslint-disable-next-line no-nested-ternary
                interactiveValue !== null
                  ? interactivePosRef.current === "start"
                    ? endValue
                    : startValue
                  : startValue
              }
              offset={
                // eslint-disable-next-line no-nested-ternary
                interactiveValue !== null
                  ? interactivePosRef.current === "start"
                    ? calcProportion(endValue)
                    : calcProportion(startValue)
                  : calcProportion(startValue)
              }
              tipFormatter={tipFormatter}
              enableTip={!enableTrackTip}
            />
          )}
          <SliderHandle
            vertical={vertical}
            classPrefix={classPrefix}
            value={interactiveValue !== null ? interactiveValue : endValue}
            offset={
              interactiveValue !== null
                ? calcProportion(interactiveValue)
                : calcProportion(endValue)
            }
            tipFormatter={tipFormatter}
            dragging={dragging}
            enableTip={!enableTrackTip}
          />

          {!rangeMode &&
            multiRangeMode &&
            displayDotValues.length > 0 &&
            displayDotValues.map(value => (
              <div
                className={`${classPrefix}-slider__dot`}
                key={value}
                style={{ [offsetKey]: `${((value - min) / total) * 100}%` }}
              ></div>
            ))}

          {/* 标尺 */}
          <div className={`${classPrefix}-slider__mark`}>
            {marks
              .filter(({ value }) => value >= min && value <= max)
              .map(({ value, label }) => (
                <span
                  key={value}
                  className={`${classPrefix}-slider__text`}
                  style={{
                    [offsetKey]: `${((value - min) / total) * 100}%`,
                    userSelect: "none",
                    pointerEvents: "none",
                    cursor: "default",
                  }}
                >
                  {label || value}
                </span>
              ))}
          </div>

          <EndFlag style={{ [offsetKey]: "100%" }} ref={endRef} />
        </div>
      </Popover>
      <div className={`${classPrefix}-slider__extend`}>{after}</div>
    </div>
  );
});

SliderTrack.displayName = "SliderTrack";

const StartFlag = forwardRef<HTMLSpanElement, StyledProps>(({ style }, ref) => (
  <span
    ref={ref}
    style={{ position: "absolute", visibility: "hidden", ...style }}
  />
));

StartFlag.displayName = "SliderTrackStartFlag";

const EndFlag = forwardRef<HTMLSpanElement, StyledProps>(({ style }, ref) => (
  <span
    ref={ref}
    style={{ position: "absolute", visibility: "hidden", ...style }}
  />
));

EndFlag.displayName = "SliderTrackEndFlag";
