import { DOCUMENT } from '@angular/common';
import {
  AfterViewInit, ApplicationRef, ComponentFactoryResolver, Directive, ElementRef, EmbeddedViewRef, EventEmitter, Inject, Injector, Input, OnDestroy, Output, Renderer2, Type
} from '@angular/core';
import { AppDragDropService, AppDragMoveEvent } from './app-drag-drop.service';

interface mousePosition {
  x: number;
  y: number;
}

@Directive({
  selector: '[appDrag]',
})
export class DragDirective implements OnDestroy, AfterViewInit {
  /**
   * The ID of the container, where this draggable element can be placed.
   */
  @Input('appDrag') dropZone?: string = null;
  @Input() previewData? = null;

  /**
   * Name of the component class. You have to predifine it in the appDragDropService previewClasses config
   */
  @Input() previewComponentType?: string = null;

  /**
   * Behavior as a callback how to resolve the component. Passing paramteres, etc.
   * cb auto parameters: (componentRef)=>{}
   */
  @Input() previewResolver? = null;

  // can not drag and can not replace its place
  @Input() disableDrag?: boolean = false;

  @Output() readonly beforeStart = new EventEmitter();
  @Output() readonly dragDropped: EventEmitter<AppDragMoveEvent> =
    new EventEmitter<AppDragMoveEvent>();

  private dragged: boolean = false;
  private dragStartPos: mousePosition = null;
  private draggedElement: HTMLElement;
  private previewComponent: any = null;
  private lastEvent: AppDragMoveEvent = null;
  private onMouseMove;
  private onMouseUp;
  private onMouseDown;
  private onTouchMove;
  private onTouchStart;
  private onTouchEnd;
  private onTouchCancel;
  private touchActive: boolean = false;
  private touchHoldStarted: boolean = false;
  private touchHoldTime = 1250; // ms; does not work on android on ~500, it tiggers google selection
  private touchHoldTimer;

  /**
   * It is the owner of the last created preview component. If we are above
   * the same dropzone, we shouldn't recreate the overview
   */
  private lastDropZoneId = null;

  constructor(
    private renderer: Renderer2,
    private el: ElementRef,
    @Inject(DOCUMENT) private document: Document,
    private appDragDropService: AppDragDropService,

    private componentFactoryResolver: ComponentFactoryResolver,
    private appRef: ApplicationRef,
    private injector: Injector
  ) {
    // register this component to the draggable field
    this.appDragDropService.registerComponent(this);

    // define the dragged preview element
    this.draggedElement = this.renderer.createElement('div');
    this.renderer.setStyle(this.draggedElement, 'width', '40px');
    this.renderer.setStyle(this.draggedElement, 'height', '40px');
    //this.renderer.setStyle(this.draggedElement, 'background-color', 'blue');
    this.renderer.setStyle(this.draggedElement, 'position', 'absolute');
    this.renderer.setStyle(this.draggedElement, 'left', '0');
    this.renderer.setStyle(this.draggedElement, 'top', '0');
    this.renderer.setStyle(this.draggedElement, 'z-index', '999');
    this.renderer.setStyle(this.draggedElement, 'display', 'none');
    this.renderer.setStyle(this.draggedElement, 'cursor', 'grab');
    this.renderer.appendChild(this.document.body, this.draggedElement);

    // we need an arrow function reference, because we are going to add it to the document.body via addEventListener, and remove it later via removeEventListener
    this.onMouseMove = (event) => {
      if (this.disableDrag) return;

      if (this.dragStartPos != null) {
        let currPos = this.getMousePosition(event);

        if (
          !this.dragged &&
          (Math.abs(currPos.x - this.dragStartPos.x) > 10 ||
            Math.abs(currPos.y - this.dragStartPos.y) > 10)
        ) {
          this.dragged = true;
          this.appDragDropService.drag(this);
          this.renderer.setStyle(this.getDOMNode(), 'opacity', '0.5');

          event.srcComponent = this;
          this.beforeStart.emit(event);
        }
      }

      if (this.dragged) {
        event.preventDefault();

        //this.movePreview(event);
        let pos = this.getMousePosition(event);

        this.renderer.setStyle(this.draggedElement, 'display', 'initial');
        this.renderer.setStyle(this.draggedElement, 'left', pos.x - 20 + 'px');
        this.renderer.setStyle(this.draggedElement, 'top', pos.y - 20 + 'px');

        let currEvent = this.appDragDropService.move(pos.x, pos.y);
        if (currEvent) {
          this.lastEvent = currEvent;
        }
      }
    };

    this.onMouseDown = (event) => {
      event.preventDefault();
      if (event.stopPropagation) event.stopPropagation();
      if (event.button === 0) {
        this.dragStartPos = this.getMousePosition(event);
        this.document.addEventListener('mousemove', this.onMouseMove);
        this.document.addEventListener('mouseup', this.onMouseUp);
      }
    };

    this.onMouseUp = (event) => {
      this.dragStartPos = null;

      if (this.dragged) {
        this.document.removeEventListener('mousemove', this.onMouseMove);
        this.document.removeEventListener('mouseup', this.onMouseUp);
        event.preventDefault();
        this.dragged = false;

        this.appDragDropService.drop();
        this.dragDropped.emit(this.lastEvent);

        this.renderer.setStyle(this.draggedElement, 'display', 'none');

        this.renderer.setStyle(this.getDOMNode(), 'cursor', 'initial');
        this.renderer.setStyle(this.getDOMNode(), 'position', 'initial');
        this.renderer.setStyle(this.getDOMNode(), 'opacity', '1');
        //this.renderer.removeChild(document.body, this.draggedElementCopy);
      }
    };

    this.onTouchMove = (event) => {
      if (this.touchHoldStarted) {
        this.onTouchEnd(event);
      }

      if (this.touchActive) {
        if (this.disableDrag) return;

        if (!this.dragged) {
          this.dragged = true;
          this.appDragDropService.drag(this);
          this.renderer.setStyle(this.getDOMNode(), 'opacity', '0.5');

          event.srcComponent = this;
          this.beforeStart.emit(event);
        }

        if (this.dragged) {
          event.preventDefault();

          //this.movePreview(event);
          let pos = this.getMousePosition(event);

          this.renderer.setStyle(this.draggedElement, 'display', 'initial');
          this.renderer.setStyle(this.draggedElement, 'left', pos.x - 20 + 'px');
          this.renderer.setStyle(this.draggedElement, 'top', pos.y - 20 + 'px');

          let currEvent = this.appDragDropService.move(pos.x, pos.y);
          if (currEvent) {
            this.lastEvent = currEvent;
          }
        }
        event.preventDefault(); // disable click event bubbling
        if (event.stopPropagation) event.stopPropagation();
      }
    };

    this.onTouchStart = (event) => {
      this.touchActive = false;
      this.touchHoldStarted = true;

      this.document.addEventListener('touchmove', this.onTouchMove);
      this.document.addEventListener('touchend', this.onTouchEnd);
      this.document.addEventListener('touchcancel', this.onTouchEnd);

      this.touchHoldTimer = setTimeout(() => {
        if (this.touchHoldStarted) {
          // document.body.style.overflow = 'hidden'; // turn off the android pull-down-for-refresh
          this.touchHoldStarted = false;
          this.touchActive = true;

          this.dragStartPos = this.getMousePosition(event);
          this.onTouchMove(event);
          event.preventDefault();
        }
      }, this.touchHoldTime);

      //event.preventDefault(); // it turns off the move after start
      if (event.stopPropagation) event.stopPropagation();
    };

    this.onTouchEnd = (event) => {
      if (this.touchHoldStarted) {
        this.touchHoldStarted = false;
        this.touchActive = false;
        clearTimeout(this.touchHoldTimer);
      }

      this.document.removeEventListener('touchmove', this.onTouchMove);
      this.document.removeEventListener('touchend', this.onTouchEnd);
      this.document.removeEventListener('touchcancel', this.onTouchEnd);
      // document.body.style.overflow = 'auto'; // turn off the android pull-down-for-refresh

      if (this.touchActive) {
        this.touchActive = false;

        this.dragStartPos = null;

        if (this.dragged) {
          event.preventDefault();
          this.dragged = false;

          this.appDragDropService.drop();
          this.dragDropped.emit(this.lastEvent);

          this.renderer.setStyle(this.draggedElement, 'display', 'none');

          this.renderer.setStyle(this.getDOMNode(), 'cursor', 'initial');
          this.renderer.setStyle(this.getDOMNode(), 'position', 'initial');
          this.renderer.setStyle(this.getDOMNode(), 'opacity', '1');
          //this.renderer.removeChild(document.body, this.draggedElementCopy);
        }

        event.preventDefault(); // disable click event bubbling
      }
    };

    this.onTouchCancel = (event) => {
      //this.onTouchEnd(event);
    };

    // register mouse/touch events
    this.el.nativeElement.addEventListener('mousedown', this.onMouseDown);

    /**
     * Turn off this feature bc it is does not work consistently in scrollbar with safari/chrome webview
     */
    //this.el.nativeElement.addEventListener('touchstart', this.onTouchStart);
  }

  public ngAfterViewInit() {
    this.renderer.setStyle(this.el.nativeElement, 'user-select', 'none');
    this.renderer.setStyle(this.el.nativeElement, '-webkit-user-select', 'none');
    if (this.el.nativeElement?.firstElementChild) {
      this.renderer.setStyle(this.el.nativeElement.firstElementChild, 'user-select', 'none');
      this.renderer.setStyle(
        this.el.nativeElement.firstElementChild,
        '-webkit-user-select',
        'none'
      );
    }
  }

  public getDOMNode(): HTMLElement {
    return this.el.nativeElement;
  }

  public getDropZone() {
    return this.dropZone;
  }

  public getRelativePosition() {
    let i = 0;
    let el: any = this.getDOMNode();
    let dragAtrribute = el.getAttribute('appdrag');
    while ((el = el.previousSibling) != null) {
      // count only the draggable and the same level dom elements
      if (el.getAttribute && el.getAttribute('appdrag') == dragAtrribute) {
        i++;
      }
    }

    return i;
  }

  public setPreview(dropZoneId: string, componentType: Type<any>, resolverCb: Function) {
    if (this.lastDropZoneId == dropZoneId) return;

    if (componentType) {
      this.lastDropZoneId = dropZoneId;

      if (this.previewComponent != null) {
        this.previewComponent.remove();
        this.previewComponent = null;
      }

      const componentRef = this.componentFactoryResolver
        .resolveComponentFactory(componentType)
        .create(this.injector);

      componentRef.changeDetectorRef.detectChanges();

      if (resolverCb) {
        resolverCb(componentRef);
      }

      const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;

      this.draggedElement.innerHTML = '';

      while (this.draggedElement.firstChild) this.draggedElement.firstChild.remove();

      this.renderer.appendChild(this.draggedElement, domElem);
    } else {
      this.lastDropZoneId = dropZoneId;
      const domElem = this.el.nativeElement.cloneNode(true);
      while (this.draggedElement.firstChild) this.draggedElement.firstChild.remove();

      this.renderer.appendChild(this.draggedElement, domElem);
    }
  }

  public getMousePosition(e): mousePosition {
    if (!e) return null;

    //let bond = e.target.getBoundingClientRect();
    if (e.touches) {
      return {
        x: e.touches[0].clientX,
        y: e.touches[0].clientY,
      };
    }

    if (e.srcEvent) {
      return {
        x: e.clientX,
        y: e.clientY,
      };
    }

    return {
      x: e.clientX /*+ this.document.querySelector('#main-content-drawer').scrollLeft*/,
      y: e.clientY /*+ this.document.querySelector('#main-content-drawer').scrollTop*/,
    };
  }

  movePreview(event) {
    let pos = this.getMousePosition(event);
    this.renderer.setStyle(this.getDOMNode(), 'position', 'absolute');
    this.renderer.setStyle(this.getDOMNode(), 'left', pos.x + 'px');
    this.renderer.setStyle(this.getDOMNode(), 'top', pos.y + 'px');
    this.renderer.setStyle(this.getDOMNode(), 'z-index', '9999');
  }

  ngOnDestroy() {
    this.appDragDropService.deregisterComponent(this);

    // register mouse/touch events
    this.el.nativeElement.removeEventListener('mousedown', this.onMouseDown);
    this.el.nativeElement.removeEventListener('mouseup', this.onMouseUp);

    this.el.nativeElement.removeEventListener('touchstart', this.onTouchStart);
    this.el.nativeElement.removeEventListener('touchend', this.onTouchEnd);
  }
}
