/**
 * TyMultiselect Web Component
 * PORTED FROM: clj/ty/components/multiselect.cljs
 *
 * A multiselect dropdown component using ty-tag for selections with:
 * - Tag-only options (only ty-tag elements supported)
 * - Multiple selection with visual tags
 * - Desktop mode with smart positioning
 * - Mobile mode with full-screen modal
 * - Search and filtering capabilities
 * - Keyboard navigation
 * - Form association for native form submission with multiple values
 * - Scroll locking when dropdown is open
 * - Outside click to close
 *
 * @example
 * ```html
 * <!-- Basic multiselect -->
 * <ty-multiselect label="Skills" placeholder="Select skills" required>
 *   <ty-tag value="js">JavaScript</ty-tag>
 *   <ty-tag value="ts">TypeScript</ty-tag>
 *   <ty-tag value="py">Python</ty-tag>
 * </ty-multiselect>
 *
 * <!-- With pre-selected values -->
 * <ty-multiselect value="js,ts">
 *   <ty-tag value="js">JavaScript</ty-tag>
 *   <ty-tag value="ts">TypeScript</ty-tag>
 *   <ty-tag value="py">Python</ty-tag>
 * </ty-multiselect>
 *
 * <!-- With rich tag content -->
 * <ty-multiselect label="Team Members">
 *   <ty-tag value="1" flavor="primary">
 *     <div class="flex items-center gap-2">
 *       <img src="avatar1.jpg" class="w-6 h-6 rounded-full" />
 *       <span>John Doe</span>
 *     </div>
 *   </ty-tag>
 * </ty-multiselect>
 * ```
 */
import { ensureStyles } from '../utils/styles.js';
import { multiselectStyles } from '../styles/multiselect.js';
import { TyComponent } from '../base/ty-component.js';
// ============================================================================
// DEVICE DETECTION
// ============================================================================
/**
 * Detect if we're on a mobile device
 * - Screen width <= 768px (mobile phones)
 * - Screen width <= 1024px + touch capability (tablets)
 */
function isMobileDevice() {
    const width = window.innerWidth;
    const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
    return width <= 768 || (width <= 1024 && hasTouch);
}
// ============================================================================
// Element Hash Utility (for consistent scroll lock IDs)
// ============================================================================
/**
 * Counter for generating unique element IDs
 */
let elementIdCounter = 0;
/**
 * WeakMap to store consistent element hashes
 * Automatically garbage collects when element is destroyed
 */
const elementIds = new WeakMap();
/**
 * Get a consistent unique ID for an element
 * Returns the same ID for the same element across multiple calls
 *
 * @param element - The element to hash
 * @returns A consistent numeric hash for the element
 */
function getElementHash(element) {
    let id = elementIds.get(element);
    if (id === undefined) {
        id = ++elementIdCounter;
        elementIds.set(element, id);
    }
    return id;
}
// ============================================================================
// SVG Icons
// ============================================================================
/**
 * Required indicator SVG icon (from Lucide)
 */
const REQUIRED_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-asterisk"><path d="M12 6v12"/><path d="M17.196 9 6.804 15"/><path d="m6.804 9 10.392 6"/></svg>`;
/**
 * Chevron down icon SVG
 */
const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
  <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>`;
/**
 * Ty Multiselect Component
 */
export class TyMultiselect extends TyComponent {
    // Debug: Log when expandedSection changes
    set expandedSection(value) {
        this._state.expandedSection = value;
    }
    get expandedSection() {
        return this._state.expandedSection;
    }
    constructor() {
        super(); // TyComponent handles attachInternals() and attachShadow()
        // ============================================================================
        // INTERNAL STATE
        // ============================================================================
        this._value = '';
        this._name = '';
        this._placeholder = 'Select options...';
        this._label = '';
        this._disabled = false;
        this._readonly = false;
        this._required = false;
        this._searchable = true;
        this._size = 'md';
        this._flavor = 'neutral';
        this._selectedLabel = 'Selected';
        this._availableLabel = 'Available';
        this._noSelectionMessage = 'No items selected';
        this._noOptionsMessage = 'No options available';
        // Component state
        this._state = {
            open: false,
            search: '',
            highlightedIndex: -1,
            filteredTags: [],
            selectedValues: [],
            mode: isMobileDevice() ? 'mobile' : 'desktop',
            expandedSection: 'selected' // Will be corrected by toggleSection call
        };
        // Event handler references for cleanup
        this._stubClickHandler = null;
        this._outsideClickHandler = null;
        this._tagClickHandler = null;
        this._tagDismissHandler = null;
        this._searchInputHandler = null;
        this._blockSearchClick = null;
        this._keyboardHandler = null;
        // Delay/debounce properties for search event
        this._delay = 0;
        this._searchDebounceTimer = null;
        const shadow = this.shadowRoot;
        ensureStyles(shadow, { css: multiselectStyles, id: 'ty-multiselect' });
        // Render based on device type
        if (this._state.mode === 'mobile') {
            this.renderMobile();
        }
        else {
            this.renderDesktop();
        }
        // CRITICAL: Ensure dialogs are closed on initialization
        // This prevents scroll locking if dialogs are in invalid state
        requestAnimationFrame(() => {
            const dialogs = shadow.querySelectorAll('dialog');
            dialogs.forEach(dialog => {
                if (dialog.open) {
                    dialog.close();
                }
            });
        });
    }
    /**
     * Called when component is connected to DOM
     * TyComponent handles property capture automatically
     */
    onConnect() {
        // SAFETY: Close any open dialogs to prevent scroll locking
        const shadow = this.shadowRoot;
        const dialogs = shadow.querySelectorAll('dialog');
        dialogs.forEach(dialog => {
            if (dialog.open) {
                console.warn('⚠️ Found open dialog on connect, closing it');
                dialog.close();
            }
        });
        // Initialize after element is connected and children are available
        requestAnimationFrame(() => {
            this.initializeState();
        });
    }
    /**
     * Called when component is disconnected from DOM
     * Clean up event listeners and timers
     */
    onDisconnect() {
        // CRITICAL: Close all dialogs to prevent scroll locking
        const shadow = this.shadowRoot;
        const dialogs = shadow.querySelectorAll('dialog');
        dialogs.forEach(dialog => {
            if (dialog.open) {
                dialog.close();
            }
        });
        // Clean up document-level listeners
        const outsideClickHandler = this.tyOutsideClickHandler;
        const keyboardHandler = this.tyKeyboardHandler;
        if (outsideClickHandler) {
            document.removeEventListener('click', outsideClickHandler);
            this.tyOutsideClickHandler = null;
        }
        if (keyboardHandler) {
            document.removeEventListener('keydown', keyboardHandler);
            this.tyKeyboardHandler = null;
        }
        // Clear any pending debounce timer
        if (this._searchDebounceTimer !== null) {
            clearTimeout(this._searchDebounceTimer);
            this._searchDebounceTimer = null;
        }
    }
    /**
     * Called when properties change
     * Handle state synchronization BEFORE render
     */
    onPropertiesChanged(changes) {
        for (const { name, newValue } of changes) {
            switch (name) {
                case 'value':
                    this._value = newValue || '';
                    const selectedValues = this.parseValue(newValue);
                    this._state.selectedValues = selectedValues;
                    this.syncSelectedTags(selectedValues);
                    this.updateSelectionDisplay();
                    this.updateMobileSelectedState();
                    break;
                case 'name':
                    this._name = newValue || '';
                    break;
                case 'placeholder':
                    this._placeholder = newValue || 'Select options...';
                    // No need to update display - placeholder text change doesn't affect visibility
                    break;
                case 'label':
                    this._label = newValue || '';
                    break;
                case 'disabled':
                    this._disabled = newValue;
                    break;
                case 'readonly':
                    this._readonly = newValue;
                    break;
                case 'required':
                    this._required = newValue;
                    break;
                case 'searchable':
                    this._searchable = newValue;
                    break;
                case 'size':
                    this._size = newValue;
                    break;
                case 'flavor':
                    this._flavor = newValue;
                    break;
                case 'delay':
                    this._delay = newValue;
                    break;
                case 'selected-label':
                    this._selectedLabel = newValue || 'Selected';
                    break;
                case 'available-label':
                    this._availableLabel = newValue || 'Available';
                    break;
                case 'no-selection-message':
                    this._noSelectionMessage = newValue || 'No items selected';
                    break;
                case 'no-options-message':
                    this._noOptionsMessage = newValue || 'No options available';
                    break;
            }
        }
    }
    /**
     * Get the form value for this component
     * Returns FormData with multiple entries (HTMX standard)
     */
    getFormValue() {
        const selectedValues = this._state.selectedValues;
        if (this._name && selectedValues.length > 0) {
            const formData = new FormData();
            selectedValues.forEach(value => {
                formData.append(this._name, value);
            });
            return formData;
        }
        return null;
    }
    /**
     * Parse multiselect value (comma-separated string to array)
     */
    parseValue(value) {
        // Defensive check: ensure value is actually a string before calling .trim()
        if (!value || typeof value !== 'string' || value.trim() === '')
            return [];
        return value.split(',').map(v => v.trim()).filter(v => v !== '');
    }
    /**
     * Initialize component state from attributes
     * Reads from both property and attribute (like ClojureScript version)
     */
    initializeState() {
        const initialValue = this.getProperty('value') || '';
        if (initialValue) {
            // Explicit value provided - use it
            const selectedValues = this.parseValue(initialValue);
            this.updateComponentValue(selectedValues, false);
        }
        else {
            // No explicit value - check for pre-selected tags
            const preSelectedTags = this.getTagElements()
                .filter(tag => tag.hasAttribute('selected'))
                .map(tag => this.getTagData(tag).value);
            if (preSelectedTags.length > 0) {
                this.updateComponentValue(preSelectedTags, false);
            }
            else {
            }
        }
    }
    // ============================================================================
    // TAG MANAGEMENT METHODS (Phase 2)
    // ============================================================================
    /**
     * Get all ty-tag elements from the component (ALL slots)
     */
    getTagElements() {
        // Get ALL ty-tag children, regardless of slot assignment
        return Array.from(this.querySelectorAll('ty-tag'));
    }
    /**
     * Extract value and text from a ty-tag element
     */
    getTagData(element) {
        // Get value from either property or attribute
        const value = element.value || element.getAttribute('value') || element.textContent || '';
        const text = element.textContent || '';
        return { value, text, element };
    }
    /**
     * Select a tag - set selected state, move to selected slot, make dismissible
     */
    selectTag(tag) {
        // Set selected attribute
        tag.setAttribute('selected', '');
        // Move to selected slot
        tag.setAttribute('slot', 'selected');
        // Make dismissible
        tag.setAttribute('dismissible', 'true');
        // Force re-slotting by removing and re-adding
        const parent = tag.parentNode;
        if (parent) {
            parent.removeChild(tag);
            parent.appendChild(tag);
        }
    }
    /**
     * Deselect a tag - remove selected state, remove from selected slot, remove dismissible
     */
    deselectTag(tag) {
        // Remove selected attribute
        tag.removeAttribute('selected');
        // Remove from selected slot
        tag.removeAttribute('slot');
        // Remove dismissible
        tag.removeAttribute('dismissible');
        // Force re-slotting by removing and re-adding
        const parent = tag.parentNode;
        if (parent) {
            parent.removeChild(tag);
            parent.appendChild(tag);
        }
    }
    /**
     * Get array of currently selected values from tags (ALWAYS reads from DOM)
     */
    getSelectedValues() {
        return this.getTagElements()
            .filter(tag => tag.hasAttribute('selected'))
            .map(tag => this.getTagData(tag).value)
            .filter(value => value !== '');
    }
    /**
     * Check if all available tags are selected
     */
    allTagsSelected() {
        const tags = this.getTagElements();
        if (tags.length === 0)
            return false;
        const selectedCount = tags.filter(tag => tag.hasAttribute('selected')).length;
        return selectedCount === tags.length;
    }
    /**
     * Sync tag selection states with desired values
     */
    syncSelectedTags(selectedValues) {
        const selectedSet = new Set(selectedValues);
        const tags = this.getTagElements();
        tags.forEach(tag => {
            const tagValue = this.getTagData(tag).value;
            const shouldBeSelected = selectedSet.has(tagValue);
            const isSelected = tag.hasAttribute('selected');
            if (shouldBeSelected && !isSelected) {
                this.selectTag(tag);
            }
            else if (!shouldBeSelected && isSelected) {
                this.deselectTag(tag);
            }
        });
    }
    /**
     * Central update function - synchronizes everything
     * Uses TyComponent's property system for proper lifecycle
     */
    updateComponentValue(newValues, dispatchChange = false, action = 'set', item = null) {
        const oldValues = this.getSelectedValues();
        const valueStr = newValues.join(',');
        // Only update if changed
        if (JSON.stringify(newValues.sort()) !== JSON.stringify(oldValues.sort())) {
            // Use TyComponent's property system - this will trigger:
            // 1. onPropertiesChanged() → syncs tags via syncSelectedTags()
            // 2. onPropertiesChanged() → updates placeholder via updateSelectionDisplay()
            // 3. updateFormValue() → automatic (formValue: true in config)
            // 4. render() → automatic if visual properties changed
            this.setProperty('value', valueStr);
            // Dispatch custom multiselect change event (with action/item details)
            if (dispatchChange) {
                this.dispatchChangeEvent({
                    values: newValues,
                    action,
                    item
                });
            }
        }
        else {
        }
    }
    // ============================================================================
    // DROPDOWN METHODS (Phase 3 & 4)
    // ============================================================================
    /**
     * Calculate and set dropdown position with smart direction detection
     */
    calculatePosition() {
        const shadow = this.shadowRoot;
        const stub = shadow.querySelector('.multiselect-stub');
        const dialog = shadow.querySelector('.dropdown-dialog');
        if (!stub || !dialog)
            return;
        const stubRect = stub.getBoundingClientRect();
        const viewportHeight = window.innerHeight;
        const viewportWidth = window.innerWidth;
        // Get dialog dimensions (it's already shown with showModal)
        const dialogRect = dialog.getBoundingClientRect();
        const estimatedHeight = dialogRect.height || 200;
        const padding = 8;
        const wrapPadding = 20;
        // Available space calculations
        const spaceBelow = viewportHeight - stubRect.bottom;
        const spaceRight = viewportWidth - stubRect.left;
        // Smart direction logic
        const positionBelow = spaceBelow >= estimatedHeight + padding;
        const fitsHorizontally = spaceRight >= stubRect.width;
        // Calculate position coordinates
        const x = fitsHorizontally
            ? stubRect.left - wrapPadding
            : Math.max(padding, viewportWidth - stubRect.width - padding);
        const y = positionBelow
            ? stubRect.top - wrapPadding
            : viewportHeight - stubRect.bottom - wrapPadding;
        const width = stubRect.width + wrapPadding + wrapPadding;
        // Set CSS variables for positioning
        this.style.setProperty('--dropdown-x', `${x}px`);
        this.style.setProperty('--dropdown-y', `${y}px`);
        this.style.setProperty('--dropdown-width', `${width}px`);
        this.style.setProperty('--dropdown-offset-x', '0px');
        this.style.setProperty('--dropdown-offset-y', '0px');
        this.style.setProperty('--dropdown-padding', `${wrapPadding}px`);
        // Set direction classes for CSS styling
        if (positionBelow) {
            dialog.classList.add('position-below');
            dialog.classList.remove('position-above');
        }
        else {
            dialog.classList.add('position-above');
            dialog.classList.remove('position-below');
        }
        // Optional: Store direction for debugging
        this.style.setProperty('--dropdown-direction', positionBelow ? 'below' : 'above');
    }
    /**
     * Open dropdown dialog (desktop mode)
     * Using <dialog> element - scroll locking handled natively
     */
    openDropdown() {
        const shadow = this.shadowRoot;
        const dialog = shadow.querySelector('.dropdown-dialog');
        if (!dialog)
            return;
        // Show modal (browser handles scroll locking automatically)
        dialog.showModal();
        dialog.classList.add('open');
        // Position dropdown AFTER showing modal
        this.calculatePosition();
        // Update component state
        this._state.open = true;
        // Update visual states
        const chevron = shadow.querySelector('.dropdown-chevron');
        if (chevron)
            chevron.classList.add('open');
        const searchChevron = shadow.querySelector('.dropdown-search-chevron');
        if (searchChevron)
            searchChevron.classList.add('open');
        // Initialize options state
        const tags = this.getTagElements().map(el => this.getTagData(el));
        this._state.filteredTags = tags;
        this._state.highlightedIndex = -1;
        // Focus search input if searchable
        if (this._searchable) {
            const searchInput = shadow.querySelector('.dropdown-search-input');
            if (searchInput) {
                setTimeout(() => searchInput.focus(), 100);
            }
        }
    }
    /**
     * Close dropdown dialog (desktop mode)
     * Using <dialog> element - scroll unlocking handled natively
     */
    closeDropdown() {
        const shadow = this.shadowRoot;
        const dialog = shadow.querySelector('.dropdown-dialog');
        if (!dialog)
            return;
        // Close dialog (browser handles scroll unlocking automatically)
        dialog.classList.remove('open');
        dialog.classList.remove('position-above');
        dialog.classList.remove('position-below');
        dialog.close();
        // Update state
        this._state.open = false;
        this._state.highlightedIndex = -1;
        // Update visual states
        const chevron = shadow.querySelector('.dropdown-chevron');
        if (chevron)
            chevron.classList.remove('open');
        const searchChevron = shadow.querySelector('.dropdown-search-chevron');
        if (searchChevron)
            searchChevron.classList.remove('open');
        // Reset search if searchable
        if (this._searchable) {
            this._state.search = '';
            const searchInput = shadow.querySelector('.dropdown-search-input');
            this._state.search = '';
            if (searchInput) {
                searchInput.value = '';
            }
            // Show all tags
            const allTags = this.getTagElements().map(el => this.getTagData(el));
            this._state.filteredTags = allTags;
            this.updateTagVisibility(allTags, allTags);
            this.clearHighlights(allTags);
            if (searchInput) {
                searchInput.value = '';
            }
        }
    }
    /**
     * Open mobile modal (mobile mode)
     * Now using <dialog> element for native z-index management
     */
    openMobileModal() {
        const shadow = this.shadowRoot;
        const dialog = shadow.querySelector('.mobile-dialog');
        if (!dialog)
            return;
        // Show dialog using native API (handles z-index automatically)
        dialog.showModal();
        dialog.classList.add('open');
        const stub_slot = shadow.querySelector('#stub-slot');
        const mobile_slot = shadow.querySelector('#mobile-slot');
        stub_slot.name = "selected-blocked";
        mobile_slot.name = "selected";
        // Update component state
        this._state.open = true;
        // Initialize options state
        const tags = this.getTagElements().map(el => this.getTagData(el));
        this._state.filteredTags = tags;
        // Focus search input if searchable
        if (this._searchable) {
            const searchInput = shadow.querySelector('.mobile-search-input');
            if (searchInput) {
                // Small delay to ensure dialog is ready
                setTimeout(() => searchInput.focus(), 100);
            }
        }
        // Initialize sections (available expanded by default)
        this._state.expandedSection = 'available';
        this.syncSectionStates();
        // Update state after slots are ready
        requestAnimationFrame(() => {
            this.updateMobileSelectedState();
        });
    }
    /**
     * Close mobile modal (mobile mode)
     * Now using <dialog> element for native management
     */
    closeMobileModal() {
        const shadow = this.shadowRoot;
        const dialog = shadow.querySelector('.mobile-dialog');
        if (!dialog)
            return;
        // Close dialog using native API
        dialog.classList.remove('open');
        dialog.close();
        const stub_slot = shadow.querySelector('#stub-slot');
        const mobile_slot = shadow.querySelector('#mobile-slot');
        stub_slot.name = "selected";
        mobile_slot.name = "selected-blocked";
        // Update state
        this._state.open = false;
        this._state.highlightedIndex = -1;
        // Reset search if searchable
        if (this._searchable) {
            this._state.search = '';
            const searchInput = shadow.querySelector('.mobile-search-input');
            if (searchInput) {
                searchInput.value = '';
            }
        }
    }
    // ============================================================================
    // EVENT HANDLERS (Phase 5 & 6)
    // ============================================================================
    handleStubClick(e) {
        e.preventDefault();
        e.stopPropagation();
        if (this._disabled || this._readonly) {
            return;
        }
        // Don't open if all tags are already selected
        if (this.allTagsSelected()) {
            return;
        }
        this.openDropdown();
    }
    handleOutsideClick(e) {
        if (!this._state.open)
            return;
        const target = e.target;
        const shadow = this.shadowRoot;
        // Check if click is inside .dropdown-wrapper
        const wrapper = shadow.querySelector('.dropdown-wrapper');
        const clickedInside = wrapper && wrapper.contains(target);
        if (!clickedInside) {
            this.closeDropdown();
        }
    }
    handleTagClick(e) {
        const target = e.target;
        // Find the ty-tag element
        const tag = target.tagName === 'TY-TAG'
            ? target
            : target.closest('ty-tag');
        if (!tag || tag.hasAttribute('disabled'))
            return;
        if (tag.hasAttribute('selected'))
            return; // Already selected
        e.preventDefault();
        e.stopPropagation();
        const tagValue = this.getTagData(tag).value;
        const currentValues = this.getSelectedValues();
        const newValues = [...currentValues, tagValue];
        // Use central update function with change event
        this.updateComponentValue(newValues, true, 'add', tagValue);
        // Auto-close if all tags selected
        if (this.allTagsSelected()) {
            if (this._state.mode === 'desktop') {
                this.closeDropdown();
            }
            else {
                this.closeMobileModal();
            }
        }
    }
    handleTagDismiss(e) {
        e.preventDefault();
        e.stopPropagation();
        const customEvent = e;
        const tag = customEvent.detail?.target;
        if (!tag)
            return;
        const tagValue = this.getTagData(tag).value;
        const currentValues = this.getSelectedValues();
        const newValues = currentValues.filter(v => v !== tagValue);
        // Use central update function with change event
        this.updateComponentValue(newValues, true, 'remove', tagValue);
    }
    blockSearchClick(e) {
        e.stopPropagation();
        e.preventDefault();
    }
    handleSearchInput(e) {
        const target = e.target;
        const query = target.value;
        // Update search state
        this._state.search = query;
        if (this._searchable) {
            // Internal search: filter tags locally
            const allTags = this.getTagElements().map(el => this.getTagData(el));
            const filtered = this.filterTags(allTags, query);
            // Update state
            this._state.filteredTags = filtered;
            this._state.highlightedIndex = -1;
            // Update visibility
            this.updateTagVisibility(filtered, allTags);
            // Clear highlights
            this.clearHighlights(allTags);
        }
        else {
            // External search: dispatch event for external handling
            this.dispatchSearchEvent(query);
        }
    }
    handleKeyboard(e) {
        if (!this._state.open)
            return;
        const shadow = this.shadowRoot;
        const searchInput = shadow.querySelector('.dropdown-search-input');
        const target = e.target;
        // Only handle navigation keys when dropdown is open and either:
        // 1. Event comes from search input, OR
        // 2. Event comes from document but search input is not focused
        const shouldHandle = target === searchInput ||
            document.activeElement !== searchInput;
        if (!shouldHandle)
            return;
        // Get current state values
        const filteredTags = this._state.filteredTags;
        const tagsCount = filteredTags.length;
        const currentHighlightedIndex = this._state.highlightedIndex;
        switch (e.key) {
            case 'Escape':
                e.preventDefault();
                e.stopPropagation();
                this.closeDropdown();
                break;
            case 'Enter':
                e.preventDefault();
                e.stopPropagation();
                // Select highlighted tag if any
                if (currentHighlightedIndex >= 0 && currentHighlightedIndex < tagsCount) {
                    const tag = filteredTags[currentHighlightedIndex];
                    this.handleTagClick({ target: tag.element });
                }
                break;
            case 'ArrowUp':
                e.preventDefault();
                e.stopPropagation();
                let newIndexUp;
                if (tagsCount === 0) {
                    newIndexUp = -1;
                }
                else if (currentHighlightedIndex === -1) {
                    // Nothing highlighted, go to last tag
                    newIndexUp = tagsCount - 1;
                }
                else if (currentHighlightedIndex === 0) {
                    // At first tag, wrap to last
                    newIndexUp = tagsCount - 1;
                }
                else {
                    // Move up one
                    newIndexUp = currentHighlightedIndex - 1;
                }
                this._state.highlightedIndex = newIndexUp;
                this.highlightTag(filteredTags, newIndexUp);
                break;
            case 'ArrowDown':
                e.preventDefault();
                e.stopPropagation();
                let newIndexDown;
                if (tagsCount === 0) {
                    newIndexDown = -1;
                }
                else if (currentHighlightedIndex === -1) {
                    // Nothing highlighted, go to first tag
                    newIndexDown = 0;
                }
                else if (currentHighlightedIndex === tagsCount - 1) {
                    // At last tag, wrap to first
                    newIndexDown = 0;
                }
                else {
                    // Move down one
                    newIndexDown = currentHighlightedIndex + 1;
                }
                this._state.highlightedIndex = newIndexDown;
                this.highlightTag(filteredTags, newIndexDown);
                break;
        }
    }
    // ============================================================================
    // SEARCH & FILTERING HELPERS (Phase 6)
    // ============================================================================
    /**
     * Filter tags based on search query
     */
    filterTags(tags, query) {
        if (!query || query.trim() === '') {
            return tags;
        }
        const searchLower = query.toLowerCase();
        return tags.filter(({ text }) => text.toLowerCase().includes(searchLower));
    }
    /**
     * Update visibility of tags based on filtered list
     */
    updateTagVisibility(filteredTags, allTags) {
        const visibleValues = new Set(filteredTags.map(tag => tag.value));
        allTags.forEach(({ value, element }) => {
            if (visibleValues.has(value)) {
                element.removeAttribute('hidden');
            }
            else {
                element.setAttribute('hidden', '');
            }
        });
    }
    /**
     * Clear all tag highlights
     */
    clearHighlights(tags) {
        tags.forEach(({ element }) => {
            element.removeAttribute('highlighted');
        });
    }
    /**
     * Highlight tag at specific index
     */
    highlightTag(tags, index) {
        this.clearHighlights(tags);
        if (index >= 0 && index < tags.length) {
            const { element } = tags[index];
            element.setAttribute('highlighted', '');
            // Scroll into view
            element.scrollIntoView({
                behavior: 'smooth',
                block: 'nearest',
                inline: 'nearest'
            });
        }
    }
    /**
     * Dispatch search event for external search handling
     * With optional delay/debounce support
     */
    dispatchSearchEvent(query) {
        // Clear existing timer
        if (this._searchDebounceTimer !== null) {
            clearTimeout(this._searchDebounceTimer);
            this._searchDebounceTimer = null;
        }
        // If delay is set, debounce the event
        if (this._delay > 0) {
            this._searchDebounceTimer = window.setTimeout(() => {
                this.fireSearchEvent(query);
                this._searchDebounceTimer = null;
            }, this._delay);
        }
        else {
            // Fire immediately if no delay
            this.fireSearchEvent(query);
        }
    }
    /**
     * Fire the actual search event
     */
    fireSearchEvent(query) {
        this.dispatchEvent(new CustomEvent('search', {
            detail: {
                query,
                element: this
            },
            bubbles: true,
            composed: true
        }));
    }
    // ============================================================================
    // CHANGE EVENT DISPATCHING (Phase 5)
    // ============================================================================
    /**
     * Dispatch custom change event
     */
    dispatchChangeEvent(detail) {
        this.dispatchEvent(new CustomEvent('change', {
            detail,
            bubbles: true,
            cancelable: true
        }));
    }
    // ============================================================================
    // RENDERING
    // ============================================================================
    /**
     * Main render method (required by TyComponent)
     * Delegates to mode-specific renderer
     */
    render() {
        if (this._state.mode === 'mobile') {
            this.renderMobile();
        }
        else {
            this.renderDesktop();
        }
    }
    /**
     * Setup event listeners
     */
    setupEventListeners() {
        const shadow = this.shadowRoot;
        const stub = shadow.querySelector('.multiselect-stub');
        const optionsSlot = shadow.querySelector('#options-slot');
        const searchInput = shadow.querySelector('.dropdown-search-input');
        if (stub) {
            this._stubClickHandler = this.handleStubClick.bind(this);
            stub.addEventListener('click', this._stubClickHandler);
        }
        // Add tag click handler to slot
        if (optionsSlot) {
            this._tagClickHandler = this.handleTagClick.bind(this);
            optionsSlot.addEventListener('click', this._tagClickHandler);
        }
        // Add search input handlers
        if (searchInput) {
            this._searchInputHandler = this.handleSearchInput.bind(this);
            this._blockSearchClick = this.blockSearchClick.bind(this);
            searchInput.addEventListener('input', this._searchInputHandler);
            searchInput.addEventListener('click', this._blockSearchClick);
            // searchInput.addEventListener('blur', this._searchBlurHandler)
        }
        // Setup document-level event listeners
        this._outsideClickHandler = this.handleOutsideClick.bind(this);
        this._keyboardHandler = this.handleKeyboard.bind(this);
        document.addEventListener('click', this._outsideClickHandler);
        document.addEventListener('keydown', this._keyboardHandler);
        // Listen for dismiss events from selected tags
        this._tagDismissHandler = this.handleTagDismiss.bind(this);
        this.addEventListener('dismiss', this._tagDismissHandler);
    }
    /**
     * Build CSS class list for stub
     */
    buildStubClasses() {
        const classes = [this._size];
        if (this._disabled)
            classes.push('disabled');
        return classes.join(' ');
    }
    /**
     * Render desktop mode with dialog
     */
    renderDesktop() {
        const shadow = this.shadowRoot;
        // Only set innerHTML and setup listeners if container doesn't exist
        if (!shadow.querySelector('.multiselect-container')) {
            const stubClasses = this.buildStubClasses();
            const labelHtml = this._label ? `
        <label class="dropdown-label">
          ${this._label}
          ${this._required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}
        </label>
      ` : '';
            const searchPlaceholder = this._searchable ? 'Search...' : this._placeholder;
            shadow.innerHTML = `
        <div class="multiselect-container dropdown-mode-mobile">
          ${labelHtml}
          <div class="dropdown-wrapper">
            <div class="dropdown-stub multiselect-stub ${stubClasses}" 
                 ${this._disabled ? 'disabled' : ''}>
              <div class="multiselect-chips">
                <slot name="selected"></slot>
              </div>
              <span class="dropdown-placeholder">${this._placeholder}</span>
              <div class="dropdown-chevron">
                ${CHEVRON_DOWN_SVG}
              </div>
            </div>
            <dialog class="dropdown-dialog">
              <div class="dropdown-header">
                <input 
                  class="dropdown-search-input ${this._size}" 
                  type="text"
                  placeholder="${searchPlaceholder}"
                  ${this._disabled ? 'disabled' : ''}
                />
                <div class="dropdown-search-chevron">
                  ${CHEVRON_DOWN_SVG}
                </div>
              </div>
              <div class="dropdown-options">
                <slot id="options-slot"></slot>
              </div>
            </dialog>
          </div>
        </div>
      `;
            // Setup event listeners ONCE
            this.setupEventListeners();
            // Don't initialize here - will be done in connectedCallback
            // after properties are set and children are available
        }
        // Always update placeholder visibility on re-render
        this.updateSelectionDisplay();
    }
    /**
     * Render mobile mode with full-screen modal
     * Following dropdown.ts mobile structure
     */
    renderMobile() {
        const shadow = this.shadowRoot;
        // Only set innerHTML and setup listeners if container doesn't exist
        if (!shadow.querySelector('.multiselect-container')) {
            const stubClasses = this.buildStubClasses();
            const labelHtml = this._label ? `
        <label class="dropdown-label">
          ${this._label}
          ${this._required ? `<span class="required-icon">${REQUIRED_ICON_SVG}</span>` : ''}
        </label>
      ` : '';
            // Close button SVG (X icon)
            const closeButtonSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <line x1="18" y1="6" x2="6" y2="18"></line>
        <line x1="6" y1="6" x2="18" y2="18"></line>
      </svg>`;
            // Conditional search header - match dropdown.ts pattern
            const searchHeaderHtml = this._searchable ? `
        <div class="mobile-search-header">
          <div class="mobile-header-content">
            <input 
              class="mobile-search-input ${this._size}" 
              type="text"
              placeholder="Search..."
              ${this._disabled ? 'disabled' : ''}
            />
            <button class="mobile-close-button" type="button" aria-label="Close">
              ${closeButtonSvg}
            </button>
          </div>
        </div>
      ` : `
        <div class="mobile-header-nosearch">
          <button class="mobile-close-button" type="button" aria-label="Close">
            ${closeButtonSvg}
          </button>
        </div>
      `;
            shadow.innerHTML = `
        <div class="multiselect-container dropdown-mode-mobile">
          ${labelHtml}
          <div class="dropdown-wrapper">
            <div class="dropdown-stub multiselect-stub ${stubClasses}" 
                 ${this._disabled ? 'disabled' : ''}>
              <div class="multiselect-chips">
                <slot id="stub-slot" name="selected"></slot>
              </div>
              <span class="dropdown-placeholder">${this._placeholder}</span>
              <div class="dropdown-chevron">
                ${CHEVRON_DOWN_SVG}
              </div>
            </div>
            
            <dialog class="mobile-dialog">
              <div class="mobile-dialog-content">
                
                <!-- HEADER (matches dropdown.ts) -->
                ${searchHeaderHtml}
                
                <!-- BODY (two sections for multiselect) -->
                <div class="mobile-body">
                  
                  <!-- SELECTED SECTION (collapsed by default) -->
                  <div class="mobile-selected-section" data-expanded="false" data-empty="true">
                    <div class="section-header">
                      <span class="section-title">${this._selectedLabel} <span class="section-count">(0)</span></span>
                      <span class="section-chevron">${CHEVRON_DOWN_SVG}</span>
                    </div>
                    <div class="section-content">
                      <slot id="mobile-slot" name="selected"></slot>
                      <div class="empty-state">${this._noSelectionMessage}</div>
                    </div>
                  </div>
                  
                  <!-- AVAILABLE SECTION (expanded by default) -->
                  <div class="mobile-available-section" data-expanded="true" data-empty="false">
                    <div class="section-header">
                      <span class="section-title">${this._availableLabel}</span>
                    </div>
                    <div class="section-content">
                      <slot id="options-slot"></slot>
                      <div class="empty-state">${this._noOptionsMessage}</div>
                    </div>
                  </div>
                  
                </div>
              </div>
            </dialog>
          </div>
        </div>
      `;
            // Setup event listeners ONCE
            this.setupMobileEventListeners();
        }
        // Always update placeholder visibility
        this.updateSelectionDisplay();
    }
    /**
     * Setup event listeners for mobile mode
     * Using <dialog> element - backdrop clicks handled natively
     */
    setupMobileEventListeners() {
        const shadow = this.shadowRoot;
        const stub = shadow.querySelector('.multiselect-stub');
        const optionsSlot = shadow.querySelector('#options-slot');
        const searchInput = shadow.querySelector('.mobile-search-input');
        const closeButton = shadow.querySelector('.mobile-close-button');
        const dialog = shadow.querySelector('.mobile-dialog');
        // Get both section headers
        const selectedHeader = shadow.querySelector('.mobile-selected-section .section-header');
        const availableHeader = shadow.querySelector('.mobile-available-section .section-header');
        if (stub) {
            stub.addEventListener('click', (e) => this.handleMobileStubClick(e));
        }
        // Add tag click handler to slot
        if (optionsSlot) {
            optionsSlot.addEventListener('click', (e) => this.handleMobileTagClick(e));
        }
        // Add search input handlers (if searchable)
        if (searchInput) {
            searchInput.addEventListener('input', (e) => this.handleSearchInput(e));
        }
        // Toggle section expansion on header click
        if (selectedHeader) {
            selectedHeader.addEventListener('click', () => this.toggleSection('selected'));
        }
        if (availableHeader) {
            availableHeader.addEventListener('click', () => this.toggleSection('available'));
        }
        // Close button click
        if (closeButton) {
            closeButton.addEventListener('click', () => this.closeMobileModal());
        }
        this._tagDismissHandler = this.handleTagDismiss.bind(this);
        this.addEventListener('dismiss', this._tagDismissHandler);
        // Backdrop click to close (native dialog behavior)
        if (dialog) {
            dialog.addEventListener('click', (e) => {
                // Only close if clicking directly on the dialog element (backdrop)
                // Not if clicking on its children (dialog-content)
                if (e.target === dialog) {
                    this.closeMobileModal();
                }
            });
            // Also handle Escape key via cancel event
            dialog.addEventListener('cancel', (e) => {
                e.preventDefault(); // Prevent default to handle it our way
                this.closeMobileModal();
            });
        }
    }
    /**
     * Handle mobile stub click - open modal
     */
    handleMobileStubClick(e) {
        e.preventDefault();
        e.stopPropagation();
        if (this._disabled || this._readonly) {
            return;
        }
        // Don't open if all tags are already selected
        if (this.allTagsSelected()) {
            return;
        }
        this.openMobileModal();
    }
    /**
     * Handle mobile tag click - select and potentially close
     */
    handleMobileTagClick(e) {
        // Use the same tag click handler as desktop
        // It already handles mobile mode for auto-close
        this.handleTagClick(e);
    }
    /**
     * Toggle section expansion (mobile only)
     * Clicking expanded section collapses it and expands the other
     * Clicking collapsed section expands it and collapses the other
     */
    toggleSection(section) {
        const shadow = this.shadowRoot;
        const selectedSection = shadow.querySelector('.mobile-selected-section');
        const availableSection = shadow.querySelector('.mobile-available-section');
        if (!selectedSection || !availableSection)
            return;
        // If clicking already expanded section, collapse it and expand the other
        if (this._state.expandedSection === section) {
            const otherSection = section === 'selected' ? 'available' : 'selected';
            this._state.expandedSection = otherSection;
        }
        else {
            // Expand clicked section
            this._state.expandedSection = section;
        }
        // Update DOM attributes
        selectedSection.setAttribute('data-expanded', String(this._state.expandedSection === 'selected'));
        availableSection.setAttribute('data-expanded', String(this._state.expandedSection === 'available'));
    }
    /**
     * Sync section states to DOM without toggle logic
     */
    syncSectionStates() {
        const shadow = this.shadowRoot;
        const selectedSection = shadow.querySelector('.mobile-selected-section');
        const availableSection = shadow.querySelector('.mobile-available-section');
        if (!selectedSection || !availableSection)
            return;
        // Update DOM attributes to match current state
        selectedSection.setAttribute('data-expanded', String(this._state.expandedSection === 'selected'));
        availableSection.setAttribute('data-expanded', String(this._state.expandedSection === 'available'));
    }
    /**
     * Update mobile selected section state (collapsed view, empty states, etc.)
     */
    updateMobileSelectedState() {
        if (this._state.mode !== 'mobile')
            return;
        const shadow = this.shadowRoot;
        const selectedSection = shadow.querySelector('.mobile-selected-section');
        const availableSection = shadow.querySelector('.mobile-available-section');
        const sectionCountSpan = shadow.querySelector('.section-count');
        if (selectedSection) {
            const selectedCount = this._state.selectedValues.length;
            const hasSelected = selectedCount > 0;
            selectedSection.setAttribute('data-empty', String(!hasSelected));
            // Update header count
            if (sectionCountSpan) {
                sectionCountSpan.textContent = `(${selectedCount})`;
            }
        }
        if (availableSection) {
            const allTags = this.getTagElements();
            const availableCount = allTags.filter(tag => !tag.hasAttribute('selected')).length;
            const hasAvailable = availableCount > 0;
            availableSection.setAttribute('data-empty', String(!hasAvailable));
            // Update available header count
            const availableTitleSpan = shadow.querySelector('.mobile-available-section .section-title');
            if (availableTitleSpan) {
                availableTitleSpan.textContent = `${this._availableLabel} (${availableCount})`;
            }
        }
    }
    /**
     * Update selection display (show/hide placeholder)
     * Matches dropdown.ts pattern - uses CSS via has-selection class
     */
    updateSelectionDisplay() {
        const shadow = this.shadowRoot;
        const stub = shadow.querySelector('.multiselect-stub');
        if (!stub)
            return;
        const tags = this.getTagElements();
        const hasSelected = tags.some(tag => tag.hasAttribute('selected'));
        if (hasSelected) {
            stub.classList.add('has-selection');
        }
        else {
            stub.classList.remove('has-selection');
        }
    }
    // ============================================================================
    // PUBLIC API - Getters/Setters
    // ============================================================================
    get value() {
        // Always read from DOM - tags with 'selected' attribute are source of truth
        return this.getSelectedValues().join(',');
    }
    set value(val) {
        this.setProperty('value', val);
    }
    get name() {
        return this.getProperty('name');
    }
    set name(val) {
        this.setProperty('name', val);
    }
    get placeholder() {
        return this.getProperty('placeholder');
    }
    set placeholder(val) {
        this.setProperty('placeholder', val);
    }
    get label() {
        return this.getProperty('label');
    }
    set label(val) {
        this.setProperty('label', val);
    }
    get disabled() {
        return this.getProperty('disabled');
    }
    set disabled(value) {
        this.setProperty('disabled', value);
    }
    get readonly() {
        return this.getProperty('readonly');
    }
    set readonly(value) {
        this.setProperty('readonly', value);
    }
    get required() {
        return this.getProperty('required');
    }
    set required(value) {
        this.setProperty('required', value);
    }
    get searchable() {
        return this.getProperty('searchable');
    }
    set searchable(value) {
        this.setProperty('searchable', value);
    }
    get delay() {
        return this.getProperty('delay');
    }
    set delay(value) {
        const numValue = typeof value === 'string' ? parseInt(value, 10) : value;
        this.setProperty('delay', numValue);
    }
    get size() {
        return this.getProperty('size');
    }
    set size(value) {
        this.setProperty('size', value);
    }
    get flavor() {
        return this.getProperty('flavor');
    }
    set flavor(value) {
        this.setProperty('flavor', value);
    }
    get form() {
        return this._internals.form;
    }
}
// ============================================================================
// PROPERTY CONFIGURATION - Declarative property lifecycle
// ============================================================================
TyMultiselect.properties = {
    value: {
        type: 'string',
        visual: true,
        formValue: true,
        emitChange: false,
        default: '',
        coerce: (v) => {
            // Handle array input (from React, Reagent, etc.)
            if (Array.isArray(v)) {
                return v.join(',');
            }
            // Handle null/undefined
            if (v === null || v === undefined) {
                return '';
            }
            // String already
            return String(v);
        }
    },
    name: {
        type: 'string',
        default: ''
    },
    placeholder: {
        type: 'string',
        visual: true,
        default: 'Select options...'
    },
    label: {
        type: 'string',
        visual: true,
        default: ''
    },
    disabled: {
        type: 'boolean',
        visual: true,
        default: false
    },
    readonly: {
        type: 'boolean',
        visual: true,
        default: false
    },
    required: {
        type: 'boolean',
        visual: true,
        default: false
    },
    searchable: {
        type: 'boolean',
        visual: true,
        default: true,
        aliases: { 'not-searchable': false }
    },
    size: {
        type: 'string',
        visual: true,
        default: 'md',
        validate: (v) => ['sm', 'md', 'lg'].includes(v),
        coerce: (v) => {
            if (!['sm', 'md', 'lg'].includes(v)) {
                console.warn(`[ty-multiselect] Invalid size. Using md.`);
                return 'md';
            }
            return v;
        }
    },
    flavor: {
        type: 'string',
        visual: true,
        default: 'neutral',
        validate: (v) => ['primary', 'secondary', 'success', 'danger', 'warning', 'neutral'].includes(v),
        coerce: (v) => {
            const valid = ['primary', 'secondary', 'success', 'danger', 'warning', 'neutral'];
            if (!valid.includes(v)) {
                console.warn(`[ty-multiselect] Invalid flavor. Using neutral.`);
                return 'neutral';
            }
            return v;
        }
    },
    delay: {
        type: 'number',
        default: 0,
        validate: (v) => v >= 0 && v <= 5000,
        coerce: (v) => {
            const num = Number(v);
            if (isNaN(num))
                return 0;
            return Math.max(0, Math.min(5000, num));
        }
    },
    'selected-label': {
        type: 'string',
        visual: true,
        default: 'Selected'
    },
    'available-label': {
        type: 'string',
        visual: true,
        default: 'Available'
    },
    'no-selection-message': {
        type: 'string',
        visual: true,
        default: 'No items selected'
    },
    'no-options-message': {
        type: 'string',
        visual: true,
        default: 'No options available'
    }
};
// Register the custom element
if (!customElements.get('ty-multiselect')) {
    customElements.define('ty-multiselect', TyMultiselect);
}
//# sourceMappingURL=multiselect.js.map