import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DOCUMENT } from '@angular/common';
import {
  Directive,
  ElementRef,
  inject,
  input,
  numberAttribute,
  OnDestroy,
  TemplateRef,
} from '@angular/core';
import { fromEvent, Subject, Subscription, takeUntil } from 'rxjs';
import { FastTipContainerComponent } from './fast-tip-container.component';
export type TOOLTIP_POSITIONS = 'top' | 'right' | 'bottom' | 'left';

export type TOOLTIP_EVENT = 'hover' | 'focus' | 'none';

let tooltipId = 0;

function getTooltipId() {
  return `tooltip-${tooltipId++}`;
}

@Directive({
  selector: '[fast-tip]',
  host: {
    '(focus)': 'onFocus()',
    '(blur)': 'onBlur()',
    '(mouseenter)': 'onMouseEnter()',
    '(mouseleave)': 'onMouseLeave()',
    '[attr.aria-describedby]': 'tooltipId'
  },
  exportAs: 'fast-tip'
})
export class FastTipDirective implements OnDestroy {
  message = input.required<string>({ alias: 'fast-tip' });
  tipPosition = input<TOOLTIP_POSITIONS>('right');
  tipEvent = input<TOOLTIP_EVENT>('hover');
  tipDelay = input(0, { transform: numberAttribute });
  tipContent = input<TemplateRef<unknown>>();
  private tooltipId = getTooltipId();

  // track all active overlays, not just the current one
  // so that we can detach them all on hide()
  // including some overlays that may be orphaned in edge cases
  private static activeOverlays: OverlayRef[] = [];

  private overlay = inject(Overlay);
  private el = inject<ElementRef<HTMLElement>>(ElementRef);
  private document = inject(DOCUMENT);
  private timeoutId: unknown | undefined;
  private escapeSub: Subscription | undefined;
  private readonly destroyed = new Subject<void>(); // Emits when the component is destroyed. */

  onFocus() {
    if (this.tipEvent() === 'focus')
      this.show();
  }

  onBlur() {
    if (this.tipEvent() === 'focus') {
      this.hide();
    }
  }

  onMouseEnter() {
    if (this.tipEvent() === 'hover')
      this.show();
  }

  onMouseLeave() {
    if (this.tipEvent() === 'hover')
      this.hide();
  }

  show(anchor?: ElementRef | Element) {
    this.clearSchedule();
    this.timeoutId = setTimeout(() => {
      const originElement = anchor
        ? (anchor instanceof ElementRef ? anchor.nativeElement : anchor)
        : this.el.nativeElement;
      const tooltipContent = this.tipContent();
      const portal = new ComponentPortal(FastTipContainerComponent);

      const positionStrategy = this.overlay
        .position()
        .flexibleConnectedTo(originElement)
        .withPositions([this.getOverlayPosition(), this.getOverlayFallbackPosition()]);

      const overlayRef = this.overlay.create({ positionStrategy: positionStrategy });
      FastTipDirective.activeOverlays.push(overlayRef);
      const componentRef = overlayRef.attach(portal);
      componentRef.instance.content.set(tooltipContent);
      componentRef.instance.message.set(this.message());
      componentRef.instance.id.set(this.tooltipId);

      // Subscribe to position changes to get the actual position used
      positionStrategy.positionChanges.pipe(takeUntil(this.destroyed)).subscribe(change => {
        const actualPosition = this.getPositionFromConnectedPosition(change.connectionPair);
        componentRef.instance.position.set(actualPosition);
      });

      this.listenToEscape();
    }, this.tipDelay()) as unknown;
  }

  hide() {
    this.escapeSub?.unsubscribe();
    this.clearSchedule();

    FastTipDirective.activeOverlays
      .filter(overlay => overlay.hasAttached())
      .forEach(overlay => {
        overlay.detach();
        overlay = undefined;
      });
    FastTipDirective.activeOverlays = [];
  }

  private offset = 6;
  private connectedPositions = {
    top: { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -this.offset } as ConnectedPosition,
    right: { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: this.offset } as ConnectedPosition,
    bottom: { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: this.offset } as ConnectedPosition,
    left: { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -this.offset } as ConnectedPosition
  }

  private getOverlayPosition(): ConnectedPosition {
    const tooltipPosition = this.tipPosition();

    switch (tooltipPosition) {
      case 'top':
        return this.connectedPositions.top;
      case 'right':
        return this.connectedPositions.right;
      case 'bottom':
        return this.connectedPositions.bottom;
      case 'left':
        return this.connectedPositions.left;
    }
  }

  private getOverlayFallbackPosition(): ConnectedPosition {
    const tooltipPosition = this.tipPosition();
    switch (tooltipPosition) {
      case 'top':
        return this.connectedPositions.bottom;
      case 'right':
        return this.connectedPositions.left;
      case 'bottom':
        return this.connectedPositions.top;
      case 'left':
        return this.connectedPositions.right;
    }
  }

  private getPositionFromConnectedPosition(connectionPair: ConnectedPosition): TOOLTIP_POSITIONS {
    // Compare the connection pair with our known positions to determine which one was used
    const positions = this.connectedPositions;

    if (this.isPositionMatch(connectionPair, positions.top)) {
      return 'top';
    } else if (this.isPositionMatch(connectionPair, positions.right)) {
      return 'right';
    } else if (this.isPositionMatch(connectionPair, positions.bottom)) {
      return 'bottom';
    } else if (this.isPositionMatch(connectionPair, positions.left)) {
      return 'left';
    }

    // Fallback to original position if no match found
    return this.tipPosition();
  }

  private isPositionMatch(pos1: ConnectedPosition, pos2: ConnectedPosition): boolean {
    return pos1.originX === pos2.originX &&
      pos1.originY === pos2.originY &&
      pos1.overlayX === pos2.overlayX &&
      pos1.overlayY === pos2.overlayY;
  }

  private clearSchedule() {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId as number);
      this.timeoutId = undefined;
    }
  }

  private listenToEscape() {
    this.escapeSub?.unsubscribe();
    this.escapeSub = fromEvent<KeyboardEvent>(
      this.document,
      'keydown'
    ).subscribe((event) => {
      if (event.key === 'Escape') {
        this.hide();
      }
    });
  }

  ngOnDestroy(): void {
    this.clearSchedule();
    this.escapeSub?.unsubscribe();
    this.destroyed.next();
    this.destroyed.complete();
  }
}
