/**
 * @fileoverview OrdoJS DOM Optimizer - Generates efficient DOM update code
 */

import {
    DirectiveType,
    ExpressionType,
    type AttributeNode,
    type ComponentAST,
    type ComponentNode,
    type ExpressionNode,
    type HTMLElementNode,
    type InterpolationNode,
    type MarkupBlockNode,
    type ReactiveVariableNode
} from '../types/index.js';

import {
    type DependencyGraph
} from './dependency-analyzer.js';

/**
 * DOM update operation
 */
export interface DOMUpdateOperation {
  id: string;
  type: DOMUpdateType;
  selector: string;
  property: string;
  expression: string;
  dependencies: string[];
  batchGroup?: string;
}

/**
 * Types of DOM update operations
 */
export enum DOMUpdateType {
  TEXT_CONTENT = 'TEXT_CONTENT',
  ATTRIBUTE = 'ATTRIBUTE',
  PROPERTY = 'PROPERTY',
  CLASS_TOGGLE = 'CLASS_TOGGLE',
  STYLE = 'STYLE',
  VISIBILITY = 'VISIBILITY',
  LIST_UPDATE = 'LIST_UPDATE',
  CONDITIONAL_RENDER = 'CONDITIONAL_RENDER'
}

/**
 * Batched update group
 */
export interface UpdateBatch {
  id: string;
  operations: DOMUpdateOperation[];
  dependencies: string[];
  priority: number;
}

/**
 * Two-way binding configuration
 */
export interface TwoWayBinding {
  elementSelector: string;
  variableName: string;
  eventType: string;
  propertyName: string;
  transformFunction?: string;
}

/**
 * DOM optimizer for efficient DOM manipulation code generation
 */
export class DOMOptimizer {
  private componentId: string = '';
  private updateOperations: Map<string, DOMUpdateOperation[]> = new Map();
  private updateBatches: UpdateBatch[] = [];
  private twoWayBindings: TwoWayBinding[] = [];
  private elementCounter: number = 0;

  /**
   * Optimize DOM updates for a component
   */
  optimize(ast: ComponentAST, dependencyGraph: DependencyGraph): {
    updateCode: string;
    setupCode: string;
    cleanupCode: string;
  } {
    this.reset();
    this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;

    // Analyze the component for DOM update opportunities
    this.analyzeComponent(ast.component, dependencyGraph);

    // Generate batched update operations
    this.generateUpdateBatches();

    // Generate the optimized update code
    const updateCode = this.generateUpdateCode();
    const setupCode = this.generateSetupCode();
    const cleanupCode = this.generateCleanupCode();

    return {
      updateCode,
      setupCode,
      cleanupCode
    };
  }

  /**
   * Generate selective DOM updates (only changed elements)
   */
  generateSelectiveUpdates(
    ast: ComponentAST,
    dependencyGraph: DependencyGraph,
    changedVariables: string[]
  ): string {
    this.reset();
    this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;

    // Only analyze operations that depend on changed variables
    this.analyzeComponent(ast.component, dependencyGraph, changedVariables);
    this.generateUpdateBatches();

    return this.generateUpdateCode();
  }

  /**
   * Reset optimizer state
   */
  private reset(): void {
    this.componentId = '';
    this.updateOperations.clear();
    this.updateBatches = [];
    this.twoWayBindings = [];
    this.elementCounter = 0;
  }

  /**
   * Analyze component for DOM update opportunities
   */
  private analyzeComponent(
    component: ComponentNode,
    dependencyGraph: DependencyGraph,
    changedVariables?: string[]
  ): void {
    if (!component.markupBlock) return;

    // Analyze markup block for update operations
    this.analyzeMarkupBlock(component.markupBlock, dependencyGraph, changedVariables);

    // Analyze reactive variables for two-way bindings
    if (component.clientBlock) {
      this.analyzeTwoWayBindings(component.markupBlock, component.clientBlock.reactiveVariables);
    }
  }

  /**
   * Analyze markup block for DOM updates
   */
  private analyzeMarkupBlock(
    markupBlock: MarkupBlockNode,
    dependencyGraph: DependencyGraph,
    changedVariables?: string[]
  ): void {
    // Analyze interpolations
    for (const interpolation of markupBlock.interpolations) {
      this.analyzeInterpolation(interpolation, dependencyGraph, changedVariables);
    }

    // Analyze HTML elements
    for (const element of markupBlock.elements) {
      this.analyzeHTMLElement(element, dependencyGraph, changedVariables);
    }
  }

  /**
   * Analyze HTML element for DOM updates
   */
  private analyzeHTMLElement(
    element: HTMLElementNode,
    dependencyGraph: DependencyGraph,
    changedVariables?: string[],
    elementPath: string = ''
  ): void {
    const elementId = this.generateElementId();
    const currentPath = elementPath ? `${elementPath} > ${element.tagName}` : element.tagName;

    // Analyze attributes for reactive updates
    for (const attr of element.attributes) {
      this.analyzeAttribute(attr, elementId, dependencyGraph, changedVariables);
    }

    // Recursively analyze children
    for (let i = 0; i < element.children.length; i++) {
      const child = element.children[i];
      if (child.type === 'HTMLElement') {
        this.analyzeHTMLElement(
          child as HTMLElementNode,
          dependencyGraph,
          changedVariables,
          `${currentPath}:nth-child(${i + 1})`
        );
      } else if (child.type === 'Interpolation') {
        this.analyzeInterpolation(
          child as InterpolationNode,
          dependencyGraph,
          changedVariables,
          `${currentPath}:nth-child(${i + 1})`
        );
      }
    }
  }

  /**
   * Analyze attribute for reactive updates
   */
  private analyzeAttribute(
    attr: AttributeNode,
    elementId: string,
    dependencyGraph: DependencyGraph,
    changedVariables?: string[]
  ): void {
    if (!attr.isDirective || typeof attr.value === 'string') {
      return;
    }

    const expression = attr.value as ExpressionNode;
    const dependencies = this.extractDependencies(expression, dependencyGraph);

    // Filter by changed variables if specified
    if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) {
      return;
    }

    const selector = `[data-ordojs-id="${elementId}"]`;

    switch (attr.directiveType) {
      case DirectiveType.BIND:
        this.createTwoWayBinding(attr, elementId, dependencies);
        break;

      case DirectiveType.ON:
        // Event handlers don't need DOM updates, just setup
        break;

      case DirectiveType.CLASS:
        this.createClassUpdate(attr, selector, expression, dependencies);
        break;

      case DirectiveType.STYLE:
        this.createStyleUpdate(attr, selector, expression, dependencies);
        break;

      default:
        this.createAttributeUpdate(attr, selector, expression, dependencies);
        break;
    }
  }

  /**
   * Analyze interpolation for reactive updates
   */
  private analyzeInterpolation(
    interpolation: InterpolationNode,
    dependencyGraph: DependencyGraph,
    changedVariables?: string[],
    elementPath?: string
  ): void {
    const dependencies = this.extractDependencies(interpolation.expression, dependencyGraph);

    // Filter by changed variables if specified
    if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) {
      return;
    }

    const elementId = this.generateElementId();
    const selector = `[data-ordojs-interpolation="${elementId}"]`;

    const operation: DOMUpdateOperation = {
      id: this.generateOperationId(),
      type: DOMUpdateType.TEXT_CONTENT,
      selector,
      property: 'textContent',
      expression: this.generateExpressionCode(interpolation.expression),
      dependencies
    };

    this.addUpdateOperation(dependencies[0] || 'default', operation);
  }

  /**
   * Create two-way binding
   */
  private createTwoWayBinding(
    attr: AttributeNode,
    elementId: string,
    dependencies: string[]
  ): void {
    const bindProperty = attr.name.substring(5); // Remove 'bind:' prefix
    const selector = `[data-ordojs-id="${elementId}"]`;

    // Determine event type based on property
    let eventType = 'input';
    let propertyName = 'value';

    switch (bindProperty) {
      case 'checked':
        eventType = 'change';
        propertyName = 'checked';
        break;
      case 'value':
        eventType = 'input';
        propertyName = 'value';
        break;
      default:
        eventType = 'input';
        propertyName = bindProperty;
    }

    const binding: TwoWayBinding = {
      elementSelector: selector,
      variableName: dependencies[0] || '',
      eventType,
      propertyName
    };

    this.twoWayBindings.push(binding);

    // Also create a property update operation
    const operation: DOMUpdateOperation = {
      id: this.generateOperationId(),
      type: DOMUpdateType.PROPERTY,
      selector,
      property: propertyName,
      expression: `component.state.${binding.variableName}`,
      dependencies
    };

    this.addUpdateOperation(binding.variableName, operation);
  }

  /**
   * Create class update operation
   */
  private createClassUpdate(
    attr: AttributeNode,
    selector: string,
    expression: ExpressionNode,
    dependencies: string[]
  ): void {
    const className = attr.name.substring(6); // Remove 'class:' prefix

    const operation: DOMUpdateOperation = {
      id: this.generateOperationId(),
      type: DOMUpdateType.CLASS_TOGGLE,
      selector,
      property: className,
      expression: this.generateExpressionCode(expression),
      dependencies
    };

    this.addUpdateOperation(dependencies[0] || 'default', operation);
  }

  /**
   * Create style update operation
   */
  private createStyleUpdate(
    attr: AttributeNode,
    selector: string,
    expression: ExpressionNode,
    dependencies: string[]
  ): void {
    const styleProperty = attr.name.substring(6); // Remove 'style:' prefix

    const operation: DOMUpdateOperation = {
      id: this.generateOperationId(),
      type: DOMUpdateType.STYLE,
      selector,
      property: styleProperty,
      expression: this.generateExpressionCode(expression),
      dependencies
    };

    this.addUpdateOperation(dependencies[0] || 'default', operation);
  }

  /**
   * Create attribute update operation
   */
  private createAttributeUpdate(
    attr: AttributeNode,
    selector: string,
    expression: ExpressionNode,
    dependencies: string[]
  ): void {
    const operation: DOMUpdateOperation = {
      id: this.generateOperationId(),
      type: DOMUpdateType.ATTRIBUTE,
      selector,
      property: attr.name,
      expression: this.generateExpressionCode(expression),
      dependencies
    };

    this.addUpdateOperation(dependencies[0] || 'default', operation);
  }

  /**
   * Analyze two-way bindings
   */
  private analyzeTwoWayBindings(
    markupBlock: MarkupBlockNode,
    reactiveVariables: ReactiveVariableNode[]
  ): void {
    // This is handled in analyzeAttribute method
    // Additional analysis could be added here if needed
  }

  /**
   * Extract dependencies from expression
   */
  private extractDependencies(
    expression: ExpressionNode,
    dependencyGraph: DependencyGraph
  ): string[] {
    const dependencies: string[] = [];

    const extractFromExpr = (expr: ExpressionNode): void => {
      switch (expr.expressionType) {
        case ExpressionType.IDENTIFIER:
          if (expr.identifier && dependencyGraph.nodes.has(expr.identifier)) {
            dependencies.push(expr.identifier);
          }
          break;

        case ExpressionType.BINARY:
        case ExpressionType.ASSIGNMENT:
          if (expr.left) extractFromExpr(expr.left);
          if (expr.right) extractFromExpr(expr.right);
          break;

        case ExpressionType.UNARY:
          if (expr.right) extractFromExpr(expr.right);
          break;

        case ExpressionType.CALL:
          if (expr.callee) extractFromExpr(expr.callee);
          if (expr.arguments) {
            expr.arguments.forEach(arg => extractFromExpr(arg));
          }
          break;

        case ExpressionType.MEMBER:
          if (expr.object) extractFromExpr(expr.object);
          // For member expressions, we don't extract dependencies from the property
          // since it's usually a literal property name, but we still need to check
          // if the property itself is an identifier that references a reactive variable
          if (expr.property && expr.property.expressionType === ExpressionType.IDENTIFIER) {
            if (expr.property.identifier && dependencyGraph.nodes.has(expr.property.identifier)) {
              dependencies.push(expr.property.identifier);
            }
          }
          break;
      }
    };

    extractFromExpr(expression);
    return [...new Set(dependencies)]; // Remove duplicates
  }

  /**
   * Generate expression code
   */
  private generateExpressionCode(expression: ExpressionNode): string {
    switch (expression.expressionType) {
      case ExpressionType.LITERAL:
        return typeof expression.value === 'string'
          ? `"${expression.value}"`
          : String(expression.value);

      case ExpressionType.IDENTIFIER:
        return `component.state.${expression.identifier}`;

      case ExpressionType.BINARY:
        if (expression.left && expression.right && expression.operator) {
          const left = this.generateExpressionCode(expression.left);
          const right = this.generateExpressionCode(expression.right);
          return `(${left} ${expression.operator} ${right})`;
        }
        return '';

      case ExpressionType.UNARY:
        if (expression.right && expression.operator) {
          const right = this.generateExpressionCode(expression.right);
          return `(${expression.operator}${right})`;
        }
        return '';

      case ExpressionType.CALL:
        if (expression.callee && expression.arguments) {
          const callee = this.generateExpressionCode(expression.callee);
          const args = expression.arguments
            .map(arg => this.generateExpressionCode(arg))
            .join(', ');
          return `${callee}(${args})`;
        }
        return '';

      case ExpressionType.MEMBER:
        if (expression.object && expression.property) {
          const object = this.generateExpressionCode(expression.object);
          // For member expressions, the property should be treated as a literal property name
          const property = expression.property.expressionType === ExpressionType.IDENTIFIER
            ? expression.property.identifier
            : this.generateExpressionCode(expression.property);
          return `${object}.${property}`;
        }
        return '';

      default:
        return '';
    }
  }

  /**
   * Add update operation to the appropriate group
   */
  private addUpdateOperation(variableName: string, operation: DOMUpdateOperation): void {
    // Use the first dependency as the key, or 'default' if no dependencies
    const key = operation.dependencies.length > 0 ? operation.dependencies[0] : 'default';

    if (!this.updateOperations.has(key)) {
      this.updateOperations.set(key, []);
    }
    this.updateOperations.get(key)!.push(operation);
  }

  /**
   * Generate batched update operations
   */
  private generateUpdateBatches(): void {
    let batchId = 0;

    for (const [variableName, operations] of this.updateOperations) {
      // Create a single batch per variable to reduce the number of batches
      const batch: UpdateBatch = {
        id: `batch_${batchId++}`,
        operations,
        dependencies: [variableName],
        priority: Math.min(...operations.map(op => this.getUpdatePriority(op.type)))
      };

      this.updateBatches.push(batch);
    }

    // Sort batches by priority
    this.updateBatches.sort((a, b) => a.priority - b.priority);
  }

  /**
   * Get update priority for operation type
   */
  private getUpdatePriority(type: DOMUpdateType): number {
    switch (type) {
      case DOMUpdateType.VISIBILITY:
        return 1;
      case DOMUpdateType.CLASS_TOGGLE:
        return 2;
      case DOMUpdateType.STYLE:
        return 3;
      case DOMUpdateType.ATTRIBUTE:
        return 4;
      case DOMUpdateType.PROPERTY:
        return 5;
      case DOMUpdateType.TEXT_CONTENT:
        return 6;
      case DOMUpdateType.LIST_UPDATE:
        return 7;
      case DOMUpdateType.CONDITIONAL_RENDER:
        return 8;
      default:
        return 9;
    }
  }

  /**
   * Generate optimized update code
   */
  private generateUpdateCode(): string {
    const lines: string[] = [];

    lines.push('// Optimized DOM update functions');
    lines.push('const updateFunctions = {');

    for (const batch of this.updateBatches) {
      lines.push(`  ${batch.id}: function() {`);
      lines.push('    // Batch DOM updates for better performance');

      // Group operations by selector for efficiency
      const operationsBySelector = new Map<string, DOMUpdateOperation[]>();
      for (const operation of batch.operations) {
        if (!operationsBySelector.has(operation.selector)) {
          operationsBySelector.set(operation.selector, []);
        }
        operationsBySelector.get(operation.selector)!.push(operation);
      }

      // Generate update code for each selector
      for (const [selector, operations] of operationsBySelector) {
        lines.push(`    const elements = document.querySelectorAll('${selector}');`);
        lines.push('    elements.forEach(el => {');

        for (const operation of operations) {
          lines.push(`      // ${operation.type}: ${operation.property}`);
          switch (operation.type) {
            case DOMUpdateType.TEXT_CONTENT:
              lines.push(`      el.textContent = ${operation.expression};`);
              break;

            case DOMUpdateType.ATTRIBUTE:
              lines.push(`      el.setAttribute('${operation.property}', ${operation.expression});`);
              break;

            case DOMUpdateType.PROPERTY:
              lines.push(`      el.${operation.property} = ${operation.expression};`);
              break;

            case DOMUpdateType.CLASS_TOGGLE:
              lines.push(`      el.classList.toggle('${operation.property}', !!(${operation.expression}));`);
              break;

            case DOMUpdateType.STYLE:
              lines.push(`      el.style.${operation.property} = ${operation.expression};`);
              break;
          }
        }

        lines.push('    });');
      }

      lines.push('  },');
    }

    lines.push('};');
    lines.push('');

    // Generate main update function
    lines.push('function updateDOM(component, changedVariables = []) {');
    lines.push('  // Execute update batches based on changed variables');

    for (const batch of this.updateBatches) {
      const condition = batch.dependencies
        .map(dep => `changedVariables.includes('${dep}')`)
        .join(' || ');

      lines.push(`  if (changedVariables.length === 0 || ${condition}) {`);
      lines.push(`    updateFunctions.${batch.id}();`);
      lines.push('  }');
    }

    lines.push('}');

    return lines.join('\n');
  }

  /**
   * Generate setup code for two-way bindings
   */
  private generateSetupCode(): string {
    if (this.twoWayBindings.length === 0) {
      return '';
    }

    const lines: string[] = [];

    lines.push('// Setup two-way data bindings');
    lines.push('function setupTwoWayBindings(component) {');

    for (const binding of this.twoWayBindings) {
      lines.push(`  const ${binding.variableName}Elements = document.querySelectorAll('${binding.elementSelector}');`);
      lines.push(`  ${binding.variableName}Elements.forEach(el => {`);
      lines.push(`    el.addEventListener('${binding.eventType}', (event) => {`);

      if (binding.transformFunction) {
        lines.push(`      const value = ${binding.transformFunction}(event.target.${binding.propertyName});`);
      } else {
        lines.push(`      const value = event.target.${binding.propertyName};`);
      }

      lines.push(`      component.state.${binding.variableName} = value;`);
      lines.push('    });');
      lines.push('  });');
    }

    lines.push('}');

    return lines.join('\n');
  }

  /**
   * Generate cleanup code
   */
  private generateCleanupCode(): string {
    if (this.twoWayBindings.length === 0) {
      return '';
    }

    const lines: string[] = [];

    lines.push('// Cleanup event listeners');
    lines.push('function cleanupBindings() {');
    lines.push('  // Event listeners will be automatically cleaned up when elements are removed');
    lines.push('  // Additional cleanup logic can be added here if needed');
    lines.push('}');

    return lines.join('\n');
  }

  /**
   * Generate unique element ID
   */
  private generateElementId(): string {
    return `${this.componentId}_el_${this.elementCounter++}`;
  }

  /**
   * Generate unique operation ID
   */
  private generateOperationId(): string {
    return `op_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
  }
}
