import clsx from 'clsx';
import React, {
  ChangeEvent,
  PropsWithChildren,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { noop } from '../../../helpers/utils';
import { Button, ButtonContext } from '../../Buttons';
import { IconName } from '../../Icons';
import { BaseFormControl, BaseInputEvents } from '../Form.models';
import { FormElementContainer } from '../FormElementContainer';
import { Select, SelectOption } from '../Select/Select';
import classes from './Tags.scss';

export interface TagsProps<T = string>
  extends BaseFormControl,
    BaseInputEvents {
  /** If set, sets the form control value */
  value?: string[];
  /** Array of options that can be selected from */
  tagsOptions?: T[];
  /** Optional Drop down label (default: '')*/
  dropDownLabel?: string;
  /** Whether or not the control should start focused (default: false) */
  autoFocus?: boolean;
  /** If the tags component uses objects as tagsOptions, this prop is used as the key to identify display value */
  displayKey?: keyof T;
  /** If the tags component uses objects as tagsOptions, this prop is used as the key to identify value */
  valueKey?: keyof T;
}

export const Tags = <T,>({
  id,
  name,
  value = [],
  dropDownLabel = '',
  tagsOptions = [],
  disabled = false,
  error,
  autoFocus = false,
  onChange = noop,
  onBlur,
  onFocus,
  displayKey,
  valueKey,
  className = '',
  ...rest
}: PropsWithChildren<TagsProps<T>>): JSX.Element => {
  const [currentTags, setCurrentTags] = useState<string[]>(value); // Current tags the user has selected

  const [shouldAnimate, setShouldAnimate] = useState<boolean>(false);

  const ref = useRef<ChangeEvent<HTMLInputElement>>();

  const transformedOptions: SelectOption[] = useMemo(() => {
    return tagsOptions.map((option) => {
      if (valueKey && displayKey) {
        return {
          value: String(option[valueKey]),
          label: String(option[displayKey]),
        };
      }
      return { value: String(option), label: String(option) };
    });
  }, [displayKey, tagsOptions, valueKey]);

  const visibleOptions = useMemo(() => {
    return transformedOptions.filter(
      (option) => !currentTags.includes(String(option.value)),
    );
  }, [currentTags, transformedOptions]);

  useEffect(() => {
    setCurrentTags(value);
  }, [value]);

  useEffect(() => {
    // Only emit if there is a current event
    if (ref.current) {
      onChange({
        ...ref.current,
        target: {
          ...ref.current.target,
          value: currentTags as unknown as string,
          name,
          id,
        },
        currentTarget: { name, id, value: currentTags as unknown as string },
      } as ChangeEvent<HTMLInputElement>);

      // Resets event data
      ref.current = undefined;
    }
  }, [currentTags, id, name, onChange]);

  /**
   * Adds a tag to currently selected list
   * @param e Select FormEvent
   */
  function addTag(e: ChangeEvent<HTMLInputElement>): void {
    setShouldAnimate(true);

    const newTag = e.currentTarget.value;

    // Set event data
    ref.current = e;

    setCurrentTags((prevState) => [...prevState, newTag]);
  }

  /**
   * Removes tags from currently selected tags
   * @param idx index of tag to be filtered
   * @param e Event
   */
  function removeTag(idx: number, e: unknown): void {
    // Set event data
    ref.current = e as ChangeEvent<HTMLInputElement>;

    setCurrentTags((prevState) =>
      prevState.filter((_: string, i: number) => i !== idx),
    );
  }

  function getDisplayValue(tag: string): string {
    if (displayKey && valueKey) {
      const option = tagsOptions.find(
        (option) => String(option[valueKey]) === tag,
      );

      return option ? String(option[displayKey]) : tag;
    }

    return tag;
  }

  return (
    <FormElementContainer
      {...rest}
      className={clsx(classes.container, 'tags-container', className)}
      error={error}
      dataTestFieldType="Tags"
      htmlFor={id ?? name}
    >
      <div className={clsx(classes.tagsWrapper)}>
        {currentTags.length < tagsOptions.length ? (
          <Select
            id={id ?? name}
            name={`tags-select-${name}`}
            className={clsx({
              [classes.hasError]: error !== undefined,
            })}
            options={visibleOptions}
            onChange={addTag}
            blurOnSelect
            disabled={disabled}
            onBlur={onBlur}
            onFocus={onFocus}
            autoFocus={autoFocus}
            inlineMode={true}
            placeholder={dropDownLabel}
          />
        ) : null}
        <TransitionGroup component={null}>
          {currentTags.map((tag, idx) => (
            <CSSTransition
              key={tag}
              timeout={{ enter: 1000, exit: 10 }}
              classNames={{
                enter: clsx(shouldAnimate && classes.tagEnter),
                enterActive: clsx(shouldAnimate && classes.tagEnterActive),
                exit: classes.tagExit,
                exitActive: classes.tagExitActive,
              }}
              onEntered={() => {
                setShouldAnimate(false);
              }}
            >
              <span
                key={tag}
                className={clsx({
                  [classes.selectedItem]: true,
                  [classes.disabled]: disabled,
                })}
                data-test-id="tag"
              >
                <span title={getDisplayValue(tag)}>{getDisplayValue(tag)}</span>
                <Button
                  type="button"
                  icon={IconName.X}
                  onButtonClicked={(e) => {
                    e.persist();
                    removeTag(idx, e);
                  }}
                  disabled={disabled}
                  buttonContext={ButtonContext.Icon}
                  dataTestId="tags-delete"
                ></Button>
              </span>
            </CSSTransition>
          ))}
        </TransitionGroup>
      </div>
    </FormElementContainer>
  );
};
