import { ConnectedPosition, Overlay, OverlayPositionBuilder, OverlayRef, ScrollDispatcher } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DOCUMENT } from '@angular/common';
import {
  ComponentRef,
  Directive,
  ElementRef,
  HostListener,
  Inject,
  Input,
  NgZone,
  OnDestroy,
  OnInit,
  TemplateRef
} from '@angular/core';
import { fromEvent, merge, Subject, takeUntil } from 'rxjs';
import { TooltipComponent } from '../tooltip.component';
import { TooltipOptions } from '../tooltip.model';

@Directive({
  selector: '[njTooltip]',
  exportAs: 'njTooltip',
  standalone: true
})
export class TooltipDirective implements OnInit, OnDestroy {
  private unsubscribe: Subject<void> = new Subject<void>();

  private _tooltipOptions: TooltipOptions;

  private overlayRef: OverlayRef;

  private tooltipRef: ComponentRef<TooltipComponent>;

  private intersectionObserver: IntersectionObserver;

  @Input()
  set tooltipOptions(value: TooltipOptions) {
    this._tooltipOptions = value;
    this.setTooltipValues();
    const scrollableAncestors = this.scrollDispatcher.getAncestorScrollContainers(this.el);
    const positionStrategy = this.overlayPositionBuilder
      .flexibleConnectedTo(this.el)
      .withPositions([this.getPositionOptions()])
      .withScrollableContainers(scrollableAncestors);
    this.overlayRef?.updatePositionStrategy(positionStrategy);
  }

  get tooltipOptions(): TooltipOptions {
    return this._tooltipOptions;
  }

  @Input() tooltipCustomContent: TemplateRef<any>;

  constructor(
    private el: ElementRef,
    private overlayPositionBuilder: OverlayPositionBuilder,
    private overlay: Overlay,
    private scrollDispatcher: ScrollDispatcher,
    @Inject(DOCUMENT) private doc: Document,
    private zone: NgZone
  ) {}

  ngOnInit(): void {
    const scrollableAncestors = this.scrollDispatcher.getAncestorScrollContainers(this.el);

    const positionStrategy = this.overlayPositionBuilder
      .flexibleConnectedTo(this.el)
      .withPositions([this.getPositionOptions()])
      .withScrollableContainers(scrollableAncestors);

    this.overlayRef = this.overlay.create({
      positionStrategy,
      scrollStrategy: this.overlay.scrollStrategies.reposition()
    });
  }

  ngOnDestroy() {
    this.overlayRef?.dispose();
    this.unsubscribe.next();
    this.unsubscribe.complete();
  }

  getPositionOptions(): ConnectedPosition {
    const defaultPosition: ConnectedPosition = {
      originX: 'center',
      originY: 'top',
      overlayX: 'center',
      overlayY: 'bottom'
    };
    switch (this.tooltipOptions?.placement) {
      case 'bottom':
        return {
          originX: 'center',
          originY: 'bottom',
          overlayX: 'center',
          overlayY: 'top'
        };
      case 'top':
        return defaultPosition;
      case 'left':
        return {
          originX: 'start',
          originY: 'center',
          overlayX: 'end',
          overlayY: 'center'
        };
      case 'right':
        return {
          originX: 'end',
          originY: 'center',
          overlayX: 'start',
          overlayY: 'center'
        };
      default:
        return defaultPosition;
    }
  }

  @HostListener('mouseenter')
  @HostListener('focusin')
  show() {
    if (this.tooltipRef) {
      return;
    }
    this.tooltipRef = this.overlayRef.attach(new ComponentPortal(TooltipComponent));
    merge(
      fromEvent(this.tooltipRef?.location?.nativeElement, 'mouseleave'),
      fromEvent(this.tooltipRef?.location?.nativeElement, 'focusout')
    )
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((event) => {
        this.hide(event);
      });

    this.zone.runOutsideAngular(() => {
      this.intersectionObserver = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          const isElementVisible = entry.isIntersecting;
          if (!isElementVisible) {
            this.zone.run(() => {
              this.hide();
            });
          }
        });
      });

      this.intersectionObserver.observe(this.el?.nativeElement);
    });
    this.setTooltipValues();

    this.el?.nativeElement?.firstElementChild?.setAttribute('aria-describedby', this.tooltipOptions.tooltipId);
  }

  @HostListener('mouseleave', ['$event'])
  @HostListener('focusout', ['$event'])
  hide(event?) {
    const focusedElement = this.doc?.activeElement;
    const isFocusedElement = focusedElement && this.el.nativeElement.contains(focusedElement);
    const newTarget = (event as MouseEvent)?.relatedTarget as Node | null;
    const isNextTargetTooltip = newTarget && this.overlayRef?.overlayElement?.contains(newTarget);
    const isNextTargetElement = newTarget && this.el?.nativeElement?.contains(newTarget);
    if (!newTarget || (!isNextTargetTooltip && !isNextTargetElement && !isFocusedElement)) {
      this.overlayRef.detach();
      this.tooltipRef = null;
      this.unsubscribe.next();
      this.intersectionObserver.disconnect();
      this.el?.nativeElement?.firstElementChild?.removeAttribute('aria-describedby');
    }
  }

  setTooltipValues() {
    const tooltipComponent = this.tooltipRef?.instance;
    if (!tooltipComponent) {
      return;
    }

    tooltipComponent.label = this.tooltipCustomContent ? null : this.tooltipOptions?.label;
    tooltipComponent.isInverse = this.tooltipOptions?.isInverse;
    tooltipComponent.hasArrow = this.tooltipOptions?.hasArrow ?? true;
    tooltipComponent.tooltipId = this.tooltipOptions?.tooltipId;
    tooltipComponent.arrowPlacement = this.tooltipOptions?.arrowPlacement ?? 'center';
    tooltipComponent.placement = this.tooltipOptions?.placement ?? 'top';
    tooltipComponent.isStandalone = this.tooltipOptions?.isStandalone ?? true;
    tooltipComponent.isAnimated = this.tooltipOptions?.isAnimated ?? true;
    tooltipComponent.contentTemplateRef = this.tooltipCustomContent;
  }
}
