import { Injectable, Renderer2, RendererFactory2, Type } from '@angular/core';
import { DragDirective } from './drag.directive';
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
    this.dargMoveSubscriptions.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];
      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];
      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(dropComponent.previewComponentType), dropComponent.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()
            }
          }          
      }
    }




    

  }
}
