Source: frontend/workbench/public/js/utils/DomUtils.js

/**
 * DOM Utilities for Semantic Memory Workbench
 * Helper functions for DOM manipulation, event handling, and UI interactions
 */

export class DomUtils {
  
  // ===== ELEMENT SELECTION AND CREATION =====

  /**
   * Safe element selector with error handling
   */
  static $(selector, context = document) {
    try {
      return context.querySelector(selector);
    } catch (error) {
      console.warn(`Invalid selector: ${selector}`, error);
      return null;
    }
  }

  /**
   * Select all elements with error handling
   */
  static $$(selector, context = document) {
    try {
      return Array.from(context.querySelectorAll(selector));
    } catch (error) {
      console.warn(`Invalid selector: ${selector}`, error);
      return [];
    }
  }

  /**
   * Create element with attributes and content
   */
  static createElement(tag, attributes = {}, content = '') {
    const element = document.createElement(tag);
    
    // Set attributes
    Object.entries(attributes).forEach(([key, value]) => {
      if (key === 'className') {
        element.className = value;
      } else if (key === 'innerHTML') {
        element.innerHTML = value;
      } else if (key === 'textContent') {
        element.textContent = value;
      } else if (key.startsWith('data-')) {
        element.setAttribute(key, value);
      } else {
        element[key] = value;
      }
    });
    
    // Set content
    if (content) {
      if (typeof content === 'string') {
        element.textContent = content;
      } else if (content instanceof HTMLElement) {
        element.appendChild(content);
      } else if (Array.isArray(content)) {
        content.forEach(child => {
          if (typeof child === 'string') {
            element.appendChild(document.createTextNode(child));
          } else if (child instanceof HTMLElement) {
            element.appendChild(child);
          }
        });
      }
    }
    
    return element;
  }

  // ===== EVENT HANDLING =====

  /**
   * Add event listener with automatic cleanup
   */
  static addListener(element, event, handler, options = {}) {
    if (!element || typeof handler !== 'function') return null;
    
    element.addEventListener(event, handler, options);
    
    // Return cleanup function
    return () => element.removeEventListener(event, handler, options);
  }

  /**
   * Add event delegation
   */
  static delegate(container, selector, event, handler) {
    if (!container || !selector || typeof handler !== 'function') return null;
    
    const delegatedHandler = (e) => {
      const target = e.target.closest(selector);
      if (target && container.contains(target)) {
        handler.call(target, e);
      }
    };
    
    container.addEventListener(event, delegatedHandler);
    
    // Return cleanup function
    return () => container.removeEventListener(event, delegatedHandler);
  }

  /**
   * Debounce event handler
   */
  static debounce(func, delay = 300) {
    let timeoutId;
    return function (...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
  }

  /**
   * Throttle event handler
   */
  static throttle(func, limit = 100) {
    let inThrottle;
    return function (...args) {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  // ===== FORM UTILITIES =====

  /**
   * Get form data as object
   */
  static getFormData(form) {
    if (!form) return {};
    
    const formData = new FormData(form);
    const data = {};
    
    // First, get all form elements to handle unchecked checkboxes
    const formElements = form.querySelectorAll('input, select, textarea');
    
    // Initialize all checkbox values to false
    formElements.forEach(element => {
      if (element.type === 'checkbox') {
        data[element.name] = false;
      }
    });
    
    // Then process FormData (this will override checkbox values for checked ones)
    for (const [key, value] of formData.entries()) {
      // Handle checkboxes explicitly
      if (form.querySelector(`input[name="${key}"][type="checkbox"]`)) {
        data[key] = true; // If it's in FormData, the checkbox is checked
      }
      // Handle multiple values (multi-select, etc.)
      else if (data.hasOwnProperty(key) && data[key] !== false) {
        if (Array.isArray(data[key])) {
          data[key].push(value);
        } else {
          data[key] = [data[key], value];
        }
      } else {
        data[key] = value;
      }
    }
    
    return data;
  }

  /**
   * Set form data from object
   */
  static setFormData(form, data) {
    if (!form || !data) return;
    
    Object.entries(data).forEach(([key, value]) => {
      const elements = form.elements[key];
      if (!elements) return;
      
      if (elements.length > 1) {
        // Multiple elements (radio buttons, checkboxes)
        Array.from(elements).forEach(element => {
          if (element.type === 'checkbox' || element.type === 'radio') {
            element.checked = Array.isArray(value) ? value.includes(element.value) : element.value === value;
          } else {
            element.value = value;
          }
        });
      } else {
        // Single element
        const element = elements;
        if (element.type === 'checkbox') {
          element.checked = Boolean(value);
        } else {
          element.value = value || '';
        }
      }
    });
  }

  /**
   * Reset form with custom values
   */
  static resetForm(form, defaults = {}) {
    if (!form) return;
    
    form.reset();
    if (Object.keys(defaults).length > 0) {
      this.setFormData(form, defaults);
    }
  }

  // ===== VISIBILITY AND ANIMATION =====

  /**
   * Show element with optional animation
   */
  static show(element, animation = 'fadeIn') {
    if (!element) return;
    
    element.style.display = '';
    
    if (animation && CSS.supports('animation', animation)) {
      element.style.animation = `${animation} 0.3s ease-out`;
      element.addEventListener('animationend', () => {
        element.style.animation = '';
      }, { once: true });
    }
  }

  /**
   * Hide element with optional animation
   */
  static hide(element, animation = 'fadeOut') {
    if (!element) return;
    
    if (animation && CSS.supports('animation', animation)) {
      element.style.animation = `${animation} 0.3s ease-out`;
      element.addEventListener('animationend', () => {
        element.style.display = 'none';
        element.style.animation = '';
      }, { once: true });
    } else {
      element.style.display = 'none';
    }
  }

  /**
   * Toggle element visibility
   */
  static toggle(element, force) {
    if (!element) return;
    
    const isHidden = element.style.display === 'none' || !element.offsetParent;
    
    if (force !== undefined) {
      force ? this.show(element) : this.hide(element);
    } else {
      isHidden ? this.show(element) : this.hide(element);
    }
  }

  /**
   * Smooth scroll to element
   */
  static scrollToElement(element, options = {}) {
    if (!element) return;
    
    const defaultOptions = {
      behavior: 'smooth',
      block: 'start',
      inline: 'nearest'
    };
    
    element.scrollIntoView({ ...defaultOptions, ...options });
  }

  // ===== CSS UTILITIES =====

  /**
   * Add CSS class with optional timeout
   */
  static addClass(element, className, timeout) {
    if (!element || !className) return;
    
    element.classList.add(className);
    
    if (timeout) {
      setTimeout(() => element.classList.remove(className), timeout);
    }
  }

  /**
   * Remove CSS class
   */
  static removeClass(element, className) {
    if (!element || !className) return;
    element.classList.remove(className);
  }

  /**
   * Toggle CSS class
   */
  static toggleClass(element, className, force) {
    if (!element || !className) return;
    return element.classList.toggle(className, force);
  }

  /**
   * Check if element has class
   */
  static hasClass(element, className) {
    if (!element || !className) return false;
    return element.classList.contains(className);
  }

  // ===== LOADING AND STATES =====

  /**
   * Set loading state on button
   */
  static setButtonLoading(button, loading) {
    if (!button) return;
    
    const textElement = button.querySelector('.button-text');
    const loaderElement = button.querySelector('.button-loader');
    
    button.disabled = loading;
    
    if (textElement) {
      textElement.style.display = loading ? 'none' : '';
    }
    
    if (loaderElement) {
      loaderElement.style.display = loading ? '' : 'none';
    }
  }

  /**
   * Show loading overlay
   */
  static showLoading(message = 'Loading...') {
    const overlay = this.$('#loading-overlay');
    if (!overlay) return;
    
    const textElement = overlay.querySelector('.loading-text');
    if (textElement && message) {
      textElement.textContent = message;
    }
    
    this.show(overlay);
    return overlay;
  }

  /**
   * Hide loading overlay
   */
  static hideLoading() {
    const overlay = this.$('#loading-overlay');
    if (overlay) {
      this.hide(overlay);
    }
  }

  // ===== TOAST NOTIFICATIONS =====

  /**
   * Show toast notification
   */
  static showToast(message, type = 'info', duration = 5000) {
    const container = this.$('#toast-container');
    if (!container) return;
    
    const icons = {
      success: '✅',
      error: '❌',
      warning: '⚠️',
      info: 'ℹ️'
    };
    
    const toast = this.createElement('div', {
      className: `toast ${type}`
    }, [
      this.createElement('span', { className: 'toast-icon' }, icons[type] || icons.info),
      this.createElement('span', { className: 'toast-message' }, message),
      this.createElement('button', { 
        className: 'toast-close',
        type: 'button'
      }, '×')
    ]);
    
    // Add close handler
    const closeButton = toast.querySelector('.toast-close');
    closeButton.addEventListener('click', () => toast.remove());
    
    // Auto-remove after duration
    if (duration > 0) {
      setTimeout(() => {
        if (toast.parentNode) {
          toast.remove();
        }
      }, duration);
    }
    
    container.appendChild(toast);
    return toast;
  }

  // ===== MESSAGE DISPLAY =====

  /**
   * Show message in a container
   */
  static showMessage(container, message, type = 'info') {
    if (!container) return;
    
    const messageElement = this.createElement('div', {
      className: `message ${type}`
    }, [
      this.createElement('div', { className: 'message-content' }, message)
    ]);
    
    // Clear previous messages
    container.innerHTML = '';
    container.appendChild(messageElement);
    this.show(container);
    
    return messageElement;
  }

  // ===== ACCESSIBILITY =====

  /**
   * Set ARIA attributes
   */
  static setAriaAttributes(element, attributes) {
    if (!element || !attributes) return;
    
    Object.entries(attributes).forEach(([key, value]) => {
      const ariaKey = key.startsWith('aria-') ? key : `aria-${key}`;
      element.setAttribute(ariaKey, value);
    });
  }

  /**
   * Announce to screen readers
   */
  static announce(message, priority = 'polite') {
    const announcer = document.createElement('div');
    announcer.setAttribute('aria-live', priority);
    announcer.setAttribute('aria-atomic', 'true');
    announcer.className = 'sr-only';
    announcer.textContent = message;
    
    document.body.appendChild(announcer);
    
    // Remove after announcement
    setTimeout(() => announcer.remove(), 1000);
  }

  // ===== DATA FORMATTING =====

  /**
   * Format bytes to human readable
   */
  static formatBytes(bytes, decimals = 2) {
    if (bytes === 0) return '0 Bytes';
    
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  }

  /**
   * Format duration to human readable
   */
  static formatDuration(seconds) {
    if (seconds < 60) return `${seconds}s`;
    if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
    
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    return `${hours}h ${minutes}m`;
  }

  /**
   * Escape HTML content
   */
  static escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  /**
   * Truncate text with ellipsis
   */
  static truncate(text, maxLength = 100, suffix = '...') {
    if (!text || text.length <= maxLength) return text;
    return text.substring(0, maxLength - suffix.length) + suffix;
  }
}

// Export static class
export default DomUtils;