import React, {
  ChangeEventHandler,
  cloneElement,
  Component,
  CompositionEvent,
  FocusEvent,
  InputHTMLAttributes,
  KeyboardEvent,
  KeyboardEventHandler,
  ReactElement,
  ReactNode,
} from 'react';
import classNames from 'classnames';
import isString from 'lodash/isString';
import isFunction from 'lodash/isFunction';
import omit from 'lodash/omit';
import Group from './Group';
import Search from './Search';
import TextArea from './TextArea';
import Icon from '../icon';
import { Size } from '../_util/enum';
import { getIeVersion, isChrome } from '../_util/browser';
import ConfigContext, { ConfigContextValue } from '../config-provider/ConfigContext';

function fixControlledValue(value: undefined | null | string) {
  if (typeof value === 'undefined' || value === null) {
    return '';
  }
  return value;
}

function isNeedTriggerAfterComposition(): boolean {
  return isChrome() || getIeVersion() === 11;
}

function upperCaseString(value: any) {
  if (isString(value)) {
    return value.toUpperCase();
  }
  return undefined;
}

function lowerCaseString(value: any) {
  if (isString(value)) {
    return value.toLowerCase();
  }
  return undefined;
}

/**
 * 判断是否全角
 */
function isDbc(s: string) {
  let dbc = false;
  if (isString(s)) {
    for (let i = 0; i < s.length; i++) {
      const c = s.charCodeAt(i);
      if ((c > 65248) || (c === 12288)) {
        dbc = true;
        break;
      }
    }
  }
  return dbc;
}

/**
 * 全角转半角
 */
function dbcToSbc(str: string) {
  const result: string[] = [];
  if (isString(str)) {
    for (let i = 0; i < str.length; i++) {
      const code = str.charCodeAt(i); // 获取当前字符的unicode编码
      if (code >= 65281 && code <= 65373) { // 在这个unicode编码范围中的是所有的英文字母已及各种字符
        result.push(String.fromCharCode(code - 65248)); // 把全角字符的unicode编码转换为对应半角字符的unicode码
      } else if (code === 12288) { // 空格
        result.push(' ');
      } else {
        result.push(str.charAt(i));
      }
    }
  }
  return result.join('');
}

export type InputSelection = {
  start: number | null,
  end: number | null,
} | null;

export interface AbstractInputProps<T> extends Omit<InputHTMLAttributes<T>, 'onChange' | 'onCopy' | 'size' | 'prefix'> {
  prefixCls?: string;
  label?: ReactNode;
  showLengthInfo?: boolean | 'never';
  showPasswordEye?: boolean | 'hold' | 'nohold';
  labelLayout?: 'float' | 'none';
  onChange?: ChangeEventHandler<T>;
  onPressEnter?: KeyboardEventHandler<T>;
}

export interface InputProps extends AbstractInputProps<HTMLInputElement> {
  copy?: boolean;
  size?: Size;
  addonBefore?: ReactNode;
  addonAfter?: ReactNode;
  onCopy?: (value: any) => void;
  prefix?: ReactNode;
  suffix?: ReactNode;
  focused?: boolean;
  border?: boolean;
  typeCase?: 'upper' | 'lower';
  dbc2sbc?: boolean;
  trimAll?: boolean;
  trim?: boolean;
  inputChinese?: boolean;
}

export interface InputState {
  value?: any;
  focused?: boolean;
  showPassword?: boolean;
}

export default class Input extends Component<InputProps, any> {
  static displayName = 'Input';

  static get contextType(): typeof ConfigContext {
    return ConfigContext;
  }

  static Group: typeof Group;

  static Search: typeof Search;

  static TextArea: typeof TextArea;

  static defaultProps = {
    type: 'text',
    disabled: false,
    readOnly: false,
    showLengthInfo: true,
    showPasswordEye: false,
    border: true,
    dbc2sbc: false,
    trim: false,
    trimAll: false,
    inputChinese: true,
    labelLayout: 'float',
  };

  context: ConfigContextValue;

  state: InputState;

  input: HTMLInputElement;

  rendered?: HTMLDivElement;

  prefix?: HTMLSpanElement;

  suffix?: HTMLSpanElement;

  isOnComposition = false;

  inputSelection: InputSelection;

  constructor(props, context: ConfigContextValue) {
    super(props, context);
    this.state = {
      value: typeof props.value === 'undefined' ? props.defaultValue : props.value,
      focused: false,
      showPassword: false,
    };
  }

  componentDidMount() {
    const { focused, autoFocus } = this.props;
    if (autoFocus) {
      this.setState({
        focused: true,
      });
    }
    if (typeof focused === 'boolean') {
      this.setState({
        focused,
      });
    }
    this.setRenderedStyle();
  }

  componentWillReceiveProps(nextProps: InputProps) {
    const { value } = this.state;
    if ('value' in nextProps && value !== nextProps.value) {
      this.setState({
        value: nextProps.value,
      });
    }
    if (nextProps.autoFocus) {
      this.setState({
        focused: true,
      });
    }
    if (typeof nextProps.focused === 'boolean') {
      this.setState({
        focused: nextProps.focused,
      });
    }
    if (nextProps.type !== 'password') {
      this.setState({
        showPassword: false,
      });
    }
  }

  componentDidUpdate(prevProps: InputProps) {
    const { inputSelection } = this;
    const { value } = prevProps;
    if (inputSelection && value && !this.isOnComposition) {
      // 在 didUpdate 时根据情况恢复光标的位置
      // 如果光标的位置小于值的长度,那么可以判定属于中间编辑的情况
      // 此时恢复光标的位置
      // 当如果不是 onComposiotionend 触发的事件时不用修改位置
      if (inputSelection.start &&
        this.transformValue(value).length &&
        inputSelection.start < this.transformValue(value).length) {
        const input = this.input;
        input.selectionStart = inputSelection.start;
        input.selectionEnd = inputSelection.end;
        this.inputSelection = null;
      }
    }
    this.setRenderedStyle();
  }

  setRenderedStyle() {
    const { rendered, input, suffix, prefix } = this;
    if (rendered || input) {
      let suffixWidth: string;
      let prefixWidth: string;
      let marginRight = '';
      let marginLeft = '';
      let width = '100%';
      if (suffix && prefix) {
        suffixWidth = `${(suffix.clientWidth || -2) + 2}px`;
        prefixWidth = `${(prefix.clientWidth || -2) + 2}px`;
        marginRight = suffixWidth;
        marginLeft = prefixWidth;
        width = `calc(100% - ${suffixWidth} - ${prefixWidth})`;
      } else if (suffix) {
        suffixWidth = `${(suffix.clientWidth || -2) + 2}px`;
        marginRight = suffixWidth;
        width = `calc(100% - ${suffixWidth})`;
      } else if (prefix) {
        prefixWidth = `${(prefix.clientWidth || -2) + 2}px`;
        marginLeft = prefixWidth;
        width = `calc(100% - ${prefixWidth})`;
      }
      if (rendered) {
        rendered.style.marginRight = marginRight;
        rendered.style.marginLeft = marginLeft;
        rendered.style.width = width;
      } else if (input) {
        input.style.paddingRight = marginRight;
        input.style.paddingLeft = marginLeft;
      }
    }
  }

  handleComposition = (e: CompositionEvent) => {
    if (e.type === 'compositionend') {
      // composition is end
      this.isOnComposition = false;
      if (isNeedTriggerAfterComposition()) {
        this.handleChange(e);
      }
    } else {
      // in composition
      this.isOnComposition = true;
    }
  };

  handleFocus = (e: FocusEvent<HTMLInputElement>) => {
    const { onFocus, readOnly } = this.props;
    if (!readOnly) {
      this.setState({
        focused: true,
      });
    }
    if (onFocus) {
      onFocus(e);
    }
  };

  handleBlur = (e: FocusEvent<HTMLInputElement>) => {
    const value = e.target.value;
    const { onChange, trim, trimAll, onBlur, readOnly } = this.props;
    if (!readOnly) {
      this.setState({
        focused: false,
      });
    }
    let trimValue = value;
    if (trim && isString(value)) {
      trimValue = value.trim();
    }
    if (trimAll && isString(value)) {
      trimValue = value.replace(/\s/g, '');
    }
    if (trimValue !== value) {
      this.input.value = trimValue;
      if (onChange && isFunction(onChange)) {
        e.target.value = trimValue;
        onChange(e);
      }
    }
    if (onBlur && isFunction(onBlur)) {
      onBlur(e);
    }
  };

  handleChange = e => {
    const { onChange } = this.props;
    if (!this.isOnComposition) {
      // 在 onChange 时记录光标的位置
      if (this.input) {
        this.inputSelection = {
          start: this.input.selectionStart,
          end: this.input.selectionEnd,
        };
      }
      const transformValue = this.transformValue(e.target.value);
      if (transformValue !== e.target.value) {
        e.target.value = this.transformValue(e.target.value);
        if (this.inputSelection && (this.inputSelection.start || this.inputSelection.end)) {
          e.target.setSelectionRange(this.inputSelection.start, this.inputSelection.end);
          this.inputSelection = null;
        }
      }
    }
    if (!('value' in this.props)) {
      this.setState({ value: e.target.value });
    }
    if (onChange && isFunction(onChange)) {
      onChange(e);
    }
  };

  handleCopy = () => {
    const { onCopy } = this.props;
    this.input.select();
    document.execCommand('Copy');
    if (onCopy) {
      onCopy(this.input.value);
    }
  };

  handleTogglePassword = () => {
    const { showPassword } = this.state;
    this.setState({
      showPassword: !showPassword,
    });
  };

  handleShowPassword = () => {
    this.setState({
      showPassword: true,
    });
  };

  handleHidePassword = () => {
    this.setState({
      showPassword: false,
    });
  };

  handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    const { onPressEnter, onKeyDown } = this.props;
    if (e.keyCode === 13 && onPressEnter) {
      onPressEnter(e);
    }
    if (onKeyDown) {
      onKeyDown(e);
    }
  };

  saveInput = (node: HTMLInputElement) => {
    this.input = node;
  };

  saveRenderedRef = (node: HTMLDivElement) => {
    this.rendered = node;
  };

  savePrefix = (node: HTMLSpanElement) => {
    this.prefix = node;
  };

  saveSuffix = (node: HTMLSpanElement) => {
    this.suffix = node;
  };

  focus() {
    this.input.focus();
  }

  blur() {
    this.input.blur();
  }

  getPrefixCls() {
    const { prefixCls } = this.props;
    const { getPrefixCls } = this.context;
    return getPrefixCls('input', prefixCls);
  }

  getInputClassName() {
    const { size, copy, disabled, typeCase, showPasswordEye, type } = this.props;
    const prefixCls = this.getPrefixCls();
    return classNames(prefixCls, {
      [`${prefixCls}-sm`]: size === Size.small,
      [`${prefixCls}-lg`]: size === Size.large,
      [`${prefixCls}-disabled`]: disabled,
      [`${prefixCls}-upper`]: typeCase === 'upper',
      [`${prefixCls}-lower`]: typeCase === 'lower',
      [`${prefixCls}-has-copy`]: copy,
      [`${prefixCls}-has-eye`]: showPasswordEye && type === 'password',
    });
  }

  transformValue(v: any) {
    const { typeCase, dbc2sbc = true, inputChinese } = this.props;
    let value = v;
    if (typeCase === 'upper') {
      value = upperCaseString(v);
    } else if (typeCase === 'lower') {
      value = lowerCaseString(v);
    }

    if (dbc2sbc && isDbc(v)) {
      value = dbcToSbc(v);
    }

    if (!inputChinese && isString(value)) {
      value = value.replace(/[\u4e00-\u9fa5]/g, '');
    }

    return value;
  }

  renderCopyIcon(prefixCls?: string) {
    const { copy } = this.props;
    return copy ? (
      <span className={`${prefixCls}-icon `} onClick={this.handleCopy}>
        <Icon className={`${prefixCls}-icon-copy`} type="library_books" />
      </span>
    ) : null;
  }

  renderShowPassword(prefixCls?: string) {
    const { type, showPasswordEye } = this.props;
    if (showPasswordEye && type === 'password') {
      const { showPassword } = this.state;
      const props: any = {};
      if (showPasswordEye === 'nohold') {
        props.onMouseDown = this.handleShowPassword;
        props.onMouseLeave = this.handleHidePassword;
        props.onMouseUp = this.handleHidePassword;
      } else {
        props.onClick = this.handleTogglePassword;
      }
      return (
        <span className={`${prefixCls}-icon ${prefixCls}-icon-eye`} {...props}>
          <Icon
            className={`${prefixCls}-icon-copy`}
            type={showPassword ? 'visibility' : 'visibility_off'}
          />
        </span>
      );
    }
  }

  getLengthInfo(prefixCls?: string) {
    const { maxLength, showLengthInfo } = this.props;
    const { value } = this.state;
    const inputLength = value ? value.length : 0;
    return showLengthInfo !== 'never' && ((maxLength && showLengthInfo === true) || (maxLength && maxLength > 0 && inputLength === maxLength)) ? (
      <div className={`${prefixCls}-length-info`}>{`${inputLength}/${maxLength}`}</div>
    ) : null;
  }

  renderFloatLabel(prefixCls?: string): ReactNode {
    const { label } = this.props;
    if (label) {
      return (
        <div className={`${prefixCls}-label-wrapper`}>
          <div className={`${prefixCls}-label`}>{label}</div>
        </div>
      );
    }
  }

  getSizeClassName(name: string, prefixCls?: string) {
    const { size } = this.props;
    return classNames(`${prefixCls}-${name}`, {
      [`${prefixCls}-${name}-sm`]: size === Size.small,
      [`${prefixCls}-${name}-lg`]: size === Size.large,
    });
  }

  hasValue() {
    const { value } = this.state;
    return value && value.length !== 0;
  }

  renderLabeledIcon(children: ReactElement<any>, prefixCls?: string) {
    const { props } = this;
    const hasBorder = props.border && props.labelLayout === 'float';
    const passwordEye = this.renderShowPassword(prefixCls);
    const copyIcon = this.renderCopyIcon(prefixCls);
    const floatLabel = hasBorder && this.renderFloatLabel(prefixCls);
    const { className } = props;
    const prefix = props.prefix ? (
      <span className={`${prefixCls}-prefix`} ref={this.savePrefix}>
        {props.prefix}
      </span>
    ) : null;

    const $suffix = props.showPasswordEye === 'nohold' && passwordEye ? passwordEye : props.suffix;
    const $passwordEye = props.showPasswordEye === 'nohold' ? undefined : passwordEye;
    const suffix = $suffix ? (
      <span className={`${prefixCls}-suffix`} ref={this.saveSuffix}>
        {$suffix}
      </span>
    ) : null;
    if (hasBorder) {
      const { focused } = this.state;
      const preProps = children.props;
      children = cloneElement<InputProps>(children, {
        className: classNames(preProps.className, className),
        placeholder: (!floatLabel || focused) ? preProps.placeholder : null,
      });
    }
    if ($passwordEye || copyIcon || floatLabel) {
      children = (
        <div
          className={this.getSizeClassName('rendered', prefixCls)}
          ref={this.saveRenderedRef}
        >
          {children}
          {floatLabel}
          {copyIcon}
          {$passwordEye}
        </div>
      );
    }

    if (prefix || suffix) {
      const affixWrapperCls = classNames(this.getSizeClassName('affix-wrapper', prefixCls), {
        [`${className}`]: className && !hasBorder,
        [`${prefixCls}-has-border`]: hasBorder,
      });
      return (
        <span
          className={affixWrapperCls}
          style={props.style}
        >
          {prefix}
          {cloneElement(children, { style: null })}
          {suffix}
        </span>
      );
    }
    if (hasBorder) {
      return (
        <span className={`${prefixCls}-has-border`} style={props.style}>
          {cloneElement(children, { style: null })}
        </span>
      );
    }
    return cloneElement(children, { className: classNames(className, children.props.className), style: props.style });
  }

  renderInput(prefixCls?: string) {
    const { type } = this.props;
    const { value, showPassword } = this.state;
    const omits = [
      'prefixCls',
      'onPressEnter',
      'addonBefore',
      'addonAfter',
      'prefix',
      'suffix',
      'label',
      'labelLayout',
      'copy',
      'style',
      'focused',
      'showLengthInfo',
      'showPasswordEye',
      'size',
      'border',
      'form',
      'onChange',
      'dbc2sbc',
      'typeCase',
      'trim',
      'trimAll',
      'inputChinese',
      'type',
    ];
    const otherProps: Omit<InputProps,
      'prefixCls'
      | 'onPressEnter'
      | 'addonBefore'
      | 'addonAfter'
      | 'prefix'
      | 'suffix'
      | 'label'
      | 'labelLayout'
      | 'copy'
      | 'style'
      | 'focused'
      | 'showLengthInfo'
      | 'showPasswordEye'
      | 'size'
      | 'border'
      | 'form'
      | 'onChange'
      | 'dbc2sbc'
      | 'typeCase'
      | 'trim'
      | 'trimAll'
      | 'inputChinese'
      | 'type'
      | 'placeholder'> = omit(this.props, omits);

    return this.renderLabeledIcon(
      <input
        {...otherProps}
        value={fixControlledValue(value)}
        className={this.getInputClassName()}
        onKeyDown={this.handleKeyDown}
        ref={this.saveInput}
        onFocus={this.handleFocus}
        onBlur={this.handleBlur}
        onChange={this.handleChange}
        onCompositionStart={this.handleComposition}
        onCompositionUpdate={this.handleComposition}
        onCompositionEnd={this.handleComposition}
        type={showPassword ? 'text' : type}
      />,
      prefixCls,
    );
  }

  getWrapperClassName(prefixCls?: string) {
    const { disabled, label, prefix, suffix } = this.props;
    const { focused } = this.state;
    return classNames({
      [`${prefixCls}-has-value`]: this.hasValue(),
      [`${prefixCls}-focused`]: focused,
      [`${prefixCls}-disabled`]: disabled,
      [`${prefixCls}-has-label`]: !!label,
      [`${prefixCls}-has-prefix`]: !!prefix,
      [`${prefixCls}-has-suffix`]: !!suffix,
    });
  }

  renderLabeledInput(children: ReactElement<any>, prefixCls?: string) {
    const props = this.props;

    const wrapperClassName = `${prefixCls}-group`;
    const addonClassName = `${wrapperClassName}-addon`;
    const addonBefore = props.addonBefore ? (
      <span className={addonClassName}>
        {props.addonBefore}
      </span>
    ) : null;

    const addonAfter = props.addonAfter ? (
      <span className={addonClassName}>
        {props.addonAfter}
      </span>
    ) : null;
    const lengthInfo = this.getLengthInfo(prefixCls);
    const className = classNames(`${prefixCls}-wrapper`, this.getWrapperClassName(prefixCls), {
      [wrapperClassName]: (addonBefore || addonAfter),
    });

    const groupClassName = this.getSizeClassName('group-wrapper', prefixCls);

    // Need another wrapper for changing display:table to display:inline-block
    // and put style prop in wrapper
    if (addonBefore || addonAfter) {
      return (
        <span
          className={groupClassName}
          style={props.style}
        >
          <span className={className}>
            {addonBefore}
            {cloneElement(children, { style: null })}
            {addonAfter}
          </span>
          {lengthInfo}
        </span>
      );
    }
    if (lengthInfo) {
      return (
        <span className={className} style={props.style}>
          {cloneElement(children, { style: null })}
          {lengthInfo}
        </span>
      );
    }
    return cloneElement(children, { className: classNames(children.props.className, className) });
  }

  render() {
    const prefixCls = this.getPrefixCls();
    return this.renderLabeledInput(this.renderInput(prefixCls), prefixCls);
  }
}