import { DOCUMENT } from "@angular/common";
import {
  Directive,
  Renderer2,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  Output,
  Inject,
  OnDestroy,
  AfterViewInit,
  ViewContainerRef,
  ComponentFactoryResolver,
  Compiler,
  ApplicationRef,
  Injector,
  EmbeddedViewRef,
  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") previewData? = null;
  @Input() disableDrag?: boolean = false;

  @Output("appDragBeforeStart") beforeStart = new EventEmitter();
  @Output("appDragDropped") 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);

    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);
  }
}
