import clsx from 'clsx';
import React, { PropsWithChildren, useCallback } from 'react';
import {
  DragDropContext,
  Draggable,
  DraggableProvided,
  DropResult,
  Droppable,
} from 'react-beautiful-dnd';
import { OptionalObjectSchema } from 'yup/lib/object';
import { noop } from '../../helpers/utils';
import { Data } from '../../types/data';
import { uuid } from '../../utils/GenerateId';
import { ActionData } from '../Actions';
import { ObjectSchemaDefinition } from '../FormStation';
import { DynamicListColumn } from './DynamicDataList.model';
import classes from './DynamicDataList.scss';
import {
  DynamicListDataEntry,
  DynamicListDataEntryMode,
  DynamicListDataEntryProps,
} from './DynamicListDataEntry/DynamicListDataEntry';
import { DynamicListHeader } from './DynamicListHeader/DynamicListHeader';
import { DynamicListRow } from './DynamicListRow/DynamicListRow';
import { useColumnDefs } from './helpers/useColumnDefs';
import { useDataHandler } from './helpers/useDataHandler';
import { useRowAnimation } from './helpers/useRowAnimation';
import { useRowEditing } from './helpers/useRowEditing';

export interface DynamicDataListProps<T extends Data> {
  /**
   * Column definitions
   * The order of this array determines the order of columns
   */
  columns: DynamicListColumn<T>[];
  /** Whether or not the header row is shown (default: true) */
  showHeader?: boolean;
  /** List data */
  value: T[];
  /** Spacing between columns */
  columnGap?: string;
  /** Minimum width of this component */
  minimumWidth?: string;
  /** Header row height */
  headerRowHeight?: string;
  /** List row height (minimum height is 50px) */
  listRowHeight?: string;
  /** List row action button and checkbox size (default: '50px') */
  listRowActionSize?: string;
  /** Spacing between rows */
  rowGap?: string;
  /** Horizontal alignment of text */
  horizontalTextAlign?: 'left' | 'right' | 'center';
  /** Vertical alignment of text */
  verticalTextAlign?: 'start' | 'center' | 'end';
  /** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
  textWrap?: boolean;
  /** Property that contains the value used in reordering the list (default: undefined) */
  positionPropertyName?: keyof T;
  /** If sets, sets the label for the position column (default: 'Position') */
  positionLabel?: string;
  /** Whether or not rows can be repositioned (default: true) */
  allowReordering?: boolean;
  /** @deprecated Determines if the Add and Delete action buttons are rendered (default: true) */
  allowAddAndRemove?: boolean;
  /** Determines if data rows can be dragged for repositioning (default: true) */
  allowRowDragging?: boolean;
  /** Whether or not new data can be added (default: false) */
  allowNewData?: boolean;
  /** Whether or not existing data can be edited (default: false) */
  allowEditing?: boolean;
  /** Custom DynamicListDataEntry component which will be rendered instead of the default component */
  customDataEntry?: React.FC<DynamicListDataEntryProps<T>>;
  /** Optional Yup validation object for validating new data */
  rowValidationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<T>>;
  /** Sets default values to be populated in the new row when new data is allowed. */
  defaultValuesForNewData?: Partial<T>;
  /** The maximum amount of items that can be added to the list.
   * If this prop is set and the amount is reached, data entry will be removed (default: undefined)
   */
  maxItemLimit?: number;
  /** CSS Class name for additional styles */
  className?: string;
  /** Whether the header and input field should stick to the top (default: false) */
  stickyHeader?: boolean;
  /** Whether add new item button should be disabled (default: false) */
  disabled?: boolean;
  /** Performs transformations to the new data object being added */
  onAddTransformData?: (data: Partial<T>) => T;
  /** CSS Class name provider for each row. Allows row style to be determined by data */
  rowClassNameProvider?: (data: T) => string;
  /** Raised when the list has changed */
  onChange?: (list: T[]) => void;
  /** Provide inline actions which are available through '...' context menu */
  inlineMenuActions?: (data: T) => ActionData[];
}

/**
 * Renders a list of data that whose rows can be added, deleted, and repositioned
 * @example
 * <DynamicDataList<DataInterface>
 *  columns={[{propertyName: 'id', size: '1fr', label: 'Id'}]}
 *  data={[{id: '1',desc: 'Description 1',title: 'Item 1'}]}
 *  onChange={list => console.log(list)}
 * />
 */
export const DynamicDataList = <T extends Data>({
  columns = [],
  value = [],
  showHeader = true,
  minimumWidth = '500px',
  columnGap,
  rowGap,
  headerRowHeight,
  listRowHeight,
  listRowActionSize,
  horizontalTextAlign,
  verticalTextAlign,
  positionPropertyName,
  positionLabel,
  allowReordering = true,
  allowRowDragging = true,
  allowNewData = false,
  allowEditing = false,
  customDataEntry: CustomDataEntry,
  maxItemLimit,
  rowValidationSchema,
  defaultValuesForNewData,
  stickyHeader = true,
  disabled = false,
  className = '',
  textWrap = false,
  onChange = noop,
  onAddTransformData = (data) => data as T,
  rowClassNameProvider,
  inlineMenuActions,
}: PropsWithChildren<DynamicDataListProps<T>>): JSX.Element => {
  const {
    columnSizes,
    showActionColumn,
    showPositionColumn,
    setColumnSizes,
    resetColumnSizes,
  } = useColumnDefs<T>(
    columns,
    allowReordering,
    allowNewData,
    allowEditing,
    allowRowDragging,
    positionPropertyName,
    !!inlineMenuActions,
  );

  const { addItem, removeItem, updateItem, canAddItems, items, nextPosition } =
    useDataHandler(
      value,
      positionPropertyName,
      maxItemLimit,
      allowNewData,
      onChange,
      onAddTransformData,
    );

  const { editingRow, isEditing, startEditing, finishEditing } = useRowEditing(
    allowEditing,
    updateItem,
  );

  const customStyles = {
    gridRowGap: rowGap,
    minWidth: minimumWidth,
  } as React.CSSProperties;

  const onPositionInputChangedHandler = useCallback(
    (currentPosition: number, newPosition: number) => {
      if (currentPosition !== newPosition && positionPropertyName) {
        const oldItem = items.find(
          (item) => item[positionPropertyName] === currentPosition,
        );

        if (oldItem) {
          removeItem(oldItem);
          addItem({ ...oldItem, [positionPropertyName]: newPosition });
        }
      }
    },
    [addItem, items, positionPropertyName, removeItem],
  );

  const onDragEndHandler = useCallback(
    (result: DropResult) => {
      if (result.destination && positionPropertyName) {
        const oldItem = items[result.source.index];

        const nextPosition =
          items[result.destination.index][positionPropertyName];

        const newItem = {
          ...oldItem,
          [positionPropertyName]: nextPosition,
        };

        removeItem(oldItem);
        addItem(newItem);
      }
    },
    [addItem, items, positionPropertyName, removeItem],
  );

  const { AnimatedRow, setLastAddedItem, setShouldAnimate } =
    useRowAnimation<T>();

  const onAddItemHandler = useCallback(
    (data: T) => {
      addItem(data);
      setLastAddedItem(data);
      setShouldAnimate(true);
    },
    [addItem, setLastAddedItem, setShouldAnimate],
  );

  const getRow = (
    data: T,
    idx: number,
    provided?: DraggableProvided,
    isDragging = false,
  ): JSX.Element =>
    editingRow !== data ? (
      <AnimatedRow data={data} key={idx}>
        <DynamicListRow<T>
          key={idx}
          columns={columns}
          data={data}
          columnSizes={columnSizes}
          columnGap={columnGap}
          rowHeight={listRowHeight}
          actionSize={listRowActionSize}
          horizontalTextAlign={horizontalTextAlign}
          verticalTextAlign={verticalTextAlign}
          textWrap={textWrap}
          allowRemove={allowNewData}
          positionKey={positionPropertyName}
          allowDragging={allowRowDragging}
          onActionClicked={removeItem}
          onPositionInputChanged={onPositionInputChangedHandler}
          inlineMenuActions={inlineMenuActions}
          rowClassNameProvider={rowClassNameProvider}
          disabled={disabled}
          provided={provided}
          dragging={isDragging}
          showActionColumn={showActionColumn}
          showPositionColumn={showPositionColumn}
          allowEditing={allowEditing}
          onRowClicked={startEditing}
        />
      </AnimatedRow>
    ) : CustomDataEntry ? (
      <CustomDataEntry
        key={idx}
        {...DynamicListDataEntryProps}
        defaultValuesForNewData={data}
        newDataPosition={undefined}
        onActionClicked={finishEditing}
        mode={DynamicListDataEntryMode.Edit}
      />
    ) : (
      <DynamicListDataEntry<T>
        key={idx}
        {...DynamicListDataEntryProps}
        defaultValuesForNewData={data}
        newDataPosition={undefined}
        onActionClicked={finishEditing}
        mode={DynamicListDataEntryMode.Edit}
      />
    );

  const DynamicListDataEntryProps: DynamicListDataEntryProps<T> = {
    columns,
    columnSizes,
    columnGap,
    positionKey: positionPropertyName,
    rowHeight: listRowHeight,
    actionSize: listRowActionSize,
    horizontalTextAlign,
    verticalTextAlign,
    allowReordering,
    allowDragging: allowRowDragging,
    newDataPosition: nextPosition,
    rowValidationSchema,
    defaultValuesForNewData,
    sticky: stickyHeader,
    showHeader,
    disabled,
    showPositionColumn,
    onActionClicked: onAddItemHandler,
  };

  return (
    <div
      className={clsx(
        classes.container,
        'dynamic-data-list-container',
        className,
      )}
      style={customStyles}
      data-test-id="dynamic-data-list"
    >
      {showHeader && (
        <DynamicListHeader
          columns={columns}
          columnSizes={columnSizes}
          columnGap={columnGap}
          rowHeight={headerRowHeight}
          horizontalTextAlign={horizontalTextAlign}
          verticalTextAlign={verticalTextAlign}
          allowReordering={allowReordering}
          positionKey={positionPropertyName}
          positionLabel={positionLabel}
          sticky={stickyHeader}
          allowDragging={allowRowDragging}
          showPositionColumn={showPositionColumn}
          showActionColumn={showActionColumn}
          onColumnSizesChanged={setColumnSizes}
          onResetColumnSizes={resetColumnSizes}
        />
      )}
      {canAddItems &&
        (CustomDataEntry ? (
          <CustomDataEntry {...DynamicListDataEntryProps} />
        ) : (
          <DynamicListDataEntry<T> {...DynamicListDataEntryProps} />
        ))}
      {allowReordering && !isEditing ? (
        <DragDropContext onDragEnd={onDragEndHandler}>
          <Droppable droppableId={uuid()}>
            {(provided) => (
              <div ref={provided.innerRef} {...provided.droppableProps}>
                {items.map((item: T, idx: number) => (
                  <Draggable draggableId={String(idx)} index={idx} key={idx}>
                    {(provided, snapshot) =>
                      getRow(item, idx, provided, snapshot.isDragging)
                    }
                  </Draggable>
                ))}
                {provided.placeholder}
              </div>
            )}
          </Droppable>
        </DragDropContext>
      ) : (
        items.map((item: T, idx: number) => getRow(item, idx))
      )}
    </div>
  );
};
