import { Injectable, Renderer2, RendererFactory2, Type } from '@angular/core';
// eslint-disable-next-line import/no-cycle
import { DragDirective } from './drag.directive';
// eslint-disable-next-line import/no-cycle
import { DropListDirective } from './drop-list.directive';

export interface AppDragMoveEvent {
  containerId: string;
  pos: number;
}

/**
 * This is a wrapper for the AppDragDropService. There are some function what is only for technical purpose and drag/drop directives use them.
 * So this interface hides all the function what you should not call directly from a component.
 */
export interface AppDragDropServiceHelper {
  subscribeDragMove(cb);
  unsubscribeDragMove(cb);
  getDraggedComponent();
  isDragOn(): boolean;
}

/**
 * This is the worker of the drag/drop directives. It is collecting them and handling their movements.
 */
@Injectable({
  providedIn: 'root',
})
export class AppDragDropService implements AppDragDropServiceHelper {
  /**
   * collection of registered components with drag directive
   * this service use them for collision detection (replace position)
   */
  private draggableComponents: DragDirective[] = [];

  /**
   * collection of registered drop zones
   * this service use them for collision detection (change the actual dropzone parent to an other)
   */
  private dropLists: DropListDirective[] = [];

  /**
   * subscription, fired when drag move happened, from components you can register for it
   */
  private dargMoveSubscriptions: Function[] = [];

  /**
   * reference of the dragged component
   */
  private draggedComponent: DragDirective = null;

  /**
   * angular built-in renderer for DOM manipulation
   */
  private renderer: Renderer2;

  constructor(rendererFactory: RendererFactory2, private previewClasses: Map<string, Type<any>>) {
    this.renderer = rendererFactory.createRenderer(null, null);
  }

  /**
   * You should not use this in your component. This is for the drag directive implementation.
   * @param comp
   */
  public registerComponent(comp) {
    this.draggableComponents.push(comp);
  }

  /**
   * You should not use this in your component. This is for the drag directive implementation.
   * @param comp
   */
  public deregisterComponent(comp) {
    const index = this.draggableComponents.indexOf(comp);

    if (index !== -1) {
      this.draggableComponents.splice(index, 1);
    }
  }

  /**
   * You should not use this in your component. This is for the drop directive implementation.
   * @param comp
   */
  public registerDropList(drop) {
    this.dropLists.push(drop);
  }

  /**
   * You should not use this in your component. This is for the drop directive implementation.
   * @param comp
   */
  public deregisterDropList(drop) {
    const index = this.dropLists.indexOf(drop);

    if (index !== -1) {
      this.dropLists.splice(index, 1);
    }
  }

  /**
   * Register a callback what fires when a drag move happening
   * @param cb
   */
  public subscribeDragMove(cb) {
    this.dargMoveSubscriptions.push(cb);
  }

  /**
   * Deregister a dragMove event, you should unsubscribe in the component desctructor
   * @param cb
   */
  public unsubscribeDragMove(cb) {
    this.dargMoveSubscriptions.splice(this.dargMoveSubscriptions.indexOf(cb), 1);
  }

  /**
   * return the current dragged component
   */
  public getDraggedComponent() {
    return this.draggedComponent;
  }

  /**
   * return the state of the drag n drop, true if there is a dragged element, false otherwise
   */
  public isDragOn(): boolean {
    return this.draggedComponent != null;
  }

  /**
   * You should not use this is your component. A drag directive use this for register the current dragged element
   * @param node
   */
  public drag(node: DragDirective) {
    this.draggedComponent = node;
  }

  /**
   * You should not use this directly. A drag directive use this for deregister the dragged element
   */
  public drop() {
    this.draggedComponent = null;
  }

  /**
   * You should not use this directly. A drag directive call this, when you dragged and moved an element.
   * How is it work:
   *  - Firstly it fires the registered callbacks under the dragMove event
   *  - Checks the dragged element. If the currently dragged element is above an other draggable element, and they have the same dropZoneId,
   *    then this service will put the dragged element before or after the other element
   *  - If the dragged element is above a dropzone, the service will change the dragged element's parent to this dropzone
   */
  public move(x, y): AppDragMoveEvent {
    // fire the registered callbacks
    let cbCopy = [...this.dargMoveSubscriptions];
    cbCopy.forEach((cb) => {
      if (cb) {
        cb({ x, y }, this.draggedComponent);
      }
    });

    if (this.getDraggedComponent() === null) {
      console.error('there is no registered drag element, but moved has been called');
      return null;
    }

    for (let i = 0; i < this.draggableComponents.length; i++) {
      let comp = this.draggableComponents[i];

      if (comp.disableDrag) continue;

      let node = comp.getDOMNode();

      // we dont check the same element and we can not interact elements when they registered under different drop zone
      if (
        comp == this.getDraggedComponent() ||
        comp.getDropZone() != this.getDraggedComponent().getDropZone()
      )
        continue;

      let rect = node.getBoundingClientRect();
      if (
        rect.top <= y &&
        rect.top + rect.height >= y &&
        rect.left <= x &&
        rect.left + rect.width >= x
      ) {
        // we are above a draggable element

        if (comp.getRelativePosition() + 1 == this.getDraggedComponent().getRelativePosition()) {
          // dragged element is the next sibling after the comp, so we need to use insertBefore

          this.renderer.removeChild(
            this.getDraggedComponent().getDOMNode().parentElement,
            this.getDraggedComponent().getDOMNode()
          );
          this.renderer.insertBefore(
            node.parentElement,
            this.getDraggedComponent().getDOMNode(),
            node
          );
        } else {
          // insert after

          this.renderer.removeChild(
            this.getDraggedComponent().getDOMNode().parentElement,
            this.getDraggedComponent().getDOMNode()
          );
          this.renderer.insertBefore(
            node.parentNode,
            this.getDraggedComponent().getDOMNode(),
            this.renderer.nextSibling(node)
          );
        }

        return {
          containerId: node.parentElement.dataset.appDropListId,
          pos: this.getDraggedComponent().getRelativePosition(),
        };

        //break; // no need to continue, we found the node
      }
    } // end of the dragged elements

    // now check the dropzones
    for (let i = 0; i < this.dropLists.length; i++) {
      let dropComponent = this.dropLists[i];
      if (dropComponent.isDropZoneDisabled) continue;

      let dropNode = dropComponent.getDOMNode();

      // we don't interact dropzones with different dropzone id
      if (this.getDraggedComponent().getDropZone() != dropComponent.getDropZone()) continue; // didn't match the destination zone

      let rect = dropNode.getBoundingClientRect();
      if (
        rect.top <= y &&
        rect.top + rect.height >= y &&
        rect.left <= x &&
        rect.left + rect.width >= x
      ) {
        // we are above a right dropzone

        this.getDraggedComponent().setPreview(
          dropComponent.getDropListId(),
          this.previewClasses.get(this.getDraggedComponent().previewComponentType),
          this.getDraggedComponent().previewResolver
        );

        if (this.getDraggedComponent().getDOMNode().parentElement == dropNode) {
          dropComponent.onMouseMove({
            clientX: x,
            clientY: y,
          });

          return {
            containerId: dropComponent.getDropListId(),
            pos: this.draggedComponent.getRelativePosition(),
          }; // we didn't moved out of its dropzone
        } else {
          // the component dragged above a good dropzone
          this.renderer.removeChild(
            this.getDraggedComponent().getDOMNode().parentElement,
            this.getDraggedComponent().getDOMNode()
          );
          this.renderer.appendChild(dropNode, this.getDraggedComponent().getDOMNode());

          return {
            containerId: dropComponent.getDropListId(),
            pos: this.draggedComponent.getRelativePosition(),
          };
        }
      }
    }
  }
}
