#!/usr/bin/env python3
#exonware/xwsyntax/src/exonware/xwsyntax/bidirectional.py
"""
Bidirectional Grammar Support for xwsyntax
Combines input grammars (parsing) and output grammars (generation)
for roundtrip text processing.
Company: eXonware.com
Author: eXonware Backend Team
Email: connect@exonware.com
Version: 0.6.0.19
Generation Date: 29-Oct-2025
"""

from __future__ import annotations
from pathlib import Path
from .base import AGrammar
from .engines import LarkGrammar
from .output_grammar import OutputGrammar
from .unparser import GrammarUnparser
from .syntax_tree import ParseNode
from .errors import GrammarError


class BidirectionalGrammar:
    """
    Bidirectional grammar supporting both parsing and generation.
    Combines:
    - Input grammar (.grammar) for parsing text → AST
    - Output grammar (.out.grammar) for generating AST → text
    Features:
    - Roundtrip processing (parse → generate → parse)
    - Format-preserving generation
    - Template-based output
    """

    def __init__(
        self,
        format_name: str,
        input_grammar: AGrammar,
        output_grammar: OutputGrammar
    ):
        """
        Initialize bidirectional grammar.
        Args:
            format_name: Format name (e.g., 'json', 'sql')
            input_grammar: Grammar for parsing
            output_grammar: Grammar for generation
        """
        self.format_name = format_name
        self.format = format_name  # Alias for format_name (expected by tests)
        self.input_grammar = input_grammar
        self.output_grammar = output_grammar
        self.unparser = GrammarUnparser(output_grammar)

    def parse(self, text: str, **options) -> ParseNode:
        """
        Parse text to parse tree.
        Args:
            text: Input text
            **options: Parse options
        Returns:
            Parse tree node
        """
        return self.input_grammar.parse(text)

    def generate(self, ast: ParseNode, **options) -> str:
        """
        Generate text from parse tree.
        Args:
            ast: Parse tree node
            **options: Generation options (pretty, indent, etc.)
        Returns:
            Generated text
        """
        return self.unparser.unparse(ast, **options)

    def roundtrip(self, text: str, **options) -> str:
        """
        Parse and regenerate text (roundtrip test).
        Args:
            text: Input text
            **options: Generation options
        Returns:
            Regenerated text
        """
        ast = self.parse(text)
        return self.generate(ast, **options)

    def validate_roundtrip(self, text: str, normalize: bool = True) -> bool:
        """
        Validate that roundtrip produces equivalent result.
        Args:
            text: Input text
            normalize: Normalize whitespace before comparison
        Returns:
            True if roundtrip succeeds
        """
        try:
            # Parse original
            ast1 = self.parse(text)
            # Generate
            generated = self.generate(ast1)
            # Re-parse
            ast2 = self.parse(generated)
            # Compare ASTs
            return self._compare_asts(ast1, ast2, normalize)
        except Exception:
            return False

    def _compare_asts(self, ast1: ParseNode, ast2: ParseNode, normalize: bool = True) -> bool:
        """Compare two AST nodes for equivalence."""
        # Compare types
        if ast1.type != ast2.type:
            return False
        # Compare values
        if normalize:
            val1 = str(ast1.value).strip() if ast1.value else None
            val2 = str(ast2.value).strip() if ast2.value else None
        else:
            val1 = ast1.value
            val2 = ast2.value
        if val1 != val2:
            return False
        # Compare children count
        if len(ast1.children) != len(ast2.children):
            return False
        # Compare children recursively
        for child1, child2 in zip(ast1.children, ast2.children):
            if not self._compare_asts(child1, child2, normalize):
                return False
        return True
    @classmethod

    def load(cls, format_name: str, grammar_dir: str | None = None) -> BidirectionalGrammar:
        """
        Load bidirectional grammar for a format.
        Args:
            format_name: Format name (e.g., 'json', 'sql')
            grammar_dir: Directory containing grammar files
        Returns:
            BidirectionalGrammar instance
        """
        if not grammar_dir:
            grammar_dir = str(Path(__file__).parent / 'grammars')
        # Load input grammar (try new format first, then legacy formats)
        input_file = Path(grammar_dir) / f"{format_name}.grammar.in.lark"
        if not input_file.exists():
            input_file = Path(grammar_dir) / f"{format_name}.in.grammar"  # Legacy
            if not input_file.exists():
                input_file = Path(grammar_dir) / f"{format_name}.grammar"  # Legacy
                if not input_file.exists():
                    raise GrammarError(f"Input grammar not found: {format_name}.grammar.in.lark, {format_name}.in.grammar, or {format_name}.grammar")
        with open(input_file, encoding='utf-8') as f:
            input_text = f.read()
        # Use LarkGrammar with Indenter for Python-like languages
        # Check if format needs Indenter (Python requires it for INDENT/DEDENT)
        use_indenter = format_name.lower() in ['python', 'python3']
        # Use LarkGrammar for better compatibility, especially for Python
        input_grammar = LarkGrammar(format_name, input_text, use_indenter=use_indenter)
        # Load output grammar (try new format first, then legacy format)
        output_file = Path(grammar_dir) / f"{format_name}.grammar.out.lark"
        if not output_file.exists():
            output_file = Path(grammar_dir) / f"{format_name}.out.grammar"  # Legacy
            if not output_file.exists():
                raise GrammarError(f"Output grammar not found: {format_name}.grammar.out.lark or {format_name}.out.grammar")
        output_grammar = OutputGrammar.load_from_file(str(output_file))
        return cls(format_name, input_grammar, output_grammar)

    def __repr__(self) -> str:
        return f"BidirectionalGrammar(format='{self.format_name}')"


class BidirectionalGrammarRegistry:
    """Registry for managing bidirectional grammars."""

    def __init__(self, grammar_dir: str | None = None):
        """Initialize registry."""
        self.grammar_dir = grammar_dir
        self._grammars: dict[str, BidirectionalGrammar] = {}

    def load_grammar(self, format_name: str) -> BidirectionalGrammar:
        """Load bidirectional grammar."""
        if format_name in self._grammars:
            return self._grammars[format_name]
        grammar = BidirectionalGrammar.load(format_name, self.grammar_dir)
        self._grammars[format_name] = grammar
        return grammar

    def load(self, format_name: str) -> BidirectionalGrammar:
        """Load bidirectional grammar (alias for load_grammar)."""
        return self.load_grammar(format_name)

    def get_grammar(self, format_name: str) -> BidirectionalGrammar | None:
        """Get cached grammar."""
        return self._grammars.get(format_name)

    def list_formats(self) -> list:
        """List all formats with bidirectional support."""
        if not self.grammar_dir:
            grammar_dir = str(Path(__file__).parent / 'grammars')
        else:
            grammar_dir = self.grammar_dir
        formats = []
        grammar_path = Path(grammar_dir)
        # Find all output grammar files (new format first, then legacy)
        for out_file in grammar_path.glob('*.grammar.out.lark'):
            format_name = out_file.stem.replace('.grammar.out', '')
            # Check if input grammar also exists (new format first, then legacy)
            input_file = grammar_path / f"{format_name}.grammar.in.lark"
            if not input_file.exists():
                input_file = grammar_path / f"{format_name}.in.grammar"
                if not input_file.exists():
                    input_file = grammar_path / f"{format_name}.grammar"
            if input_file.exists():
                formats.append(format_name)
        # Also check legacy .out.grammar files
        for out_file in grammar_path.glob('*.out.grammar'):
            format_name = out_file.stem.replace('.out', '')
            # Check if input grammar also exists (new format first, then legacy)
            input_file = grammar_path / f"{format_name}.grammar.in.lark"
            if not input_file.exists():
                input_file = grammar_path / f"{format_name}.in.grammar"
                if not input_file.exists():
                    input_file = grammar_path / f"{format_name}.grammar"
            if input_file.exists():
                formats.append(format_name)
        return sorted(formats)
# Global registry
_global_registry = None


def get_bidirectional_registry() -> BidirectionalGrammarRegistry:
    """Get global bidirectional grammar registry."""
    global _global_registry
    if _global_registry is None:
        _global_registry = BidirectionalGrammarRegistry()
    return _global_registry
__all__ = [
    'BidirectionalGrammar',
    'BidirectionalGrammarRegistry',
    'get_bidirectional_registry'
]
