import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, DoCheck, ElementRef, inject, Injector, input, OnDestroy, OnInit, output, signal } from '@angular/core';
import { FAST_KENDO_COMMON } from '../../app.config';
import { ControlValueAccessor, FormControl, FormControlDirective, FormControlName, FormGroupDirective, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { DropDownFilterSettings } from '@progress/kendo-angular-dropdowns';
import { isMobileDevice } from '../utils/util';

@Component({
  selector: 'fast-combobox',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FAST_KENDO_COMMON],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: FastComboboxComponent
  },
  {
    provide: NgControl,
    multi: true,
    useExisting: FastComboboxComponent
  }],
  template: `
  <kendo-combobox
    [data]="data()"
    [textField]="textField()"
    [valueField]="valueField()"
    [valuePrimitive]="valuePrimitive()"
    [kendoDropDownFilter]="filterSettings"
    [value]="internalValue()"
    [placeholder]="placeholder()"
    [clearButton]="clearButton()"
    [virtual]="{ itemHeight: isMobileDevice() ? 48 : 28 }"
    (valueChange)="onValueChange($event)"
    (filterChange)="onFilterChange($event)"
    (selectionChange)="onSelectionChange($event)"
    [class]="conditionalClasses()"
    [disabled]="isDisabled()"
    [readonly]="readOnly()"
    [adaptiveMode]="'auto'"
    [popupSettings]="{ appendTo: 'component', width: dropdownWidth() ? dropdownWidth() + 'px' : 'auto' }"
    [listHeight]="listHeight()"
    [allowCustom]="allowCustom()"
    (blur)="onBlur($event)"
  />
  `
})
export class FastComboboxComponent<T extends object | string> implements ControlValueAccessor, OnInit, DoCheck, AfterViewInit, OnDestroy {
  cdr = inject(ChangeDetectorRef);
  injector = inject(Injector);
  elementRef = inject(ElementRef);
  isMobileDevice = isMobileDevice;

  data = input.required<T[]>();
  textField = input<string>(null);
  valueField = input<string>(null);
  valuePrimitive = input<boolean>(true);
  clearButton = input<boolean>(true);
  placeholder = input<string>('');
  disabled = input<boolean>(false);
  valueChange = output<T | null>();
  filterChange = output<string>();
  selectionChange = output<T | null>();
  value = input<T | null>(null);
  showDropdownOnHover = input<boolean>(false);
  dropdownWidth = input<number>(null);
  allowCustom = input<boolean>(false);
  blurOut = output<FocusEvent>();
  listHeight = input<number>(200);
  //internalValue so that we can change the kendo [value] but keep `value = input<T | null>(null);` without making it a model
  //making it a model would cause an additional valueChange in the background that overrides our `valueChange = output<T | null>();`
  internalValue = signal<T | null>(null);
  clearOnValueChange = input<boolean>(false);
  readOnly = input<boolean>(false);
  invalidTrigger = signal(0);
  formControl: FormControl;
  private inputElement: HTMLInputElement | null = null;

  // Internal signal to track form control disabled state
  private formControlDisabled = signal<boolean>(false);

  // Computed signal that combines input disabled state and form control disabled state
  isDisabled = computed(() => this.disabled() || this.formControlDisabled());

  onChange: (value: T | null) => void = () => { };
  onTouched = () => { };

  onBlur(event: FocusEvent) { 
    this.blurOut.emit(event); 
  }

  ngAfterViewInit() {
    this.inputElement = this.elementRef.nativeElement.querySelector('input');
    if (this.inputElement)
      this.inputElement.addEventListener('keydown', this.handleEscapeKey, { capture: true });
  }

  ngDoCheck() {
    //needed so that isInvalid is recomputed when the form control is touched with changing the value
    // and when `markAllAsTouched` is called on the parent form
    if (this.formControl?.touched)
      this.invalidTrigger.update((v) => v + 1);
  }

  ngOnDestroy() {
    if (this.inputElement)
      this.inputElement.removeEventListener('keydown', this.handleEscapeKey, { capture: true });
  }

  private handleEscapeKey = (event: KeyboardEvent) => {
    if (event.key !== 'Escape') return;

    // Prevent Kendo from clearing the combobox
    event.preventDefault();
    event.stopImmediatePropagation();

    // Find and close the parent dialog using native closest()
    const dialog = this.elementRef.nativeElement.closest('dialog') as HTMLDialogElement;
    if (dialog?.open) {
      dialog.close();
    }
  };

  ngOnInit() {
    const ngControl = this.injector.get(NgControl);

    if (ngControl instanceof FormControlName) {
      this.formControl = this.injector
        .get(FormGroupDirective)
        .getControl(ngControl)
    } else {
      this.formControl = (ngControl as FormControlDirective).form as FormControl;
    }
  }

  isInvalid = computed(() => {
    this.invalidTrigger();
    const control = this.formControl;
    if (control && !this.isDisabled())
      return control.invalid && (control.touched || control.dirty)
    else
      return false;
  });

  writeValue(value: T | null) {
    this.internalValue.set(value);
    this.cdr.markForCheck();
  }

  registerOnChange(fn: () => void) {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void) {
    this.onTouched = fn;
  }

  onValueChange(newValue: T | null) {
    this.internalValue.set(newValue);
    this.onChange(newValue);
    this.invalidTrigger.update((v) => v + 1);
    this.valueChange.emit(newValue);

    //If clearOnValueChange() is true then we need to set the value twice
    //Once with the new value so that the caller's valueChange event (if the event exists) is fired with the selected value
    //Then again with null so that the combobox is cleared
    if (this.clearOnValueChange()) {
      setTimeout(() => {
        this.internalValue.set(null);
        this.onChange(null);
        this.invalidTrigger.update((v) => v + 1);
      });
    }
  }

  onFilterChange(event: string): void {
    this.filterChange.emit(event);
  }

  onSelectionChange(event: T | null): void {
    this.selectionChange.emit(event);
  }

  setDisabledState(isDisabled: boolean) {
    this.formControlDisabled.set(isDisabled);
    this.cdr.markForCheck();
  }

  conditionalClasses = computed(() => {
    const classes = [] as string[];

    classes.push(...this.getCommonClasses());
    classes.push(...this.getLightBaseClasses());
    classes.push(...this.getDarkBaseClasses());
    if (!this.isDisabled()) {
      classes.push(...this.getLightHoverClasses());
      classes.push(...this.getDarkHoverClasses());
      classes.push(...this.getLightActiveClasses());
      classes.push(...this.getDarkActiveClasses());
    }

    const conditionalClasses = this.getConditionalClassesFromArrays(classes);
    return conditionalClasses;
  });


  getConditionalClassesFromArrays(classArray: string[]): { [key: string]: boolean } {
    const classes: { [key: string]: boolean } = {};
    classArray.forEach(className => {
      classes[className] = true;
    });
    return classes;
  }

  getCommonClasses() {
    const commonClasses = [];

    commonClasses.push(
      "h-7",
      "items-center",
      "[&_button]:border-none",
      "[&_button]:w-6.5",
      "[&_button]:items-center",
      "[&_.k-input-inner]:z-1",
      "[&_.k-input-inner]:pr-0",
      "[&_.k-input-inner]:w-full",
      "[&_.k-clear-value]:opacity-0",
      "[&_.k-clear-value]:right-6",
      "[&_.k-clear-value]:absolute",
      "[&_.k-clear-value]:z-2",
      "hover:[&_.k-clear-value]:opacity-100",
      "active:[&_.k-clear-value]:text-alt-red-500",
      "focus-within:ring-4",); 

    if (this.showDropdownOnHover()) {
      commonClasses.push(
        "flex",
        "[&_.k-input-inner]:overflow-none",
        "[&_.k-input-inner]:text-clip",
        "[&_.k-input-inner]:relative",
        "[&_.k-input-button]:hidden",
        "hover:[&_.k-input-button]:flex",
        "hover:[&_.k-input-button]:right-0",
        "hover:[&_.k-input-button]:z-3",
      );
    }
    else {
      commonClasses.push(
        "[&_.k-input-inner]:truncate",
      );
    }

    return commonClasses;
  }

  getLightBaseClasses() {
    const classes = [
      "text-base-black-1000",
      "[&_button]:text-base-black-1000",
      "[&_button]:bg-base-white-250",
      "[&_.k-clear-value]:bg-base-white-250",];

    if (this.isInvalid()) {
      classes.push(
        "border-red-500",
        "ring-red-500/50",
        "ring-2"
      );
    } else {
      classes.push(
        "border-base-gray-500",
        "ring-base-blue-250/50"
      );
    }

    return classes;
  }

  getDarkBaseClasses() {
    const classes = [
      "dark:text-base-white-500",
      "dark:[&_button]:text-base-white-500",
      "dark:[&_button]:bg-alt-gray-500",
      "dark:[&_.k-clear-value]:bg-alt-gray-1000"];

    if (this.isInvalid()) {
      classes.push(
        "dark:border-red-500",
        "dark:ring-red-500/50"
      );
    } else {
      classes.push(
        "dark:border-alt-blue-250",
        "dark:ring-base-blurple-250/50"
      );
    }

    return classes;
  }

  getLightHoverClasses() {
    return [
      "[&_button]:hover:bg-base-white-750"];
  }

  getDarkHoverClasses() {
    return [
      "dark:[&_button]:hover:bg-alt-blue-750"];
  }

  getLightActiveClasses() {
    return [
      "[&_button]:active:bg-base-white-1000"];
  }

  getDarkActiveClasses() {
    return [
      "dark:[&_button]:active:bg-alt-blue-250"];
  }

  public filterSettings: DropDownFilterSettings = {
    caseSensitive: false,
    operator: "contains",
  };
}
