/**
 * 扫码组件的两种形态：
 * 1. 页面采集：监听全局键盘事件：addEventListenerKeyDown2Scan
 * 2. 输入框采集：监听onKeyDown事件
 */

/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, {
  useState,
  useEffect,
  useCallback,
  useRef,
  ReactNode,
  useMemo,
} from 'react';
import classnames from 'classnames';
import _ from 'lodash';
import event from '../event';

import './index.scss';

type noop = (value: string) => void;
type AnonymousFunction = (...args: any[]) => any;

// 扫码枪输入最大间隔时长(ms)
const MAX_KEEP_TIME = 100;
// 浏览器热键黑名单
const WHITE_CTRL_KEYS = ['a', 'c', 'v', 'x'];
const WHITE_SPECIAL_KEYS = ['ENTER', 'BACKSPACE'];
export interface ScanProps {
  /** 默认值  */
  defaultValue?: string | number;
  /** 默认展示输入框 */
  defaultShowInput?: boolean;
  /** 组件文字 */
  placeholder?: string;
  /** 扫码框大小 */
  size?: 'large' | 'medium' | 'small';
  /** 是否允许手输/复制粘贴条码 */
  manual?: boolean;
  /** 是否可以通过空格键切换扫码框形态 */
  hasToggle?: boolean;
  /** 是否处于禁用态，禁用态时Enter事件会失效 */
  disabled?: boolean;
  /** 扫码/回车时触发函数 */
  onEnter?: (value: string, isScan: boolean) => void;
  /** 输入框状态聚焦时触发事件 */
  onFocus?: AnonymousFunction;
  /** 输入框状态失焦时触发事件 */
  onBlur?: AnonymousFunction;
  /** 输入框状态值更改时触发事件 */
  onChange?: AnonymousFunction;
  /** 组件状态变更时触发事件 */
  onToggle?: (showInput: boolean) => void;
  className?: string;
  /** 自定义内联样式 */
  style?: React.CSSProperties;
  prefix?: boolean | ReactNode;
  // 显示 Input 时 自动聚焦
  autoFocus?: boolean;
  value?: string | number;
}

const globalEventCallbacks: noop[] = [];
Scan.addGlobalEnterEvent = function (callback: (value: string) => void) {
  if (
    callback instanceof Function &&
    !globalEventCallbacks.includes(callback)
  ) {
    globalEventCallbacks.push(callback);
  }
};
Scan.removeGlobalEnterEvent = function (callback: (value: string) => void) {
  const findIndex = globalEventCallbacks.findIndex(
    (callItem) => callItem === callback,
  );
  if (callback instanceof Function && findIndex > -1) {
    globalEventCallbacks.splice(findIndex, 1);
  }
};

let scanning = false; // 特殊标记：是否正在扫码，用来处理扫码时识别到特殊字符‘space’从而触发toggle事件的问题

export default function Scan({
  defaultValue = '',
  defaultShowInput,
  placeholder = '',
  size = 'large',
  manual = true,
  hasToggle = true,
  disabled,
  onEnter,
  onFocus,
  onBlur,
  onChange,
  onToggle,
  className = '',
  prefix = true,
  autoFocus,
  value,
  ...props
}: ScanProps): JSX.Element {
  const [showInput, setShowInput] = useState(defaultShowInput);
  const inputEl = useRef<HTMLInputElement | null>(null);
  const focusRecord = useHasInputFocus();
  const defaultInputValue = useRef<string | number>(defaultValue);
  // 缓存，不参与渲染
  const cache = useMemo<{ lastPressKeyTime?: number }>(() => ({}), []);
  const toggle = useCallback(
    (e?: KeyboardEvent) => {
      (e as KeyboardEvent).preventDefault();
      // 如果扫码枪在扫码过程中识别到了空格键， 禁用toggle
      if (scanning) {
        return;
      }
      manual && setShowInput((v) => !v);
      if (inputEl.current) {
        inputEl.current.value = '';
      }
      showInput && focusRecord.unFocus();
      onToggle instanceof Function && onToggle(!showInput);
    },
    [manual, showInput, focusRecord, onToggle],
  );

  const onEnterRef = useRef<AnonymousFunction | null>(null);

  // 支持 显示输入框时自动聚焦
  useEffect(() => {
    if (showInput && autoFocus) inputEl.current?.focus();
  }, [showInput, autoFocus]);

  const enterHub = useCallback(
    (val: string, isScan: boolean) => {
      if (disabled) {
        return;
      }
      const onEnterCur = onEnterRef.current as AnonymousFunction;
      onEnterCur instanceof Function &&
        onEnterCur(val.trim().replace(/Enter/g, ''), isScan);
      globalEventCallbacks.forEach((callback) => {
        callback(val);
      });
    },
    [disabled],
  );

  useEffect(() => {
    onEnterRef.current = onEnter ?? null;
  }, [onEnter]);

  // 监听空格键
  useEffect(() => {
    if (hasToggle) {
      // 延时处理toggle，避免扫码状态还未设置成功(扫码状态需要在读取到第二个按键时才能设置成功)就触发toggle，导致状态混乱
      const toggleDebounce = _.debounce(toggle, 1);
      event.bind('space', toggleDebounce);
      return () => {
        event.unbind('space', toggleDebounce);
      };
    }
  }, [toggle, hasToggle]);

  // 处理扫码枪事件
  useEffect(() => {
    return addEventListenerKeyDown2Scan(
      function (value: string) {
        !focusRecord.hasFocus() && enterHub(value, true);
      },
      focusRecord,
      (inputval) => {
        defaultInputValue.current = inputval;
      },
    );
  }, [enterHub]);

  // 处理输入框事件
  const onKeyDown = useCallback(
    function (e: React.KeyboardEvent<HTMLInputElement>) {
      const { key = '' } = e;
      const upperKey = key.toLocaleUpperCase();
      const isScan =
        !!cache.lastPressKeyTime &&
        Date.now() - cache.lastPressKeyTime < MAX_KEEP_TIME;
      if (!scanning && isScan) {
        scanning = true;
      }
      if (isScan && upperKey === 'TAB') {
        e.preventDefault();
        return;
      }
      if (upperKey === 'ENTER') {
        enterHub((inputEl.current?.value || '').trim(), isScan);
        if (inputEl.current) {
          inputEl.current.value = '';
          inputEl.current.focus();
        }
        // 延长scanning状态，避免与toggle冲突
        setTimeout(() => {
          scanning = false;
        }, 2);
        e.stopPropagation();
      }
      cache.lastPressKeyTime = Date.now();
    },
    [enterHub],
  );

  const handleFocus = useCallback(
    (event) => {
      focusRecord.doFocus();
      onFocus instanceof Function && onFocus(event);
    },
    [focusRecord, onFocus],
  );

  const handleBlur = useCallback(
    (event) => {
      hasInputFocus = false;
      focusRecord.unFocus();
      onBlur instanceof Function && onBlur(event);
    },
    [focusRecord, onBlur],
  );

  const handleChange = useCallback(
    (event) => {
      onChange instanceof Function && onChange(event.target.value);
    },
    [onChange],
  );
  const inputOtherProps = useMemo(() => {
    return value ? { value } : {};
  }, [value]);
  return (
    <div
      data-hottag='cn-op.scan'
      className={classnames('op-scan-container', className)}
      onClick={() => manual && setShowInput(true)}
      {...props}
    >
      {showInput ? (
        <input
          ref={inputEl}
          className={classnames('op-input', {
            [`op-input-${size}`]: size,
          })}
          defaultValue={defaultInputValue.current}
          onKeyDown={onKeyDown}
          autoFocus
          placeholder={placeholder}
          onFocus={handleFocus}
          onBlur={handleBlur}
          onChange={handleChange}
          {...inputOtherProps}
        />
      ) : (
        <div
          className={classnames('op-scan', {
            [`op-scan-${size}`]: size,
          })}
        >
          {prefix ? (
            typeof prefix === 'boolean' ? (
              <div className='op-scan-icon'></div>
            ) : (
              prefix
            )
          ) : null}
          <span className='op-scan-title'>{placeholder}</span>
        </div>
      )}
    </div>
  );
}

// 通过监听keydown模拟扫码事件
function addEventListenerKeyDown2Scan(
  cb: (value: string) => void,
  focusRecord: IFocusOp,
  setDefaultInputValue: (value: string) => void,
) {
  let prevInputTime = Date.now();
  let inputValue = '';

  function onDocumentKeyDown(event?: KeyboardEvent) {
    const key = (event as KeyboardEvent).key;

    // 如果是在页面的文本框输入，则不处理
    if (event && ['INPUT'].includes((event.target as HTMLElement)?.tagName)) {
      return;
    }

    if (focusRecord.hasFocus()) {
      if (
        [
          'f1',
          'f2',
          'f3',
          'f4',
          'f5',
          'f6',
          'f7',
          'f8',
          'f9',
          'f10',
          'f11',
          'f12',
        ].includes(key?.toLowerCase?.())
      ) {
        // input focus 的时候，需要阻止默认快捷键，避免受到默认快捷的影响
        (event as KeyboardEvent)?.preventDefault?.();
      }
      return;
    }
    const upperKey = key.toUpperCase();
    const timestamp = Date.now();
    const timeDiff = timestamp - prevInputTime;
    prevInputTime = timestamp;
    const isScan = timeDiff < MAX_KEEP_TIME; // 是否扫码枪扫描
    if (!scanning && isScan) {
      scanning = true;
    }
    // 如下场景需屏蔽：
    // 1. ctrl+a/c/v/x,alt+a/c/v/x
    const cond1 =
      ((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).altKey) &&
      !WHITE_CTRL_KEYS.includes(key);
    // 2. 不是输入一个字符，比如shift up, down 等
    const cond2 =
      String(key).length !== 1 && !WHITE_SPECIAL_KEYS.includes(upperKey);
    // 3. 扫码状态屏蔽tab及浏览器默认行为
    const cond3 = isScan && upperKey === 'TAB';
    if (cond1 || cond2 || cond3) {
      event?.preventDefault();
      return;
    }
    if (isScan && upperKey === 'ENTER' && String(inputValue).length) {
      cb(inputValue);
      inputValue = '';
      // 延长scanning状态，避免与toggle冲突
      setTimeout(() => {
        scanning = false;
      }, 2);
      return;
    }
    if (isScan) {
      inputValue += key;
    } else {
      inputValue = key;
    }
    setDefaultInputValue(inputValue);
  }
  event.bind('all', onDocumentKeyDown);
  return () => {
    event.unbind('all', onDocumentKeyDown);
  };
}

let hasInputFocus = false;
interface IFocusOp {
  hasFocus: () => boolean;
  doFocus: () => void;
  unFocus: () => void;
}
function useHasInputFocus(): IFocusOp {
  useEffect(() => {
    return () => {
      // 实例销毁，需要做个判断
      if (hasInputFocus) {
        hasInputFocus = false;
      }
    };
  }, []);

  return {
    hasFocus() {
      return hasInputFocus;
    },
    doFocus() {
      hasInputFocus = true;
    },
    unFocus() {
      hasInputFocus = false;
    },
  };
}
