Source: js/chart-utils.js

/**
 * @module ChartUtils
 * @category Visualization - Shared Chart.js & D3.js Utilities
 * 
 * @description
 * **Shared Chart Configuration & Utility Functions**
 * 
 * Centralized utility module for Chart.js and D3.js visualizations across all
 * 9 dashboard sections. Provides responsive configuration, empty/loading states,
 * accessibility helpers, and cyberpunk theme integration.
 * 
 * ## Features
 * 
 * 1. **Responsive Chart Configuration**: Mobile-first responsive options for Chart.js
 * 2. **Empty/Loading States**: User-friendly fallback UI components
 * 3. **Accessibility Helpers**: ARIA labels, keyboard navigation, screen reader support
 * 4. **Theme Integration**: Cyberpunk color palette with CSS custom properties
 * 5. **Number Formatting**: Swedish locale formatting (1,234,567)
 * 6. **Performance**: Debounced resize handlers, lazy rendering
 * 
 * ## Usage Examples
 * 
 * ```javascript
 * // Responsive Chart.js configuration
 * const chart = new Chart(ctx, {
 *   ...ChartUtils.getResponsiveOptions('bar'),
 *   data: { ... }
 * });
 * 
 * // Show loading state
 * ChartUtils.showLoadingState('partyEffectivenessChart');
 * 
 * // Handle empty data
 * if (!data || data.length === 0) {
 *   ChartUtils.showEmptyState('partyEffectivenessChart', 'No party data available');
 *   return;
 * }
 * ```
 * 
 * @author Hack23 AB - Political Intelligence Team
 * @license Apache-2.0
 * @version 1.0.0
 * @since 2026
 * 
 * @requires Chart.js v4.4.1
 */

(function(window) {
  'use strict';

  // ============================================================================
  // CONFIGURATION & CONSTANTS
  // ============================================================================

  /**
   * Cyberpunk theme colors from CSS custom properties
   * Fallback values provided for browsers without CSS variable support
   */
  const THEME_COLORS = {
    // Cyberpunk primary colors
    cyan: getComputedStyle(document.documentElement).getPropertyValue('--primary-cyan').trim() || '#00d9ff',
    magenta: getComputedStyle(document.documentElement).getPropertyValue('--primary-magenta').trim() || '#ff006e',
    yellow: getComputedStyle(document.documentElement).getPropertyValue('--primary-yellow').trim() || '#ffbe0b',
    
    // Background colors
    darkBg: getComputedStyle(document.documentElement).getPropertyValue('--dark-bg').trim() || '#0a0e27',
    midBg: getComputedStyle(document.documentElement).getPropertyValue('--mid-bg').trim() || '#1a1e3d',
    
    // Text colors
    lightText: getComputedStyle(document.documentElement).getPropertyValue('--light-text').trim() || '#e0e0e0',
    
    // Party colors (Swedish political parties)
    parties: {
      'S': '#E8112d',   // Socialdemokraterna (Red)
      'M': '#52B6EC',   // Moderaterna (Blue)
      'SD': '#DDDD00',  // Sverigedemokraterna (Yellow)
      'C': '#009933',   // Centerpartiet (Green)
      'V': '#DA291C',   // Vänsterpartiet (Red)
      'KD': '#000077',  // Kristdemokraterna (Dark Blue)
      'L': '#006AB3',   // Liberalerna (Blue)
      'MP': '#83CF39'   // Miljöpartiet (Green)
    }
  };

  /**
   * Responsive breakpoints (mobile-first)
   */
  const BREAKPOINTS = {
    mobile: 320,
    tablet: 768,
    desktop: 1024,
    large: 1440
  };

  // ============================================================================
  // RESPONSIVE CHART CONFIGURATION
  // ============================================================================

  /**
   * Get responsive Chart.js options based on chart type and screen size
   * 
   * @param {string} chartType - Chart type: 'bar', 'line', 'pie', 'doughnut', 'scatter', 'radar'
   * @param {Object} customOptions - Optional custom options to merge
   * @returns {Object} Chart.js configuration object
   */
  function getResponsiveOptions(chartType = 'bar', customOptions = {}) {
    const isMobile = window.innerWidth < BREAKPOINTS.tablet;
    
    const baseOptions = {
      responsive: true,
      maintainAspectRatio: false, // Allow height control via CSS
      plugins: {
        legend: {
          position: isMobile ? 'bottom' : 'top',
          labels: {
            font: {
              family: "'Inter', sans-serif",
              size: isMobile ? 10 : 12
            },
            color: THEME_COLORS.lightText,
            padding: isMobile ? 8 : 12,
            usePointStyle: true, // Use circles instead of rectangles
            boxWidth: isMobile ? 8 : 12,
            boxHeight: isMobile ? 8 : 12
          }
        },
        tooltip: {
          backgroundColor: THEME_COLORS.darkBg,
          titleColor: THEME_COLORS.cyan,
          bodyColor: THEME_COLORS.lightText,
          borderColor: THEME_COLORS.cyan,
          borderWidth: 1,
          padding: 12,
          displayColors: true,
          callbacks: {
            label: function(context) {
              let label = context.dataset.label || '';
              if (label) {
                label += ': ';
              }
              // Format numbers with Swedish locale (1,234,567)
              if (context.parsed.y !== null) {
                label += formatNumber(context.parsed.y);
              } else if (context.parsed !== null) {
                label += formatNumber(context.parsed);
              }
              return label;
            }
          }
        }
      }
    };

    // Add axes configuration for bar/line/scatter charts
    if (['bar', 'line', 'scatter'].includes(chartType)) {
      baseOptions.scales = {
        y: {
          ticks: {
            color: THEME_COLORS.lightText,
            font: {
              family: "'Inter', sans-serif",
              size: isMobile ? 9 : 11
            },
            callback: function(value) {
              return formatNumber(value);
            }
          },
          grid: {
            color: 'rgba(255, 255, 255, 0.1)',
            borderColor: THEME_COLORS.lightText
          }
        },
        x: {
          ticks: {
            color: THEME_COLORS.lightText,
            font: {
              family: "'Inter', sans-serif",
              size: isMobile ? 9 : 11
            },
            maxRotation: isMobile ? 90 : 45,
            minRotation: isMobile ? 90 : 0
          },
          grid: {
            color: 'rgba(255, 255, 255, 0.05)',
            borderColor: THEME_COLORS.lightText
          }
        }
      };
    }

    // Merge custom options
    return deepMerge(baseOptions, customOptions);
  }

  // ============================================================================
  // EMPTY/LOADING/ERROR STATES
  // ============================================================================

  /**
   * Show loading state in chart container
   * 
   * @param {string} containerId - ID of chart container element
   */
  function showLoadingState(containerId) {
    const container = document.getElementById(containerId);
    if (!container) {
      console.warn(`Container not found: ${containerId}`);
      return;
    }

    // Build loading state overlay safely using DOM APIs
    const loadingState = document.createElement('div');
    loadingState.className = 'chart-loading-state';
    loadingState.setAttribute('role', 'status');
    loadingState.setAttribute('aria-live', 'polite');
    loadingState.setAttribute('aria-label', 'Loading chart data');

    const spinner = document.createElement('div');
    spinner.className = 'spinner';
    spinner.setAttribute('aria-hidden', 'true');

    const message = document.createElement('p');
    message.textContent = 'Loading data...';

    loadingState.appendChild(spinner);
    loadingState.appendChild(message);

    // Insert before the container
    if (container.parentNode) {
      container.parentNode.insertBefore(loadingState, container);
    }
    
    // Hide the canvas/container temporarily
    container.style.display = 'none';
  }

  /**
   * Show empty state when no data available
   * 
   * @param {string} containerId - ID of chart container element
   * @param {string} message - Custom message (optional)
   */
  function showEmptyState(containerId, message = 'No data available') {
    const container = document.getElementById(containerId);
    if (!container) {
      console.warn(`Container not found: ${containerId}`);
      return;
    }

    // Remove loading state if present
    const loadingState = container.previousElementSibling;
    if (loadingState && loadingState.classList.contains('chart-loading-state')) {
      loadingState.remove();
    }

    // Build empty state overlay safely using DOM APIs
    const emptyState = document.createElement('div');
    emptyState.className = 'chart-empty-state';
    emptyState.setAttribute('role', 'status');
    emptyState.setAttribute('aria-live', 'polite');

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('class', 'empty-icon');
    svg.setAttribute('width', '64');
    svg.setAttribute('height', '64');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'currentColor');
    svg.setAttribute('aria-hidden', 'true');

    const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path1.setAttribute('d', 'M3 3v18h18');
    svg.appendChild(path1);

    const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path2.setAttribute('d', 'M18 17V9');
    svg.appendChild(path2);

    const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path3.setAttribute('d', 'M13 17V5');
    svg.appendChild(path3);

    const path4 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path4.setAttribute('d', 'M8 17v-3');
    svg.appendChild(path4);

    const heading = document.createElement('h3');
    heading.textContent = 'No Data Available';

    const messagePara = document.createElement('p');
    // Use textContent to avoid injecting untrusted HTML
    messagePara.textContent = message;

    const helpText = document.createElement('p');
    helpText.className = 'help-text';
    helpText.textContent = 'Check back later or ';
    
    const contactLink = document.createElement('a');
    contactLink.href = 'mailto:support@riksdagsmonitor.com';
    contactLink.textContent = 'contact support';
    helpText.appendChild(contactLink);
    helpText.appendChild(document.createTextNode('.'));

    emptyState.appendChild(svg);
    emptyState.appendChild(heading);
    emptyState.appendChild(messagePara);
    emptyState.appendChild(helpText);

    // Insert before the container
    if (container.parentNode) {
      container.parentNode.insertBefore(emptyState, container);
    }
    
    container.style.display = 'none';
  }

  /**
   * Show error state when data loading fails
   * 
   * @param {string} containerId - ID of chart container element
   * @param {string} error - Error message
   */
  function showErrorState(containerId, error = 'Failed to load data') {
    const container = document.getElementById(containerId);
    if (!container) {
      console.warn(`Container not found: ${containerId}`);
      return;
    }

    // Remove loading state if present
    const loadingState = container.previousElementSibling;
    if (loadingState && loadingState.classList.contains('chart-loading-state')) {
      loadingState.remove();
    }

    // Build error state overlay safely using DOM APIs
    const errorState = document.createElement('div');
    errorState.className = 'chart-error-state';
    errorState.setAttribute('role', 'alert');
    errorState.setAttribute('aria-live', 'assertive');

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('class', 'error-icon');
    svg.setAttribute('width', '64');
    svg.setAttribute('height', '64');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'currentColor');
    svg.setAttribute('aria-hidden', 'true');

    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', '12');
    circle.setAttribute('cy', '12');
    circle.setAttribute('r', '10');
    svg.appendChild(circle);

    const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line1.setAttribute('x1', '12');
    line1.setAttribute('y1', '8');
    line1.setAttribute('x2', '12');
    line1.setAttribute('y2', '12');
    svg.appendChild(line1);

    const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line2.setAttribute('x1', '12');
    line2.setAttribute('y1', '16');
    line2.setAttribute('x2', '12.01');
    line2.setAttribute('y2', '16');
    svg.appendChild(line2);

    const heading = document.createElement('h3');
    heading.textContent = 'Error Loading Data';

    const messagePara = document.createElement('p');
    // Use textContent to avoid injecting untrusted HTML
    messagePara.textContent = error;

    const retryButton = document.createElement('button');
    retryButton.className = 'retry-button';
    retryButton.textContent = 'Retry';
    // Attach click handler programmatically to avoid inline event handlers
    retryButton.addEventListener('click', function() {
      window.location.reload();
    });

    errorState.appendChild(svg);
    errorState.appendChild(heading);
    errorState.appendChild(messagePara);
    errorState.appendChild(retryButton);

    // Insert before the container
    if (container.parentNode) {
      container.parentNode.insertBefore(errorState, container);
    }
    
    container.style.display = 'none';
  }

  /**
   * Hide empty/loading/error states and show chart
   * 
   * @param {string} containerId - ID of chart container element
   */
  function hideStateOverlays(containerId) {
    const container = document.getElementById(containerId);
    if (!container) return;

    // Remove all state overlays
    const states = ['chart-loading-state', 'chart-empty-state', 'chart-error-state'];
    states.forEach(stateClass => {
      const element = container.previousElementSibling;
      if (element && element.classList.contains(stateClass)) {
        element.remove();
      }
    });

    // Show the container
    container.style.display = '';
  }

  // ============================================================================
  // ACCESSIBILITY HELPERS
  // ============================================================================

  /**
   * Add keyboard navigation to chart canvas
   * 
   * @param {HTMLCanvasElement} canvas - Canvas element
   * @param {Chart} chart - Chart.js instance
   */
  function addKeyboardNavigation(canvas, chart) {
    let currentDataPointIndex = 0;
    
    canvas.setAttribute('tabindex', '0');
    canvas.setAttribute('role', 'img');
    
    canvas.addEventListener('keydown', (e) => {
      const datasetLength = chart.data.datasets[0].data.length;
      
      if (e.key === 'ArrowRight') {
        e.preventDefault();
        currentDataPointIndex = (currentDataPointIndex + 1) % datasetLength;
        announceDataPoint(chart, currentDataPointIndex);
        highlightDataPoint(chart, currentDataPointIndex);
      } else if (e.key === 'ArrowLeft') {
        e.preventDefault();
        currentDataPointIndex = (currentDataPointIndex - 1 + datasetLength) % datasetLength;
        announceDataPoint(chart, currentDataPointIndex);
        highlightDataPoint(chart, currentDataPointIndex);
      }
    });
  }

  /**
   * Announce data point to screen readers
   * 
   * @param {Chart} chart - Chart.js instance
   * @param {number} index - Data point index
   */
  function announceDataPoint(chart, index) {
    const label = chart.data.labels[index];
    const value = chart.data.datasets[0].data[index];
    const announcement = `${label}: ${formatNumber(value)}`;
    
    // Create or update live region
    let liveRegion = document.getElementById('chart-live-region');
    if (!liveRegion) {
      liveRegion = document.createElement('div');
      liveRegion.id = 'chart-live-region';
      liveRegion.setAttribute('role', 'status');
      liveRegion.setAttribute('aria-live', 'polite');
      liveRegion.className = 'sr-only';
      document.body.appendChild(liveRegion);
    }
    
    liveRegion.textContent = announcement;
  }

  /**
   * Highlight data point on chart (for keyboard navigation)
   * 
   * @param {Chart} chart - Chart.js instance
   * @param {number} index - Data point index
   */
  function highlightDataPoint(chart, index) {
    // Guard for disabled tooltips
    const tooltip = chart.tooltip;
    if (!tooltip) {
      return;
    }

    const meta = chart.getDatasetMeta(0);
    const element = meta && meta.data && meta.data[index];
    if (!element) {
      return;
    }

    // Determine tooltip position for Chart.js v4
    const position = typeof element.tooltipPosition === 'function'
      ? element.tooltipPosition()
      : { x: element.x, y: element.y };

    // Trigger tooltip programmatically with position
    tooltip.setActiveElements(
      [{ datasetIndex: 0, index: index }],
      position
    );
    chart.update();
  }

  // ============================================================================
  // FORMATTING UTILITIES
  // ============================================================================

  /**
   * Format number with Swedish locale (thousands separator)
   * 
   * @param {number} value - Number to format
   * @param {number} decimals - Number of decimal places (default: 0)
   * @returns {string} Formatted number
   */
  function formatNumber(value, decimals = 0) {
    if (value === null || value === undefined || isNaN(value)) {
      return 'N/A';
    }
    
    return value.toLocaleString('sv-SE', {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals
    });
  }

  /**
   * Format percentage
   * 
   * @param {number} value - Number to format as percentage
   * @returns {string} Formatted percentage
   */
  function formatPercent(value) {
    if (value === null || value === undefined || isNaN(value)) {
      return 'N/A';
    }
    
    return value.toLocaleString('sv-SE', {
      style: 'percent',
      minimumFractionDigits: 1,
      maximumFractionDigits: 1
    });
  }

  // ============================================================================
  // PERFORMANCE UTILITIES
  // ============================================================================

  /**
   * Debounce function for performance optimization
   * 
   * @param {Function} func - Function to debounce
   * @param {number} wait - Wait time in milliseconds
   * @returns {Function} Debounced function
   */
  function debounce(func, wait = 250) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  /**
   * Create debounced resize handler for chart updates
   * 
   * @param {Chart[]} charts - Array of Chart.js instances
   * @returns {Function} Debounced resize handler
   */
  function createResizeHandler(charts) {
    return debounce(() => {
      const isMobile = window.innerWidth < BREAKPOINTS.tablet;
      
      charts.forEach(chart => {
        if (chart && chart.options && chart.options.plugins && chart.options.plugins.legend) {
          chart.options.plugins.legend.position = isMobile ? 'bottom' : 'top';
          chart.update();
        }
      });
    }, 250);
  }

  // ============================================================================
  // UTILITY FUNCTIONS
  // ============================================================================

  /**
   * Deep merge objects
   * 
   * @param {Object} target - Target object
   * @param {Object} source - Source object
   * @returns {Object} Merged object
   */
  function deepMerge(target, source) {
    const output = Object.assign({}, target);
    if (isObject(target) && isObject(source)) {
      Object.keys(source).forEach(key => {
        if (isObject(source[key])) {
          if (!(key in target)) {
            Object.assign(output, { [key]: source[key] });
          } else {
            output[key] = deepMerge(target[key], source[key]);
          }
        } else {
          Object.assign(output, { [key]: source[key] });
        }
      });
    }
    return output;
  }

  /**
   * Check if value is an object
   * 
   * @param {*} item - Value to check
   * @returns {boolean} True if object
   */
  function isObject(item) {
    return item && typeof item === 'object' && !Array.isArray(item);
  }

  // ============================================================================
  // PUBLIC API
  // ============================================================================

  window.ChartUtils = {
    // Configuration
    THEME_COLORS,
    BREAKPOINTS,
    
    // Responsive options
    getResponsiveOptions,
    
    // State management
    showLoadingState,
    showEmptyState,
    showErrorState,
    hideStateOverlays,
    
    // Accessibility
    addKeyboardNavigation,
    announceDataPoint,
    
    // Formatting
    formatNumber,
    formatPercent,
    
    // Performance
    debounce,
    createResizeHandler
  };

})(window);