/**
 * @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 {Platform} from '@angular/cdk/platform';
import {
  Directive,
  ElementRef,
  InjectionToken,
  Input,
  NgZone,
  OnDestroy,
  OnInit,
  ANIMATION_MODULE_TYPE,
  Injector,
  inject,
} from '@angular/core';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
import {RippleAnimationConfig, RippleConfig, RippleRef} from './ripple-ref';
import {RippleRenderer, RippleTarget} from './ripple-renderer';

/** Configurable options for `matRipple`. */
export interface RippleGlobalOptions {
  /**
   * Whether ripples should be disabled. Ripples can be still launched manually by using
   * the `launch()` method. Therefore focus indicators will still show up.
   */
  disabled?: boolean;

  /**
   * Default configuration for the animation duration of the ripples. There are two phases with
   * different durations for the ripples: `enter` and `leave`. The durations will be overwritten
   * by the value of `matRippleAnimation` or if the `NoopAnimationsModule` is included.
   */
  animation?: RippleAnimationConfig;

  /**
   * Whether ripples should start fading out immediately after the mouse or touch is released. By
   * default, ripples will wait for the enter animation to complete and for mouse or touch release.
   */
  terminateOnPointerUp?: boolean;

  /**
   * A namespace to use for ripple loader to allow multiple instances to exist on the same page.
   */
  namespace?: string;
}

/** Injection token that can be used to specify the global ripple options. */
export const MAT_RIPPLE_GLOBAL_OPTIONS = new InjectionToken<RippleGlobalOptions>(
  'mat-ripple-global-options',
);

@Directive({
  selector: '[mat-ripple], [matRipple]',
  exportAs: 'matRipple',
  host: {
    'class': 'mat-ripple',
    '[class.mat-ripple-unbounded]': 'unbounded',
  },
})
export class MatRipple implements OnInit, OnDestroy, RippleTarget {
  private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});

  /** Custom color for all ripples. */
  @Input('matRippleColor') color: string;

  /** Whether the ripples should be visible outside the component's bounds. */
  @Input('matRippleUnbounded') unbounded: boolean;

  /**
   * Whether the ripple always originates from the center of the host element's bounds, rather
   * than originating from the location of the click event.
   */
  @Input('matRippleCentered') centered: boolean;

  /**
   * If set, the radius in pixels of foreground ripples when fully expanded. If unset, the radius
   * will be the distance from the center of the ripple to the furthest corner of the host element's
   * bounding rectangle.
   */
  @Input('matRippleRadius') radius: number = 0;

  /**
   * Configuration for the ripple animation. Allows modifying the enter and exit animation
   * duration of the ripples. The animation durations will be overwritten if the
   * `NoopAnimationsModule` is being used.
   */
  @Input('matRippleAnimation') animation: RippleAnimationConfig;

  /**
   * Whether click events will not trigger the ripple. Ripples can be still launched manually
   * by using the `launch()` method.
   */
  @Input('matRippleDisabled')
  get disabled() {
    return this._disabled;
  }
  set disabled(value: boolean) {
    if (value) {
      this.fadeOutAllNonPersistent();
    }
    this._disabled = value;
    this._setupTriggerEventsIfEnabled();
  }
  private _disabled: boolean = false;

  /**
   * The element that triggers the ripple when click events are received.
   * Defaults to the directive's host element.
   */
  @Input('matRippleTrigger')
  get trigger() {
    return this._trigger || this._elementRef.nativeElement;
  }
  set trigger(trigger: HTMLElement) {
    this._trigger = trigger;
    this._setupTriggerEventsIfEnabled();
  }
  private _trigger: HTMLElement;

  /** Renderer for the ripple DOM manipulations. */
  private _rippleRenderer: RippleRenderer;

  /** Options that are set globally for all ripples. */
  private _globalOptions: RippleGlobalOptions;

  /** @docs-private Whether ripple directive is initialized and the input bindings are set. */
  _isInitialized: boolean = false;

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

  constructor() {
    const ngZone = inject(NgZone);
    const platform = inject(Platform);
    const globalOptions = inject<RippleGlobalOptions>(MAT_RIPPLE_GLOBAL_OPTIONS, {optional: true});
    const injector = inject(Injector);

    // Note: cannot use `inject()` here, because this class
    // gets instantiated manually in the ripple loader.
    this._globalOptions = globalOptions || {};
    this._rippleRenderer = new RippleRenderer(this, ngZone, this._elementRef, platform, injector);
  }

  ngOnInit() {
    this._isInitialized = true;
    this._setupTriggerEventsIfEnabled();
  }

  ngOnDestroy() {
    this._rippleRenderer._removeTriggerEvents();
  }

  /** Fades out all currently showing ripple elements. */
  fadeOutAll() {
    this._rippleRenderer.fadeOutAll();
  }

  /** Fades out all currently showing non-persistent ripple elements. */
  fadeOutAllNonPersistent() {
    this._rippleRenderer.fadeOutAllNonPersistent();
  }

  /**
   * Ripple configuration from the directive's input values.
   * @docs-private Implemented as part of RippleTarget
   */
  get rippleConfig(): RippleConfig {
    return {
      centered: this.centered,
      radius: this.radius,
      color: this.color,
      animation: {
        ...this._globalOptions.animation,
        ...(this._animationMode === 'NoopAnimations' ? {enterDuration: 0, exitDuration: 0} : {}),
        ...this.animation,
      },
      terminateOnPointerUp: this._globalOptions.terminateOnPointerUp,
    };
  }

  /**
   * Whether ripples on pointer-down are disabled or not.
   * @docs-private Implemented as part of RippleTarget
   */
  get rippleDisabled(): boolean {
    return this.disabled || !!this._globalOptions.disabled;
  }

  /** Sets up the trigger event listeners if ripples are enabled. */
  private _setupTriggerEventsIfEnabled() {
    if (!this.disabled && this._isInitialized) {
      this._rippleRenderer.setupTriggerEvents(this.trigger);
    }
  }

  /**
   * Launches a manual ripple using the specified ripple configuration.
   * @param config Configuration for the manual ripple.
   */
  launch(config: RippleConfig): RippleRef;

  /**
   * Launches a manual ripple at the specified coordinates relative to the viewport.
   * @param x Coordinate along the X axis at which to fade-in the ripple. Coordinate
   *   should be relative to the viewport.
   * @param y Coordinate along the Y axis at which to fade-in the ripple. Coordinate
   *   should be relative to the viewport.
   * @param config Optional ripple configuration for the manual ripple.
   */
  launch(x: number, y: number, config?: RippleConfig): RippleRef;

  /** Launches a manual ripple at the specified coordinated or just by the ripple config. */
  launch(configOrX: number | RippleConfig, y: number = 0, config?: RippleConfig): RippleRef {
    if (typeof configOrX === 'number') {
      return this._rippleRenderer.fadeInRipple(configOrX, y, {...this.rippleConfig, ...config});
    } else {
      return this._rippleRenderer.fadeInRipple(0, 0, {...this.rippleConfig, ...configOrX});
    }
  }
}
