import {
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import css from 'styled-jsx/css';

import type { WithSpacingProps } from '~/src/lib/hocs/withSpacing';
import type {
  ChangeEvent,
  ChangeEventHandler,
  CSSProperties,
  InputHTMLAttributes,
  ReactNode,
  RefObject,
} from 'react';

import Box from '~/src/components/Box';
import Clickable from '~/src/components/Clickable';
import useDebounce from '~/src/hooks/useDebounce';
import useTheme from '~/src/hooks/useTheme';
import withSpacing from '~/src/lib/hocs/withSpacing';
import { useI18n } from '~/src/lib/i18n';
import { useTracker } from '~/src/lib/tracker/useTracker';
import { stripNonDomProps } from '~/src/lib/utils/stripNonDomProps';
import CrossIcon from '../Icon/CrossIcon';
import { TYPE_TO_PATTERN } from './constants';

const styles = css.resolve`
  .root {
    position: relative;
    align-items: center;
  }

  .root:focus-within {
    border-color: #555 !important;
  }

  .root.invalid {
    border-color: #812b2b !important;
    background: rgba(121, 42, 42, 0.38) !important;
  }

  .input {
    position: relative;
    height: 100%;
    flex: 1;
  }

  input {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;

    padding: 0;
    background: none;
    border-radius: 0;
    border: 0;
    width: 100%;
    height: 100%;
    color: inherit;
    font-family: inherit;
    font-weight: 300;
    letter-spacing: 0.02em;
  }

  input::placeholder {
    color: inherit;
    opacity: 0.6;
    letter-spacing: 0.03em;
  }

  input::-ms-clear,
  input::-ms-reveal {
    display: none;
    width: 0;
    height: 0;
  }

  input::-webkit-search-decoration,
  input::-webkit-search-cancel-button,
  input::-webkit-search-results-button,
  input::-webkit-search-results-decoration {
    display: none;
  }

  /* https://css-tricks.com/snippets/css/change-autocomplete-styles-webkit-browsers/ */
  input:-webkit-autofill,
  input:-webkit-autofill:hover,
  input:-webkit-autofill:focus {
    -webkit-text-fill-color: white;
    -webkit-box-shadow: 0 0 0px 1000px #000 inset;
    transition: background-color 5000s ease-in-out 0s;
    font: inherit;
  }
`;

export const TEXT_INPUT_HORIZONTAL_PADDING = '0.75em';
export const TEXT_INPUT_DEFAULT_HEIGHT = '4.8rem';

// >= 16px to avoid ios zooming in
export const TEXT_INPUT_DEFAULT_FONT_SIZE = '1.7rem';

const DEFAULT_DEBOUNCE_TIMEOUT = 300;

export interface TextInputApi {
  reportValidity(params: { value: string; el: HTMLInputElement }): boolean;
  setValue(value: string): void;
}

export type TextInputToValidationMessage = (params: {
  value: string;
}) => string | undefined;

export type TextInputOnChange = (params: {
  value: string;
  event: ChangeEvent<HTMLInputElement>;
  el: HTMLInputElement;
  setValue: (nextValue: string) => void;
}) => void;

export type TextInputOnInputEnd = (params: {
  value: string;
  event: ChangeEvent<HTMLInputElement>;
  isValid: boolean;
}) => void;

export interface TextInputProps
  extends Omit<
      InputHTMLAttributes<HTMLInputElement>,
      'onChange' | 'size' | 'pattern'
    >,
    WithSpacingProps {
  borderColor?: string;
  debounceTimeout?: number;
  value?: string;
  defaultValue?: string;
  fontSize?: number | string;
  type?: 'text' | 'email' | 'url';
  renderBefore?: () => ReactNode;
  renderAfter?: (params: {
    clear: () => void;
    background: string;
  }) => ReactNode;
  centerText?: boolean;
  withBorder?: boolean;
  withBackground?: boolean;
  withShadow?: boolean;
  isDisabled?: boolean;
  withTracking?: boolean;
  showClearButtonOnEmpty?: boolean;
  testId?: string;

  /**
   * Cleans leading and excess whitespace as the user types.
   *
   * @default true
   */
  withCleanOnChange?: boolean;

  toValidationMessage?: TextInputToValidationMessage;
  onChange?: TextInputOnChange;

  /**
   * Debounce callback, called when user finishes typing.
   *
   * WARN: make sure to memo-ize this function using `useCallback()`
   * otherwise strange bugs will happen.
   */
  onInputEnd?: TextInputOnInputEnd;

  onClear?: () => void;
  inputRef?: RefObject<HTMLInputElement | null>;
  apiRef?: RefObject<TextInputApi | null>;
}

const TextInput = withSpacing<TextInputProps>(
  ({
    borderColor,
    onClear,
    onChange,
    onInputEnd,
    placeholder,
    type = 'text',
    renderBefore,
    renderAfter,
    centerText,
    withBorder = true,
    withBackground = true,
    withShadow = true,
    testId,
    inputRef,
    apiRef,
    className = '',
    isDisabled,
    fontSize = TEXT_INPUT_DEFAULT_FONT_SIZE,
    toValidationMessage,
    withCleanOnChange = true,
    showClearButtonOnEmpty = false,
    style,
    autoComplete = 'off',
    debounceTimeout = DEFAULT_DEBOUNCE_TIMEOUT,
    withTracking = false,
    autoFocus,
    ...inputProps
  }) => {
    const { t } = useI18n('app');
    const theme = useTheme();
    const { trackEvent } = useTracker();
    const defaultRef = useRef<HTMLInputElement>(null);
    const [isInvalid, setIsInvalid] = useState(false);

    const height = style?.height || TEXT_INPUT_DEFAULT_HEIGHT;
    const internalInputRef = inputRef || defaultRef;

    const clear = () => {
      const el = internalInputRef?.current;

      if (el) {
        el.value = '';
        el.focus();
      }
    };

    const renderClearButton = (showOnEmpty: boolean) => {
      const hasQuery = !!inputProps.value;
      const opacity = hasQuery || showOnEmpty ? '' : 0;

      return (
        <Clickable
          onClick={onClear}
          width="auto"
          margin="0 .5em"
          testId={`${testId}-clearButton`}
        >
          <CrossIcon style={{ opacity }} />
        </Clickable>
      );
    };

    const reportValidity = ({
      value,
      el,
      dryRun = false,
    }: {
      value: string;
      el: HTMLInputElement;
      dryRun?: boolean;
    }) => {
      if (toValidationMessage) {
        const message = toValidationMessage({ value });

        // setting a custom validation message causes el.checkValidity() to return false
        el.setCustomValidity(message ?? '');
      } else {
        const valueEmpty = Boolean(inputProps.required && !value);
        const { valueMissing, typeMismatch, patternMismatch } = el.validity;

        if (valueEmpty || valueMissing) {
          el.setCustomValidity(t('errors.required'));
        } else if (typeMismatch || patternMismatch) {
          const messages = {
            email: t('errors.invalidEmail'),
            default: t('errors.invalidPattern'),
          };

          el.setCustomValidity(messages[type] ?? messages.default);
        } else {
          // default validation message if no custom function provided
          el.setCustomValidity('');
        }
      }

      // If the field is `required` and empty or a `pattern` doesn't match
      // the value this will return `false` and the state change will turn the field red.
      const isValid = !value || !!el.checkValidity();

      if (isValid) {
        setIsInvalid(false);
      } else {
        setIsInvalid(true);

        if (!dryRun) {
          // show browser native validation message tooltip
          el.reportValidity();
        }
      }

      return isValid;
    };

    // REVIEW: it's kinda annoying UX how fields are validated on keypress.
    // For example you get shouted out when you've entered partial data
    // that doesn't match a validation pattern (eg. email or url). It would
    // we better to show invalid state on <form> submit, maybe one field at a time.
    // In order to do this we could set the validation message onChange, but only
    // .reportValidity() on submit. We'd need to revise how we show the red input
    // background, perhaps an `input:invalid:before`  selector could work with an
    // absolutely positioned div that sits under the content?
    const onChangeDebounced = useDebounce<TextInputOnChange>(
      ({ value, event, el }) => {
        const isValid = reportValidity({ value, el });

        if (withTracking) {
          trackEvent({
            type: 'input-text',
            value,
            className: el.className,
            id: el.id || testId,
          });
        }

        if (onInputEnd) {
          onInputEnd({ value, event, isValid });
        }
      },
      debounceTimeout,
      [onInputEnd, toValidationMessage]
    );

    const onChangeInternal = useCallback<ChangeEventHandler<HTMLInputElement>>(
      (event) => {
        const el = event.target;
        let value = el.value;

        const setValue = (nextValue: string) => {
          if (nextValue !== el.value) {
            const delta = nextValue.length - el.value.length;
            const cursorPosition = el.selectionStart;

            el.value = nextValue;

            // after value changed ensure caret position is preserved
            if (typeof cursorPosition === 'number') {
              const nextCaretPosition = cursorPosition + delta;
              el.setSelectionRange(nextCaretPosition, nextCaretPosition);
            }
          }
        };

        // trim leading whitespace and excess spaces
        if (withCleanOnChange) {
          value = el.value.trimStart().replace(/ +/g, ' ');
          setValue(value);
        }

        if (onChange) {
          onChange({ setValue, value, event, el });
        }

        // don't show invalid styling while typing
        setIsInvalid(false);

        // hide validation messages while the user is typing
        if ((toValidationMessage && isInvalid) || el.validationMessage) {
          // HACK: empty string doesn't work here to hide native browser tooltip
          el.setCustomValidity(' ');
        }

        onChangeDebounced({ setValue, value, event, el });
      },
      [isInvalid, onChange, onChangeDebounced]
    );

    const background = withBackground ? theme.textInputBackground : '#000';

    const inputStyle: CSSProperties = {
      textAlign: centerText ? 'center' : 'left',
      fontSize: '1em',
      color: theme.textColor90,
      scrollPadding: '1em',
      paddingRight: centerText ? '' : `0 ${TEXT_INPUT_HORIZONTAL_PADDING}`,
      textIndent: centerText ? '' : TEXT_INPUT_HORIZONTAL_PADDING,
    };

    if (isDisabled) inputStyle.opacity = 0.4;
    if (isInvalid) className += ' invalid';

    const beforeContent = renderBefore && renderBefore();

    useImperativeHandle(apiRef, () => ({
      reportValidity,
      setValue(value: string) {
        const el = internalInputRef?.current;

        if (el) {
          el.value = value;
        }
      },
    }));

    useEffect(() => {
      const el = internalInputRef?.current;

      if (el) {
        // set initial validity state on mount
        // it needs when user not interact with input but form is submitted and browser native tooltip is shown
        reportValidity({ value: el.value, el, dryRun: true });
      }

      // re-validate on value change from outside (eg. form reset)
    }, [inputProps.defaultValue, inputProps.value]);

    useEffect(() => {
      if (autoFocus) {
        const el = internalInputRef?.current;

        if (el) {
          el.focus();
          const valueLength = el.value.length;
          el.setSelectionRange(valueLength, valueLength);
        }
      }
    }, []);

    return (
      <Box
        flexRow
        className={`${styles.className} ${className} root`}
        style={{
          background,
          border: withBorder
            ? `solid 1px ${borderColor || theme.textInputBorderColor}`
            : 'none',
          borderRadius: withBorder ? `${theme.borderRadius}px` : undefined,
          boxShadow: withShadow ? '0 1px 3px rgba(0,0,0,1)' : undefined,
          overflow: 'hidden',
          contain: 'strict',
          fontSize,
          height,
          ...style,
        }}
      >
        {beforeContent}
        <div className={`${styles.className} input`}>
          <input
            {...stripNonDomProps(inputProps, { stripChildren: true })}
            ref={internalInputRef}
            type={type}
            pattern={TYPE_TO_PATTERN[type]}
            className={styles.className}
            placeholder={placeholder}
            onChange={onChangeInternal}
            autoComplete={autoComplete}
            autoCorrect="off"
            autoCapitalize="off"
            autoFocus={autoFocus}
            spellCheck="false"
            data-testid={testId}
            style={inputStyle}
            disabled={isDisabled}
          />
        </div>
        {(() => {
          let content;

          if (renderAfter) content = renderAfter({ clear, background });
          else if (onClear) return renderClearButton(showClearButtonOnEmpty);

          return content;
        })()}
        {styles.styles}
      </Box>
    );
  }
);

export default TextInput;
