import {
  HorizontalConnectionPos,
  Overlay,
  OverlayPositionBuilder,
  OverlayRef,
  VerticalConnectionPos,
} from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import {
  ChangeDetectorRef,
  Directive,
  ElementRef,
  EmbeddedViewRef,
  HostListener,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  SimpleChanges,
  ViewContainerRef,
  Inject,
  DOCUMENT
} from '@angular/core';
import { asapScheduler, Subject, fromEvent, Subscription } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { UiOverlayTrigger } from './overlay-trigger';


@Directive({
  selector: '[uiOverlayTrigger]',
  exportAs: 'uiOverlayTrigger',
  standalone: false,
})
export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
  @Input('uiOverlayTrigger')
  component: UiOverlayTrigger;

  @Input()
  triggerOn: 'click' | 'hover' | 'init' | 'none' = 'click';

  @Input()
  overlayTriggerDisabled: boolean;

  private overlayRef: OverlayRef;
  private viewRef: EmbeddedViewRef<any>;
  private _onDestroy$ = new Subject<void>();
  private _clickListenerSub: Subscription;

  get opened() {
    return !!this.viewRef;
  }

  constructor(
    private overlayPositionBuilder: OverlayPositionBuilder,
    private viewContainerRef: ViewContainerRef,
    private elementRef: ElementRef,
    private overlay: Overlay,
    private cdr: ChangeDetectorRef,
    @Inject(DOCUMENT) private _document: Document,
  ) {}

  ngOnChanges({ position }: SimpleChanges): void {
    if (position) {
      this.updatePosition();
    }
  }

  ngOnInit() {
    this.createOverlay();

    asapScheduler.schedule(() => {
      if (this.triggerOn === 'init') {
        this.toggle();
      }
    });
  }

  ngOnDestroy() {
    this.overlayRef.dispose();
    this._onDestroy$.next();
    this._onDestroy$.complete();
  }

  @HostListener('click', ['$event'])
  onClick(event: Event) {
    if (this.triggerOn === 'click') {
      event.stopPropagation();
      event.preventDefault();
      this.toggle();
    }
  }

  @HostListener('hover', ['$event'])
  onHover(event: Event) {
    if (this.triggerOn === 'hover') {
      event.stopPropagation();
      event.preventDefault();
      this.toggle();
    }
  }

  toggle() {
    if (this.overlayTriggerDisabled) {
      return;
    }

    if (this.viewRef) {
      this.close();
    } else {
      this.open();
    }
  }

  open() {
    const dropdownPortal = new TemplatePortal(this.component.templateRef, this.viewContainerRef);

    if (this.viewRef) {
      this.close();
    }

    this.updatePositionStrategy();

    this.viewRef = this.overlayRef.attach(dropdownPortal);

    this.registerCloseOnClickListener();

    this.component.close = () => this.close();

    this.cdr.markForCheck();
  }

  close() {
    this.viewRef?.destroy();
    this.overlayRef.detach();
    delete this.viewRef;
    this._clickListenerSub.unsubscribe();
    this.cdr.markForCheck();
  }

  createOverlay() {
    this.overlayRef = this.overlay.create({
      positionStrategy: this.getPositionStrategy(),
      hasBackdrop: false,
    });
    this.overlayRef
      .backdropClick()
      .pipe(takeUntil(this._onDestroy$))
      .subscribe(() => this.close());
  }

  registerCloseOnClickListener() {
    asapScheduler.schedule(() => {
      this._clickListenerSub = fromEvent(this._document.body, 'click')
        .pipe(take(1))
        .subscribe((event) => {
          if (this.viewRef && !this.overlayRef?.hostElement?.contains(event.target as HTMLElement)) {
            this.close();
          }
        });
    }, 1);
  }

  updatePositionStrategy() {
    this.overlayRef.updatePositionStrategy(this.getPositionStrategy());
  }

  getPositionStrategy() {
    let [originX, originFallbackX]: HorizontalConnectionPos[] = [
      this.component.xPosition === 'before' ? 'start' : 'end',
      'start',
    ];

    let [originY, overlayFallbackY]: VerticalConnectionPos[] = [
      this.component.yPosition === 'above' ? 'top' : 'bottom',
      'top',
    ];

    let [overlayY, originFallbackY]: VerticalConnectionPos[] = [
      originY === 'bottom' ? 'top' : 'bottom',
      overlayFallbackY,
    ];
    let [overlayX, overlayFallbackX] = [originX, originFallbackX];
    let offsetY = this.component.yOffset ?? 0;
    let offsetX = this.component.xOffset ?? 0;

    return this.overlayPositionBuilder.flexibleConnectedTo(this.elementRef).withPositions([
      { originX, originY, overlayX, overlayY, offsetY, offsetX },
      { originX: originFallbackX, originY, overlayX: overlayFallbackX, overlayY, offsetY, offsetX },
      {
        originX,
        originY: originFallbackY,
        overlayX,
        overlayY: overlayFallbackY,
        offsetY: -offsetY,
        offsetX: -offsetX,
      },
      {
        originX: originFallbackX,
        originY: originFallbackY,
        overlayX: overlayFallbackX,
        overlayY: overlayFallbackY,
        offsetY: -offsetY,
        offsetX: -offsetX,
      },
    ]);
  }

  updatePosition() {
    this.overlayRef?.updatePositionStrategy(this.getPositionStrategy());
  }
}
