import {CommonModule, DOCUMENT} from '@angular/common';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ContentChild,
  ContentChildren,
  ElementRef,
  forwardRef,
  Inject,
  Input,
  OnDestroy,
  QueryList,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {finalize, race, Subject, takeUntil} from 'rxjs';
import {selectAnimations} from '../../shared/animations';
import {CustomLabelDirective} from '../custom-label/custom-label.directive';
import {FormFieldDirective} from '../form-field/form-field.directive';
import {FormItemComponent} from '../form-item/form-item.component';
import {ListGroupComponent} from '../list-group/list-group.component';
import {ListItemComponent} from '../list-item/list-item.component';
import {SelectCustomLabelContext} from './select-custom-label-context.model';

@Component({
  selector: 'nj-select',
  templateUrl: './select.component.html',
  styleUrls: ['./select.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SelectComponent),
      multi: true,
    },
  ],
  animations: [selectAnimations.transformList],
  encapsulation: ViewEncapsulation.None,
  standalone: true,
  imports: [ListGroupComponent, FormItemComponent, FormFieldDirective, CommonModule]
})
export class SelectComponent
  extends FormItemComponent
  implements AfterViewInit, ControlValueAccessor, OnDestroy {
  private static readonly ESCAPE_CODE = 'Escape';
  private static readonly ENTER_CODE = 'Enter';
  private static readonly UP_CODE = 'ArrowUp';
  private static readonly DOWN_CODE = 'ArrowDown';
  /*
    Regex matching every alpha-numeric characters.

    \d : every digits
    \p{Letter} : every letters in the latin alphabet including letters with diacritics

    The "u" flag enables unicode mode required to use `\p{Letter}`.

    See :
    - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes#general_categories
    - https://unicode.org/reports/tr18/#General_Category_Property
  */
  private static readonly ALPHA_NUMERIC_REGEX = /^[\d\p{Letter}]$/u;

  /**
   * @ignore
   */
  private _onChange = (_: any): void => {
  };

  /**
   * @ignore
   */
  private _onTouched = (): void => {
  };

  /**
   * Notifier used to stop items click event subscription.
   * @ignore
   */
  private unsubscribe = new Subject<void>();

  private childOptionsChange = new Subject<void>();

  /**
   * @ignore
   */
  isOpen = false;

  /**
   * @ignore
   */
  selectedValue = '';

  /**
   * @ignore
   */
  selectedIndex = -1;

  @Input() iconName = 'keyboard_arrow_down';

  /**
   * Label used for accessibility related attributes on button and list.
   * Should be the same value (text only) as the `<label>` element
   */
  @Input() fieldLabel: string;

  /**
   * Instructions on how to navigate the list. It is append after the input label.
   * @example "Use up and down arrows and Enter to select a value"
   */
  @Input() listNavigationLabel: string;

  /**
   * Button default label when no value is selected. It is append after the input label.
   * @example "Select a value"
   */
  @Input() buttonDefaultValueLabel: string;

  /**
   * Trigger button to toggle the list
   * @ignore
   */
  @ViewChild('button') buttonEl: ElementRef<HTMLButtonElement>;

  @ViewChild('customLabelEl') protected customLabelEl: ElementRef<HTMLElement>;

  /**
   * List containing options
   * @ignore
   */
  @ViewChild(ListGroupComponent) listEl: ListGroupComponent;

  /**
   * Label to display instead of raw text value
   * @ignore
   * @example
   * <ng-template njCustomLabel let-value let-index="index">
   *  Value: {{value}} - Index: {{index}}
   * </ng-template>
   *
   * @example
   * <span *njCustomLabel="let value;let index=index">
   *  Value: {{value}} - Index: {{index}}
   * </span>
   */
  @ContentChild(CustomLabelDirective) protected customLabel?: CustomLabelDirective<SelectCustomLabelContext>;

  /**
   * Option items
   * @ignore
   */
  @ContentChildren(ListItemComponent, {descendants: true})
  selectOptions: QueryList<ListItemComponent>;

  constructor(
    private readonly element: ElementRef<HTMLElement>,
    private readonly cdr: ChangeDetectorRef,
    @Inject(DOCUMENT) private document
  ) {
    super();
  }

  ngAfterViewInit() {
    setTimeout(() => {
      this.setInputsAndListenersOnOptions();

      this.selectOptions?.changes
        .pipe(takeUntil(this.unsubscribe))
        .subscribe(() => {
          setTimeout(() => {
            this.setInputsAndListenersOnOptions();
          });
        });
    });
  }

  ngOnDestroy() {
    this.unsubscribe.next();
    this.unsubscribe.complete();
    this.childOptionsChange.complete();
  }

  setInputsAndListenersOnOptions() {
    this.childOptionsChange.next();
    const unsubscribeCond$ = race(this.unsubscribe, this.childOptionsChange);

    this.selectOptions?.forEach((item) => {
      item.role = 'option';

      if (this.selectedValue?.trim() !== '') {
        item.updateSelected(this.selectedValue === item.getValue());
      }


      item.itemClick
        .pipe(
          takeUntil(unsubscribeCond$),
        )
        .subscribe(() => {
          const value = item.getValue();
          this.writeValue(value);
          this._onChange(value);
          this.closeList();

          setTimeout(() => {
            this.buttonEl?.nativeElement.focus();
          });
        });
    });

    // Get selected index on mount based on current value
    this.selectedIndex = this.selectOptions?.toArray().findIndex(opt => {
      return opt.getValue() === this.selectedValue;
    });

    this.cdr.markForCheck();
  }

  /**
   * @ignore
   */
  getAdditionalClass(): string {
    const classes = ['nj-form-item--select', 'nj-form-item--custom-list'];
    if (this.isOpen) {
      classes.push('nj-form-item--open');
    }

    if (this.customLabel?.templateRef) {
      classes.push('nj-form-item--custom-label');
    }
    return classes.join(' ');
  }

  getSubscriptId(): string {
    return `${this.inputId}-subscript`;
  }

  getInstructionsId(): string {
    return `${this.inputId}-instructions`;
  }

  getDescriptionId(): string {
    return `${this.getSubscriptId()} ${this.getInstructionsId()}`;
  }

  /**
   * Get index of the selected value
   */
  private indexForValue(value: string): number {
    return this.selectOptions
      ?.toArray()
      .findIndex((item) => item.getValue() === value);
  }

  private openList() {
    this.isOpen = true;
    this.focusedIndex = this.selectedIndex;

    setTimeout(() => {
      if (this.selectedIndex === -1) {
        // Focus the `ul` element
        this.listEl?.rootEl.nativeElement.focus();
        // The scrolling element is not the `ul` node but the `nj-list-group`
        this.listEl?.element.nativeElement.scrollTo({top: 0});
      }
    });
  }

  private closeList() {
    this.isOpen = false;
  }

  toggleIsOpen() {
    if (this.isOpen) {
      this.closeList();
    } else {
      this.openList();
    }
  }

  /**
   * Index of the currently focused option.
   */
  private get focusedIndex(): number {
    return this.selectOptions
      ?.toArray()
      .findIndex(
        (item) => this.document.activeElement === item.el.nativeElement
      );
  }

  private set focusedIndex(value: number) {
    this.selectOptions?.forEach((el, i) => {
      el.ariaSelected = i === value;
    });

    setTimeout(() => {
      if (value !== -1) {
        this.selectOptions?.get(value).el.nativeElement.focus();
      }
    });
  }

  handleListKeydown(e: KeyboardEvent) {
    // Escape key closes the list and focuses the button
    if (e.code === SelectComponent.ESCAPE_CODE) {
      this.closeList();
      setTimeout(() => {
        this.buttonEl?.nativeElement.focus();
      });
    }

    // Navigate between options and set `focusedIndex`
    if (e.code === SelectComponent.UP_CODE) {
      e.preventDefault();
      // Dont loop back to the end of the list
      if (this.focusedIndex > 0) {
        this.focusedIndex -= 1;
      }
    }

    if (e.code === SelectComponent.DOWN_CODE) {
      e.preventDefault();
      // Dont loop back to the beginning of the list
      if (this.focusedIndex < this.selectOptions?.length - 1) {
        this.focusedIndex += 1;
      }
    }

    // Select the current `focusedIndex` option
    if (e.code === SelectComponent.ENTER_CODE) {
      e.preventDefault();
      if (this.focusedIndex !== -1) {
        const value = this.selectOptions?.get(this.focusedIndex).getValue();
        this.writeValue(value);
        this._onChange(value);
      }
      this.closeList();

      setTimeout(() => {
        this.buttonEl?.nativeElement.focus();
      });
    }

    // Jump to first option matching first letter
    if (SelectComponent.ALPHA_NUMERIC_REGEX.test(e.key)) {
      const goToIndex = this.selectOptions
        ?.toArray()
        .findIndex(
          (item) => item.getValue()[0].toLowerCase() === e.key.toLowerCase()
        );

      if (goToIndex !== -1) {
        this.focusedIndex = goToIndex;
      }
    }
  }

  handleFocusout(e: FocusEvent) {
    if (!this.element.nativeElement?.contains(e.relatedTarget as Node)) {
      this.closeList();

      if (this._onTouched) {
        this._onTouched();
      }
    }
  }

  /**
   * Implemented as part of ControlValueAccessor.
   * @ignore
   */
  registerOnChange(fn: any): void {
    this._onChange = fn;
  }

  /**
   * Implemented as part of ControlValueAccessor.
   * @ignore
   */
  registerOnTouched(fn: any): void {
    this._onTouched = fn;
  }

  /**
   * Implemented as part of ControlValueAccessor.
   * @ignore
   */
  setDisabledState(isDisabled: boolean): void {
    if (!this.selectedValue) {
      return;
    }
    this.isDisabled = isDisabled;
  }

  /**
   * Implemented as part of ControlValueAccessor.
   * @ignore
   */
  writeValue(value: string): void {
    this.selectedValue = value;
    this.selectedIndex = this.indexForValue(value);
    this.selectOptions?.forEach((item) => {
      item.updateSelected(item.getValue() === value);
    });
    this.cdr.markForCheck();
  }

  protected get customLabelContext(): SelectCustomLabelContext {
    const value = this.selectedValue;
    const index = this.selectedIndex;
    return {$implicit: value, value, index};
  }

  /**
   * Label (≠ value) of selected option
   * @ignore
   */
  get selectedLabel(): string {
    return this.selectOptions?.get(this.selectedIndex)?.getLabel() ?? '';
  }

  /**
   * Aria-label for the trigger button element.
   * @ignore
   */
  get buttonLabel(): string {
    return `${this.fieldLabel} - ${
      this.customLabelEl?.nativeElement.innerText || this.selectedValue || this.buttonDefaultValueLabel
    }`;
  }
}
