/**
 * @fileoverview CSS Scoping System for OrdoJS Framework
 * Handles scoping CSS selectors to prevent style conflicts between components
 */

import {
    type CSSRuleNode,
    type StyleBlockNode
} from '../types/index.js';

/**
 * CSS Scoping options
 */
export interface CSSScopingOptions {
  /**
   * Prefix to use for scoped selectors
   */
  scopePrefix?: string;

  /**
   * Whether to use data attributes for scoping (true) or class names (false)
   */
  useDataAttributes?: boolean;

  /**
   * Whether to preserve original selectors in addition to scoped ones
   */
  preserveOriginalSelectors?: boolean;
}

/**
 * Default CSS Scoping options
 */
const DEFAULT_CSS_SCOPING_OPTIONS: CSSScopingOptions = {
  scopePrefix: 'ordojs',
  useDataAttributes: true,
  preserveOriginalSelectors: false
};

/**
 * CSS Scoper for OrdoJS components
 */
export class OrdoJSCSSScoper {
  private options: CSSScopingOptions;

  constructor(options: Partial<CSSScopingOptions> = {}) {
    this.options = { ...DEFAULT_CSS_SCOPING_OPTIONS, ...options };
  }

  /**
   * Scope CSS rules to a specific component
   */
  scopeStyles(styleBlock: StyleBlockNode, componentName: string): StyleBlockNode {
    // If the style block is not scoped, return it as is
    if (!styleBlock.scoped) {
      return styleBlock;
    }

    // Generate a unique scope identifier for the component
    const scopeId = this.generateScopeId(componentName);

    // Scope each rule
    const scopedRules = styleBlock.rules.map(rule => this.scopeRule(rule, scopeId));

    return {
      ...styleBlock,
      rules: scopedRules
    };
  }

  /**
   * Scope a single CSS rule
   */
  private scopeRule(rule: CSSRuleNode, scopeId: string): CSSRuleNode {
    const scopedSelector = this.scopeSelector(rule.selector, scopeId);

    return {
      ...rule,
      selector: scopedSelector
    };
  }

  /**
   * Scope a CSS selector
   */
  private scopeSelector(selector: string, scopeId: string): string {
    // Split complex selectors (comma-separated)
    const selectors = selector.split(',').map(s => s.trim());

    // Scope each individual selector
    const scopedSelectors = selectors.map(s => this.scopeSingleSelector(s, scopeId));

    // If preserving original selectors, include both versions
    if (this.options.preserveOriginalSelectors) {
      return [...selectors, ...scopedSelectors].join(', ');
    }

    return scopedSelectors.join(', ');
  }

  /**
   * Scope a single CSS selector (without commas)
   */
  private scopeSingleSelector(selector: string, scopeId: string): string {
    // Handle special selectors
    if (selector === ':root' || selector === 'html' || selector === 'body') {
      return selector;
    }

    // Handle complex selectors with combinators
    const combinatorRegex = /(\s*[>+~]\s*)/;
    const parts = selector.split(combinatorRegex);

    // If there are combinators, scope each part separately
    if (parts.length > 1) {
      const scopedParts = parts.map((part, index) => {
        // Skip combinator parts (odd indices)
        if (index % 2 === 1) {
          return part;
        }

        // Scope the selector part
        return this.scopeSelectorPart(part.trim(), scopeId);
      });

      return scopedParts.join('');
    }

    // Simple selector without combinators
    return this.scopeSelectorPart(selector, scopeId);
  }

  /**
   * Scope a single selector part (no combinators)
   */
  private scopeSelectorPart(selector: string, scopeId: string): string {
    // Handle pseudo-elements and pseudo-classes
    const hasPseudoElement = /::([a-z-]+)$/.test(selector);
    const hasPseudoClass = /:([a-z-]+)$/.test(selector);

    let baseSelector = selector;
    let pseudoSuffix = '';

    // Extract pseudo-elements and pseudo-classes to append them after scoping
    if (hasPseudoElement) {
      const match = selector.match(/::([a-z-]+)$/);
      if (match) {
        baseSelector = selector.substring(0, selector.length - match[0].length);
        pseudoSuffix = match[0];
      }
    } else if (hasPseudoClass) {
      const match = selector.match(/:([a-z-]+)$/);
      if (match) {
        baseSelector = selector.substring(0, selector.length - match[0].length);
        pseudoSuffix = match[0];
      }
    }

    // Apply scoping attribute or class
    let scopedSelector: string;
    if (this.options.useDataAttributes) {
      scopedSelector = `${baseSelector}[data-${this.options.scopePrefix}-${scopeId}]${pseudoSuffix}`;
    } else {
      scopedSelector = `${baseSelector}.${this.options.scopePrefix}-${scopeId}${pseudoSuffix}`;
    }

    return scopedSelector;
  }

  /**
   * Generate a unique scope ID for a component
   */
  private generateScopeId(componentName: string): string {
    // Create a hash from the component name for uniqueness
    const hash = this.hashString(componentName);
    return `${componentName.toLowerCase()}-${hash}`;
  }

  /**
   * Generate a simple hash from a string
   */
  private hashString(str: string): string {
    let hash = 0;

    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // Convert to 32bit integer
    }

    // Convert to a positive hex string and take the first 8 characters
    return Math.abs(hash).toString(16).substring(0, 8);
  }

  /**
   * Generate the data attribute or class name for component markup
   */
  generateScopeAttribute(componentName: string): string {
    const scopeId = this.generateScopeId(componentName);

    if (this.options.useDataAttributes) {
      return `data-${this.options.scopePrefix}-${scopeId}`;
    } else {
      return `class="${this.options.scopePrefix}-${scopeId}"`;
    }
  }

  /**
   * Convert a scoped StyleBlockNode back to CSS text
   */
  generateScopedCSS(styleBlock: StyleBlockNode): string {
    return styleBlock.rules.map(rule => {
      const declarations = rule.declarations.map(decl =>
        `  ${decl.property}: ${decl.value};`
      ).join('\n');

      return `${rule.selector} {\n${declarations}\n}`;
    }).join('\n\n');
  }
}
