/**
 * @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 {DOWN_ARROW, hasModifierKey, ModifierKey} from '@angular/cdk/keycodes';
import {
  Directive,
  ElementRef,
  EventEmitter,
  Input,
  OnDestroy,
  Output,
  AfterViewInit,
  OnChanges,
  SimpleChanges,
  booleanAttribute,
  inject,
} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  ValidationErrors,
  Validator,
  ValidatorFn,
} from '@angular/forms';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats, ThemePalette} from '@angular/material/core';
import {Subscription, Subject} from 'rxjs';
import {createMissingDateImplError} from './datepicker-errors';
import {
  ExtractDateTypeFromSelection,
  MatDateSelectionModel,
  DateSelectionModelChange,
} from './date-selection-model';

/**
 * An event used for datepicker input and change events. We don't always have access to a native
 * input or change event because the event may have been triggered by the user clicking on the
 * calendar popup. For consistency, we always use MatDatepickerInputEvent instead.
 */
export class MatDatepickerInputEvent<D, S = unknown> {
  /** The new value for the target datepicker input. */
  value: D | null;

  constructor(
    /** Reference to the datepicker input component that emitted the event. */
    public target: MatDatepickerInputBase<S, D>,
    /** Reference to the native input element associated with the datepicker input. */
    public targetElement: HTMLElement,
  ) {
    this.value = this.target.value;
  }
}

/**
 * Function that can be used to filter out dates from a calendar.
 * Datepicker can sometimes receive a null value as input for the date argument.
 * This doesn't represent a "null date" but rather signifies that no date has been selected yet in the calendar.
 */
export type DateFilterFn<D> = (date: D | null) => boolean;

/**
 * Partial representation of `MatFormField` that is used for backwards-compatibility
 * between the legacy and non-legacy variants.
 */
export interface _MatFormFieldPartial {
  getConnectedOverlayOrigin(): ElementRef;
  getLabelId(): string | null;
  color: ThemePalette;
  _elementRef: ElementRef;
  _shouldLabelFloat(): boolean;
  _hasFloatingLabel(): boolean;
  _labelId: string;
}

/** Base class for datepicker inputs. */
@Directive()
export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection<S>>
  implements ControlValueAccessor, AfterViewInit, OnChanges, OnDestroy, Validator
{
  protected _elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
  _dateAdapter = inject<DateAdapter<D>>(DateAdapter, {optional: true})!;
  private _dateFormats = inject<MatDateFormats>(MAT_DATE_FORMATS, {optional: true})!;

  /** Whether the component has been initialized. */
  private _isInitialized: boolean;

  /** The value of the input. */
  @Input()
  get value(): D | null {
    return this._model ? this._getValueFromModel(this._model.selection) : this._pendingValue;
  }
  set value(value: any) {
    this._assignValueProgrammatically(value);
  }
  protected _model: MatDateSelectionModel<S, D> | undefined;

  /** Whether the datepicker-input is disabled. */
  @Input({transform: booleanAttribute})
  get disabled(): boolean {
    return !!this._disabled || this._parentDisabled();
  }
  set disabled(value: boolean) {
    const newValue = value;
    const element = this._elementRef.nativeElement;

    if (this._disabled !== newValue) {
      this._disabled = newValue;
      this.stateChanges.next(undefined);
    }

    // We need to null check the `blur` method, because it's undefined during SSR.
    // In Ivy static bindings are invoked earlier, before the element is attached to the DOM.
    // This can cause an error to be thrown in some browsers (IE/Edge) which assert that the
    // element has been inserted.
    if (newValue && this._isInitialized && element.blur) {
      // Normally, native input elements automatically blur if they turn disabled. This behavior
      // is problematic, because it would mean that it triggers another change detection cycle,
      // which then causes a changed after checked error if the input element was focused before.
      element.blur();
    }
  }
  private _disabled: boolean;

  /** Emits when a `change` event is fired on this `<input>`. */
  @Output() readonly dateChange: EventEmitter<MatDatepickerInputEvent<D, S>> = new EventEmitter<
    MatDatepickerInputEvent<D, S>
  >();

  /** Emits when an `input` event is fired on this `<input>`. */
  @Output() readonly dateInput: EventEmitter<MatDatepickerInputEvent<D, S>> = new EventEmitter<
    MatDatepickerInputEvent<D, S>
  >();

  /** Emits when the internal state has changed */
  readonly stateChanges = new Subject<void>();

  _onTouched = () => {};
  _validatorOnChange = () => {};

  private _cvaOnChange: (value: any) => void = () => {};
  private _valueChangesSubscription = Subscription.EMPTY;
  private _localeSubscription = Subscription.EMPTY;

  /**
   * Since the value is kept on the model which is assigned in an Input,
   * we might get a value before we have a model. This property keeps track
   * of the value until we have somewhere to assign it.
   */
  private _pendingValue: D | null;

  /** The form control validator for whether the input parses. */
  private _parseValidator: ValidatorFn = (): ValidationErrors | null => {
    return this._lastValueValid
      ? null
      : {'matDatepickerParse': {'text': this._elementRef.nativeElement.value}};
  };

  /** The form control validator for the date filter. */
  private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
    const controlValue = this._dateAdapter.getValidDateOrNull(
      this._dateAdapter.deserialize(control.value),
    );
    return !controlValue || this._matchesFilter(controlValue)
      ? null
      : {'matDatepickerFilter': true};
  };

  /** The form control validator for the min date. */
  private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
    const controlValue = this._dateAdapter.getValidDateOrNull(
      this._dateAdapter.deserialize(control.value),
    );
    const min = this._getMinDate();
    return !min || !controlValue || this._dateAdapter.compareDate(min, controlValue) <= 0
      ? null
      : {'matDatepickerMin': {'min': min, 'actual': controlValue}};
  };

  /** The form control validator for the max date. */
  private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
    const controlValue = this._dateAdapter.getValidDateOrNull(
      this._dateAdapter.deserialize(control.value),
    );
    const max = this._getMaxDate();
    return !max || !controlValue || this._dateAdapter.compareDate(max, controlValue) >= 0
      ? null
      : {'matDatepickerMax': {'max': max, 'actual': controlValue}};
  };

  /** Gets the base validator functions. */
  protected _getValidators(): ValidatorFn[] {
    return [this._parseValidator, this._minValidator, this._maxValidator, this._filterValidator];
  }

  /** Gets the minimum date for the input. Used for validation. */
  abstract _getMinDate(): D | null;

  /** Gets the maximum date for the input. Used for validation. */
  abstract _getMaxDate(): D | null;

  /** Gets the date filter function. Used for validation. */
  protected abstract _getDateFilter(): DateFilterFn<D> | undefined;

  /** Registers a date selection model with the input. */
  _registerModel(model: MatDateSelectionModel<S, D>): void {
    this._model = model;
    this._valueChangesSubscription.unsubscribe();

    if (this._pendingValue) {
      this._assignValue(this._pendingValue);
    }

    this._valueChangesSubscription = this._model.selectionChanged.subscribe(event => {
      if (this._shouldHandleChangeEvent(event)) {
        const value = this._getValueFromModel(event.selection);
        this._lastValueValid = this._isValidValue(value);
        this._cvaOnChange(value);
        this._onTouched();
        this._formatValue(value);
        this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
        this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
      }
    });
  }

  /** Opens the popup associated with the input. */
  protected abstract _openPopup(): void;

  /** Assigns a value to the input's model. */
  protected abstract _assignValueToModel(model: D | null): void;

  /** Converts a value from the model into a native value for the input. */
  protected abstract _getValueFromModel(modelValue: S): D | null;

  /** Combined form control validator for this input. */
  protected abstract _validator: ValidatorFn | null;

  /** Predicate that determines whether the input should handle a particular change event. */
  protected abstract _shouldHandleChangeEvent(event: DateSelectionModelChange<S>): boolean;

  /** Whether the last value set on the input was valid. */
  protected _lastValueValid = false;

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

  constructor() {
    if (typeof ngDevMode === 'undefined' || ngDevMode) {
      if (!this._dateAdapter) {
        throw createMissingDateImplError('DateAdapter');
      }
      if (!this._dateFormats) {
        throw createMissingDateImplError('MAT_DATE_FORMATS');
      }
    }

    // Update the displayed date when the locale changes.
    this._localeSubscription = this._dateAdapter.localeChanges.subscribe(() => {
      this._assignValueProgrammatically(this.value);
    });
  }

  ngAfterViewInit() {
    this._isInitialized = true;
  }

  ngOnChanges(changes: SimpleChanges) {
    if (dateInputsHaveChanged(changes, this._dateAdapter)) {
      this.stateChanges.next(undefined);
    }
  }

  ngOnDestroy() {
    this._valueChangesSubscription.unsubscribe();
    this._localeSubscription.unsubscribe();
    this.stateChanges.complete();
  }

  /** @docs-private */
  registerOnValidatorChange(fn: () => void): void {
    this._validatorOnChange = fn;
  }

  /** @docs-private */
  validate(c: AbstractControl): ValidationErrors | null {
    return this._validator ? this._validator(c) : null;
  }

  // Implemented as part of ControlValueAccessor.
  writeValue(value: D): void {
    this._assignValueProgrammatically(value);
  }

  // Implemented as part of ControlValueAccessor.
  registerOnChange(fn: (value: any) => void): void {
    this._cvaOnChange = fn;
  }

  // Implemented as part of ControlValueAccessor.
  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }

  // Implemented as part of ControlValueAccessor.
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  _onKeydown(event: KeyboardEvent) {
    const ctrlShiftMetaModifiers: ModifierKey[] = ['ctrlKey', 'shiftKey', 'metaKey'];
    const isAltDownArrow =
      hasModifierKey(event, 'altKey') &&
      event.keyCode === DOWN_ARROW &&
      ctrlShiftMetaModifiers.every((modifier: ModifierKey) => !hasModifierKey(event, modifier));

    if (isAltDownArrow && !this._elementRef.nativeElement.readOnly) {
      this._openPopup();
      event.preventDefault();
    }
  }

  _onInput(value: string) {
    const lastValueWasValid = this._lastValueValid;
    let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
    this._lastValueValid = this._isValidValue(date);
    date = this._dateAdapter.getValidDateOrNull(date);
    const hasChanged = !this._dateAdapter.sameDate(date, this.value);

    // We need to fire the CVA change event for all
    // nulls, otherwise the validators won't run.
    if (!date || hasChanged) {
      this._cvaOnChange(date);
    } else {
      // Call the CVA change handler for invalid values
      // since this is what marks the control as dirty.
      if (value && !this.value) {
        this._cvaOnChange(date);
      }

      if (lastValueWasValid !== this._lastValueValid) {
        this._validatorOnChange();
      }
    }

    if (hasChanged) {
      this._assignValue(date);
      this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
    }
  }

  _onChange() {
    this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
  }

  /** Handles blur events on the input. */
  _onBlur() {
    // Reformat the input only if we have a valid value.
    if (this.value) {
      this._formatValue(this.value);
    }

    this._onTouched();
  }

  /** Formats a value and sets it on the input element. */
  protected _formatValue(value: D | null) {
    this._elementRef.nativeElement.value =
      value != null ? this._dateAdapter.format(value, this._dateFormats.display.dateInput) : '';
  }

  /** Assigns a value to the model. */
  private _assignValue(value: D | null) {
    // We may get some incoming values before the model was
    // assigned. Save the value so that we can assign it later.
    if (this._model) {
      this._assignValueToModel(value);
      this._pendingValue = null;
    } else {
      this._pendingValue = value;
    }
  }

  /** Whether a value is considered valid. */
  private _isValidValue(value: D | null): boolean {
    return !value || this._dateAdapter.isValid(value);
  }

  /**
   * Checks whether a parent control is disabled. This is in place so that it can be overridden
   * by inputs extending this one which can be placed inside of a group that can be disabled.
   */
  protected _parentDisabled() {
    return false;
  }

  /** Programmatically assigns a value to the input. */
  protected _assignValueProgrammatically(value: D | null) {
    value = this._dateAdapter.deserialize(value);
    this._lastValueValid = this._isValidValue(value);
    value = this._dateAdapter.getValidDateOrNull(value);
    this._assignValue(value);
    this._formatValue(value);
  }

  /** Gets whether a value matches the current date filter. */
  _matchesFilter(value: D | null): boolean {
    const filter = this._getDateFilter();
    return !filter || filter(value);
  }
}

/**
 * Checks whether the `SimpleChanges` object from an `ngOnChanges`
 * callback has any changes, accounting for date objects.
 */
export function dateInputsHaveChanged(
  changes: SimpleChanges,
  adapter: DateAdapter<unknown>,
): boolean {
  const keys = Object.keys(changes);

  for (let key of keys) {
    const {previousValue, currentValue} = changes[key];

    if (adapter.isDateInstance(previousValue) && adapter.isDateInstance(currentValue)) {
      if (!adapter.sameDate(previousValue, currentValue)) {
        return true;
      }
    } else {
      return true;
    }
  }

  return false;
}
