/**
 * @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 {
  AfterViewInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef,
  Input,
  NgZone,
  OnDestroy,
  ViewChild,
  ViewEncapsulation,
  inject,
} from '@angular/core';
import {MatRipple, RippleAnimationConfig, RippleRef, RippleState} from '@angular/material/core';
import {
  _MatThumb,
  _MatSlider,
  _MatSliderThumb,
  _MatSliderVisualThumb,
  MAT_SLIDER,
  MAT_SLIDER_VISUAL_THUMB,
} from './slider-interface';
import {Platform} from '@angular/cdk/platform';

/**
 * The visual slider thumb.
 *
 * Handles the slider thumb ripple states (hover, focus, and active),
 * and displaying the value tooltip on discrete sliders.
 * @docs-private
 */
@Component({
  selector: 'mat-slider-visual-thumb',
  templateUrl: './slider-thumb.html',
  styleUrl: 'slider-thumb.css',
  host: {
    'class': 'mdc-slider__thumb mat-mdc-slider-visual-thumb',
  },
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  providers: [{provide: MAT_SLIDER_VISUAL_THUMB, useExisting: MatSliderVisualThumb}],
  imports: [MatRipple],
})
export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewInit, OnDestroy {
  readonly _cdr = inject(ChangeDetectorRef);
  private readonly _ngZone = inject(NgZone);
  private _slider = inject<_MatSlider>(MAT_SLIDER);

  /** Whether the slider displays a numeric value label upon pressing the thumb. */
  @Input() discrete: boolean;

  /** Indicates which slider thumb this input corresponds to. */
  @Input() thumbPosition: _MatThumb;

  /** The display value of the slider thumb. */
  @Input() valueIndicatorText: string;

  /** The MatRipple for this slider thumb. */
  @ViewChild(MatRipple) readonly _ripple: MatRipple;

  /** The slider thumb knob. */
  @ViewChild('knob') _knob: ElementRef<HTMLElement>;

  /** The slider thumb value indicator container. */
  @ViewChild('valueIndicatorContainer')
  _valueIndicatorContainer: ElementRef<HTMLElement>;

  /** The slider input corresponding to this slider thumb. */
  private _sliderInput: _MatSliderThumb;

  /** The native html element of the slider input corresponding to this thumb. */
  private _sliderInputEl: HTMLInputElement | undefined;

  /** The RippleRef for the slider thumbs hover state. */
  private _hoverRippleRef: RippleRef | undefined;

  /** The RippleRef for the slider thumbs focus state. */
  private _focusRippleRef: RippleRef | undefined;

  /** The RippleRef for the slider thumbs active state. */
  private _activeRippleRef: RippleRef | undefined;

  /** Whether the slider thumb is currently being hovered. */
  private _isHovered: boolean = false;

  /** Whether the slider thumb is currently being pressed. */
  _isActive = false;

  /** Whether the value indicator tooltip is visible. */
  _isValueIndicatorVisible: boolean = false;

  /** The host native HTML input element. */
  _hostElement = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;

  private _platform = inject(Platform);

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

  ngAfterViewInit() {
    const sliderInput = this._slider._getInput(this.thumbPosition);

    // No-op if the slider isn't configured properly. `MatSlider` will
    // throw an error instructing the user how to set up the slider.
    if (!sliderInput) {
      return;
    }

    this._ripple.radius = 24;
    this._sliderInput = sliderInput;
    this._sliderInputEl = this._sliderInput._hostElement;

    // These listeners don't update any data bindings so we bind them outside
    // of the NgZone to prevent Angular from needlessly running change detection.
    this._ngZone.runOutsideAngular(() => {
      const input = this._sliderInputEl!;
      input.addEventListener('pointermove', this._onPointerMove);
      input.addEventListener('pointerdown', this._onDragStart);
      input.addEventListener('pointerup', this._onDragEnd);
      input.addEventListener('pointerleave', this._onMouseLeave);
      input.addEventListener('focus', this._onFocus);
      input.addEventListener('blur', this._onBlur);
    });
  }

  ngOnDestroy() {
    const input = this._sliderInputEl;

    if (input) {
      input.removeEventListener('pointermove', this._onPointerMove);
      input.removeEventListener('pointerdown', this._onDragStart);
      input.removeEventListener('pointerup', this._onDragEnd);
      input.removeEventListener('pointerleave', this._onMouseLeave);
      input.removeEventListener('focus', this._onFocus);
      input.removeEventListener('blur', this._onBlur);
    }
  }

  private _onPointerMove = (event: PointerEvent): void => {
    if (this._sliderInput._isFocused) {
      return;
    }

    const rect = this._hostElement.getBoundingClientRect();
    const isHovered = this._slider._isCursorOnSliderThumb(event, rect);
    this._isHovered = isHovered;

    if (isHovered) {
      this._showHoverRipple();
    } else {
      this._hideRipple(this._hoverRippleRef);
    }
  };

  private _onMouseLeave = (): void => {
    this._isHovered = false;
    this._hideRipple(this._hoverRippleRef);
  };

  private _onFocus = (): void => {
    // We don't want to show the hover ripple on top of the focus ripple.
    // Happen when the users cursor is over a thumb and then the user tabs to it.
    this._hideRipple(this._hoverRippleRef);
    this._showFocusRipple();
    this._hostElement.classList.add('mdc-slider__thumb--focused');
  };

  private _onBlur = (): void => {
    // Happens when the user tabs away while still dragging a thumb.
    if (!this._isActive) {
      this._hideRipple(this._focusRippleRef);
    }
    // Happens when the user tabs away from a thumb but their cursor is still over it.
    if (this._isHovered) {
      this._showHoverRipple();
    }
    this._hostElement.classList.remove('mdc-slider__thumb--focused');
  };

  private _onDragStart = (event: PointerEvent): void => {
    if (event.button !== 0) {
      return;
    }
    this._isActive = true;
    this._showActiveRipple();
  };

  private _onDragEnd = (): void => {
    this._isActive = false;
    this._hideRipple(this._activeRippleRef);
    // Happens when the user starts dragging a thumb, tabs away, and then stops dragging.
    if (!this._sliderInput._isFocused) {
      this._hideRipple(this._focusRippleRef);
    }

    // On Safari we need to immediately re-show the hover ripple because
    // sliders do not retain focus from pointer events on that platform.
    if (this._platform.SAFARI) {
      this._showHoverRipple();
    }
  };

  /** Handles displaying the hover ripple. */
  private _showHoverRipple(): void {
    if (!this._isShowingRipple(this._hoverRippleRef)) {
      this._hoverRippleRef = this._showRipple({enterDuration: 0, exitDuration: 0});
      this._hoverRippleRef?.element.classList.add('mat-mdc-slider-hover-ripple');
    }
  }

  /** Handles displaying the focus ripple. */
  private _showFocusRipple(): void {
    // Show the focus ripple event if noop animations are enabled.
    if (!this._isShowingRipple(this._focusRippleRef)) {
      this._focusRippleRef = this._showRipple({enterDuration: 0, exitDuration: 0}, true);
      this._focusRippleRef?.element.classList.add('mat-mdc-slider-focus-ripple');
    }
  }

  /** Handles displaying the active ripple. */
  private _showActiveRipple(): void {
    if (!this._isShowingRipple(this._activeRippleRef)) {
      this._activeRippleRef = this._showRipple({enterDuration: 225, exitDuration: 400});
      this._activeRippleRef?.element.classList.add('mat-mdc-slider-active-ripple');
    }
  }

  /** Whether the given rippleRef is currently fading in or visible. */
  private _isShowingRipple(rippleRef?: RippleRef): boolean {
    return rippleRef?.state === RippleState.FADING_IN || rippleRef?.state === RippleState.VISIBLE;
  }

  /** Manually launches the slider thumb ripple using the specified ripple animation config. */
  private _showRipple(
    animation: RippleAnimationConfig,
    ignoreGlobalRippleConfig?: boolean,
  ): RippleRef | undefined {
    if (this._slider.disabled) {
      return;
    }
    this._showValueIndicator();
    if (this._slider._isRange) {
      const sibling = this._slider._getThumb(
        this.thumbPosition === _MatThumb.START ? _MatThumb.END : _MatThumb.START,
      );
      sibling._showValueIndicator();
    }
    if (this._slider._globalRippleOptions?.disabled && !ignoreGlobalRippleConfig) {
      return;
    }
    return this._ripple.launch({
      animation: this._slider._noopAnimations ? {enterDuration: 0, exitDuration: 0} : animation,
      centered: true,
      persistent: true,
    });
  }

  /**
   * Fades out the given ripple.
   * Also hides the value indicator if no ripple is showing.
   */
  private _hideRipple(rippleRef?: RippleRef): void {
    rippleRef?.fadeOut();

    if (this._isShowingAnyRipple()) {
      return;
    }

    if (!this._slider._isRange) {
      this._hideValueIndicator();
    }

    const sibling = this._getSibling();
    if (!sibling._isShowingAnyRipple()) {
      this._hideValueIndicator();
      sibling._hideValueIndicator();
    }
  }

  /** Shows the value indicator ui. */
  _showValueIndicator(): void {
    this._hostElement.classList.add('mdc-slider__thumb--with-indicator');
  }

  /** Hides the value indicator ui. */
  _hideValueIndicator(): void {
    this._hostElement.classList.remove('mdc-slider__thumb--with-indicator');
  }

  _getSibling(): _MatSliderVisualThumb {
    return this._slider._getThumb(
      this.thumbPosition === _MatThumb.START ? _MatThumb.END : _MatThumb.START,
    );
  }

  /** Gets the value indicator container's native HTML element. */
  _getValueIndicatorContainer(): HTMLElement | undefined {
    return this._valueIndicatorContainer?.nativeElement;
  }

  /** Gets the native HTML element of the slider thumb knob. */
  _getKnob(): HTMLElement {
    return this._knob.nativeElement;
  }

  _isShowingAnyRipple(): boolean {
    return (
      this._isShowingRipple(this._hoverRippleRef) ||
      this._isShowingRipple(this._focusRippleRef) ||
      this._isShowingRipple(this._activeRippleRef)
    );
  }
}
