import { FocusMonitor } from '@angular/cdk/a11y';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import {
  Component,
  ElementRef,
  EventEmitter,
  HostBinding,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  ViewChild,
} from '@angular/core';
import { FormControl, NgControl, ValidationErrors } from '@angular/forms';
import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatChipInput, MatChipInputEvent } from '@angular/material/chips';
import { MatFormFieldControl } from '@angular/material/form-field';
import { Observable, of, Subject } from 'rxjs';
import { map, mergeMap, startWith, tap } from 'rxjs/operators';
import { ChipOption } from './chip-option.model';
import { BciSharedIntl } from '../shared-intl';

export type FilterableChipOptionProvider = (userInput: string | null) => Observable<ChipOption[]>;

@Component({
  selector: 'bci-chip-list-autocomplete',
  templateUrl: './chip-list-autocomplete.component.html',
  styleUrls: ['./chip-list-autocomplete.component.scss'],
  providers: [{ provide: MatFormFieldControl, useExisting: BciChipListAutocompleteComponent }],
})
export class BciChipListAutocompleteComponent implements MatFormFieldControl<ChipOption[]>, OnInit, OnDestroy, OnChanges {
  static nextId = 0;

  separatorKeysCodes: number[] = [ENTER, COMMA];

  readonly maxChipsVisibleLimit = Number.MAX_SAFE_INTEGER;

  private readonly _stateChanges: Subject<void> = new Subject();
  readonly stateChanges = this._stateChanges.asObservable();

  @HostBinding()
  id = `chip-list-input-autocomplete-${BciChipListAutocompleteComponent.nextId++}`;

  @HostBinding('attr.aria-describedby')
  describedBy = '';
  pendingFreeTextInput = false;
  availableOptions: ChipOption[] = [];
  @Output()
  valueChange = new EventEmitter<ChipOption[]>();
  userInputControl: FormControl = new FormControl();
  userFilteredOptions: Observable<ChipOption[]>;
  /** Number of chips after which the placeholder will be displayed. When Number.MAX_SAFE_INTEGER or 0, the placeholder won't be displayed. */
  @Input() maxChipsVisible = this.maxChipsVisibleLimit;
  /** When true, the chips will be displayed in reverse order so the last chip added will be displayed first in the input field. */
  @Input() showReversedChips = false;
  @Input() addOnBlur = false;
  @Input()
  inlineStyle = true;
  @HostBinding('class.chip-list-inline') get isChipListInline() {
    return this.inlineStyle;
  }
  focused = false;
  touched = false;
  controlType = 'chip-list-input-autocomplete';
  @ViewChild('userInput', { static: false })
  userInput: ElementRef<HTMLInputElement>;
  @ViewChild(MatAutocompleteTrigger, { static: false })
  autocompleteTrigger: MatAutocompleteTrigger;
  readonly ngControl: NgControl = null;
  private _readonlyUserInput = false;
  private _placeholder: string;
  private _optionsProvider: FilterableChipOptionProvider = this.buildStaticOptionsProvider();
  private _options: ChipOption[] = [];
  private _value: ChipOption[] = [];
  private _externalFreeTextValidator: (userInput: string) => ValidationErrors;
  private _allowFreeText = false;
  private _required = false;
  private _disabled = false;
  private _chipLimit: number = undefined;

  constructor(
    public intl: BciSharedIntl,
    private focusMonitor: FocusMonitor,
    private htmlElementRef: ElementRef<HTMLElement>
  ) {
    focusMonitor.monitor(this.htmlElementRef.nativeElement, true).subscribe((origin) => {
      this.touched = this.touched || !origin;
      this.focused = !!origin;
      this._stateChanges.next();
    });
  }

  get readonlyUserInput(): boolean {
    return this._readonlyUserInput;
  }

  @Input()
  set readonlyUserInput(value: boolean) {
    this._readonlyUserInput = value;
    this._stateChanges.next();
  }

  get placeholder(): string {
    return this._placeholder;
  }

  @Input()
  set placeholder(value: string) {
    this._placeholder = value;
    this._stateChanges.next();
  }

  get optionsProvider(): FilterableChipOptionProvider {
    return this._optionsProvider;
  }

  @Input()
  set optionsProvider(optionsProvider: FilterableChipOptionProvider) {
    if (optionsProvider && this._options && this._options.length > 0) {
      throw new Error(
        'Initialising failed. When [optionsProvider] input is set, you cant pass static options via [options].'
      );
    }

    if (!optionsProvider) {
      this._optionsProvider = this.buildStaticOptionsProvider();
    } else {
      this._optionsProvider = optionsProvider;
    }
    this._stateChanges.next();
  }

  get options(): ChipOption[] {
    return this._options;
  }

  @Input()
  set options(staticOptions: ChipOption[]) {
    if (staticOptions) {
      this._options = staticOptions;
      this.availableOptions = this.filterAvailableOptions(staticOptions);
      this._optionsProvider = this.buildStaticOptionsProvider();
    } else {
      this._options = [];
      this.availableOptions = [];
    }
    this.resetUserInput();
    this._stateChanges.next();
  }

  @Input('value')
  get value(): ChipOption[] {
    return this._value;
  }

  set value(value: ChipOption[]) {
    if (value) {
      this._value = value.slice();
      this.availableOptions = this.filterAvailableOptions(this._options);
    } else {
      this._value = undefined;
    }
    this.resetUserInput();
    this._stateChanges.next();
  }

  get empty(): boolean {
    return (!this._value || this._value.length === 0) && this.inputEmpty;
  }

  get hasChips(): boolean {
    return this._value && this._value.length > 0;
  }

  get inputEmpty(): boolean {
    return !this.userInputControl.value;
  }

  get externalFreeTextValidator(): (userInput: string) => ValidationErrors {
    return this._externalFreeTextValidator;
  }

  @Input()
  set externalFreeTextValidator(value: (userInput: string) => ValidationErrors) {
    this._externalFreeTextValidator = value;
    this._stateChanges.next();
  }

  get allowFreeText(): boolean {
    return this._allowFreeText;
  }

  @Input()
  set allowFreeText(value: boolean) {
    this._allowFreeText = value;
    this._stateChanges.next();
  }

  @HostBinding('class.floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  get errorState(): boolean {
    return this.userInputControl.hasError('externalError') || (this.touched && !!this.userInputControl.errors);
  }

  get required() {
    return this._required;
  }

  @Input()
  set required(req: boolean) {
    this._required = req;
    this._stateChanges.next();
  }

  get disabled(): boolean {
    return this._disabled;
  }

  @Input()
  set disabled(value: boolean) {
    this._disabled = value;
    this._disabled ? this.userInputControl.disable() : this.userInputControl.enable();
    this._stateChanges.next();
  }

  get chipLimit(): number {
    return this._chipLimit;
  }

  @Input()
  set chipLimit(value: number) {
    this._chipLimit = value;
    this._stateChanges.next();
  }

  ngOnInit() {
    this.userInputControl.setValidators([
      (control: FormControl) => this.validateUserInputIsValidOption(control.value),
      (control: FormControl) => this.validateFreeTextWithExternalValidator(control.value),
      (control: FormControl) => this.validatePendingTextInput(control.value),
      (control: FormControl) => this.validateChipListNotEmpty(control.value),
    ]);

    window.addEventListener('scroll', this.scrollEvent, true);

    this.userFilteredOptions = this.userInputControl.valueChanges.pipe(
      startWith(''),
      mergeMap((userInput) => {
        return this._optionsProvider(userInput);
      }),
      map((resolvedChipOptions) => this.filterAvailableOptions(resolvedChipOptions)),
      tap((filteredChipOptions) => {
        this.availableOptions = filteredChipOptions;
        this._stateChanges.next();
      })
    );
  }

  // If  required variable change on Runtime, the Validator must be updated
  ngOnChanges(): void {
    this.userInputControl.updateValueAndValidity();
  }


  ngOnDestroy() {
    this._stateChanges.next();
    this._stateChanges.complete();
    this.focusMonitor.stopMonitoring(this.htmlElementRef.nativeElement);
    window.removeEventListener('scroll', this.scrollEvent, true);
  }

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(): void {
    this.htmlElementRef.nativeElement.querySelector('input').focus();
  }

  onAutocompleteOptionSelected(event: MatAutocompleteSelectedEvent): void {
    if (!this.isChipLimitReached()) {
      const newChip = this.availableOptions.find((option) => option.id === event.option.id);
      this.addNewChip(newChip);

      this.resetUserInput();
      this.valueChange.emit(this._value);
      this._stateChanges.next();
    }
  }

  // Determines if the passed element is overflowing its bounds,
  // either vertically or horizontally.
  // Will temporarily modify the "overflow" style to detect this
  // if necessary.
  isTextOverflow(el: HTMLElement): boolean {
    const curOverflow = el.style.overflow;

    if (!curOverflow || curOverflow === "visible")
      el.style.overflow = "hidden";

    const isTextOverflow = el.clientWidth < el.scrollWidth
      || el.clientHeight < el.scrollHeight;

    el.style.overflow = curOverflow;

    return isTextOverflow;
  }

  isChipLimitReached() {
    return !!this._chipLimit && this.value.length >= this._chipLimit;
  }

  onSelectedOptionRemoved(optionId: string): void {
    const index = this._value.findIndex((el) => el.id === optionId);

    if (index >= 0) {
      this._value.splice(index, 1);
    }

    this.resetUserInput();
    this.valueChange.emit(this._value);
    this._stateChanges.next();
  }

  onHiddenChipsRemoved(): void {
    this._value.splice(this.maxChipsVisible);
  }
  /**
   * 
   * If you use a autocomplete panel and addOnBlur option = true you will face a bug.
   * If you start free typing and select one option of the autocomplete flyout the free text and the selected option will be added.
   * This is due to the event fired by the selection and the add on blur funtionality. Both events are fired in parallel.
   * That is why 2 chips get added. With this function which gets called on the blur event, this bug is fixed.
   * By default now the add On Blur option is deactivated for the chip list but still possible with this function and the standard blur event.
   * 
   * @param event 
   * @returns nothing
   */
  addChipOnBlur(event: FocusEvent) {
    if (!this.addOnBlur) {
      return;
    }
    const target: HTMLElement = event.relatedTarget as HTMLElement;
    if (!target || target.tagName !== 'MAT-OPTION') {
      const matChipEvent: MatChipInputEvent = {
        input: this.userInput.nativeElement, value: this.userInput.nativeElement.value,
        chipInput: event as unknown as MatChipInput
      };
      this.addFreeTextChip(matChipEvent);
    }
  }

  addFreeTextChip(event: MatChipInputEvent) {
    if (!this.isChipLimitReached() && this._allowFreeText && !this.userInputControl.hasError('externalError')) {
      const value = event.value;

      if (value && value.trim()) {
        const freeTextinput = value.trim();

        const existingOption = this.availableOptions.find((option) => option.name === freeTextinput);
        const newChip = existingOption
          ? existingOption
          : {
              id: freeTextinput,
              name: freeTextinput,
            };

        this.addNewChip(newChip);
        this.resetUserInput();
        this.valueChange.emit(this._value);
        this._stateChanges.next();
      }
    }
  }

  /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
  onClick($event: MouseEvent) {
    this.autocompleteTrigger.openPanel();
  }

  /**
   * Method to update the position of autocomplete overlay on window scroll
   */
  scrollEvent = (): void => {
    if(this.autocompleteTrigger.panelOpen)
      // this.autoComplete.closePanel();
      this.autocompleteTrigger.updatePosition();
  };

  private addNewChip(newChip: ChipOption): void {
    this.showReversedChips ? this._value.unshift(newChip) : this._value.push(newChip);
  }

  private buildStaticOptionsProvider(): FilterableChipOptionProvider {
    return (userInput: string) => of(this.filterAvailablePredefinedOptions(userInput));
  }

  private validateUserInputIsValidOption(userInput: string): ValidationErrors | null {
    let errors: ValidationErrors = {};
    const userInputIsValidOption = this.availableOptions.map((option) => option.name).indexOf(userInput) !== -1;

    if (!!userInput && !this.allowFreeText && !userInputIsValidOption) {
      errors.requireMatch = true;
    } else {
      errors = null;
    }
    return errors;
  }

  private validateFreeTextWithExternalValidator(userInput: string): ValidationErrors | null {
    let errors: ValidationErrors = null;
    if (!!userInput && this.allowFreeText && this.externalFreeTextValidator) {
      errors = this.externalFreeTextValidator(userInput);
      if (errors) {
        errors.externalError = true;
      }
    }
    return errors;
  }

  private validatePendingTextInput(userInput: string): null {
    this.pendingFreeTextInput = !!userInput;
    return null;
  }

  private validateChipListNotEmpty(userInput: string): ValidationErrors | null {
    let errors: ValidationErrors = {};

    if (this.required && !userInput && !!this._value && this._value.length === 0) {
      errors.required = true;
    } else {
      errors = null;
    }
    return errors;
  }

  private resetUserInput() {
    if (this.userInput) {
      this.userInput.nativeElement.value = '';
    }
    this.userInputControl.setValue('');
  }

  private filterAvailableOptions(allOptions: ChipOption[]): ChipOption[] {
    let nonSelectedOptions: ChipOption[];
    if (!allOptions) {
      nonSelectedOptions = [];
    } else if (!this._value) {
      nonSelectedOptions = allOptions;
    } else {
      nonSelectedOptions = allOptions.filter(
        (chipOption) => this._value.map((selOption) => selOption.id).indexOf(chipOption.id) === -1
      );
    }
    return nonSelectedOptions;
  }

  private filterAvailablePredefinedOptions(filter: string): ChipOption[] {
    if (filter) {
      return this._options.filter(this.byName(filter));
    } else {
      return this._options.slice();
    }
  }

  private byName(filter: string): (chipOption: ChipOption) => boolean {
    return (availableOption) => availableOption.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
  }
}
