 /**
 * Copyright Aquera Inc 2025
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

 import {
  LitElement,
  html,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styles } from './nile-virtual-select.css';

import '../nile-icon';
import '../nile-popup/nile-popup';
import '../nile-tag/nile-tag';
import '../nile-checkbox/nile-checkbox';
import '../nile-loader/nile-loader';
import { animateTo, stopAnimations } from '../internal/animate';
import { classMap } from 'lit/directives/class-map.js';
import { query, state } from 'lit/decorators.js';
import { defaultValue } from '../internal/default-value';
import { FormControlController } from '../internal/form';
import {
  getAnimation,
  setDefaultAnimation,
} from '../utilities/animation-registry';
import { HasSlotController } from '../internal/slot';
import { waitForEvent } from '../internal/event';
import { watch } from '../internal/watch';
import NileElement from '../internal/nile-element';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
import type { NileFormControl } from '../internal/nile-element';
import type NileOption from '../nile-option/nile-option';
import type NilePopup from '../nile-popup/nile-popup';
import { ifDefined } from 'lit/directives/if-defined.js';

import type { VirtualOption, NileRemoveEvent, RenderItemConfig } from './types.js';
import { VirtualSelectSelectionManager } from './selection-manager.js';
import { VirtualSelectSearchManager } from './search-manager.js';
import { VirtualSelectRenderer } from './renderer.js';
import { PortalManager } from './portal-manager.js';
import { ResizeController } from '@lit-labs/observers/resize-controller.js';
import { VisibilityManager } from '../utilities/visibility-manager.js';

/**
 * Nile Virtual Select component.
 *
 * @tag nile-virtual-select
 *
 */

/**
 * @summary Virtual select component for handling large datasets with virtual scrolling.
 * @status stable
 * @since 2.0
 *
 * @dependency nile-icon
 * @dependency nile-popup
 * @dependency nile-tag
 * @dependency nile-checkbox
 *
 * @slot label - The input's label. Alternatively, you can use the `label` attribute.
 * @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
 * @slot clear-icon - An icon to use in lieu of the default clear icon.
 * @slot expand-icon - The icon to show when the control is expanded and collapsed.
 * @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
 *
 * @event nile-change - Emitted when the control's value changes.
 * @event nile-clear - Emitted when the control's value is cleared.
 * @event nile-input - Emitted when the control receives input.
 * @event nile-focus - Emitted when the control gains focus.
 * @event nile-blur - Emitted when the control loses focus.
 * @event nile-show - Emitted when the select's menu opens.
 * @event nile-after-show - Emitted after the select's menu opens and all animations are complete.
 * @event nile-hide - Emitted when the select's menu closes.
 * @event nile-after-hide - Emitted after the select's menu closes and all animations are complete.
 * @event nile-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
 * @event nile-search - Emitted when the user types in the search input. The event payload includes the search query for backend search functionality.
 * @event nile-scroll - Emitted when the user scrolls within the virtualized container. The event payload includes scroll position information.
 * @event nile-scroll-start - Emitted when the user starts scrolling within the virtualized container.
 * @event nile-scroll-end - Emitted when the user stops scrolling and reaches the bottom of the virtualized container (debounced).
 *
 * @csspart form-control - The form control that wraps the label, input, and help text.
 * @csspart form-control-label - The label's wrapper.
 * @csspart form-control-input - The select's wrapper.
 * @csspart form-control-help-text - The help text's wrapper.
 * @csspart combobox - The container the wraps the prefix, combobox, clear icon, and expand button.
 * @csspart prefix - The container that wraps the prefix slot.
 * @csspart display-input - The element that displays the selected option's label, an `<input>` element.
 * @csspart listbox - The listbox container where options are slotted.
 * @csspart tags - The container that houses option tags when `multiple` is used.
 * @csspart tag - The individual tags that represent each multiselect option.
 * @csspart clear-button - The clear button.
 * @csspart expand-icon - The container that wraps the expand icon.
 * @csspart footer - The footer container with show selected and clear all options.
 */

@customElement('nile-virtual-select')
export class NileVirtualSelect extends NileElement implements NileFormControl {
  static styles: CSSResultGroup = styles;

  private readonly formControlController = new FormControlController(this, {
    assumeInteractionOn: ['nile-blur', 'nile-input'],
  });
  private readonly hasSlotController = new HasSlotController(
    this,
    'help-text',
    'label'
  );
  private readonly portalManager = new PortalManager(this);

  @query('.select') popup: NilePopup;
  @query('.select__combobox') combobox: HTMLElement;
  @query('.select__display-input') displayInput: HTMLInputElement;
  @query('.select__value-input') valueInput: HTMLInputElement;
  @query('.virtualized') virtualizedContainer: HTMLElement;

  @state() private hasFocus = false;
  @state() displayLabel = '';
  @state() selectedOptions: VirtualOption[] = [];
  @state() oldValue: string | string[] = '';
  
  private scrollTimeout: number | undefined;
  private scrolling = false;
  private visibilityManager?: VisibilityManager;

  /** The name of the select, submitted as a name/value pair with form data. */
  @property() name = '';

  /** Array of all option items for virtual scrolling */
  @property({ type: Array }) data: any = [];

  /** Original unfiltered option items for search functionality */
  @state() private originalOptionItems: any = [];

  /**
   * The current value of the select. When `multiple` is enabled, the value will be an array of selected values.
   */
  @property({
    converter: {
      fromAttribute: (value: string) => value.split(' '),
      toAttribute: (value: string[]) => value.join(' '),
    },
  })
  value: string | string[] = '';

  /** The default value of the form control. Primarily used for resetting the form control. */
  @defaultValue() defaultValue: string | string[] = '';

  /** The select's size. */
  @property() size: 'small' | 'medium' | 'large' = 'medium';

  /** Placeholder text to show as a hint when the select is empty. */
  @property() placeholder = 'Select...';

    /** Enable automatic resizing of tags area */
    @property({ type: Boolean }) autoResize = false;

  /** Current search value */
  @state() searchValue: string = '';

  /** Enable search functionality */
  @property({ type: Boolean, reflect: true }) searchEnabled = false;

  @property({ type: Boolean, reflect: true, attribute: true }) enableVisibilityEffect = false;

  /** Search input placeholder */
  @property({attribute:'internal-search-placeholder'}) internalSearchPlaceHolder = 'Search...';

  /** Disable local search filtering */
  @property({ type: Boolean, reflect: true }) disableLocalSearch = false;

  /** Show loading state */
  @property({ type: Boolean, reflect: true }) optionsLoading = false;

  /** Show loading state using nile-loader */
  @property({ type: Boolean, reflect: true, attribute: true }) loading = false;

  /** Allows more than one option to be selected. */
  @property({ type: Boolean, reflect: true }) multiple = false;

  /** Help text */
  @property({ attribute: 'help-text', reflect: true }) helpText = '';

  /** Error message */
  @property({ attribute: 'error-message', reflect: true }) errorMessage = '';

  /** Sets the input to a warning state */
  @property({ type: Boolean }) warning = false;

  /** Sets the input to an error state */
  @property({ type: Boolean }) error = false;

  /** Sets the input to a success state */
  @property({ type: Boolean }) success = false;

  /** Disables the select control. */
  @property({ type: Boolean, reflect: true }) disabled = false;

  /** Adds a clear button when the select is not empty. */
  @property({ type: Boolean, reflect: true }) clearable = false;

  /** The select's open state. */
  @property({ type: Boolean, reflect: true }) open = false;

  /**
   * Enable this option to prevent the listbox from being clipped when the component is placed inside a container with
   * `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
   */
  @property({ type: Boolean }) hoist = false;

  /** Draws a filled select. */
  @property({ type: Boolean, reflect: true }) filled = false;

  /** Draws a pill-style select with rounded edges. */
  @property({ type: Boolean, reflect: true }) pill = false;

  /** The select's label. If you need to display HTML, use the `label` slot instead. */
  @property() label = '';

  /**
   * The preferred placement of the select's menu. Note that the actual placement may vary as needed to keep the listbox
   * inside of the viewport.
   */
  @property({ reflect: true }) placement: 'top' | 'bottom' = 'bottom';

  /**
   * By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
   * to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
   * the same document or shadow root for this to work.
   */
  @property({ reflect: true }) form = '';

  /** The select's required attribute. */
  @property({ type: Boolean, reflect: true }) required = false;

  /** Show no results message */
  @property({ type: Boolean }) showNoResults: boolean = false;

  /** No results message */
  @property({ type: String }) noResultsMessage: string = 'No results found';

  /** Show selected options only */
  @property({ type: Boolean }) showSelected = false;

  /** Enhanced configuration for rendering items with support for display text, value, and search text */
  @property({ attribute:false}) renderItemConfig?: RenderItemConfig;

  /** Block value change events */
  @property({ type: Boolean, reflect: true }) blockValueChange = false;

  /** Disable width synchronization */
  @property({ type: Boolean, reflect: true }) noWidthSync = false;

  /** 
   * When true, the listbox will be appended to the document body instead of the parent container.
   * This is useful when the parent has overflow: hidden, clip-path, or transform applied.
   */
  @property({ type: Boolean, reflect: true }) portal = false;

  /**
   * The maximum number of selected options to show when `multiple` is true. After the maximum, "+n" will be shown to
   * indicate the number of additional items that are selected. Set to 0 to remove the limit.
   */
  @property({ attribute: 'max-options-visible', type: Number })
  maxOptionsVisible = 3;

  @state() oldMaxOptionsVisible: number = 1;

  @property({ type: Boolean, reflect: true, attribute: true }) enableTabClose = false;

  @property({ type: Boolean, reflect: true, attribute: true }) descriptionSearchEnabled = false;

  @property({
    type: Boolean,
    reflect: true,
    attribute: true,
    converter: {
      fromAttribute: (value) => value === 'true',
      toAttribute: (value) => String(value),
    },
  }) allowHtmlLabel = true;

  @property({ type: Boolean, reflect: true, attribute: true }) enableDescription = false;

  @state() showListbox: boolean = false;
  @state() private wasShowSelectedCheckedOnClose: boolean = false;

  /** Gets the validity state object */
  get validity() {
    return this.valueInput?.validity;
  }

  /** Gets the validation message */
  get validationMessage() {
    return this.valueInput?.validationMessage ?? '';
  }

  connectedCallback() {
    super.connectedCallback();
    this.initializeComponent();
    this.setupEventListeners();
    
    this.updateComplete.then(() => {
      if (this.value && this.data.length > 0) {
        this.selectionChanged();
      }
    });
  }

  disconnectedCallback() {
    this.removeOpenListeners();
    this.visibilityManager?.cleanup();
    // Clear any pending scroll timeout to prevent memory leaks
    if (this.scrollTimeout) {
      clearTimeout(this.scrollTimeout);
      this.scrollTimeout = undefined;
    }
    // Clean up body append elements
    this.portalManager.cleanupPortalAppend();
  }

  protected updated(changedProperties: PropertyValues): void {
      if(changedProperties.has('value')) {
         this.selectionChanged();
      }
      if (changedProperties.has('autoResize')) {
        const tagsDiv = this.shadowRoot?.querySelector('.select__tags') as HTMLElement;
        if (this.autoResize && tagsDiv) {
          this.resizeController.observe(tagsDiv);
        } else if (tagsDiv) {
          this.resizeController.unobserve(tagsDiv);
        }
      }
  }

  private initializeComponent(): void {
    this.open = false;
    this.emit('nile-init');
  }

  /**
   * Get display text for an item using renderItemConfig
   */
  private getDisplayText(item: any): string {
    if (this.renderItemConfig?.getDisplayText) {
      return this.renderItemConfig.getDisplayText(item);
    }
    // Fallback to basic display
    return item?.label || item?.name || item?.toString() || '';
  }

  /**
   * Get value for an item using renderItemConfig or fallback to item.value
   */
  private getItemValue(item: any): string {
    if (this.renderItemConfig?.getValue) {
      return this.renderItemConfig.getValue(item);
    }
    return item?.value || item;
  }

  /**
   * Get description for an item using renderItemConfig or fallback to item.description
   */
  private getItemDescription(item: any): string {
    if (this.renderItemConfig?.getDescription) {
      return this.renderItemConfig.getDescription(item);
    }
    return item?.description || item || '';
  }

  /**
   * Get description for an item using renderItemConfig or fallback to item.description
   */
  private getItemPrefix(item: any): string {
    if (this.renderItemConfig?.getPrefix) {
      return this.renderItemConfig.getPrefix(item);
    }
    return item?.prefix || item || '';
  }

  /**
   * Get suffix for an item using renderItemConfig or fallback to item.suffix
   */
  private getItemSuffix(item: any): string {
    if (this.renderItemConfig?.getSuffix) {
      return this.renderItemConfig.getSuffix(item);
    }
    return item?.suffix || item || '';
  }

  /**
   * Get search text for an item using renderItemConfig or fallback to display text
   */
  private getSearchText(item: any): string {
    if (this.renderItemConfig?.getSearchText) {
      return this.renderItemConfig.getSearchText(item);
    }
    return this.getDisplayText(item);
  }

  private setupEventListeners(): void {
    this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
    this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
    this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
    this.handleWindowError = this.handleWindowError.bind(this);
    this.handleWindowResize = this.handleWindowResize.bind(this);
    this.handleWindowScroll = this.handleWindowScroll.bind(this);
  }

  private addOpenListeners(): void {
    document.addEventListener('focusin', this.handleDocumentFocusIn);
    document.addEventListener('keydown', this.handleDocumentKeyDown);
    document.addEventListener('mousedown', this.handleDocumentMouseDown);
    window.addEventListener('error', this.handleWindowError);
    
    if (this.portal) {
      window.addEventListener('resize', this.handleWindowResize);
      window.addEventListener('scroll', this.handleWindowScroll, true);
    }
  }

  private removeOpenListeners(): void {
    document.removeEventListener('focusin', this.handleDocumentFocusIn);
    document.removeEventListener('keydown', this.handleDocumentKeyDown);
    document.removeEventListener('mousedown', this.handleDocumentMouseDown);
    window.removeEventListener('error', this.handleWindowError);
    window.removeEventListener('resize', this.handleWindowResize);
    window.removeEventListener('scroll', this.handleWindowScroll, true);
  }

  private handleFocus(): void {
    this.hasFocus = true;
    this.emit('nile-focus');
  }

  private handleBlur(): void {
    this.hasFocus = false;
    this.emit('nile-blur');
  }

  
   protected firstUpdated(_changed: PropertyValues) {
    this.visibilityManager = new VisibilityManager({
      host: this,
      target: this.combobox,               
      enableVisibilityEffect: this.enableVisibilityEffect,
      enableTabClose: this.enableTabClose,
      isOpen: () => this.open,
      onAnchorOutOfView: () => {
        this.hide();
        this.emit('nile-visibility-change', {
          visible: false,
          reason: 'anchor-out-of-view',
          name: this.name,
        });
      },
      onDocumentHidden: () => {
        this.hide();
        this.emit('nile-visibility-change', {
          visible: false,
          reason: 'document-hidden',
          name: this.name,
        });
      },
      emit: (event, detail) => this.emit(`nile-${event}`, detail),
    });
  }
 
  


  private handleDocumentFocusIn(event: FocusEvent): void {
    if (!this.open) return;                         
    const path = event.composedPath(); 
    const hitSelf  = path.includes(this);
    const hitPopup = this.popup && path.includes(this.popup);
    const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);

    if (!hitSelf && !hitPopup && !hitPortalAppend) {
      this.hide();
    }
  }

  private handleDocumentKeyDown(event: KeyboardEvent): void {
    if (this.shouldIgnoreKeyPress(event)) {
      return;
    }

    if (this.isEscapeKey(event)) {
      this.handleEscapeKey(event);
    }

    if (this.isEnterOrSpaceKey(event)) {
      this.handleEnterOrSpaceKey(event);
    }
  }

  private shouldIgnoreKeyPress(event: KeyboardEvent): boolean {
    const target = event.target as HTMLElement;
    const isClearButton = target.closest('.select__clear') !== null;
    const isIconButton = target.closest('nile-icon-button') !== null;
    return isClearButton || isIconButton;
  }

  private isEscapeKey(event: KeyboardEvent): boolean {
    return event.key === 'Escape' && this.open;
  }

  private handleEscapeKey(event: KeyboardEvent): void {
    event.preventDefault();
    event.stopPropagation();
    this.hide();
    this.displayInput.focus({ preventScroll: true });
  }

  private isEnterOrSpaceKey(event: KeyboardEvent): boolean {
    return event.key === 'Enter' || event.key === ' ';
  }

  private handleEnterOrSpaceKey(event: KeyboardEvent): void {
    event.preventDefault();
    event.stopImmediatePropagation();

    if (!this.open) {
      this.show();
      return;
    }

    if (!this.multiple) {
      this.hide();
      this.displayInput.focus({ preventScroll: true });
    }
  }

  private handleDocumentMouseDown = (event: MouseEvent): void => {
    if (!this.open) return;                        
  
    const path = event.composedPath();
    const hitSelf  = path.includes(this);
    const hitPopup = this.popup && path.includes(this.popup);
    const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
  
    if (!hitSelf && !hitPopup && !hitPortalAppend) {
      this.hide();
    }
  };

  /**
   * This is a workaround for an error in the Lit Labs virtualizer.
   * Since there are no specific guidelines available to fix the issue,
   * we are catching only the error message related to the virtualizer.
   */
  private handleWindowError = (event: ErrorEvent): void => {
    const errorMessage = event.error?.message || event.message || '';
    if (errorMessage.includes('Cannot read properties of null (reading \'insertBefore\')')) {
      event.preventDefault();
      return;
    }
  };

  private handleWindowResize = (): void => {
    this.portalManager.updatePortalAppendPosition();
  };

  private handleWindowScroll = (): void => {
    this.portalManager.updatePortalAppendPosition();
  };

  private handleFooterClick(event: MouseEvent): void {
    event.stopPropagation();
    event.preventDefault();
  }

  private toggleShowSelected(event: Event): void {
    event.stopPropagation();
    event.preventDefault();
    if (this.selectedOptions?.length === 0) {
      return;
    }
    this.showSelected = !this.showSelected;

    if (this.showSelected) {
      this.searchValue = "";
      const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
      this.data = this.originalOptionItems.filter((item: any) => {
        const itemValue = this.getItemValue(item);
        return selectedValues.some(val => String(val) === String(itemValue));
      });
    } else {
      this.data = [...this.originalOptionItems];
    }

    // Reset measured height when show selected toggles (content height may change)
    this.portalManager.resetMeasuredHeight();

    this.requestUpdate();
    this.repaintOptionsContainer();
  }

  private unSelectAll(): void {
    this.showSelected = false;
    this.value = this.multiple ? [] : '';
    this.data = [...this.originalOptionItems];
    this.selectionChanged();
    this.emit('nile-change', { value: this.value, name: this.name });
    this.emit('nile-clear', { value: this.multiple ? this.value : '', name: this.name });
    this.resetScrollPosition();
  }

  private handleLabelClick(): void {
    this.displayInput.focus();
    this.hide();
  }

  private handleComboboxMouseDown(event: MouseEvent): void {
    if (this.shouldIgnoreComboboxClick(event)) {
      return;
    }

    event.preventDefault();
    this.displayInput.focus({ preventScroll: true });
    this.open = !this.open;
  }

  private resizeController = new ResizeController(this, {
    callback: (entries: ResizeObserverEntry[]) => {
      for (const entry of entries) {
        if (entry.target.classList.contains('select__tags')) {
          this.calculateTotalWidthOfTags();
        }
      }
    }
  });

  private shouldIgnoreComboboxClick(event: MouseEvent): boolean {
    const path = event.composedPath();
    const isIconButton = path.some(
      el =>
        el instanceof Element && el.tagName.toLowerCase() === 'nile-icon-button'
    );
    return this.disabled || isIconButton;
  }

  private handleComboboxKeyDown(event: KeyboardEvent): void {
    if (this.isEnterOrSpaceKey(event)) {
      event.preventDefault();
      this.open = !this.open;
    }
  }

  private handleClearClick(event: MouseEvent): void {
    event.stopPropagation();
    this.clearSelection();
  }

  private clearSelection(): void {
    const oldValue = this.value;
    this.value = this.multiple ? [] : '';
    this.selectionChanged();

    this.updateComplete.then(() => {
      this.nileInput({ value: this.value, name: this.name });
      this.nileChange({ value: this.value, name: this.name });
      this.emit('nile-clear');
    });
  }

  private handleClearMouseDown(event: MouseEvent): void {
    event.stopPropagation();
  }

  private handleOptionClick(event: MouseEvent): void {
    const target = event.target as HTMLElement;
    const option = target.closest('nile-option');

    if (this.shouldBlockValueChange(option)) {
      return;
    }

    const oldValue = this.value;
    this.oldValue = oldValue;

    if (option && !option.disabled) {
      this.handleOptionSelection(option);
    }
  }

  private shouldBlockValueChange(option: Element | null): boolean {
    if (this.blockValueChange && option) {
      this.emit('nile-block-change', { value: (option as any)?.value, name: this.name });
      this.hide();
      return true;
    }
    return false;
  }

  private handleOptionSelection(option: Element): void {
    const optionValue = (option as any).value;
    
    if (this.multiple) {
      this.toggleOptionSelection(optionValue);
    } else {
      this.setSelectedOptions(optionValue);
    }

    this.updateComplete.then(() =>
      this.displayInput.focus({ preventScroll: true })
    );

    if (this.value !== this.oldValue) {
      this.updateComplete.then(() => {
        this.nileInput({ value: this.value, name: this.name });
        this.nileChange({ value: this.value, name: this.name });
      });
    }

    if (!this.multiple) {
      this.hide();
      this.displayInput.focus({ preventScroll: true });
    }
  }

  private setSelectedOptions(optionValue: string): void {
    this.value = optionValue;
    this.selectionChanged();
  }

  private toggleOptionSelection(optionValue: string): void {
    const currentValues = Array.isArray(this.value) ? this.value : [];
    
    if (currentValues.includes(optionValue)) {
      this.value = currentValues.filter(v => v !== optionValue);
    } else {
      this.value = [...currentValues, optionValue];
    }
    
    this.selectionChanged();
  }

  private handleTagRemove(event: NileRemoveEvent, option: VirtualOption): void {
    event.stopPropagation();

    if (!this.disabled) {
      this.removeTagFromSelection(option);
      this.emitTagRemovalEvent(option);
    }
  }

  private removeTagFromSelection(option: VirtualOption): void {
    let currentValue = this.value;
    if (!Array.isArray(currentValue)) {
      currentValue = currentValue ? [currentValue] : [];
    }
    const newValue = currentValue.filter((v: string) => v !== option.value);
    this.value = newValue;
    this.selectionChanged();
  }

  private emitTagRemovalEvent(option: VirtualOption): void {
    this.updateComplete.then(() => {
      this.nileInput({ value: this.value, name: this.name });
      this.nileChange({ value: this.value, name: this.name });
      this.emit('nile-tag-remove', { value: this.value, name: this.name, removedtagvalue: option.value });
    });
  }

  private selectionChanged(): void {
    const itemsToSearch = this.originalOptionItems.length > 0 ? this.originalOptionItems : this.data;
    this.selectedOptions = VirtualSelectSelectionManager.createVirtualOptionsFromValues(
      this.value, 
      itemsToSearch, 
      this.getDisplayText.bind(this),
      this.renderItemConfig?.getValue,
      this.allowHtmlLabel
    );

    if (this.multiple) {
      if (this.placeholder && this.value.length === 0) {
        this.displayLabel = '';
      } else {
        this.displayLabel = this.selectedOptions.length + ' selected';
      }
    } else {
      const currentValue = Array.isArray(this.value) ? this.value[0] : this.value;
      const label = this.selectedOptions[0]?.getTextLabel();
      this.displayLabel = label ? label : currentValue ?? '';
    }

    this.updateValidity();

    if (this.selectedOptions.length === 0) {
      this.showSelected = false;
      if(this.originalOptionItems?.length > 0 && !this.searchValue) {
          this.data = [...this.originalOptionItems];
      }
      this.repaintOptionsContainer();
    }

    this.calculateTotalWidthOfTags();
  }
 

  handleSearchFocus(): void {
    document.removeEventListener('keydown', this.handleDocumentKeyDown);
  }

  handleSearchBlur(): void {
    document.addEventListener('keydown', this.handleDocumentKeyDown);
  }

  handleSearchChange(e: any): void {
    this.searchValue = e.detail.value;
    if (this.portal) {
      this.portalManager.updatePortalAppendPosition();
    }
    
    this.emit('nile-search', { 
      query: this.searchValue,
      name: this.name 
    });
    
    if (!this.disableLocalSearch) {
      this.filterVirtualOptions(this.searchValue);
      this.repaintOptionsContainer();
      
    // Reset measured height when search changes (content height may change)
    this.portalManager.resetMeasuredHeight();
    }
  }
  
  repaintOptionsContainer() {
      this.resetScrollPosition();
      this.updateComplete.then(() => {
        const virtualized = this.shadowRoot?.querySelector('.virtualized') as HTMLElement;
        if (virtualized) {
          if (this.data.length < 5) {
            virtualized.classList.add('no-scroll');
          } else {
            virtualized.classList.remove('no-scroll');
          }
        }
      });
  }

  handleScroll(e: Event): void {
    if(this.showSelected) {
      return;
    }

    const target = e.target as HTMLElement;
    
    this.emit('nile-scroll', {
      scrollTop: target.scrollTop,
      scrollLeft: target.scrollLeft,
      name: this.name
    });

    if (!this.scrolling) {
      this.scrolling = true;
      this.emit('nile-scroll-start', {
        scrollTop: target.scrollTop,
        scrollLeft: target.scrollLeft,
        name: this.name
      });
    }

    clearTimeout(this.scrollTimeout);
    this.scrollTimeout = window.setTimeout(() => {
      if (this.scrolling) {
        this.scrolling = false;
      }
    }, 300);

    const isAtBottom = Math.ceil(target.scrollTop) >= Math.floor(target.scrollHeight - target.offsetHeight);
    if (isAtBottom && !this.searchValue) {
        this.emit('nile-scroll-end', {
          scrollTop: target.scrollTop,
          scrollLeft: target.scrollLeft,
          name: this.name,
          isAtBottom: true
        });
    }
  }

  filterVirtualOptions(searchValue: string): void {
    const result = VirtualSelectSearchManager.filterVirtualOptions(
      searchValue,
      this.originalOptionItems,
      this.data,
      this.getDisplayText.bind(this),
      this.getItemDescription.bind(this),
      this.descriptionSearchEnabled,
      this.renderItemConfig?.getSearchText
    );
    if (this.portal) {
      this.portalManager.updatePortalAppendPosition();
      this.updateComplete.then(() => {
        requestAnimationFrame(() => {
            this.resetScrollPosition();
          });
      });
    } else {
      this.resetScrollPosition();
    }
    this.data = result.filteredItems;
    this.showNoResults = result.showNoResults;
    this.showSelected = false;
    this.requestUpdate();
  }

  private handleInvalid(event: Event): void {
    this.formControlController.setValidity(false);
    this.formControlController.emitInvalidEvent(event);
  }

  @watch('disabled', { waitUntilFirstUpdate: true })
  handleDisabledChange(): void {
    if (this.disabled) {
      this.open = false;
      this.handleOpenChange();
    }
  }

  
  @watch('value', { waitUntilFirstUpdate: true })
  handleValueChange(): void {
    this.selectionChanged();
    this.requestUpdate();
    
    // Update body append content if it's active
    if (this.portal && this.portalManager.portalContainerElement) {
      this.portalManager.updatePortalAppendPosition();
    }
  }

  @watch('data', { waitUntilFirstUpdate: true })
  handleDataChange(): void {
    if (this.data.length > 0 && this.open && !this.showSelected && !this.searchValue) {
      this.originalOptionItems = [...this.data];
    }
    this.selectionChanged();
    // Show no results message when data is empty and not loading
    if (!this.optionsLoading && !this.loading && this.data.length === 0) {
      this.showNoResults = true;
    } else if (this.data.length > 0) {
      this.showNoResults = false;
    }
    this.requestUpdate();
    
    // Reset measured height when data changes (content height may change)
    this.portalManager.resetMeasuredHeight();
    
    // Update body append content if it's active
    if (this.portal && this.portalManager.portalContainerElement) {
      this.portalManager.updatePortalAppendPosition();
    }
  }

  @watch('renderItemConfig', { waitUntilFirstUpdate: true })
  handleRenderItemConfigChange(): void {
    if (this.value && this.data.length > 0) {
      this.selectionChanged();
      this.requestUpdate();
    }
  }

  @watch('optionsLoading', { waitUntilFirstUpdate: true })
  handleOptionsLoadingChange(): void {
    // Show no results message when loading stops and there are no results
    if (!this.optionsLoading && this.data.length === 0) {
      this.showNoResults = true;
    }
    this.requestUpdate();
    
    // Update body append content if it's active
    if (this.portal && this.portalManager.portalContainerElement) {
      this.portalManager.updatePortalAppendPosition();
    }
  }

  @watch('portal', { waitUntilFirstUpdate: true })
  handlePortalAppendChange(): void {
    if (this.open) {
      if (this.portal) {
        this.portalManager.setupPortalAppend();
      } else {
        this.portalManager.cleanupPortalAppend();
      }
    }
  }

  @watch('open', { waitUntilFirstUpdate: true })
  async handleOpenChange(): Promise<void> {
    if (this.open && !this.disabled) {
      this.visibilityManager?.setup();
      this.showListbox = true;
      await this.updateComplete;
      await this.handleOpen();
      
      if (this.portal) {
        this.portalManager.setupPortalAppend();
      }
    } else {
      this.visibilityManager?.cleanup();
      await this.handleClose();
      this.showListbox = false;
      
      if (this.portal) {
        this.portalManager.cleanupPortalAppend();
      }
    }
  }

  private async handleOpen(): Promise<void> {
    this.emit('nile-show', { value: this.value, name: this.name });
    this.addOpenListeners();
    this.showNoResults = !this.data?.length;

    await stopAnimations(this);
    
    // Hide popup initially to prevent flickering during positioning calculation
    // This allows content to be measured while invisible
    if (this.popup?.popup) {
      this.popup.popup.style.visibility = 'hidden';
    }
    
    this.popup.active = true;
    
    // Wait for positioning to complete before showing the popup
    await new Promise(resolve => requestAnimationFrame(resolve));
    // Wait for next frame to allow Floating UI to calculate position
    await new Promise(resolve => requestAnimationFrame(resolve));
    
    // Show popup after positioning is calculated
    if (this.popup?.popup) {
      this.popup.popup.style.visibility = '';
    }

    const { keyframes, options } = getAnimation(this, 'select.show', {
      dir: 'ltr',
    });
    await animateTo(this.popup.popup, keyframes, options);

    if (this.wasShowSelectedCheckedOnClose) {
      this.showSelected = false;
      this.data = [...this.originalOptionItems];
      this.wasShowSelectedCheckedOnClose = false;
    }
 
    this.filterVirtualOptions("");
    this.resetScrollPosition();
    this.emit('nile-after-show', { value: this.value, name: this.name });
  }

  private async handleClose(): Promise<void> {
    this.emit('nile-hide', { value: this.value, name: this.name });
    this.removeOpenListeners();

    this.wasShowSelectedCheckedOnClose = this.showSelected;

    await stopAnimations(this);
    const { keyframes, options } = getAnimation(this, 'select.hide', {
      dir: 'ltr',
    });
    await animateTo(this.popup.popup, keyframes, options);
    this.popup.active = false;
    
    // Reset visibility style when closing
    if (this.popup?.popup) {
      this.popup.popup.style.visibility = '';
    }

    this.searchValue = '';
    
    // Reset measured height when popup closes (content may change on next open)
    this.portalManager.resetMeasuredHeight();
    
    this.emit('nile-after-hide', { value: this.value, name: this.name });
  }

  async show(): Promise<unknown> {
    if (this.open || this.disabled) {
      this.open = false;
      return undefined;
    }

    this.open = true;
    return waitForEvent(this, 'nile-after-show');
  }

  async hide(): Promise<unknown> {
    if (!this.open || this.disabled) {
      this.open = false;
      return undefined;
    }

    this.open = false;
    return waitForEvent(this, 'nile-after-hide');
  }

  checkValidity(): boolean {
    return this.valueInput.checkValidity();
  }

  getForm(): HTMLFormElement | null {
    return this.formControlController.getForm();
  }

  reportValidity(): boolean {
    return this.valueInput.reportValidity();
  }

  setCustomValidity(message: string): void {
    this.valueInput.setCustomValidity(message);
    this.formControlController.updateValidity();
  }

  focus(options?: FocusOptions): void {
    this.displayInput.focus(options);
  }

  blur(): void {
    this.displayInput.blur();
  }

  onInputChange(event: Event): void {
    event.stopPropagation();
  }

  render(): TemplateResult {
    const hasLabelSlot = this.hasSlotController.test('label');
    const hasHelpTextSlot = this.hasSlotController.test('help-text');
    const hasLabelSuffixSlot = this.hasSlotController.test('label-suffix');
    const hasCustomSelect = this.hasSlotController.test('custom-select');
    const hasLabel = this.label ? true : !!hasLabelSlot;
    const hasClearIcon =
      this.clearable && !this.disabled && this.value.length > 0;
    const isPlaceholderVisible = !!this.placeholder && this.value.length === 0;
    const hasHelpText = !!this.helpText;
    const hasErrorMessage = !!this.errorMessage;

    return html`
      <div
        part="form-control"
        class=${classMap({
          'form-control': true,
          'form-control--small': this.size === 'small',
          'form-control--medium': this.size === 'medium',
          'form-control--large': this.size === 'large',
          'form-control--has-label': hasLabel,
          'form-control--has-help-text': hasHelpText,
        })}
      >
        ${this.renderLabel(hasLabel, hasLabelSuffixSlot)}
        ${this.renderFormControlInput(hasCustomSelect, hasClearIcon, isPlaceholderVisible, hasHelpText, hasErrorMessage)}
      </div>
    `;
  }

  private renderLabel(hasLabel: boolean, hasLabelSuffixSlot: boolean): TemplateResult {
    return html`
      <label
        id="label"
        part="form-control-label"
        class="form-control__label"
        aria-hidden=${hasLabel ? 'false' : 'true'}
        @click=${this.handleLabelClick}
      >
        <slot name="label">${this.label}</slot>
      </label>

      ${hasLabelSuffixSlot ? html` <slot name="label-suffix"></slot> ` : ``}
    `;
  }

  private renderFormControlInput(
    hasCustomSelect: boolean, 
    hasClearIcon: boolean, 
    isPlaceholderVisible: boolean, 
    hasHelpText: boolean, 
    hasErrorMessage: boolean
  ): TemplateResult {
    return html`
      <div part="form-control-input" class="form-control-input">
        ${this.renderPopup(hasCustomSelect, hasClearIcon, isPlaceholderVisible)}
        ${this.renderHelpText(hasHelpText, hasErrorMessage)}
      </div>
    `;
  }

  private renderPopup(hasCustomSelect: boolean, hasClearIcon: boolean, isPlaceholderVisible: boolean): TemplateResult {
    return html`
      <nile-popup
        class=${classMap({
          select: true,
          'select--warning': this.warning,
          'select--error': this.error,
          'select--success': this.success,
          'select--standard': true,
          'select--filled': this.filled,
          'select--pill': this.pill,
          'select--open': this.open,
          'select--disabled': this.disabled,
          'select--multiple': this.multiple,
          'select--focused': this.hasFocus,
          'select--placeholder-visible': isPlaceholderVisible,
          'select--top': this.placement === 'top',
          'select--bottom': this.placement === 'bottom',
          'select--small': this.size === 'small',
          'select--medium': this.size === 'medium',
          'select--large': this.size === 'large',
        })}
        placement=${this.placement}
        strategy=${this.portal ? 'fixed' : (this.hoist ? 'fixed' : 'absolute')}
        distance="6"
        ?flip=${!this.portal}
        ?shift=${!this.portal}
        sync=${ifDefined(!this.noWidthSync ? 'width' : undefined)}
        auto-size="vertical"
        auto-size-padding="10"
        exportparts="tag__content"
      >
        ${this.renderCustomSelect(hasCustomSelect)}
        ${this.renderCombobox(hasCustomSelect, hasClearIcon)}
        ${this.showListbox ? this.renderListbox() : html``}
      </nile-popup>
    `;
  }

  private renderCustomSelect(hasCustomSelect: boolean): TemplateResult {
    return hasCustomSelect
      ? html`<slot
          slot="anchor"
          name="custom-select"
          class="custom-select"
          @keydown=${this.handleComboboxKeyDown}
          @mousedown=${this.handleComboboxMouseDown}
        ></slot>`
      : html``;
  }

  private renderCombobox(hasCustomSelect: boolean, hasClearIcon: boolean): TemplateResult {
    return html`
      <div
        part="combobox"
        class=${classMap({
          select__combobox: true,
          'select__hide-default': hasCustomSelect,
        })}
        slot="anchor"
        @keydown=${this.handleComboboxKeyDown}
        @mousedown=${this.handleComboboxMouseDown}
      >
        ${this.renderPrefix()}
        ${this.renderDisplayInput()}
        ${this.renderTags()}
        ${this.renderValueInput()}
        ${this.renderClearButton(hasClearIcon)}
        ${this.renderSuffix()}
        ${this.renderExpandIcon()}
      </div>
    `;
  }

  private renderPrefix(): TemplateResult {
    return html`
      <slot
        part="prefix"
        name="prefix"
        class=${classMap({
          select__prefix: true,
          multiple: this.multiple,
        })}
      ></slot>
    `;
  }

  private renderDisplayInput(): TemplateResult {
    return html`
      <input
        part="display-input"
        class="select__display-input"
        type="text"
        placeholder=${this.placeholder}
        .disabled=${this.disabled}
        .value=${this.displayLabel}
        autocomplete="off"
        spellcheck="false"
        autocapitalize="off"
        readonly
        aria-controls="listbox"
        aria-expanded=${this.open ? 'true' : 'false'}
        aria-haspopup="listbox"
        aria-labelledby="label"
        aria-disabled=${this.disabled ? 'true' : 'false'}
        aria-describedby="help-text"
        role="combobox"
        tabindex="0"
      />
    `;
  }

  private renderTags(): TemplateResult {
    return this.multiple
      ? html`
          <div part="tags" class="select__tags">
            ${this.selectedOptions.map((option, index) => {
              if (
                index < this.maxOptionsVisible ||
                this.maxOptionsVisible <= 0
              ) {
                const classes = {
                  select__invisible:
                    index + 1 > this.oldMaxOptionsVisible &&
                    this.maxOptionsVisible === Infinity,
                };
                return html`
                  <nile-tag
                    class=${classMap(classes)}
                    part="tag"
                    exportparts="
                        base:tag__base,
                        content:tag__content,
                        remove-button:tag__remove-button,
                        remove-button__base:tag__remove-button__base
                      "
                    ?pill=${this.pill}
                    size=${this.size}
                    removable
                    @nile-remove=${(event: NileRemoveEvent) =>
                      this.handleTagRemove(event, option)}
                  >
                    ${option.getTextLabel()}
                  </nile-tag>
                `;
              } else if (index === this.maxOptionsVisible) {
                return html`
                  <span class="select__tags-count"
                    >+${this.selectedOptions.length - index} More
                  </span>
                `;
              } else {
                return null;
              }
            })}
          </div>
        `
      : html``;
  }

  private renderValueInput(): TemplateResult {
    return html`
      <input
        class="select__value-input"
        type="text"
        ?disabled=${this.disabled}
        ?required=${this.required}
        .value=${Array.isArray(this.value)
          ? this.value.join(', ')
          : this.value}
        tabindex="-1"
        aria-hidden="true"
        @focus=${() => this.focus()}
        @invalid=${this.handleInvalid}
      />
    `;
  }

  private renderClearButton(hasClearIcon: boolean): TemplateResult {
    return hasClearIcon
      ? html`
          <button
            part="clear-button"
            class="select__clear"
            type="button"
            aria-label="clearEntry"
            @mousedown=${this.handleClearMouseDown}
            @click=${this.handleClearClick}
            tabindex="-1"
          >
            <slot name="clear-icon">
              <nile-icon name="var(--nile-icon-close, var(--ng-icon-x-close))" size="var(--nile-height-14px, var(--ng-height-16px))"  method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" library="system" color="var(--nile-colors-dark-500, var(--ng-colors-fg-quaternary-400))"></nile-icon>
            </slot>
          </button>
        `
      : html``;
  }

  private renderSuffix(): TemplateResult {
    return html`
      <slot
        part="suffix"
        name="suffix"
        class=${classMap({
          select__suffix: true,
        })}
      ></slot>
    `;
  }

  private renderExpandIcon(): TemplateResult {
    return html`
      <slot
        name="expand-icon"
        part="expand-icon"
        class="select__expand-icon"
      >
        <nile-icon library="system" name="var(--nile-icon-arrow-down, var(--ng-icon-chevron-down))" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" color="var(--nile-colors-dark-500, var(--ng-colors-fg-quaternary-400))"></nile-icon>
      </slot>
    `;
  }

  private renderListbox(): TemplateResult {
    return html`
      <div
        id="listbox"
        role="listbox"
        aria-expanded=${this.open ? 'true' : 'false'}
        aria-multiselectable=${this.multiple ? 'true' : 'false'}
        aria-labelledby="label"
        part="listbox"
        class="select__listbox ${this.searchEnabled
          ? 'select__search-enabled '
          : ''} ${this.portal ? 'select__portal-append-hidden' : ''}"
        tabindex="-1"
        @mouseup=${this.handleOptionClick}
        style=${this.portal ? 'display: none;' : ''}
      >
        ${this.renderSearch()}
        ${this.renderLoader()}
        ${this.getVirtualizedContent()}
        ${this.renderFooter()}
      </div>
    `;
  }

  private renderSearch(): TemplateResult {
    return this.searchEnabled
      ? html`
          <div part="search" class="select__search">
            <nile-input
              size="small"
              clearable
              .value=${this.searchValue}
              placeholder="${this.internalSearchPlaceHolder}"
              @nile-input=${this.handleSearchChange}
              @nile-focus=${this.handleSearchFocus}
              @nile-change="${this.onInputChange}"
            >
              <nile-icon name="var(--nile-icon-search, var(--ng-icon-search-lg))" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" slot="suffix"> </nile-icon>
            </nile-input>
          </div>
        `
      : html``;
  }

  private renderLoader(): TemplateResult {
    if (this.loading) {
      return html`
        <span part="loader" class="virtual-select-loader">
          <nile-loader size="sm"></nile-loader>
        </span>
      `;
    }
    
    if (this.optionsLoading) {
      return html`
        <span part="loader" class="select__loader">
          <nile-icon
            class="select__loader--icon"
            name="button-loading-blue"
          ></nile-icon>
        </span>
      `;
    }
    
    return html``;
  }

  private renderFooter(): TemplateResult {
    return this.multiple
      ? html`
          <div
            part="footer"
            class="select__footer ${this.loading ? 'loading' : ''}"
            @click="${this.handleFooterClick}"
          >
            <span
              @click="${this.toggleShowSelected}"
              style="cursor: pointer;"
            >
              <nile-checkbox
                ?disabled="${this.selectedOptions.length === 0}"
                ?checked="${this.showSelected}"
              >
                &nbsp; Show Selected</nile-checkbox
              >
            </span>
            ${this.selectedOptions.length !== 0
              ? html`
                  <span
                    class="select__clear"
                    @click="${this.unSelectAll}"
                  >
                    Clear All
                  </span>
                `
              : ``}
          </div>
        `
      : html``;
  }

  private renderHelpText(hasHelpText: boolean, hasErrorMessage: boolean): TemplateResult {
    return html`
      ${hasHelpText
        ? html` <nile-form-help-text>${this.helpText}</nile-form-help-text> `
        : ``}
      ${hasErrorMessage
        ? html`
            <nile-form-error-message
              >${this.errorMessage}</nile-form-error-message
            >
          `
        : ``}
    `;
  }

  getVirtualizedContent(): TemplateResult {
    return VirtualSelectRenderer.getVirtualizedContent(
      this.data,
      this.searchEnabled,
      this.getDisplayText.bind(this),
      this.value,
      this.multiple,
      this.renderItemConfig?.getDisplayText,
      this.renderItemConfig?.getValue,
      this.renderItemConfig?.getDescription,
      this.renderItemConfig?.getPrefix,
      this.renderItemConfig?.getSuffix,
      this.showNoResults,
      this.noResultsMessage,
      this.optionsLoading || this.loading,
      this.handleScroll.bind(this),
      this.allowHtmlLabel,
      this.enableDescription
    );
  }

  nileInput(value: any): void {
    this.emit('nile-input', value);
  }

  nileChange(value: any): void {
    this.emit('nile-change', value);
  }

  private updateValidity(): void {
    this.updateComplete.then(() => {
      this.formControlController.updateValidity();
    });
  }

  private calculateWidthOfSelectTagsDiv(): number | null {
    if (this.shadowRoot) {
      const selectTagsDiv = this.shadowRoot.querySelector('.select__tags') as HTMLElement;
      if (selectTagsDiv) {
        const width = selectTagsDiv.offsetWidth;
        return width - 70;
      }
    }
    return null;
  }

  private calculateTotalWidthOfTags(): void {
    if (this.maxOptionsVisible !== Infinity) {
      this.oldMaxOptionsVisible = this.maxOptionsVisible;
    }

    this.maxOptionsVisible = Infinity;
    setTimeout(() => {
      let widths: number[] = [];
      if (this.shadowRoot) {
        const tags = this.shadowRoot.querySelectorAll('nile-tag');
        tags.forEach(tag => {
          if (tag instanceof HTMLElement) {
            widths.push(tag.offsetWidth);
          }
        });
      }
      if (this.value.length !== widths.length) {
        return;
      }

      let sum = widths.reduce(
        (accumulator, currentValue) => accumulator + currentValue,
        0
      );
      const widthOfSelectTagsDiv = this.calculateWidthOfSelectTagsDiv();
      if (!widthOfSelectTagsDiv) {
        return;
      }

      let summ = 0;
      let lastindex = 0;

      for (let i = 0; i < widths.length; i++) {
        summ += widths[i];

        if (summ > widthOfSelectTagsDiv) {
          lastindex = i;
          break;
        }
      }
      this.maxOptionsVisible = lastindex;
    }, 1);
  }

  private async resetScrollPosition(): Promise<void> {
    await this.portalManager.resetScrollPosition();
  }
}

setDefaultAnimation('select.show', {
  keyframes: [
    { opacity: 0, scale: 0.9 },
    { opacity: 1, scale: 1 },
  ],
  options: { duration: 100, easing: 'ease' },
});

setDefaultAnimation('select.hide', {
  keyframes: [
    { opacity: 1, scale: 1 },
    { opacity: 0, scale: 0.9 },
  ],
  options: { duration: 100, easing: 'ease' },
});

export default NileVirtualSelect;

declare global {
  interface HTMLElementTagNameMap {
    'nile-virtual-select': NileVirtualSelect;
  }
}

