#!/usr/bin/env python3
"""
Convert openCypher XML grammar to Lark format.

This converter handles the openCypher XML grammar specification and produces
a complete Lark grammar file.
"""

import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, field


@dataclass
class Production:
    name: str
    body: str
    inline: bool = False
    legacy: bool = False
    lexer: bool = False


class OpenCypherXMLToLark:
    """Convert openCypher XML grammar to Lark format."""

    # Default namespace URI
    DEFAULT_NS = 'http://opencypher.org/grammar'

    def __init__(self, xml_dir: Path):
        self.xml_dir = xml_dir
        self.productions: dict[str, Production] = {}
        self.keywords: set[str] = set()
        self.processed_files: set[str] = set()

    def load_xml(self, filename: str) -> ET.Element:
        """Load and parse XML file with entity expansion."""
        filepath = self.xml_dir / filename

        # Read file and manually expand entities
        content = filepath.read_text()

        # Expand common entities (with both quote styles)
        entities = {
            "&WS;": '<opt><non-terminal ref="SP"/></opt>',
            "&SP;": '<non-terminal ref="SP"/>',
            "&expr;": '<non-terminal ref="Expression"/>',
            "&var;": '<non-terminal ref="Variable"/>',
        }
        for entity, replacement in entities.items():
            content = content.replace(entity, replacement)

        # Remove DOCTYPE declaration (causes parsing issues)
        content = re.sub(r'<!DOCTYPE[^>]+\[.*?\]>', '', content, flags=re.DOTALL)

        # Remove the default namespace to simplify parsing
        content = re.sub(r'\s+xmlns="[^"]*"', '', content)

        return ET.fromstring(content)

    def process_file(self, filename: str):
        """Process an XML grammar file."""
        if filename in self.processed_files:
            return
        self.processed_files.add(filename)

        try:
            root = self.load_xml(filename)
        except FileNotFoundError:
            print(f"Warning: {filename} not found, skipping")
            return
        except ET.ParseError as e:
            print(f"Warning: Error parsing {filename}: {e}")
            return

        print(f"Processing {filename}...")

        # Process vocabulary references first
        for vocab in root.iter('vocabulary'):
            vocab_file = vocab.get('file')
            if vocab_file:
                self.process_file(vocab_file)

        # Process all productions (iter finds all descendants)
        for prod in root.iter('production'):
            self._process_production(prod)

    def _process_production(self, prod: ET.Element):
        """Process a single production element."""
        name = prod.get('name')
        if not name or name in self.productions:
            return

        # Check for inline/legacy/lexer attributes (with or without namespace prefix)
        inline = (prod.get('{http://opencypher.org/railroad}inline') == 'true' or
                  prod.get('rr:inline') == 'true')
        legacy = (prod.get('{http://opencypher.org/opencypher}legacy') == 'true' or
                  prod.get('oc:legacy') == 'true')
        lexer = (prod.get('{http://opencypher.org/opencypher}lexer') == 'true' or
                 prod.get('oc:lexer') == 'true')

        body = self._convert_element(prod)

        self.productions[name] = Production(
            name=name,
            body=body.strip(),
            inline=inline,
            legacy=legacy,
            lexer=lexer,
        )
        print(f"  Added production: {name}")

    # Operators and punctuation that need quoting
    OPERATORS = {'+', '-', '*', '/', '%', '^', '=', '<', '>', '!', '&', '|',
                 '(', ')', '[', ']', '{', '}', ',', '.', ':', ';', '$', '@',
                 '<>', '<=', '>=', '!=', '..', '+=', '-=', '=~'}

    def _quote_if_needed(self, word: str) -> str:
        """Quote operators and literals properly."""
        if word in self.OPERATORS:
            return f'"{word}"'
        elif len(word) == 1 and not word.isalnum():
            return f'"{word}"'
        elif word.isupper() or (word and word[0].isupper()):
            # Keyword - make case-insensitive
            self.keywords.add(word.upper())
            return f'"{word}"i'
        else:
            return word

    def _convert_element(self, elem: ET.Element) -> str:
        """Convert an XML element to Lark syntax."""
        parts = []

        # Handle text content (keywords and operators)
        if elem.text and elem.text.strip():
            text = elem.text.strip()
            # Split on whitespace to handle multiple tokens
            for word in text.split():
                parts.append(self._quote_if_needed(word))

        # Process child elements
        for child in elem:
            converted = self._convert_child(child)
            if converted:
                parts.append(converted)

            # Handle tail text (text after child element)
            if child.tail and child.tail.strip():
                tail = child.tail.strip()
                for word in tail.split():
                    parts.append(self._quote_if_needed(word))

        return ' '.join(parts)

    def _convert_child(self, elem: ET.Element) -> Optional[str]:
        """Convert a child element to Lark syntax."""
        # Strip namespace from tag
        tag = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag

        if tag == 'non-terminal':
            ref = elem.get('ref')
            if ref:
                # Convert to lowercase for parser rules
                return ref.lower()

        elif tag == 'alt':
            # Alternation: (a | b | c)
            alts = []
            for child in elem:
                converted = self._convert_child(child)
                if converted:
                    alts.append(converted)
            if alts:
                if len(alts) == 1:
                    return alts[0]
                return '(' + ' | '.join(alts) + ')'

        elif tag == 'seq':
            # Sequence
            return self._convert_element(elem)

        elif tag == 'opt':
            # Optional: [x] or x?
            inner = self._convert_element(elem)
            if inner:
                if ' ' in inner or '|' in inner:
                    return f'[{inner}]'
                return f'{inner}?'

        elif tag == 'repeat':
            # Repetition
            inner = self._convert_element(elem)
            if not inner:
                return None

            min_val = elem.get('min', '0')
            max_val = elem.get('max')

            if min_val == '1' and not max_val:
                # One or more
                if ' ' in inner or '|' in inner:
                    return f'({inner})+'
                return f'{inner}+'
            elif min_val == '0' and not max_val:
                # Zero or more
                if ' ' in inner or '|' in inner:
                    return f'({inner})*'
                return f'{inner}*'
            else:
                # Specific range
                if max_val:
                    return f'({inner}) ~ {min_val}..{max_val}'
                return f'({inner}) ~ {min_val}..'

        elif tag == 'literal':
            value = elem.get('value', '')
            # Escape quotes if needed
            if '"' in value:
                return f"'{value}'"
            return f'"{value}"'

        elif tag == 'character':
            char_set = elem.get('set')
            if char_set == 'EOI':
                return ''  # End of input - handled by Lark automatically
            elif char_set:
                # Character class - convert to regex terminal name
                return char_set.upper()
            else:
                # Check for except clause
                for child in elem:
                    child_tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
                    if child_tag == 'except':
                        lit = child.get('literal', '')
                        return f'/[^{re.escape(lit)}]/'
                return '/./s'  # Any character

        elif tag == 'vocabulary':
            # Already processed
            return None

        return None

    def to_lark(self) -> str:
        """Generate complete Lark grammar."""
        lines = [
            "// openCypher Grammar - Auto-generated from XML specification",
            "// Source: https://github.com/opencypher/openCypher",
            "",
            "// === Parser Rules ===",
            "",
        ]

        # Start rule
        if 'Cypher' in self.productions:
            lines.append("start: cypher")
            lines.append("")

        # Parser rules (lowercase names)
        parser_rules = []
        lexer_rules = []

        for name, prod in sorted(self.productions.items()):
            if prod.lexer or name.isupper():
                lexer_rules.append((name, prod))
            else:
                parser_rules.append((name, prod))

        # Output parser rules
        for name, prod in parser_rules:
            rule_name = name.lower()
            comment = ""
            if prod.legacy:
                comment = "  // legacy"
            if prod.inline:
                comment = "  // inline"

            body = prod.body if prod.body else '""'
            lines.append(f"{rule_name}: {body}{comment}")

        lines.append("")
        lines.append("// === Lexer Rules (Terminals) ===")
        lines.append("")

        # Output lexer rules
        for name, prod in lexer_rules:
            rule_name = name.upper()
            body = prod.body if prod.body else '""'
            lines.append(f"{rule_name}: {body}")

        # Add common terminals
        lines.extend([
            "",
            "// === Common Terminals ===",
            "",
            "// Whitespace",
            "SP: /\\s+/",
            "WS: /\\s*/",
            "",
            "// Comments",
            "COMMENT: /\\/\\/[^\\n]*/",
            "BLOCK_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//",
            "",
            "// Unicode character classes (simplified)",
            "ID_START: /[a-zA-Z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D]/",
            "ID_CONTINUE: /[a-zA-Z0-9_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D]/",
            "",
            "// Ignore whitespace and comments",
            "%ignore SP",
            "%ignore COMMENT",
            "%ignore BLOCK_COMMENT",
        ])

        return '\n'.join(lines)

    def convert(self, output_path: Path):
        """Convert XML grammar files and write to output."""
        # Start with main grammar file
        self.process_file('cypher.xml')

        # Also process basic-grammar if not already included
        self.process_file('basic-grammar.xml')

        # Generate and write Lark grammar
        lark_content = self.to_lark()
        output_path.write_text(lark_content)

        print(f"Converted {len(self.productions)} productions to {output_path}")
        print(f"Found {len(self.keywords)} keywords")

        return self.productions


def main():
    import sys

    xml_dir = Path(__file__).parent / 'opencypher-xml'
    output_path = Path(__file__).parent / 'lark_grammars' / 'cypher_from_xml.lark'

    converter = OpenCypherXMLToLark(xml_dir)
    productions = converter.convert(output_path)

    # Print summary
    print(f"\nProduction types:")
    print(f"  Parser rules: {sum(1 for p in productions.values() if not p.lexer)}")
    print(f"  Lexer rules: {sum(1 for p in productions.values() if p.lexer)}")
    print(f"  Legacy rules: {sum(1 for p in productions.values() if p.legacy)}")


if __name__ == '__main__':
    main()
