/**
 * @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 {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {Platform} from '@angular/cdk/platform';
import {
  AfterViewInit,
  ANIMATION_MODULE_TYPE,
  booleanAttribute,
  Directive,
  ElementRef,
  inject,
  InjectionToken,
  Input,
  NgZone,
  numberAttribute,
  OnDestroy,
  OnInit,
} from '@angular/core';
import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';

/** Object that can be used to configure the default options for the button component. */
export interface MatButtonConfig {
  /** Whether disabled buttons should be interactive. */
  disabledInteractive?: boolean;

  /** Default palette color to apply to buttons. */
  color?: ThemePalette;
}

/** Injection token that can be used to provide the default options the button component. */
export const MAT_BUTTON_CONFIG = new InjectionToken<MatButtonConfig>('MAT_BUTTON_CONFIG');

/** Shared host configuration for all buttons */
export const MAT_BUTTON_HOST = {
  '[attr.disabled]': '_getDisabledAttribute()',
  '[attr.aria-disabled]': '_getAriaDisabled()',
  '[class.mat-mdc-button-disabled]': 'disabled',
  '[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive',
  '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
  // MDC automatically applies the primary theme color to the button, but we want to support
  // an unthemed version. If color is undefined, apply a CSS class that makes it easy to
  // select and style this "theme".
  '[class.mat-unthemed]': '!color',
  // Add a class that applies to all buttons. This makes it easier to target if somebody
  // wants to target all Material buttons.
  '[class.mat-mdc-button-base]': 'true',
  '[class]': 'color ? "mat-" + color : ""',
};

/** List of classes to add to buttons instances based on host attribute selector. */
const HOST_SELECTOR_MDC_CLASS_PAIR: {attribute: string; mdcClasses: string[]}[] = [
  {
    attribute: 'mat-button',
    mdcClasses: ['mdc-button', 'mat-mdc-button'],
  },
  {
    attribute: 'mat-flat-button',
    mdcClasses: ['mdc-button', 'mdc-button--unelevated', 'mat-mdc-unelevated-button'],
  },
  {
    attribute: 'mat-raised-button',
    mdcClasses: ['mdc-button', 'mdc-button--raised', 'mat-mdc-raised-button'],
  },
  {
    attribute: 'mat-stroked-button',
    mdcClasses: ['mdc-button', 'mdc-button--outlined', 'mat-mdc-outlined-button'],
  },
  {
    attribute: 'mat-fab',
    mdcClasses: ['mdc-fab', 'mat-mdc-fab-base', 'mat-mdc-fab'],
  },
  {
    attribute: 'mat-mini-fab',
    mdcClasses: ['mdc-fab', 'mat-mdc-fab-base', 'mdc-fab--mini', 'mat-mdc-mini-fab'],
  },
  {
    attribute: 'mat-icon-button',
    mdcClasses: ['mdc-icon-button', 'mat-mdc-icon-button'],
  },
];

/** Base class for all buttons.  */
@Directive()
export class MatButtonBase implements AfterViewInit, OnDestroy {
  _elementRef = inject(ElementRef);
  _platform = inject(Platform);
  _ngZone = inject(NgZone);
  _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});

  private readonly _focusMonitor = inject(FocusMonitor);

  /**
   * Handles the lazy creation of the MatButton ripple.
   * Used to improve initial load time of large applications.
   */
  protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader);

  /** Whether this button is a FAB. Used to apply the correct class on the ripple. */
  protected _isFab = false;

  /**
   * Theme color of the button. This API is supported in M2 themes only, it has
   * no effect in M3 themes.
   *
   * For information on applying color variants in M3, see
   * https://material.angular.io/guide/theming#using-component-color-variants.
   */
  @Input() color?: string | null;

  /** Whether the ripple effect is disabled or not. */
  @Input({transform: booleanAttribute})
  get disableRipple(): boolean {
    return this._disableRipple;
  }
  set disableRipple(value: any) {
    this._disableRipple = value;
    this._updateRippleDisabled();
  }
  private _disableRipple: boolean = false;

  /** Whether the button is disabled. */
  @Input({transform: booleanAttribute})
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: any) {
    this._disabled = value;
    this._updateRippleDisabled();
  }
  private _disabled: boolean = false;

  /** `aria-disabled` value of the button. */
  @Input({transform: booleanAttribute, alias: 'aria-disabled'})
  ariaDisabled: boolean | undefined;

  /**
   * Natively disabled buttons prevent focus and any pointer events from reaching the button.
   * In some scenarios this might not be desirable, because it can prevent users from finding out
   * why the button is disabled (e.g. via tooltip).
   *
   * Enabling this input will change the button so that it is styled to be disabled and will be
   * marked as `aria-disabled`, but it will allow the button to receive events and focus.
   *
   * Note that by enabling this, you need to set the `tabindex` yourself if the button isn't
   * meant to be tabbable and you have to prevent the button action (e.g. form submissions).
   */
  @Input({transform: booleanAttribute})
  disabledInteractive: boolean;

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

  constructor() {
    inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
    const config = inject(MAT_BUTTON_CONFIG, {optional: true});
    const element = this._elementRef.nativeElement;
    const classList = (element as HTMLElement).classList;

    this.disabledInteractive = config?.disabledInteractive ?? false;
    this.color = config?.color ?? null;
    this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'});

    // For each of the variant selectors that is present in the button's host
    // attributes, add the correct corresponding MDC classes.
    for (const {attribute, mdcClasses} of HOST_SELECTOR_MDC_CLASS_PAIR) {
      if (element.hasAttribute(attribute)) {
        classList.add(...mdcClasses);
      }
    }
  }

  ngAfterViewInit() {
    this._focusMonitor.monitor(this._elementRef, true);
  }

  ngOnDestroy() {
    this._focusMonitor.stopMonitoring(this._elementRef);
    this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
  }

  /** Focuses the button. */
  focus(origin: FocusOrigin = 'program', options?: FocusOptions): void {
    if (origin) {
      this._focusMonitor.focusVia(this._elementRef.nativeElement, origin, options);
    } else {
      this._elementRef.nativeElement.focus(options);
    }
  }

  protected _getAriaDisabled() {
    if (this.ariaDisabled != null) {
      return this.ariaDisabled;
    }

    return this.disabled && this.disabledInteractive ? true : null;
  }

  protected _getDisabledAttribute() {
    return this.disabledInteractive || !this.disabled ? null : true;
  }

  private _updateRippleDisabled(): void {
    this._rippleLoader?.setDisabled(
      this._elementRef.nativeElement,
      this.disableRipple || this.disabled,
    );
  }
}

/** Shared host configuration for buttons using the `<a>` tag. */
export const MAT_ANCHOR_HOST = {
  // Note that this is basically a noop on anchors,
  // but it appears that some internal apps depend on it.
  '[attr.disabled]': '_getDisabledAttribute()',
  '[class.mat-mdc-button-disabled]': 'disabled',
  '[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive',
  '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',

  // Note that we ignore the user-specified tabindex when it's disabled for
  // consistency with the `mat-button` applied on native buttons where even
  // though they have an index, they're not tabbable.
  '[attr.tabindex]': 'disabled && !disabledInteractive ? -1 : tabIndex',
  '[attr.aria-disabled]': '_getAriaDisabled()',
  // MDC automatically applies the primary theme color to the button, but we want to support
  // an unthemed version. If color is undefined, apply a CSS class that makes it easy to
  // select and style this "theme".
  '[class.mat-unthemed]': '!color',
  // Add a class that applies to all buttons. This makes it easier to target if somebody
  // wants to target all Material buttons.
  '[class.mat-mdc-button-base]': 'true',
  '[class]': 'color ? "mat-" + color : ""',
};

/**
 * Anchor button base.
 */
@Directive()
export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy {
  @Input({
    transform: (value: unknown) => {
      return value == null ? undefined : numberAttribute(value);
    },
  })
  tabIndex: number;

  ngOnInit(): void {
    this._ngZone.runOutsideAngular(() => {
      this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents);
    });
  }

  override ngOnDestroy(): void {
    super.ngOnDestroy();
    this._elementRef.nativeElement.removeEventListener('click', this._haltDisabledEvents);
  }

  _haltDisabledEvents = (event: Event): void => {
    // A disabled button shouldn't apply any actions
    if (this.disabled) {
      event.preventDefault();
      event.stopImmediatePropagation();
    }
  };

  protected override _getAriaDisabled() {
    if (this.ariaDisabled != null) {
      return this.ariaDisabled;
    }
    return this.disabled || null;
  }
}
