import classnames from 'classnames';
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid';
import type { GenericCompletionConfig } from './Editor/genericCompletion';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
import { Input } from './Input';

export type PairEditorProps = {
  pairs: Pair[];
  onChange: (pairs: Pair[]) => void;
  className?: string;
  namePlaceholder?: string;
  valuePlaceholder?: string;
  nameAutocomplete?: GenericCompletionConfig;
  valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
};

type Pair = {
  name: string;
  value: string;
};

type PairContainer = {
  pair: Pair;
  id: string;
};

export const PairEditor = memo(function PairEditor({
  pairs: originalPairs,
  nameAutocomplete,
  valueAutocomplete,
  namePlaceholder,
  valuePlaceholder,
  className,
  onChange,
}: PairEditorProps) {
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
  const [pairs, setPairs] = useState<PairContainer[]>(() => {
    // Remove empty headers on initial render
    const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
    const pairs = nonEmpty.map((pair) => newPairContainer(pair));
    return [...pairs, newPairContainer()];
  });

  const setPairsAndSave = useCallback(
    (fn: (pairs: PairContainer[]) => PairContainer[]) => {
      setPairs((oldPairs) => {
        const pairs = fn(oldPairs).map((p) => p.pair);
        onChange(pairs);
        return fn(oldPairs);
      });
    },
    [onChange],
  );

  const handleMove = useCallback<FormRowProps['onMove']>(
    (id, side) => {
      const dragIndex = pairs.findIndex((r) => r.id === id);
      setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
    },
    [pairs],
  );

  const handleEnd = useCallback<FormRowProps['onEnd']>(
    (id: string) => {
      if (hoveredIndex === null) return;
      setHoveredIndex(null);

      setPairsAndSave((pairs) => {
        const index = pairs.findIndex((p) => p.id === id);
        const pair = pairs[index];
        if (pair === undefined) return pairs;

        const newPairs = pairs.filter((p) => p.id !== id);
        if (hoveredIndex > index) newPairs.splice(hoveredIndex - 1, 0, pair);
        else newPairs.splice(hoveredIndex, 0, pair);
        return newPairs;
      });
    },
    [hoveredIndex],
  );

  const handleChangeHeader = useCallback((pair: PairContainer) => {
    setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair)));
  }, []);

  // Ensure there's always at least one pair
  useEffect(() => {
    if (pairs.length === 0) {
      setPairs((pairs) => [...pairs, newPairContainer()]);
    }
  }, [pairs]);

  const handleDelete = useCallback((pair: PairContainer) => {
    setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
  }, []);

  const handleFocus = useCallback(
    (pair: PairContainer) => {
      const isLast = pair.id === pairs[pairs.length - 1]?.id;
      if (isLast) {
        setPairs((pairs) => [...pairs, newPairContainer()]);
      }
    },
    [pairs],
  );

  return (
    <div
      className={classnames(
        className,
        'pb-6 grid',
        // NOTE: Add padding to top so overflow doesn't hide drop marker
        'py-1',
      )}
    >
      {pairs.map((p, i) => {
        const isLast = i === pairs.length - 1;
        return (
          <Fragment key={p.id}>
            {hoveredIndex === i && <DropMarker />}
            <FormRow
              pairContainer={p}
              isLast={isLast}
              onChange={handleChangeHeader}
              nameAutocomplete={nameAutocomplete}
              valueAutocomplete={valueAutocomplete}
              namePlaceholder={namePlaceholder}
              valuePlaceholder={valuePlaceholder}
              onFocus={handleFocus}
              onDelete={isLast ? undefined : handleDelete}
              onEnd={handleEnd}
              onMove={handleMove}
            />
            {hoveredIndex === pairs.length && <DropMarker />}
          </Fragment>
        );
      })}
    </div>
  );
});

enum ItemTypes {
  ROW = 'pair-row',
}

type FormRowProps = {
  pairContainer: PairContainer;
  onMove: (id: string, side: 'above' | 'below') => void;
  onEnd: (id: string) => void;
  onChange: (pair: PairContainer) => void;
  onDelete?: (pair: PairContainer) => void;
  onFocus?: (pair: PairContainer) => void;
  isLast?: boolean;
} & Pick<
  PairEditorProps,
  'nameAutocomplete' | 'valueAutocomplete' | 'namePlaceholder' | 'valuePlaceholder'
>;

const FormRow = memo(function FormRow({
  pairContainer,
  onChange,
  onDelete,
  onFocus,
  onMove,
  onEnd,
  isLast,
  nameAutocomplete,
  valueAutocomplete,
  namePlaceholder,
  valuePlaceholder,
}: FormRowProps) {
  const { id } = pairContainer;
  const ref = useRef<HTMLDivElement>(null);

  const handleChangeName = useMemo(
    () => (name: string) => onChange({ id, pair: { name, value: pairContainer.pair.value } }),
    [onChange, pairContainer.pair.value],
  );

  const handleChangeValue = useMemo(
    () => (value: string) => onChange({ id, pair: { value, name: pairContainer.pair.name } }),
    [onChange, pairContainer.pair.name],
  );

  const nameEditorConfig = useMemo(
    () => ({ useTemplating: true, autocomplete: nameAutocomplete }),
    [nameAutocomplete],
  );

  const valueEditorConfig = useMemo(
    () => ({ useTemplating: true, autocomplete: valueAutocomplete?.(pairContainer.pair.name) }),
    [valueAutocomplete, pairContainer.pair.name],
  );

  const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
  const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]);

  const [, connectDrop] = useDrop<PairContainer>(
    {
      accept: ItemTypes.ROW,
      hover: (item, monitor) => {
        if (!ref.current) return;
        const hoverBoundingRect = ref.current?.getBoundingClientRect();
        const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
        const clientOffset = monitor.getClientOffset();
        const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
        onMove(pairContainer.id, hoverClientY < hoverMiddleY ? 'above' : 'below');
      },
    },
    [onMove],
  );

  const [, connectDrag] = useDrag<PairContainer>(
    {
      type: ItemTypes.ROW,
      item: () => pairContainer,
      collect: (m) => ({ isDragging: m.isDragging() }),
      end: () => onEnd(pairContainer.id),
    },
    [pairContainer, onEnd],
  );

  connectDrag(ref);
  connectDrop(ref);

  return (
    <div
      ref={ref}
      className="pb-2 group grid grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)_auto] grid-rows-1 gap-2 items-center"
    >
      {!isLast ? (
        <div
          className={classnames(
            '-mr-2 py-2 h-9 w-3 flex items-center',
            'justify-center opacity-0 hover:opacity-100',
          )}
        >
          <Icon icon="drag" className="pointer-events-none" />
        </div>
      ) : (
        <span className="w-1" />
      )}
      <Input
        hideLabel
        containerClassName={classnames(isLast && 'border-dashed')}
        defaultValue={pairContainer.pair.name}
        label="Name"
        name="name"
        onChange={handleChangeName}
        onFocus={handleFocus}
        placeholder={namePlaceholder ?? 'name'}
        useEditor={nameEditorConfig}
      />
      <Input
        hideLabel
        containerClassName={classnames(isLast && 'border-dashed')}
        defaultValue={pairContainer.pair.value}
        label="Value"
        name="value"
        onChange={handleChangeValue}
        onFocus={handleFocus}
        placeholder={valuePlaceholder ?? 'value'}
        useEditor={valueEditorConfig}
      />
      {onDelete ? (
        <IconButton
          icon="trash"
          title="Delete header"
          onClick={handleDelete}
          tabIndex={-1}
          className={classnames('opacity-0 group-hover:opacity-100')}
        />
      ) : (
        <IconButton title="" icon="empty" />
      )}
    </div>
  );
});

const newPairContainer = (pair?: Pair): PairContainer => {
  return { pair: pair ?? { name: '', value: '' }, id: uuid() };
};

const DropMarker = memo(
  function DropMarker() {
    return (
      <div className="relative w-full h-0 overflow-visible pointer-events-none">
        <div className="absolute z-50 left-0 right-0 bottom-[1px] h-[0.2em] bg-blue-500/50 rounded-full" />
      </div>
    );
  },
  () => true,
);
