/**
 * @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,
  inject,
  Input,
  NgZone,
  OnDestroy,
  InjectionToken,
} from '@angular/core';
import {SharedResizeObserver} from '@angular/cdk/observers/private';
import {Subscription} from 'rxjs';

/** An interface that the parent form-field should implement to receive resize events. */
export interface FloatingLabelParent {
  _handleLabelResized(): void;
}

/** An injion token for the parent form-field. */
export const FLOATING_LABEL_PARENT = new InjectionToken<FloatingLabelParent>('FloatingLabelParent');

/**
 * Internal directive that maintains a MDC floating label. This directive does not
 * use the `MDCFloatingLabelFoundation` class, as it is not worth the size cost of
 * including it just to measure the label width and toggle some classes.
 *
 * The use of a directive allows us to conditionally render a floating label in the
 * template without having to manually manage instantiation and destruction of the
 * floating label component based on.
 *
 * The component is responsible for setting up the floating label styles, measuring label
 * width for the outline notch, and providing inputs that can be used to toggle the
 * label's floating or required state.
 */
@Directive({
  selector: 'label[matFormFieldFloatingLabel]',
  host: {
    'class': 'mdc-floating-label mat-mdc-floating-label',
    '[class.mdc-floating-label--float-above]': 'floating',
  },
})
export class MatFormFieldFloatingLabel implements OnDestroy {
  private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);

  /** Whether the label is floating. */
  @Input()
  get floating() {
    return this._floating;
  }
  set floating(value: boolean) {
    this._floating = value;
    if (this.monitorResize) {
      this._handleResize();
    }
  }
  private _floating = false;

  /** Whether to monitor for resize events on the floating label. */
  @Input()
  get monitorResize() {
    return this._monitorResize;
  }
  set monitorResize(value: boolean) {
    this._monitorResize = value;
    if (this._monitorResize) {
      this._subscribeToResize();
    } else {
      this._resizeSubscription.unsubscribe();
    }
  }
  private _monitorResize = false;

  /** The shared ResizeObserver. */
  private _resizeObserver = inject(SharedResizeObserver);

  /** The Angular zone. */
  private _ngZone = inject(NgZone);

  /** The parent form-field. */
  private _parent = inject(FLOATING_LABEL_PARENT);

  /** The current resize event subscription. */
  private _resizeSubscription = new Subscription();

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

  ngOnDestroy() {
    this._resizeSubscription.unsubscribe();
  }

  /** Gets the width of the label. Used for the outline notch. */
  getWidth(): number {
    return estimateScrollWidth(this._elementRef.nativeElement);
  }

  /** Gets the HTML element for the floating label. */
  get element(): HTMLElement {
    return this._elementRef.nativeElement;
  }

  /** Handles resize events from the ResizeObserver. */
  private _handleResize() {
    // In the case where the label grows in size, the following sequence of events occurs:
    // 1. The label grows by 1px triggering the ResizeObserver
    // 2. The notch is expanded to accommodate the entire label
    // 3. The label expands to its full width, triggering the ResizeObserver again
    //
    // This is expected, but If we allow this to all happen within the same macro task it causes an
    // error: `ResizeObserver loop limit exceeded`. Therefore we push the notch resize out until
    // the next macro task.
    setTimeout(() => this._parent._handleLabelResized());
  }

  /** Subscribes to resize events. */
  private _subscribeToResize() {
    this._resizeSubscription.unsubscribe();
    this._ngZone.runOutsideAngular(() => {
      this._resizeSubscription = this._resizeObserver
        .observe(this._elementRef.nativeElement, {box: 'border-box'})
        .subscribe(() => this._handleResize());
    });
  }
}

/**
 * Estimates the scroll width of an element.
 * via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts
 */
function estimateScrollWidth(element: HTMLElement): number {
  // Check the offsetParent. If the element inherits display: none from any
  // parent, the offsetParent property will be null (see
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
  // This check ensures we only clone the node when necessary.
  const htmlEl = element as HTMLElement;
  if (htmlEl.offsetParent !== null) {
    return htmlEl.scrollWidth;
  }

  const clone = htmlEl.cloneNode(true) as HTMLElement;
  clone.style.setProperty('position', 'absolute');
  clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
  document.documentElement.appendChild(clone);
  const scrollWidth = clone.scrollWidth;
  clone.remove();
  return scrollWidth;
}
