import React, { useEffect, useState } from 'react';
import {
  DragDropContext,
  Draggable,
  DragUpdate,
  Droppable,
  DropResult
} from 'react-beautiful-dnd';
import { Controller, FieldValues, Path, useFormContext } from 'react-hook-form';

export interface IMapping<T> {
  source: T;
  dest: T;
}

export class PlaceHolderItem {
  private _id: string;

  get id(): string {
    return this._id;
  }

  constructor(id: string) {
    this._id = id;
  }
}

const useList = <T,>(
  initSourceValue: T[],
  initDestValue: T[],
  reset: () => void
): {
  sourceList: (T | PlaceHolderItem)[];
  destList: T[];
  selection: Map<string, IMapping<T>>;
  controller: {
    reorder: (startIndex: number, endIndex: number) => void;
    remove: (indices: number[]) => void;
    prepend: (mapping: IMapping<T>[]) => void;
    addSelection: (newSelection: Map<string, IMapping<T>>) => void;
    clearSelection: (...rowIds: string[]) => void;
    clearSelectionAll: () => void;
  };
} => {
  const getInitSourceList = () => {
    const result: (T | PlaceHolderItem)[] = [...initSourceValue];
    const lengthDiff = initDestValue.length - initSourceValue.length;
    if (lengthDiff > 0) {
      for (let i = 0; i < lengthDiff; i++) {
        result.push(new PlaceHolderItem(`placeholder-${i}`));
      }
    }
    return result;
  };

  const [sourceList, setSourceList] = useState(getInitSourceList);
  const [destList, setDestList] = useState(initDestValue);
  const [selection, setSelection] = useState(new Map<string, IMapping<T>>());

  useEffect(() => {
    setSourceList(getInitSourceList());
    setDestList(initDestValue);
    setSelection(new Map<string, IMapping<T>>());
    reset();
  }, [initSourceValue, initDestValue]);

  const reorder = (startIndex: number, endIndex: number) => {
    const sourceResult = Array.from(sourceList);
    const [sourceRemoved] = sourceResult.splice(startIndex, 1);
    sourceResult.splice(endIndex, 0, sourceRemoved);
    setSourceList(sourceResult);
  };

  const remove = (indices: number[]) => {
    const idx = new Set(indices);
    const sourceResult = sourceList.filter((v, i) => !idx.has(i));
    const destResult = destList.filter((v, i) => !idx.has(i));
    setSourceList(sourceResult);
    setDestList(destResult);
  };

  const prepend = (mapping: IMapping<T>[]) => {
    const sourceResult = mapping.map(v => v.source);
    const destResult = mapping.map(v => v.dest);
    setSourceList([...sourceResult, ...sourceList]);
    setDestList([...destResult, ...destList]);
  };

  const addSelection = (newSelection: Map<string, IMapping<T>>) => {
    const newState = new Map(selection);
    newSelection.forEach((v, k) => newState.set(k, v));
    setSelection(newState);
  };

  const clearSelection = (...rowIds: string[]) => {
    const newSelState = new Map(selection);
    rowIds.forEach(v => newSelState.delete(v));
    setSelection(newSelState);
  };

  const clearSelectionAll = () => {
    setSelection(new Map());
  };

  return {
    sourceList,
    destList,
    selection,
    controller: {
      reorder,
      remove,
      prepend,
      addSelection,
      clearSelection,
      clearSelectionAll
    }
  };
};

const DndSortList = <T,>({
  initSourceList,
  initDestList,
  sourceName,
  destName,
  matchFn,
  createRowId,
  formatItemName,
  onChange,
  errmsg
}: {
  initSourceList: T[];
  initDestList: T[];
  sourceName: string;
  destName: string;
  matchFn: (source?: T | PlaceHolderItem, dest?: T) => boolean;
  createRowId: (
    source?: T | PlaceHolderItem,
    dest?: T,
    idx?: number
  ) => { sourceId: string; destId: string; rowId: string };
  formatItemName: (value: T, isSource: boolean) => string;
  onChange: (value: IMapping<T>[]) => void;
  errmsg?: string;
}): React.ReactElement => {
  const [mappings, setMappings] = useState<IMapping<T>[]>([]);
  const { sourceList, destList, selection, controller } = useList(
    initSourceList,
    initDestList,
    () => {
      setMappings([]);
      onChange([]);
    }
  );
  const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);

  const selectable = new Map<string, IMapping<T>>();
  const listLength = Math.max(sourceList.length, destList.length);
  const maxLength = Math.max(initSourceList.length, initDestList.length, 5);

  const onDragEnd = (result: DropResult) => {
    setDragOverIndex(null);
    if (!result.destination) {
      return;
    }
    const idx1 = result.source.index;
    const idx2 = result.destination.index;
    const source1 = sourceList[idx1];
    const dest1 = destList[idx1];
    const source2 = sourceList[idx2];
    const dest2 = destList[idx2];
    const { rowId: rowId1 } = createRowId(source1, dest1);
    const { rowId: rowId2 } = createRowId(source2, dest2);
    controller.clearSelection(rowId1, rowId2);
    controller.reorder(idx1, idx2);
  };

  const onDragUpdate = (result: DragUpdate) => {
    if (!result.destination) {
      setDragOverIndex(null);
    } else {
      setDragOverIndex(result.destination.index);
    }
  };

  const handleCheckbox = (
    checked: boolean,
    rowId: string,
    source: T,
    dest: T
  ) => {
    if (checked) {
      controller.addSelection(new Map([[rowId, { source, dest }]]));
    } else {
      controller.clearSelection(rowId);
    }
  };

  const handleSelectAll = () => {
    if (selectable.size === 0) {
      controller.clearSelectionAll();
    } else {
      controller.addSelection(selectable);
    }
  };

  const handleAddData = () => {
    const indices: number[] = [];
    for (let i = 0; i < listLength; i++) {
      const source = sourceList[i];
      const dest = destList[i];
      const { rowId } = createRowId(source, dest, i);
      if (selection.has(rowId)) {
        indices.push(i);
      }
    }
    controller.remove(indices);
    const newMappings = [...mappings, ...Array.from(selection.values())];
    setMappings(newMappings);
    onChange(newMappings);
    controller.clearSelectionAll();
  };

  const handleRemoveMapping = (row: IMapping<T>) => {
    const newMappings = mappings.filter(mapping => row !== mapping);
    controller.prepend([row]);
    setMappings(newMappings);
    onChange(newMappings);
  };

  const handleRemoveAllMapping = () => {
    controller.prepend(mappings);
    setMappings([]);
    onChange([]);
  };

  const checkList: JSX.Element[] = [];
  const hintList: JSX.Element[] = [];
  const sourceElemList: JSX.Element[] = [];
  const destElemList: JSX.Element[] = [];

  for (let i = 0; i < maxLength; i++) {
    const source = sourceList[i] as T | PlaceHolderItem | undefined;
    const dest = destList[i] as T | undefined;
    const isMatch = matchFn(source, dest);
    const { sourceId, destId, rowId } = createRowId(source, dest, i);

    if (
      source &&
      dest &&
      !(source instanceof PlaceHolderItem) &&
      !selection.has(rowId) &&
      isMatch
    ) {
      selectable.set(rowId, {
        source,
        dest
      });
    }

    if (source && !(source instanceof PlaceHolderItem) && dest) {
      checkList.push(
        <div key={rowId} className="form-list-row">
          <label className="form-list-check">
            <input
              type="checkbox"
              disabled={!isMatch}
              checked={selection.has(rowId)}
              onChange={evt =>
                handleCheckbox(evt.target.checked, rowId, source, dest)
              }
            />
          </label>
        </div>
      );
      hintList.push(
        <div key={rowId} className="form-list-row">
          <div
            className={
              'form-link-separator ' + (isMatch ? '' : 'form-list-item-diabled')
            }
          />
        </div>
      );
    } else {
      checkList.push(
        <div key={rowId} className="form-list-row">
          <label className="form-list-check" />
        </div>
      );
      hintList.push(
        <div key={rowId} className="form-list-row">
          <div className="form-link-separator form-link-empty" />
        </div>
      );
    }
    if (source) {
      if (!(source instanceof PlaceHolderItem)) {
        sourceElemList.push(
          <Draggable key={sourceId} draggableId={sourceId} index={i}>
            {(provided, snapshot) => (
              <div
                className="form-list-row"
                ref={provided.innerRef}
                {...provided.draggableProps}
                {...provided.dragHandleProps}
                tabIndex={-1}
              >
                <div className="form-list-item">
                  <span className="form-list-cell">
                    {formatItemName(source, true)}
                  </span>
                </div>
              </div>
            )}
          </Draggable>
        );
      } else {
        sourceElemList.push(
          <Draggable key={sourceId} draggableId={sourceId} index={i}>
            {(provided, snapshot) => (
              <div
                className="form-list-row"
                ref={provided.innerRef}
                {...provided.draggableProps}
                tabIndex={-1}
              >
                <div {...provided.dragHandleProps} />
                <div className="form-list-item">
                  <span className="form-list-cell form-list-cell-empty" />
                </div>
              </div>
            )}
          </Draggable>
        );
      }
    } else {
      sourceElemList.push(
        <div key={sourceId} className="form-list-row">
          <div className="form-list-item">
            <span className="form-list-cell form-list-cell-empty" />
          </div>
        </div>
      );
    }
    if (dest) {
      destElemList.push(
        <div key={destId} className="form-list-row">
          <div className="form-list-item">
            <span
              className={
                'form-list-cell ' +
                (i === dragOverIndex ? 'form-list-cell-light' : '')
              }
            >
              {formatItemName(dest, false)}
            </span>
          </div>
        </div>
      );
    } else {
      destElemList.push(
        <div key={destId} className="form-list-row">
          <div className="form-list-item">
            <span className="form-list-cell form-list-cell-empty" />
          </div>
        </div>
      );
    }
  }

  const mappingElemList = mappings.map((row, idx) => {
    const { rowId } = createRowId(row.source, row.dest, idx);
    const desc1 = formatItemName(row.source, true);
    const desc2 = formatItemName(row.dest, false);
    return (
      <div key={rowId} className="form-list-row">
        <div className="form-list-item">
          <span className="form-list-cell">{desc1}</span>
          <span className="form-link-separator" />
          <span className="form-list-cell">{desc2}</span>
        </div>
        <button
          type="button"
          className="form-list-clear"
          onClick={() => handleRemoveMapping(row)}
        />
      </div>
    );
  });

  return (
    <div className="form-group">
      <div className="form-list">
        <div className="form-list-header">
          <button
            type="button"
            className="form-list-check"
            onClick={handleSelectAll}
          >
            全选
          </button>
          <div className="form-list-item">
            <span className="form-list-cell">提取源：{sourceName}</span>
            <span className="form-link-separator" />
            <span className="form-list-cell">加载源：{destName}</span>
          </div>
        </div>
        <div className="form-list-body">
          <div className="form-list-checklist">{checkList}</div>
          <DragDropContext onDragUpdate={onDragUpdate} onDragEnd={onDragEnd}>
            <Droppable droppableId="form-list-sourcelist">
              {(provided, snapshot) => (
                <div
                  className="form-list-sourcelist"
                  ref={provided.innerRef}
                  {...provided.droppableProps}
                >
                  {sourceElemList}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>
          </DragDropContext>
          <div className="form-list-hintlist">{hintList}</div>
          <div className="form-list-destlist">{destElemList}</div>
        </div>
      </div>
      <div className="form-arrow-separator">
        <button
          type="button"
          className="form-list-add-btn"
          onClick={handleAddData}
        />
      </div>
      <div className={'form-list ' + (errmsg ? 'form-list-error' : '')}>
        <div className="form-list-header">
          <div className="form-list-item">
            <span className="form-list-cell">映射关系：</span>
          </div>
          <button
            type="button"
            className="form-list-clear"
            onClick={handleRemoveAllMapping}
          >
            清空
          </button>
        </div>
        <div className="form-list-body">{mappingElemList}</div>
        {errmsg && <div className="form-list-errmsg">{errmsg}</div>}
      </div>
    </div>
  );
};

export const FormDndSortList = <T, U extends FieldValues>({
  name,
  required,
  ...otherProps
}: {
  initSourceList: T[];
  initDestList: T[];
  sourceName: string;
  destName: string;
  matchFn: (source?: T, dest?: T) => boolean;
  createRowId: (
    source?: T,
    dest?: T,
    idx?: number
  ) => { sourceId: string; destId: string; rowId: string };
  formatItemName: (value: T, isSource: boolean) => string;
  name: Path<U>;
  required?: boolean;
}): React.ReactElement => {
  const { clearErrors, control } = useFormContext<U>();

  return (
    <Controller
      control={control}
      name={name}
      rules={{ required }}
      render={({ field: { onChange }, fieldState: { error } }) => (
        <DndSortList
          {...otherProps}
          onChange={value => {
            clearErrors(name);
            onChange(value);
          }}
          errmsg={error && '该项不能为空'}
        />
      )}
    />
  );
};
