/*
 * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
 * Ministerpräsidenten des Landes Schleswig-Holstein
 * Staatskanzlei
 * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
 *
 * Lizenziert unter der EUPL, Version 1.2 oder - sobald
 * diese von der Europäischen Kommission genehmigt wurden -
 * Folgeversionen der EUPL ("Lizenz");
 * Sie dürfen dieses Werk ausschließlich gemäß
 * dieser Lizenz nutzen.
 * Eine Kopie der Lizenz finden Sie hier:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Sofern nicht durch anwendbare Rechtsvorschriften
 * gefordert oder in schriftlicher Form vereinbart, wird
 * die unter der Lizenz verbreitete Software "so wie sie
 * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
 * ausdrücklich oder stillschweigend - verbreitet.
 * Die sprachspezifischen Genehmigungen und Beschränkungen
 * unter der Lizenz sind dem Lizenztext zu entnehmen.
 */
import { isEscapeKey, isNotNull } from '@alfa-client/tech-shared';
import { InteractivityChecker } from '@angular/cdk/a11y';
import { ComponentRef, Directive, ElementRef, HostListener, inject, Input, OnDestroy, Renderer2, ViewContainerRef, } from '@angular/core';
import { isEmpty, isNull, uniqueId } from 'lodash-es';
import { TooltipComponent } from './tooltip.component';

export enum TooltipPosition {
  ABOVE = 'above',
  BELOW = 'below',
}

const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
type TooltipAriaType = 'aria-describedby' | 'aria-labelledby';

@Directive({
  selector: '[tooltip]',
  standalone: true,
})
export class TooltipDirective implements OnDestroy {
  @Input() set tooltip(value: string) {
    if (isNotNull(this.componentRef)) {
      this.destroy();
    }

    if (isEmpty(value)) {
      return;
    }

    this.createTooltip(value);
  }

  @Input() tooltipPosition: TooltipPosition = TooltipPosition.BELOW;
  @Input() tooltipAriaType: TooltipAriaType = 'aria-describedby';

  componentRef: ComponentRef<TooltipComponent> = null;
  parentElement: HTMLElement = null;
  tooltipId: string;
  attachedToFocused: boolean = false;
  position: TooltipPosition;
  leftOffset: number = 0;

  public readonly viewContainerRef: ViewContainerRef = inject(ViewContainerRef);
  public readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef);
  public readonly renderer: Renderer2 = inject(Renderer2);
  public readonly interactivityChecker: InteractivityChecker = inject(InteractivityChecker);

  ngOnDestroy(): void {
    this.destroy();
  }

  @HostListener('mouseenter')
  @HostListener('focusin')
  showTooltip(): void {
    if (isNull(this.componentRef)) {
      return;
    }

    const nativeElement: HTMLElement = this.elementRef.nativeElement;
    this.attachedToFocused = nativeElement.contains(document.activeElement);
    this.setTooltipProperties();
  }

  @HostListener('mouseleave')
  @HostListener('focusout')
  hideTooltip(): void {
    this.hide();
  }

  @HostListener('keydown', ['$event'])
  onKeydown(e: KeyboardEvent): void {
    if (isEscapeKey(e)) {
      this.hide();
    }
  }

  createTooltip(tooltipText: string): void {
    if (isEmpty(tooltipText)) {
      return;
    }

    const nativeElement: HTMLElement = this.elementRef.nativeElement;
    this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
    this.parentElement = this.getParentElement(nativeElement);
    this.parentElement.appendChild(this.componentRef.location.nativeElement);
    this.tooltipId = uniqueId('tooltip');
    this.setInitialTooltipProperties(tooltipText, this.tooltipId);
    this.setAriaAttribute(this.tooltipAriaType);
  }

  setInitialTooltipProperties(text: string, id: string) {
    this.componentRef.instance.text = text;
    this.componentRef.instance.id = id;
  }

  setTooltipProperties(): void {
    const { left, right, top, bottom } = this.elementRef.nativeElement.getBoundingClientRect();
    this.setTooltipOffsetAndPosition();

    this.componentRef.instance.left = (right + left) / 2 + this.leftOffset;
    this.componentRef.instance.top = this.getTopPosition(top, bottom, this.position, this.attachedToFocused);
    this.componentRef.instance.position = this.position;
    this.componentRef.instance.leftOffset = this.leftOffset;
    this.componentRef.instance.show = true;
  }

  setAutoPosition(tooltipHeight: number, parentRect: DOMRect, windowHeight: number): void {
    const { top, bottom } = parentRect;

    if (tooltipHeight > windowHeight - bottom) {
      this.position = TooltipPosition.ABOVE;
      return;
    }

    if (tooltipHeight > top) {
      this.position = TooltipPosition.BELOW;
      return;
    }

    this.position = this.tooltipPosition;
  }

  setLeftOffset(tooltipWidth: number, parentRect: DOMRect, windowWidth: number): void {
    const { left, right, width } = parentRect;
    const halfTooltipWidth: number = tooltipWidth / 2;

    if (tooltipWidth < width) {
      this.leftOffset = 0;
      return;
    }

    if (halfTooltipWidth > left) {
      this.leftOffset = halfTooltipWidth - left;
      return;
    }

    if (halfTooltipWidth > windowWidth - right) {
      this.leftOffset = windowWidth - right - halfTooltipWidth;
      return;
    }

    this.leftOffset = 0;
  }

  setTooltipOffsetAndPosition(): void {
    const { width, height } = this.componentRef.location.nativeElement.children[0].getBoundingClientRect();
    const parentRect: DOMRect = this.elementRef.nativeElement.getBoundingClientRect();
    this.setLeftOffset(width, parentRect, window.innerWidth);
    this.setAutoPosition(height, parentRect, window.innerHeight);
  }

  setAriaAttribute(ariaType: TooltipAriaType): void {
    this.renderer.setAttribute(this.parentElement, ariaType, this.tooltipId);
  }

  removeAriaAttribute(ariaType: TooltipAriaType): void {
    this.renderer.removeAttribute(this.parentElement, ariaType);
  }

  getParentElement(element: HTMLElement): HTMLElement {
    if (this.interactivityChecker.isFocusable(element)) {
      return element;
    }
    return this.getFocusableElement(element) ?? element;
  }

  getFocusableElement(element: HTMLElement): HTMLElement {
    return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
  }

  getTopPosition(parentTop: number, parentBottom: number, tooltipPosition: TooltipPosition, attachedToFocused: boolean): number {
    if (tooltipPosition === TooltipPosition.ABOVE) {
      return parentTop - (attachedToFocused ? OUTLINE_INDENT : 0);
    }
    return parentBottom + (attachedToFocused ? OUTLINE_INDENT : 0);
  }

  hide(): void {
    if (isNull(this.componentRef)) {
      return;
    }

    this.componentRef.instance.show = false;
  }

  destroy(): void {
    if (isNull(this.componentRef)) {
      return;
    }

    this.componentRef.destroy();
    this.componentRef = null;
    this.removeAriaAttribute(this.tooltipAriaType);
    this.parentElement = null;
  }
}