import React, { useState, useEffect, useRef } from "react";
import classNames from "classnames";
import { StyledProps } from "../_type";
import { ControlledProps, useDefaultValue } from "../form/controlled";
import { getPrecision } from "../_util/get-precision";
import { useConfig } from "../_util/config-context";
import { noop } from "../_util/noop";
import { KeyMap } from "../_util/key-map";
import { mergeEventProps } from "../_util/merge-event-props";
import { forwardRefWithStatics } from "../_util/forward-ref-with-statics";
import { callBoth } from "../util";

export interface InputNumberProps extends ControlledProps<number>, StyledProps {
  /** 最小值 */
  min?: number;

  /** 最大值 */
  max?: number;

  /**
   * 使用按钮增减时的步长
   * @default 1
   */
  step?: number;

  /** 单位 */
  unit?: string;

  /**
   * 是否禁用
   * @default false
   */
  disabled?: boolean;

  /**
   * 精度（保留小数位数）
   */
  precision?: number;

  /**
   * 输入框大小
   * @default "m"
   * @since 2.2.1
   */
  size?: "m" | "l";

  /**
   * 输入框展示值的格式
   *
   * *配合 `parser` 使用*
   *
   * @since 2.7.0
   */
  formatter?: (value: number | string) => string;

  /**
   * 从 `formatter` 格式转回数字的方法
   * @since 2.7.0
   */
  parser?: (content: string) => number;

  /**
   * 隐藏输入框两侧按钮
   * @default false
   * @since 2.2.2
   */
  hideButton?: boolean;

  /**
   * 输入值变化回调
   * @since 2.5.0
   */
  onInputChange?: (inputValue: string) => void;

  /**
   * 自定义输入框属性
   * @since 2.7.0
   */
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;

  /**
   * 是否允许不输入值
   *
   * *使用 `null` 表示空值*
   *
   * @default false
   * @since 2.7.0
   */
  allowEmpty?: boolean;

  /**
   * 输入框聚焦事件
   * @since 2.2.1
   */
  onFocus?: React.DOMAttributes<HTMLInputElement>["onFocus"];

  /**
   * 输入框失焦事件
   * @since 2.2.1
   */
  onBlur?: React.DOMAttributes<HTMLInputElement>["onBlur"];
}

const DISABLED_CLS = "is-disabled";

/**
 * 数字输入组件
 */
export const InputNumber = forwardRefWithStatics(
  function InputNumber(
    props: InputNumberProps,
    ref: React.Ref<HTMLDivElement>
  ) {
    const { classPrefix } = useConfig();
    const {
      size,
      unit,
      disabled,
      style,
      className,
      onFocus = noop,
      onBlur = noop,
      hideButton,
      inputProps = {},
    } = props;
    const { minus, input, plus } = useInputNumberHooks(props);

    return (
      <span
        ref={ref}
        className={classNames(`${classPrefix}-inputnum`, className, {
          "size-l": size === "l",
          [DISABLED_CLS]: disabled,
        })}
        style={style}
        {...mergeEventProps(props)}
      >
        {!hideButton && (
          <span
            className={classNames(`${classPrefix}-inputnum__minus`, {
              [DISABLED_CLS]: minus.disabled,
            })}
            onMouseDown={disabled ? noop : minus.handleMouseDown}
          >
            -
          </span>
        )}
        <input
          className={classNames(`${classPrefix}-input`, inputProps.className)}
          value={input.value || ""}
          disabled={disabled}
          {...inputProps}
          onChange={callBoth(input.handleChange, inputProps.onChange)}
          onFocus={callBoth(onFocus, inputProps.onFocus)}
          onBlur={callBoth(e => {
            onBlur(e);
            input.handleBlur(e);
          }, inputProps.onBlur)}
          onKeyDown={callBoth(input.handleKeyDown, inputProps.onKeyDown)}
        />
        {!hideButton && (
          <span
            className={classNames(`${classPrefix}-inputnum__plus`, {
              [DISABLED_CLS]: plus.disabled,
            })}
            onMouseDown={disabled ? noop : plus.handleMouseDown}
          >
            +
          </span>
        )}
        {unit && (
          <div className={`${classPrefix}-form__help-text--inline`}>{unit}</div>
        )}
      </span>
    );
  },
  {
    defaultLabelAlign: "middle",
  }
);

InputNumber.displayName = "InputNumber";

function getDefaultDefaultValue(min: number, max: number) {
  if (min && min > 0) {
    return min;
  }
  if (max && max < 0) {
    return max;
  }
  return 0;
}

const defaultFormatter = (x: number | string) => String(x);
const defaultParser = (x: string) => Number(x);

/**
 * InputNumber 状态管理
 */
function useInputNumberHooks(props: InputNumberProps) {
  let { step } = props;
  const {
    min,
    max,
    value,
    onChange = noop,
    onInputChange = noop,
    allowEmpty,
    formatter = defaultFormatter,
    parser = defaultParser,
  } = useDefaultValue(props, getDefaultDefaultValue(props.min, props.max));

  const isValidNumber = (num: any) =>
    typeof num === "number" && !Number.isNaN(num);

  step = isValidNumber(step) ? step : 1;

  const precision = props.precision ?? getPrecision(step);

  const hasMax = isValidNumber(max);
  const hasMinus = isValidNumber(min);

  const [isEmpty, setIsEmpty] = useState<boolean>(allowEmpty && value === null);
  const [inputValue, setInputValue] = useState<string>(null);
  const hasInputValue = inputValue !== null;

  const parse = (newValue: string): number =>
    newValue.trim() === "" ? 0 : parseFloat(newValue);

  const getCurrentValue = () => {
    let currentValue = value;
    if (hasInputValue && /\d/.test(inputValue)) {
      currentValue = parse(inputValue);
      // 解析失败，退回当前值
      if (Number.isNaN(currentValue)) {
        currentValue = value;
      }
    }
    return currentValue;
  };

  const canMinus = (value: number) => !hasMinus || value > min;
  const canPlus = (value: number) => !hasMax || value < max;

  const commit = (_newValue: string | number, event: React.SyntheticEvent) => {
    let newValue = _newValue;
    // 从 input 回调过来的，是字符串
    if (typeof newValue === "string") {
      if (allowEmpty && !newValue) {
        onChange(null, { event });
        return;
      }
      newValue = parser(newValue);
    }
    // 不能小于最小值
    if (hasMinus) {
      newValue = Math.max(min, newValue);
    }
    // 不能大于最大值
    if (hasMax) {
      newValue = Math.min(max, newValue);
    }
    // 解析失败，还原旧值
    if (Number.isNaN(newValue)) {
      newValue = value;
    }
    // 还原预置初始值
    if (typeof newValue !== "number" || Number.isNaN(newValue)) {
      newValue = getDefaultDefaultValue(min, max);
    }

    // 处理精度
    newValue = parse(newValue.toFixed(precision));
    onChange(newValue, { event });
    onInputChange(formatter(newValue));
  };

  // value 发生变更，清空暂存的值
  useEffect(() => setInputValue(null), [value]);

  // 长按增减按钮会持续操作，鼠标抬起，或者组件销毁时要清空
  const autoStepTimer = useRef<any>(0);
  const clear = () => clearTimeout(autoStepTimer.current);
  useEffect(() => {
    window.addEventListener("mouseup", clear, true);

    // destroy
    return () => {
      clear();
      window.removeEventListener("mouseup", clear);
    };
  }, []);

  // 为按钮提供步长调整逻辑
  const stepper = (step: number) => {
    // 步长的符号影响是否允许进行下一步长的判断方法
    const canStep = step > 0 ? canPlus : canMinus;
    return {
      disabled: !canStep(value),

      // 点击按钮的时候，先变更一次。然后，只要鼠标不抬起，自动连续变更
      handleMouseDown: (evt: React.MouseEvent) => {
        // 只有鼠标左键可用
        if (evt.nativeEvent.which >= 2) {
          return;
        }

        if (allowEmpty) {
          setIsEmpty(false);
        }

        let currentValue = getCurrentValue();
        const performStep = () => {
          if (canStep(currentValue)) {
            commit((currentValue += step), evt);
          }
        };
        performStep();

        clear();
        // 1 秒后，开始自动递增
        autoStepTimer.current = setTimeout(() => {
          const autoPerform = () => {
            performStep();
            autoStepTimer.current = setTimeout(autoPerform, 50);
          };
          autoPerform();
        }, 700);

        // 保留事件内容，否则在自动步长执行的时候，事件不可用
        evt.persist();

        // 阻止事件变成 blur 事件，导致二次设置
        evt.preventDefault();
      },
    };
  };

  return {
    minus: stepper(-step),
    plus: stepper(step),
    input: {
      value:
        hasInputValue || isEmpty
          ? inputValue
          : formatter(Number(value).toFixed(precision)),
      handleChange: (evt: React.ChangeEvent<HTMLInputElement>) => {
        const { value } = evt.target;
        if (allowEmpty) {
          setIsEmpty(!value);
        }
        setInputValue(value);
        onInputChange(value);
      },
      handleBlur: (evt: React.FocusEvent<HTMLInputElement>) => {
        commit(evt.target.value, evt);
        setInputValue(null);
      },
      handleKeyDown: (evt: React.KeyboardEvent<HTMLInputElement>) => {
        const currentValue = getCurrentValue();
        if (canPlus(currentValue) && evt.key === KeyMap.Up) {
          // ArrowUp
          evt.preventDefault();
          if (allowEmpty) {
            setIsEmpty(false);
          }
          commit(currentValue + step, evt);
        } else if (canMinus(currentValue) && evt.key === KeyMap.Down) {
          // ArrowDown
          evt.preventDefault();
          if (allowEmpty) {
            setIsEmpty(false);
          }
          commit(currentValue - step, evt);
        }
        // Enter
        if (evt.key === KeyMap.Enter) {
          evt.preventDefault();
          commit(evt.currentTarget.value, evt);
          setInputValue(null);
        }
      },
    },
  };
}
