/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  Directive,
  ElementRef,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  SimpleChanges,
  inject,
} from '@angular/core';
import {_IdGenerator} from '@angular/cdk/a11y';
import {CdkScrollable} from '@angular/cdk/scrolling';

import {MatDialog} from './dialog';
import {_closeDialogVia, MatDialogRef} from './dialog-ref';

/**
 * Button that will close the current dialog.
 */
@Directive({
  selector: '[mat-dialog-close], [matDialogClose]',
  exportAs: 'matDialogClose',
  host: {
    '(click)': '_onButtonClick($event)',
    '[attr.aria-label]': 'ariaLabel || null',
    '[attr.type]': 'type',
  },
})
export class MatDialogClose implements OnInit, OnChanges {
  dialogRef = inject<MatDialogRef<any>>(MatDialogRef, {optional: true})!;
  private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private _dialog = inject(MatDialog);

  /** Screen-reader label for the button. */
  @Input('aria-label') ariaLabel: string;

  /** Default to "button" to prevents accidental form submits. */
  @Input() type: 'submit' | 'button' | 'reset' = 'button';

  /** Dialog close input. */
  @Input('mat-dialog-close') dialogResult: any;

  @Input('matDialogClose') _matDialogClose: any;

  constructor(...args: unknown[]);
  constructor() {}

  ngOnInit() {
    if (!this.dialogRef) {
      // When this directive is included in a dialog via TemplateRef (rather than being
      // in a Component), the DialogRef isn't available via injection because embedded
      // views cannot be given a custom injector. Instead, we look up the DialogRef by
      // ID. This must occur in `onInit`, as the ID binding for the dialog container won't
      // be resolved at constructor time.
      this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
    }
  }

  ngOnChanges(changes: SimpleChanges) {
    const proxiedChange = changes['_matDialogClose'] || changes['_matDialogCloseResult'];

    if (proxiedChange) {
      this.dialogResult = proxiedChange.currentValue;
    }
  }

  _onButtonClick(event: MouseEvent) {
    // Determinate the focus origin using the click event, because using the FocusMonitor will
    // result in incorrect origins. Most of the time, close buttons will be auto focused in the
    // dialog, and therefore clicking the button won't result in a focus change. This means that
    // the FocusMonitor won't detect any origin change, and will always output `program`.
    _closeDialogVia(
      this.dialogRef,
      event.screenX === 0 && event.screenY === 0 ? 'keyboard' : 'mouse',
      this.dialogResult,
    );
  }
}

@Directive()
export abstract class MatDialogLayoutSection implements OnInit, OnDestroy {
  protected _dialogRef = inject<MatDialogRef<any>>(MatDialogRef, {optional: true})!;
  private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private _dialog = inject(MatDialog);

  constructor(...args: unknown[]);

  constructor() {}

  protected abstract _onAdd(): void;
  protected abstract _onRemove(): void;

  ngOnInit() {
    if (!this._dialogRef) {
      this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!;
    }

    if (this._dialogRef) {
      Promise.resolve().then(() => {
        this._onAdd();
      });
    }
  }

  ngOnDestroy() {
    // Note: we null check because there are some internal
    // tests that are mocking out `MatDialogRef` incorrectly.
    const instance = this._dialogRef?._containerInstance;

    if (instance) {
      Promise.resolve().then(() => {
        this._onRemove();
      });
    }
  }
}

/**
 * Title of a dialog element. Stays fixed to the top of the dialog when scrolling.
 */
@Directive({
  selector: '[mat-dialog-title], [matDialogTitle]',
  exportAs: 'matDialogTitle',
  host: {
    'class': 'mat-mdc-dialog-title mdc-dialog__title',
    '[id]': 'id',
  },
})
export class MatDialogTitle extends MatDialogLayoutSection {
  @Input() id: string = inject(_IdGenerator).getId('mat-mdc-dialog-title-');

  protected _onAdd() {
    // Note: we null check the queue, because there are some internal
    // tests that are mocking out `MatDialogRef` incorrectly.
    this._dialogRef._containerInstance?._addAriaLabelledBy?.(this.id);
  }

  protected override _onRemove(): void {
    this._dialogRef?._containerInstance?._removeAriaLabelledBy?.(this.id);
  }
}

/**
 * Scrollable content container of a dialog.
 */
@Directive({
  selector: `[mat-dialog-content], mat-dialog-content, [matDialogContent]`,
  host: {'class': 'mat-mdc-dialog-content mdc-dialog__content'},
  hostDirectives: [CdkScrollable],
})
export class MatDialogContent {}

/**
 * Container for the bottom action buttons in a dialog.
 * Stays fixed to the bottom when scrolling.
 */
@Directive({
  selector: `[mat-dialog-actions], mat-dialog-actions, [matDialogActions]`,
  host: {
    'class': 'mat-mdc-dialog-actions mdc-dialog__actions',
    '[class.mat-mdc-dialog-actions-align-start]': 'align === "start"',
    '[class.mat-mdc-dialog-actions-align-center]': 'align === "center"',
    '[class.mat-mdc-dialog-actions-align-end]': 'align === "end"',
  },
})
export class MatDialogActions extends MatDialogLayoutSection {
  /**
   * Horizontal alignment of action buttons.
   */
  @Input() align?: 'start' | 'center' | 'end';

  protected _onAdd() {
    this._dialogRef._containerInstance?._updateActionSectionCount?.(1);
  }

  protected override _onRemove(): void {
    this._dialogRef._containerInstance?._updateActionSectionCount?.(-1);
  }
}

/**
 * Finds the closest MatDialogRef to an element by looking at the DOM.
 * @param element Element relative to which to look for a dialog.
 * @param openDialogs References to the currently-open dialogs.
 */
function getClosestDialog(element: ElementRef<HTMLElement>, openDialogs: MatDialogRef<any>[]) {
  let parent: HTMLElement | null = element.nativeElement.parentElement;

  while (parent && !parent.classList.contains('mat-mdc-dialog-container')) {
    parent = parent.parentElement;
  }

  return parent ? openDialogs.find(dialog => dialog.id === parent!.id) : null;
}
