import { NestedTableKeyedItem, NestedTableNestedModeState } from '../../state';
import { NestedTableNodeState } from '../../state/NestedTableState/NestedTableNodeState';
import { createOptimisticActionsOrderByModeReaction } from '../../state/DragAndDrop/createOptimisticActionsOrderByModeReaction';
import { DragAndDropState, IndexedItem } from '../DragAndDrop';
import { action, computed, makeObservable } from 'mobx';
import {
  ActionMove,
  CollectionState,
  ComputedQuery,
  FiltersMap,
  TypedEmitter,
} from '@wix/bex-core';
import { EventEmitter } from 'events';

export interface NestedTableDragAndDropStateParams<C extends string> {
  readonly nestedState: NestedTableNestedModeState<C>;
}

export interface NestedDragEndEvent<C extends string, T, F extends FiltersMap> {
  from: IndexedItem<T>;
  after: IndexedItem<{}> | null;
  over: IndexedItem<{}>;
  parentId: string | undefined;
  filters: ComputedQuery<F>['filters'];
  node: NestedTableNodeState<C, T, F>;
}

export interface DragAndDropDisabledParams<T> {
  from: IndexedItem<T>;
  over: IndexedItem<{}>;
}

export class NestedTableDragAndDropState<C extends string> {
  readonly tableState;
  readonly dnd;
  readonly nestedState;

  _isExpandedBeforeDrag?: boolean;

  constructor(params: NestedTableDragAndDropStateParams<C>) {
    const { nestedState } = params;

    const { container } = nestedState;

    this.tableState = nestedState.tableState;
    this.nestedState = nestedState;

    this.dnd = new DragAndDropState({
      container,
      state: nestedState.wrapper.toolbar,
      collection: nestedState,
      move: () => this._move(),
      announcements: {
        dragHandleLabel: (id) => {
          const { translate: t } = container;
          const item = nestedState.getKeyedItem(id as string)?.item;
          const itemName = item?.state.collection.itemName;

          return t('cairo.dragAndDrop.button-role-label.sr', {
            itemName:
              item && itemName ? itemName(item.originalKeyedItem?.item) : '',
          });
        },
        itemDescription: (id) => {
          const item = nestedState.getKeyedItem(id as string)?.item;
          const itemName = item?.state.collection.itemName;

          return item && itemName ? itemName(item.originalKeyedItem?.item) : '';
        },
      },
    });

    this.dnd.events.on('dragStart', () => {
      const { activeNode } = this;

      this._isExpandedBeforeDrag = activeNode?.node._expanded;
      if (activeNode?.node._expanded) {
        activeNode.node._toggleExpand();
      }
    });

    this.dnd.onCancel = this._onCancel;

    makeObservable(this, {
      dragEventItems: computed,
      dragEventDirectionItems: computed,
      dragEventDepth: computed,
      activeNode: computed,
      dragEventParentAndAfter: computed,
      reachedMaxDepth: computed,
      dragAndDropDisabled: computed,
      _createDragEndEvent: action,
      _move: action,
    });
  }

  _onCancel = async () => {
    const { dragEventParentAndAfter, dragAndDropDisabled } = this;
    const { node, activeItem, overItem } = dragEventParentAndAfter;

    if (dragAndDropDisabled) {
      return true;
    }

    if (
      !node ||
      !activeItem ||
      !overItem ||
      !activeItem.item.originalKeyedItem
    ) {
      return null;
    }

    const dragEndEvent = this._createDragEndEvent();

    if (!dragEndEvent) {
      return null;
    }

    const { levelDescriptor } = activeItem.item.state;

    const shouldCancel = await levelDescriptor.dragAndDropCancel?.(
      this._createSimpleDragEndEvent(dragEndEvent, node),
    );

    if (!shouldCancel) {
      return false;
    }

    this.dnd._maybeShowErrorToast(shouldCancel, dragEndEvent);

    return true;
  };

  private _createNewParentData<T, F extends FiltersMap>(
    node: NestedTableNodeState<C, T, F>,
  ) {
    if (!node.parent || !node.parentNode) {
      return {};
    }

    return {
      parentItem: node.parent.item as {},
    };
  }

  _createDragEndEvent({
    setNewParent = true,
  }: { setNewParent?: boolean } = {}): ActionMove<any> | null {
    const { dragEventParentAndAfter, dnd, nestedState } = this;

    const { node, activeItem, after, overItem } = dragEventParentAndAfter;

    if (
      !node ||
      !activeItem ||
      !overItem ||
      !activeItem.item.originalKeyedItem ||
      !overItem.item.originalKeyedItem
    ) {
      return null;
    }

    const { levelDescriptor } = activeItem.item.state;

    if (!levelDescriptor.setParent) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(
          'setParent is not defined in levelDescriptor, skipping drag end event creation',
        );
      }
      return null;
    }

    return {
      moveId: dnd._moveId,
      from: setNewParent
        ? {
            ...activeItem.item.originalKeyedItem,
            item: levelDescriptor.setParent(
              activeItem.item.originalKeyedItem.item,
              node.parent?.id,
              this._createNewParentData(node),
            ),
          }
        : activeItem.item.originalKeyedItem,
      over: overItem.item.originalKeyedItem,
      after: after?.item.originalKeyedItem,
      query: nestedState.query.asComputed,
      filtersKeyHash: nestedState.query.filtersKeyHash,
      isFromHandle: Boolean(dnd._isHandleActive),
    };
  }

  _createSimpleDragEndEvent<T, F extends FiltersMap>(
    dragEndEvent: ActionMove<T>,
    node: NestedTableNodeState<C, T, F>,
  ) {
    const { dnd } = this;
    return {
      ...dnd._createSimpleDragEndEvent(dragEndEvent),
      parentId: node.parent?.id,
      node,
    };
  }

  _move() {
    const {
      dragEventParentAndAfter,
      nestedState,
      reachedMaxDepth,
      activeNode,
    } = this;

    if (reachedMaxDepth) {
      if (
        activeNode &&
        this._isExpandedBeforeDrag != null &&
        this._isExpandedBeforeDrag !== activeNode.node._expanded
      ) {
        activeNode._toggleExpand();
      }
      return;
    }

    const { node, activeItem, overItem } = dragEventParentAndAfter;

    if (
      !node ||
      !activeItem ||
      !overItem ||
      !activeItem.item.originalKeyedItem
    ) {
      return;
    }

    const dragEndEvent = this._createDragEndEvent();

    if (!dragEndEvent) {
      return;
    }

    /**
     * A submit function that will be shared between 3 optimistic actions.
     * How it works: The `dragAndDropSubmit` function provided by the consumer will be called only inside the `move` action submit function.
     * Using EventEmitter to resolve the promise only after the `move` action submit function completes.
     */

    const submitEvents = new EventEmitter() as TypedEmitter<{
      submit: (err?: unknown) => void;
    }>;

    const submit = () =>
      new Promise<void>((resolve, reject) =>
        submitEvents.once('submit', (err) => (err ? reject(err) : resolve())),
      );

    activeItem.item.state.collection.optimisticActions.updateOne(
      dragEndEvent.from.item,
      {
        submit,
        keepPosition: true,
      },
    );

    if (
      activeItem.item.state === node ||
      node.unprocessedMap.has(dragEndEvent.from.key)
    ) {
      node.collection.optimisticActions.updateOne(dragEndEvent.from.item, {
        submit,
        keepPosition: true,
      });
    } else {
      node.collection.optimisticActions.createOne(dragEndEvent.from.item, {
        submit,
      });
    }

    node.collection.optimisticActions.move(dragEndEvent, {
      submit: async () => {
        const { levelDescriptor } = activeItem.item.state;

        try {
          await levelDescriptor.dragAndDropSubmit?.(
            this._createSimpleDragEndEvent(dragEndEvent, node),
          );

          submitEvents.emit('submit');
        } catch (e) {
          submitEvents.emit('submit', e);
          throw e;
        }
      },
      collectionSnapshot: nestedState.getCollectionSnapshot(),
    });

    const nodeAfterMove = node.createNode({
      parent: activeItem.item.originalKeyedItem,
      notifyAll: true,
    });
    if (nodeAfterMove && this._isExpandedBeforeDrag != null) {
      nodeAfterMove.node._expanded = this._isExpandedBeforeDrag;
    }
    node._notify();
    node.parentNode?._notify();
    node._preRegisterAll();
  }

  getStickyColumnsCount({
    stickyColumns,
    horizontalScroll,
  }: {
    stickyColumns: number;
    showSelection?: boolean;
    horizontalScroll?: boolean;
  }) {
    const { dnd } = this;
    return horizontalScroll && !dnd.isDisabled
      ? stickyColumns + 1
      : stickyColumns;
  }

  get dragEventItems() {
    const { dnd } = this;
    const { collection, activeId, overId } = dnd;

    if (!overId || !activeId) {
      return null;
    }

    const overItem = collection.getKeyedItem(overId) as NestedTableKeyedItem<
      C,
      any,
      any
    >;

    const activeItem = collection.getKeyedItem(
      activeId,
    ) as NestedTableKeyedItem<C, any, any>;

    if (activeItem == null || overItem == null) {
      return null;
    }

    return {
      overItem,
      activeItem,
    };
  }

  get dragEventDirectionItems() {
    const {
      nestedState: { keyedItems: items },
      dragEventItems,
    } = this;

    if (!dragEventItems) {
      return null;
    }

    const { overItem, activeItem } = dragEventItems;

    const activeItemIndex = activeItem.index;
    const overItemIndex = overItem.index;

    const previousItem: NestedTableKeyedItem<C, any, any> | null =
      activeItemIndex >= overItemIndex
        ? items[overItemIndex - 1]
        : items[overItemIndex];

    const nextItem: NestedTableKeyedItem<C, any, any> | null =
      activeItemIndex <= overItemIndex
        ? items[overItemIndex + 1]
        : items[overItemIndex];

    return {
      nextItem,
      previousItem,
      overItem,
      activeItem,
    };
  }

  get dragEventDepth() {
    const {
      nestedState: { _indentationWidth },
      dragEventDirectionItems,
      dnd: { _moveDelta },
    } = this;

    if (!dragEventDirectionItems) {
      return null;
    }

    const { nextItem, previousItem, activeItem, overItem } =
      dragEventDirectionItems;

    const dragDepth = Math.round(_moveDelta.x / _indentationWidth); // getDragDepth
    const projectedDepth = activeItem.item.state.childrenDepth + dragDepth;

    const maxDepth = previousItem
      ? previousItem.item.state.childrenDepth + 1
      : 0;
    const minDepth = nextItem ? nextItem.item.state.childrenDepth : 0;

    let depth = projectedDepth;
    if (!this.isSameEntityAsOtherLevels) {
      depth = activeItem.item.state.childrenDepth;
    } else if (projectedDepth >= maxDepth) {
      depth = maxDepth;
    } else if (projectedDepth < minDepth) {
      depth = minDepth;
    }

    return {
      depth,
      nextItem,
      previousItem,
      activeItem,
      overItem,
    };
  }

  get activeNode() {
    const {
      dnd: { activeId },
      nestedState,
    } = this;

    if (!activeId) {
      return null;
    }

    return nestedState.root._getNode(activeId);
  }

  get dragEventParentAndAfter() {
    const { nestedState, dragEventDepth } = this;

    if (!dragEventDepth) {
      return {};
    }

    const { keyedItems: items } = nestedState;
    const { previousItem, depth, overItem, activeItem } = dragEventDepth;

    if (previousItem == null) {
      return {
        node: nestedState.root,
        after: null,
        // before: nestedState.root.keyedItems[0],
        activeItem,
        overItem,
      };
    }

    if (depth === previousItem.item.state.childrenDepth) {
      return {
        node: previousItem.item.state,
        after: previousItem,
        // before: nextItem && nextItem.item.state.depth === depth ? nextItem : null,
        activeItem,
        overItem,
      };
    }

    if (depth > previousItem.item.state.childrenDepth) {
      const node = previousItem.item.state.parentKeyToChildrenMap.get(
        previousItem.id,
      );
      return {
        node,
        after: null,
        // before: node?.keyedItems[0],
        activeItem,
        overItem,
      };
    }

    for (let i = previousItem.index; i >= 0; i--) {
      const item = items[i];

      if (item.item.state.childrenDepth === depth) {
        return {
          node: item.item.state,
          after: item,
          activeItem,
          overItem,
        };
      }
    }

    return {
      activeItem,
      overItem,
    };
  }

  get reachedMaxDepth() {
    const {
      dragEventParentAndAfter,
      dragEventDirectionItems,
      nestedState,
      activeNode,
    } = this;

    if (!dragEventParentAndAfter || !dragEventDirectionItems) {
      return false;
    }

    const { node } = dragEventParentAndAfter;

    if (!node) {
      return true;
    }

    const activeNodeDeepestBranchDepth = activeNode?.deepestBranchDepth ?? 0;

    return (
      node.levelDescriptor.depth + activeNodeDeepestBranchDepth >=
      nestedState.levels.length
    );
  }

  get dragAndDropDisabled() {
    const { dragEventParentAndAfter } = this;
    const { node } = dragEventParentAndAfter;

    if (!node) {
      return null;
    }

    const dragEndEvent = this._createDragEndEvent({ setNewParent: false });

    if (!dragEndEvent) {
      return null;
    }

    return node.levelDescriptor.dragAndDropDisabled?.(
      this._createSimpleDragEndEvent(dragEndEvent, node),
    );
  }

  get isSameEntityAsOtherLevels() {
    return this.nestedState.levels[0]?.isSameEntityAsOtherLevels;
  }

  initCollection<T, F extends FiltersMap>(collection: CollectionState<T, F>) {
    return createOptimisticActionsOrderByModeReaction(this.dnd, collection);
  }

  init() {
    return this.dnd.init();
  }
}
