import {
  AsyncPipe,
  NgClass,
  NgFor,
} from '@angular/common';
import {
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  Output,
  QueryList,
  SimpleChanges,
  ViewChild,
  ViewChildren,
} from '@angular/core';
import {
  ControlValueAccessor,
  FormsModule,
} from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';

import {
  hasValue,
  isNotEmpty,
} from '../empty.util';
import { ClickOutsideDirective } from '../utils/click-outside.directive';
import { DebounceDirective } from '../utils/debounce.directive';

@Component({
  selector: 'ds-input-suggestions',
  templateUrl: './input-suggestions.component.html',
  standalone: true,
  imports: [FormsModule, ClickOutsideDirective, DebounceDirective, NgClass, NgFor, AsyncPipe, TranslateModule],
})

/**
 * Component representing a form with a autocomplete functionality
 */
export class InputSuggestionsComponent implements ControlValueAccessor, OnChanges {
  /**
   * The suggestions that should be shown
   */
  @Input() suggestions: any[] = [];

  /**
   * The time waited to detect if any other input will follow before requesting the suggestions
   */
  @Input() debounceTime = 500;

  /**
   * Placeholder attribute for the input field
   */
  @Input() placeholder = '';

  /**
   * Action attribute for the form
   */
  @Input() action;

  /**
   * Name attribute for the input field
   */
  @Input() name;

  /**
   * Whether or not the current input is valid
   */
  @Input() valid = true;

  /**
   * Label for the input field. Used for screen readers.
   */
  @Input() label? = '';

  /**
   * Output for when the form is submitted
   */
  @Output() submitSuggestion = new EventEmitter();

  /**
   * Output for when a suggestion is clicked
   */
  @Output() clickSuggestion = new EventEmitter();

  /**
   * Output for when something is typed in the input field
   */
  @Output() typeSuggestion = new EventEmitter();

  /**
   * Output for when new suggestions should be requested
   */
  @Output() findSuggestions = new EventEmitter();

  /**
   * Emits true when the list of suggestions should be shown
   */
  show = new BehaviorSubject<boolean>(false);

  /**
   * Index of the currently selected suggestion
   */
  selectedIndex = -1;

  /**
   * True when the dropdown should not reopen
   */
  blockReopen = false;

  /**
   * Reference to the input field component
   */
  @ViewChild('inputField') queryInput: ElementRef;
  /**
   * Reference to the suggestion components
   */
  @ViewChildren('suggestion') resultViews: QueryList<ElementRef>;

  /**
   * Value of the input field
   */
  _value: string;

  /** Fields needed to add ngModel */
  @Input() disabled = false;
  propagateChange = (_: any) => {
    /* Empty implementation */
  };
  propagateTouch = (_: any) => {
    /* Empty implementation */
  };

  /**
   * When any of the inputs change, check if we should still show the suggestions
   */
  ngOnChanges(changes: SimpleChanges) {
    if (hasValue(changes.suggestions)) {
      this.show.next(isNotEmpty(changes.suggestions.currentValue) && !changes.suggestions.firstChange);
    }
  }

  /**
   * Move the focus on one of the suggestions up to the previous suggestion
   * When no suggestion is currently in focus OR the first suggestion is in focus: shift to the last suggestion
   */
  shiftFocusUp(event: KeyboardEvent) {
    event.preventDefault();
    if (this.selectedIndex > 0) {
      this.selectedIndex--;
      this.selectedIndex = (this.selectedIndex + this.resultViews.length) % this.resultViews.length; // Prevent negative modulo outcome
    } else {
      this.selectedIndex = this.resultViews.length - 1;
    }
    this.changeFocus();
  }

  /**
   * Move the focus on one of the suggestions up to the next suggestion
   * When no suggestion is currently in focus OR the last suggestion is in focus: shift to the first suggestion
   */
  shiftFocusDown(event: KeyboardEvent) {
    event.preventDefault();
    if (this.selectedIndex >= 0) {
      this.selectedIndex++;
      this.selectedIndex %= this.resultViews.length;
    } else {
      this.selectedIndex = 0;
    }
    this.changeFocus();
  }

  /**
   * Perform the change of focus to the current selectedIndex
   */
  changeFocus() {
    if (this.resultViews.length > 0) {
      this.resultViews.toArray()[this.selectedIndex].nativeElement.focus();
    }
  }

  /**
   * When any key is pressed (except for the Enter button) the query input should move to the input field
   * @param {KeyboardEvent} event The keyboard event
   */
  onKeydown(event: KeyboardEvent) {
    if (event.key !== 'Enter') {
      this.queryInput.nativeElement.focus();
    }
  }

  /**
   * Changes the show variable so the suggestion dropdown closes
   */
  close() {
    this.show.next(false);
  }

  /**
   * Changes the show variable so the suggestion dropdown opens
   */
  open() {
    if (!this.blockReopen) {
      this.show.next(true);
    }
  }

  /**
   * For usage of the isNotEmpty function in the template
   */
  isNotEmpty(data) {
    return isNotEmpty(data);
  }

  onSubmit(data: any) {
    // sub class should decide how to handle the date
  }

  /**
   * Make sure that if a suggestion is clicked, the suggestions dropdown closes, does not reopen and the focus moves to the input field
   */
  onClickSuggestion(data: any) {
    // sub class should decide how to handle the date
  }

  /**
   * Finds new suggestions when necessary
   * @param data The query value to emit
   */
  find(data) {
    if (!this.blockReopen) {
      this.findSuggestions.emit(data);
      this.typeSuggestion.emit(data);
    }
    this.blockReopen = false;
  }

  /* START - Method's needed to add ngModel (ControlValueAccessor) to a component */
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.propagateTouch = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  writeValue(value: any): void {
    this.value = value;
  }

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
    this.propagateChange(this._value);
  }
  /* END - Method's needed to add ngModel to a component */
}