import * as React from 'react';
import { TagsInput } from 'react-tag-input-component';
import clsx from 'clsx';
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils';
import { TextControl } from '@tesseract-design/web-base';
import plugin from 'tailwindcss/plugin';

const TAG_INPUT_SEPARATOR_MAP = {
  'comma': ',',
  'newline': 'Enter',
  'semicolon': ';',
} as const;

const TAG_INPUT_VALUE_SEPARATOR_MAP = {
  'comma': ',',
  'newline': '\n',
  'semicolon': ';',
} as const;

/**
 * Separator for splitting the input value into multiple tags.
 */
export type TagInputSeparator = keyof typeof TAG_INPUT_SEPARATOR_MAP

/**
 * Derived HTML element of the {@link TagInput} component.
 */
export type TagInputDerivedElement = HTMLTextAreaElement | HTMLInputElement;

/**
 * Proxied HTML element of the {@link TagInput} component.
 */
export type TagInputProxiedElement = HTMLTextAreaElement & HTMLInputElement;

/**
 * Props of the {@link TagsInput} component.
 */
export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
  /**
   * Short textual description indicating the nature of the component's value.
   */
  label?: React.ReactNode,
  /**
   * Short textual description as guidelines for valid input values.
   */
  hint?: React.ReactNode,
  /**
   * Size of the component.
   */
  size?: TextControl.Size,
  /**
   * Additional description, usually graphical, indicating the nature of the component's value.
   */
  indicator?: React.ReactNode,
  /**
   * Should the component display a border?
   */
  border?: boolean,
  /**
   * Should the component occupy the whole width of its parent?
   */
  block?: boolean,
  /**
   * Style of the component.
   */
  variant?: TextControl.Variant,
  /**
   * Is the label hidden?
   */
  hiddenLabel?: boolean,
  /**
   * Should the component be enhanced?
   */
  enhanced?: boolean,
  /**
   * Separators for splitting the input value into multiple tags.
   */
  separator?: TagInputSeparator[],
  /**
   * Should the last tag be editable when removed?
   */
  editOnRemove?: boolean,
  /**
   * Fallback element for non-enhanced mode.
   */
  fallbackElement?: 'textarea' | 'input',
  /**
   * Separator used on the value of the input.
   */
  valueSeparator?: TagInputSeparator,
}

export const tagInputPlugin = plugin(({ addComponents }) => {
  addComponents({
    '.tag-input': {
      '& label + * + div > input': {
        'flex': 'auto',
      },
      '&[data-size="small"] label + * + div > input': {
        'font-size': '0.625rem',
      },
      '&[data-size="medium"] label + * + div > input': {
        'font-size': '0.75rem',
      },
      '&[data-variant="default"] label + * + div': {
        'padding-left': '1rem',
        'padding-right': '1rem',
      },

      '&[data-variant="alternate"] label + * + div': {
        'padding-left': '0.375rem',
        'padding-right': '0.375rem',
      },

      '&[data-size="small"][data-variant="default"] label + * + div': {
        'padding-top': '0.625rem',
        'padding-bottom': '0.875rem',
      },

      '&[data-size="medium"][data-variant="default"] label + * + div': {
        'padding-top': '0.75rem',
        'padding-bottom': '1rem',
      },

      '&[data-size="large"][data-variant="default"] label + * + div': {
        'padding-top': '1rem',
        'padding-bottom': '1.25rem',
      },

      '&[data-size="small"][data-variant="alternate"] label + * + div': {
        'padding-top': '1.375rem',
      },

      '&[data-size="medium"][data-variant="alternate"] label + * + div': {
        'padding-top': '1.5rem',
      },

      '&[data-size="large"][data-variant="alternate"] label + * + div': {
        'padding-top': '1.75rem',
      },

      '&[data-size="small"] label + * + div': {
        'gap': '0.25rem',
        'min-height': '2.5rem',
      },

      '&[data-size="small"].tag-input-indicator label + * + div': {
        'padding-right': '2.5rem',
      },

      '&[data-size="medium"] label + * + div': {
        'gap': '0.375rem',
        'min-height': '3rem',
      },

      '&[data-size="medium"].tag-input-indicator label + * + div': {
        'padding-right': '3rem',
      },

      '&[data-size="large"] label + * + div': {
        'gap': '0.375rem',
        'min-height': '4rem',
      },

      '&[data-size="large"].tag-input-indicator label + * + div': {
        'padding-right': '4rem',
      },

      '& label + * + div > span': {
        'padding': '0.125rem',
        'border-radius': '0.25rem',
        'line-height': '1',
        'background-color': 'rgb(var(--color-positive) / 25%)',
      },

      '& label + * + div > span:focus-within': {
        'background-color': 'rgb(var(--color-secondary) / 25%)',
      },

      '& label + * + div > span span': {
        'pointer-events': 'none',
      },

      '& label + * + div > span button': {
        'color': 'rgb(var(--color-primary))',
        'padding': '0',
        'width': '1rem',
        'margin-left': '0.25rem',
      },

      '& label + * + div > span button:focus': {
        'outline': 'none',
        'color': 'rgb(var(--color-secondary))',
      },

      '& label + * + div > span button:focus:-moz-focusring': {
        'display': 'none',
      },

      '& label + * + div > span button:hover': {
        'color': 'rgb(var(--color-primary))',
      },
    },
  });
});

/**
 * Component for inputting textual values.
 *
 * This component supports multiline input and adjusts its layout accordingly.
 */
export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>((
  {
    label,
    hint,
    indicator,
    size = 'medium' as const,
    border = false as const,
    block = false as const,
    variant = 'default' as const,
    hiddenLabel = false as const,
    className,
    enhanced: enhancedProp = false as const,
    separator = ['newline'],
    valueSeparator = separator?.[0] ?? 'newline',
    defaultValue,
    value,
    disabled,
    id: idProp,
    onFocus,
    onBlur,
    editOnRemove = false as const,
    placeholder,
    fallbackElement: FallbackElement = 'textarea' as const,
    ...etcProps
  },
  forwardedRef,
) => {
  const EffectiveFallbackElement = valueSeparator === 'newline' ? 'textarea' : FallbackElement;
  const { clientSide } = useClientSide({ clientSide: enhancedProp });
  const [tags, setTags] = React.useState<string[]>(() => {
    const effectiveValue = value ?? defaultValue;
    if (effectiveValue === undefined) {
      return [];
    }
    if (typeof effectiveValue === 'string') {
      return effectiveValue.split(TAG_INPUT_VALUE_SEPARATOR_MAP[valueSeparator]);
    }
    if (typeof effectiveValue === 'number') {
      return [effectiveValue.toString()];
    }
    return effectiveValue as string[];
  });
  const {
    defaultRef,
    handleChange: handleTagsInputChange,
  } = useProxyInput<
    string[],
    TagInputDerivedElement,
    TagInputProxiedElement
  >({
    forwardedRef,
    valueSetterFn: (v) => {
      setTags(v);
    },
    transformChangeHandlerArgs: (newTags) => {
      const thisNewTags = newTags as string[];
      return (
        thisNewTags.map((t) => t.trim()).join(TAG_INPUT_VALUE_SEPARATOR_MAP[valueSeparator])
      );
    },
  });
  const ref = forwardedRef ?? defaultRef;
  const labelId = React.useId();
  const id = useFallbackId(idProp);

  const handleFocus: React.FocusEventHandler<TagInputDerivedElement> = (e) => {
    if (!clientSide) {
      onFocus?.(e);
    }
  };

  const handleBlur: React.FocusEventHandler<TagInputDerivedElement> = (e) => {
    if (!clientSide) {
      onBlur?.(e);
    }
  };

  const handleRemoveTag = () => {
    if (!(typeof ref === 'object' && ref)) {
      return;
    }
    const { current: input } = ref;
    if (!input) {
      return;
    }
    setTimeout(() => {
      const sibling = input.nextElementSibling as HTMLDivElement;
      const tagsInput = sibling.children[sibling.children.length - 1] as HTMLInputElement;
      tagsInput.focus();
    });
  };

  const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
    if (!(typeof ref === 'object' && ref)) {
      return;
    }
    const { current: input } = ref;
    if (!input) {
      return;
    }
    onBlur?.({
      ...e,
      target: input,
      currentTarget: input,
    });
  };

  const handleFocusCapture: React.FocusEventHandler<HTMLDivElement> = (e) => {
    const { currentTarget } = e;
    if (!clientSide) {
      return;
    }
    const { activeElement } = window.document;
    if (!activeElement) {
      return;
    }
    const tagInputWrapper = currentTarget.children[1] as HTMLDivElement;
    const tagInput = (
      tagInputWrapper.children[tagInputWrapper.children.length - 1] as HTMLInputElement
    );
    if (activeElement !== tagInput) {
      return;
    }
    if (!(typeof ref === 'object' && ref)) {
      return;
    }
    const { current: input } = ref;
    if (!input) {
      return;
    }
    if (activeElement.tagName === 'INPUT') {
      onFocus?.({
        ...e,
        target: input,
        currentTarget: input,
      });
    }
  };

  return (
    <div
      data-size={size}
      data-variant={variant}
      className={clsx(
        'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
        'focus-within:ring-4',
        {
          'block': block,
          'inline-block align-middle': !block,
        },
        clientSide && {
          'min-h-10': size === 'small',
          'min-h-12': size === 'medium',
          'min-h-16': size === 'large',
        },
        'tag-input',
        indicator && 'tag-input-indicator',
        className,
      )}
      data-testid="base"
      onFocusCapture={handleFocusCapture}
    >
      {label && (
        <>
          <label
            data-testid="label"
            id={labelId}
            htmlFor={id}
            className={clsx(
              'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold group-focus-within:text-secondary text-primary leading-none bg-negative select-none',
              {
                'sr-only': hiddenLabel,
              },
              {
                'pr-1': !indicator,
              },
              {
                'pr-10': indicator && size === 'small',
                'pr-12': indicator && size === 'medium',
                'pr-16': indicator && size === 'large',
              },
            )}
          >
            <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
              {label}
            </span>
          </label>
          {' '}
        </>
      )}
      <EffectiveFallbackElement
        {...etcProps}
        placeholder={placeholder}
        disabled={disabled}
        ref={defaultRef}
        id={id}
        aria-labelledby={labelId}
        data-testid="input"
        defaultValue={defaultValue}
        value={value}
        onFocus={handleFocus}
        onBlur={handleBlur}
        style={{
          height: 0,
        }}
        tabIndex={clientSide ? -1 : undefined}
        className={clsx(
          'bg-negative rounded-inherit peer block font-inherit',
          'focus:outline-0',
          'disabled:opacity-50 disabled:cursor-not-allowed',
          {
            'resize': !block,
            'resize-y': block,
          },
          {
            'text-xxs': size === 'small',
            'text-xs': size === 'medium',
          },
          !clientSide && {
            'pl-4': variant === 'default',
            'pl-1.5': variant === 'alternate',
          },
          !clientSide && {
            'pt-4': variant === 'alternate' && size === 'small',
            'pt-5': variant === 'alternate' && size === 'medium',
            'pt-8': variant === 'alternate' && size === 'large',
          },
          !clientSide && {
            'py-2.5': variant === 'default' && size === 'small',
            'py-3': variant === 'default' && size === 'medium',
            'py-5': variant === 'default' && size === 'large',
          },
          !clientSide && {
            'pr-4': variant === 'default' && !indicator,
            'pr-1.5': variant === 'alternate' && !indicator,
          },
          !clientSide && {
            'pr-10': indicator && size === 'small',
            'pr-12': indicator && size === 'medium',
            'pr-16': indicator && size === 'large',
          },
          {
            'min-h-10': size === 'small',
            'min-h-12': size === 'medium',
            'min-h-16': size === 'large',
          },
          !clientSide && 'peer',
          !clientSide && 'w-full',
          clientSide && 'sr-only',
        )}
      />
      {clientSide && (
        <TagsInput
          value={tags}
          classNames={{
            input: 'peer bg-transparent font-inherit',
            tag: 'text-xs p-2 select-none font-inherit',
          }}
          isEditOnRemove={editOnRemove}
          placeHolder={placeholder}
          disabled={disabled}
          onBlur={handleInputBlur}
          onChange={handleTagsInputChange}
          onRemoved={handleRemoveTag}
          separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])}
        />
      )}
      {hint && (
        <div
          data-testid="hint"
          className={clsx(
            'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none',
            {
              'bottom-0 pl-4 pb-1': variant === 'default',
              'top-0.5': variant === 'alternate',
            },
            {
              'pt-2': variant === 'alternate' && size === 'small',
              'pt-3': variant === 'alternate' && size !== 'small',
            },
            {
              'pr-4': !indicator && variant === 'default',
              'pr-1': !indicator && variant === 'alternate',
            },
            {
              'pr-10': indicator && size === 'small',
              'pr-12': indicator && size === 'medium',
              'pr-16': indicator && size === 'large',
            },
          )}
        >
          <div
            className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
          >
            {hint}
          </div>
        </div>
      )}
      {indicator && (
        <div
          className={clsx(
            'text-center flex items-center justify-center aspect-square absolute bottom-0 right-0 pointer-events-none select-none',
            {
              'w-10': size === 'small',
              'w-12': size === 'medium',
              'w-16': size === 'large',
            },
          )}
        >
          {indicator}
        </div>
      )}
      {border && (
        <span
          data-testid="border"
          className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
        />
      )}
    </div>
  );
});

TagInput.displayName = 'TagInput';

TagInput.defaultProps = {
  label: undefined,
  hint: undefined,
  indicator: undefined,
  size: 'medium' as const,
  variant: 'default' as const,
  separator: ['newline'],
  border: false as const,
  block: false as const,
  hiddenLabel: false as const,
  enhanced: false as const,
  editOnRemove: false as const,
  fallbackElement: 'textarea' as const,
  valueSeparator: 'newline' as const,
};
