import {
  AsyncPipe,
  NgClass,
  NgFor,
  NgIf,
  NgTemplateOutlet,
} from '@angular/common';
import {
  Component,
  Input,
  OnDestroy,
  OnInit,
  TemplateRef,
  ViewChild,
} from '@angular/core';
import {
  NgbTooltip,
  NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap';
import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning';
import { TranslateService } from '@ngx-translate/core';
import {
  BehaviorSubject,
  combineLatest,
  Observable,
  Subscription,
} from 'rxjs';
import {
  distinctUntilChanged,
  map,
  mergeMap,
} from 'rxjs/operators';

import { ContextHelp } from '../context-help.model';
import { ContextHelpService } from '../context-help.service';
import { hasValueOperator } from '../empty.util';
import { PlacementDir } from './placement-dir.model';

type ParsedContent = (string | {href: string, text: string})[];

/**
 * This component renders an info icon next to the wrapped element which
 * produces a tooltip when clicked.
 */
@Component({
  selector: 'ds-context-help-wrapper',
  templateUrl: './context-help-wrapper.component.html',
  styleUrls: ['./context-help-wrapper.component.scss'],
  standalone: true,
  imports: [NgFor, NgIf, NgClass, NgbTooltipModule, NgTemplateOutlet, AsyncPipe],
})
export class ContextHelpWrapperComponent implements OnInit, OnDestroy {
  /**
   * Template reference for the wrapped element.
   */
  @Input() templateRef: TemplateRef<any>;

  /**
   * Identifier for the context help tooltip.
   */
  @Input() id: string;

  /**
   * Indicate where the tooltip should show up, relative to the info icon.
   */
  @Input() tooltipPlacement?: PlacementArray = [];

  /**
   * Indicate whether the info icon should appear to the left or to
   * the right of the wrapped element.
   */
  @Input() iconPlacement?: PlacementDir = 'left';

  /**
   * If true, don't process text to render links.
   */
  @Input() set dontParseLinks(dont: boolean) {
    this.dontParseLinks$.next(dont);
  }
  private dontParseLinks$: BehaviorSubject<boolean> = new BehaviorSubject(false);

  shouldShowIcon$: Observable<boolean>;

  tooltip: NgbTooltip;

  @Input() set content(translateKey: string) {
    this.content$.next(translateKey);
  }
  private content$: BehaviorSubject<string | undefined> = new BehaviorSubject(undefined);

  parsedContent$: Observable<ParsedContent>;

  private subs: Subscription[] = [];

  constructor(
    private translateService: TranslateService,
    private contextHelpService: ContextHelpService,
  ) { }

  ngOnInit() {
    this.parsedContent$ = combineLatest([
      this.content$.pipe(distinctUntilChanged(), mergeMap(translateKey => this.translateService.get(translateKey))),
      this.dontParseLinks$.pipe(distinctUntilChanged()),
    ]).pipe(
      map(([text, dontParseLinks]) =>
        dontParseLinks ? [text] : this.parseLinks(text)),
    );
    this.shouldShowIcon$ = this.contextHelpService.shouldShowIcons$();
  }

  @ViewChild('tooltip', { static: false }) set setTooltip(tooltip: NgbTooltip) {
    this.tooltip = tooltip;
    this.clearSubs();
    if (this.tooltip !== undefined) {
      this.subs = [
        this.contextHelpService.getContextHelp$(this.id)
          .pipe(hasValueOperator())
          .subscribe((ch: ContextHelp) => {

            if (ch.isTooltipVisible && !this.tooltip.isOpen()) {
              this.tooltip.open();
            } else if (!ch.isTooltipVisible && this.tooltip.isOpen()) {
              this.tooltip.close();
            }
          }),

        this.tooltip.shown.subscribe(() => {
          this.contextHelpService.showTooltip(this.id);
        }),

        this.tooltip.hidden.subscribe(() => {
          this.contextHelpService.hideTooltip(this.id);
        }),
      ];
    }
  }

  ngOnDestroy() {
    this.clearSubs();
  }

  onClick() {
    this.contextHelpService.toggleTooltip(this.id);
  }

  /**
   * Parses Markdown-style links, splitting up a given text
   * into link-free pieces of text and objects of the form
   * {href: string, text: string} (which represent links).
   * This function makes no effort to check whether the href is a
   * correct URL. Currently, this function does not support escape
   * characters: its behavior when given a string containing square
   * brackets that do not deliminate a link is undefined.
   * Regular parentheses outside of links do work, however.
   *
   * For example:
   * parseLinks("This is text, [this](https://google.com) is a link, and [so is this](https://youtube.com)")
   * =
   * [ "This is text, ",
   *   {href: "https://google.com", text: "this"},
   *   " is a link, and ",
   *   {href: "https://youtube.com", text: "so is this"}
   * ]
   */
  private parseLinks(text: string): ParsedContent {
    // Implementation note: due to `matchAll` method on strings not being available for all versions,
    // separate "split" and "parse" steps are needed.

    // We use splitRegexp (the outer `match` call) to split the text
    // into link-free pieces of text (matched by /[^\[]+/) and pieces
    // of text of the form "[some link text](some.link.here)" (matched
    // by /\[([^\]]*)\]\(([^\)]*)\)/)
    const splitRegexp = /[^\[]+|\[([^\]]*)\]\(([^\)]*)\)/g;

    // Once the array is split up in link-representing strings and
    // non-link-representing strings, we use parseRegexp (the inner
    // `match` call) to transform the link-representing strings into
    // {href: string, text: string} objects.
    const parseRegexp = /^\[([^\]]*)\]\(([^\)]*)\)$/;

    return text.match(splitRegexp).map((substring: string) => {
      const match = substring.match(parseRegexp);
      return match === null
        ? substring
        : ({ href: match[2], text: match[1] });
    });
  }

  private clearSubs() {
    this.subs.forEach(sub => sub.unsubscribe());
    this.subs = [];
  }
}