#!/usr/bin/env python3
"""
lime-compose -- Compose modular .lime grammar files into a single parser file.

Modular .lime files use directive-based metadata (%module_name, %module_version,
%require, %export, %import) embedded directly in the grammar. This tool extracts
metadata, resolves inter-module dependencies via topological sort, and concatenates
the results into a single .lime file suitable for the Lime parser generator.

Usage:
    lime-compose [options] <file.lime>...
    lime-compose --help

Options:
    -o FILE, --output FILE   Write composed grammar to FILE (default: stdout)
    -v, --verbose            Print module resolution order to stderr
    -n, --dry-run            Validate and show resolution order without output
    --no-validate            Skip dependency validation
    --list-modules           List modules found in input files and exit
    -h, --help               Show this help message and exit
"""

from __future__ import print_function

import argparse
import os
import re
import sys


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

class Module(object):
    """A single grammar module extracted from a .lime file."""

    def __init__(self, name, version, description, dependencies, exports,
                 imports, content, source_file):
        self.name = name
        self.version = version
        self.description = description
        self.dependencies = dependencies  # list of (name, constraint) tuples
        self.exports = exports            # list of symbol names
        self.imports = imports            # list of (symbol, from_module) tuples
        self.content = content            # full file content
        self.source_file = source_file


# ---------------------------------------------------------------------------
# Parsing
# ---------------------------------------------------------------------------

def extract_directive(lines, directive_name):
    """Extract the first occurrence of a %directive from lines.

    Returns the value (without quotes) or None if not found.
    """
    pattern = r'^\s*%' + re.escape(directive_name) + r'\s+(.+)$'
    for line in lines:
        m = re.match(pattern, line)
        if m:
            value = m.group(1).strip()
            # Remove trailing period if present
            if value.endswith('.'):
                value = value[:-1].strip()
            # Remove quotes
            if (value.startswith('"') and value.endswith('"')) or \
               (value.startswith("'") and value.endswith("'")):
                value = value[1:-1]
            return value
    return None


def extract_multi_directive(lines, directive_name):
    """Extract all occurrences of a multi-value %directive.

    Returns a list of tuples. For %require, returns [(name, constraint), ...]
    For %export, returns [symbol, ...]. For %import, returns [(symbol, from_module), ...].
    """
    results = []
    pattern = r'^\s*%' + re.escape(directive_name) + r'\s+(.+)$'

    for line in lines:
        m = re.match(pattern, line)
        if m:
            rest = m.group(1).strip()
            # Remove trailing period
            if rest.endswith('.'):
                rest = rest[:-1].strip()

            if directive_name == 'require':
                # Parse: module_name [constraint]
                parts = rest.split(None, 1)
                module_name = parts[0]
                constraint = parts[1].strip('"\'') if len(parts) > 1 else ''
                results.append((module_name, constraint))

            elif directive_name == 'export':
                # Parse: symbol1 symbol2 ...
                symbols = rest.split()
                results.extend(symbols)

            elif directive_name == 'import':
                # Parse: symbol1 symbol2 ... from module_name
                if ' from ' in rest:
                    parts = rest.split(' from ', 1)
                    symbols = parts[0].strip().split()
                    module_name = parts[1].strip()
                    for sym in symbols:
                        results.append((sym, module_name))

    return results


def strip_module_directives(content):
    """Remove all module directives from content, keeping everything else."""
    lines = content.split('\n')
    filtered = []

    module_directive_pattern = re.compile(
        r'^\s*%(module_name|module_version|module_description|require|export|import)\b')

    for line in lines:
        if not module_directive_pattern.match(line):
            filtered.append(line)

    return '\n'.join(filtered)


def parse_module_file(filepath):
    """Parse a modular .lime file and return a Module.

    The file should contain %module_name and %module_version directives.
    Other directives (%require, %export, %import) are optional.
    """
    if not os.path.isfile(filepath):
        raise FileNotFoundError("File not found: %s" % filepath)

    with open(filepath, 'r') as f:
        content = f.read()

    lines = content.splitlines()

    # Extract metadata
    name = extract_directive(lines, 'module_name')
    version = extract_directive(lines, 'module_version')
    description = extract_directive(lines, 'module_description') or ''

    # If no module_name, treat as non-modular grammar
    if not name:
        raise ValueError(
            "%s: no %%module_name directive found. "
            "Modular grammars must have %%module_name." % filepath)

    if not version:
        raise ValueError(
            "%s: no %%module_version directive found. "
            "%%module_name requires %%module_version." % filepath)

    # Validate name is ASCII identifier
    if not re.match(r'^[A-Za-z_][A-Za-z0-9_.-]*$', name):
        raise ValueError(
            "%s: module name '%s' contains invalid characters. "
            "Use ASCII letters, digits, underscores, dots, and hyphens."
            % (filepath, name))

    # Extract dependencies, exports, imports
    dependencies = extract_multi_directive(lines, 'require')
    exports = extract_multi_directive(lines, 'export')
    imports = extract_multi_directive(lines, 'import')

    return Module(
        name=name,
        version=version,
        description=description,
        dependencies=dependencies,
        exports=exports,
        imports=imports,
        content=content,
        source_file=filepath,
    )


# ---------------------------------------------------------------------------
# Dependency resolution
# ---------------------------------------------------------------------------

def validate_dependencies(modules):
    """Check that all dependencies are satisfied by available modules.

    Returns a list of error strings (empty if valid).
    """
    # Build the set of all module names
    available = {mod.name for mod in modules}

    errors = []
    for mod in modules:
        for dep_name, constraint in mod.dependencies:
            if dep_name not in available:
                errors.append(
                    "Module '%s' (%s) requires '%s', "
                    "which is not available in the input modules."
                    % (mod.name, mod.source_file, dep_name))

    # Check for duplicate module names
    seen_names = {}
    for mod in modules:
        if mod.name in seen_names:
            errors.append(
                "Duplicate module name '%s': found in both '%s' and '%s'."
                % (mod.name, seen_names[mod.name], mod.source_file))
        seen_names[mod.name] = mod.source_file

    # Check for duplicate exports (symbols exported by multiple modules)
    seen_exports = {}
    for mod in modules:
        for symbol in mod.exports:
            if symbol in seen_exports and seen_exports[symbol] != mod.name:
                errors.append(
                    "Symbol '%s' is exported by both '%s' and '%s'."
                    % (symbol, seen_exports[symbol], mod.name))
            seen_exports[symbol] = mod.name

    # Check that imports reference exported symbols
    for mod in modules:
        for symbol, from_module in mod.imports:
            # Find the module that should export this symbol
            found = False
            for other in modules:
                if other.name == from_module:
                    if symbol in other.exports:
                        found = True
                        break
            if not found:
                errors.append(
                    "Module '%s' imports '%s' from '%s', "
                    "but '%s' does not export it."
                    % (mod.name, symbol, from_module, from_module))

    return errors


def topological_sort(modules):
    """Sort modules so that dependencies come before dependents.

    Uses Kahn's algorithm. Raises ValueError on cyclic dependencies.
    Returns a new list of modules in dependency order.
    """
    # Map module name -> module
    by_name = {mod.name: mod for mod in modules}

    # Build adjacency: mod.name -> set of module names it depends on
    deps = {}
    for mod in modules:
        deps[mod.name] = set()
        for dep_name, constraint in mod.dependencies:
            if dep_name in by_name and dep_name != mod.name:
                deps[mod.name].add(dep_name)

    # Reverse adjacency: module name -> set of modules that depend on it
    rdeps = {mod.name: set() for mod in modules}
    for name, dep_set in deps.items():
        for d in dep_set:
            rdeps[d].add(name)

    # Kahn's algorithm
    in_degree = {name: len(dep_set) for name, dep_set in deps.items()}
    queue = sorted([name for name, deg in in_degree.items() if deg == 0])
    result = []

    while queue:
        # Take the alphabetically first to ensure deterministic ordering
        current = queue.pop(0)
        result.append(current)
        for dependent in sorted(rdeps[current]):
            in_degree[dependent] -= 1
            if in_degree[dependent] == 0:
                queue.append(dependent)
        queue.sort()

    if len(result) != len(modules):
        remaining = set(by_name.keys()) - set(result)
        raise ValueError(
            "Cyclic dependency detected among modules: %s"
            % ', '.join(sorted(remaining)))

    return [by_name[name] for name in result]


# ---------------------------------------------------------------------------
# Composition
# ---------------------------------------------------------------------------

def compose(modules, include_provenance=True):
    """Compose sorted modules into a single .lime grammar string."""
    parts = []

    parts.append("/*")
    parts.append(" * Composed grammar generated by lime-compose")
    parts.append(" *")
    parts.append(" * Modules (%d):" % len(modules))
    for mod in modules:
        parts.append(" *   - %s %s" % (mod.name, mod.version))
        if mod.description:
            parts.append(" *     %s" % mod.description)
    parts.append(" */")
    parts.append("")

    for mod in modules:
        if include_provenance:
            parts.append(
                "/* " + "=" * 66 + " */")
            parts.append(
                "/* Module: %-55s */" % mod.name)
            if mod.version != '0.0.0':
                parts.append(
                    "/* Version: %-54s */" % mod.version)
            parts.append(
                "/* Source: %-55s */" % os.path.basename(mod.source_file))
            parts.append(
                "/* " + "=" * 66 + " */")
            parts.append("")

        # Strip module directives from content
        clean_content = strip_module_directives(mod.content)
        parts.append(clean_content.rstrip())
        parts.append("")

    return '\n'.join(parts) + '\n'


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def build_parser():
    parser = argparse.ArgumentParser(
        prog='lime-compose',
        description='Compose modular .lime grammar files into a single parser file.',
        epilog='See docs/MODULE_FORMAT.md for the directive-based module specification.',
    )
    parser.add_argument(
        'files', metavar='FILE', nargs='*',
        help='Modular .lime files to compose')
    parser.add_argument(
        '-o', '--output', metavar='FILE', default=None,
        help='Write composed grammar to FILE (default: stdout)')
    parser.add_argument(
        '-v', '--verbose', action='store_true',
        help='Print module resolution order to stderr')
    parser.add_argument(
        '-n', '--dry-run', action='store_true',
        help='Validate and show resolution order without producing output')
    parser.add_argument(
        '--no-validate', action='store_true',
        help='Skip dependency validation')
    parser.add_argument(
        '--list-modules', action='store_true',
        help='List modules found in input files and exit')
    return parser


def main(argv=None):
    parser = build_parser()
    args = parser.parse_args(argv)

    if not args.files:
        parser.print_help(sys.stderr)
        return 1

    # Parse all input files
    modules = []
    errors = []
    for filepath in args.files:
        try:
            mod = parse_module_file(filepath)
            modules.append(mod)
        except (FileNotFoundError, SyntaxError, ValueError) as e:
            errors.append(str(e))

    if errors:
        for err in errors:
            print("ERROR: %s" % err, file=sys.stderr)
        return 1

    # List modules mode
    if args.list_modules:
        for mod in modules:
            deps = ', '.join([d[0] for d in mod.dependencies]) if mod.dependencies else '(none)'
            exports = ', '.join(mod.exports) if mod.exports else '(none)'
            print("%-20s v%-10s exports: %-30s depends: %s"
                  % (mod.name, mod.version, exports, deps))
        return 0

    # Validate dependencies
    if not args.no_validate:
        dep_errors = validate_dependencies(modules)
        if dep_errors:
            for err in dep_errors:
                print("ERROR: %s" % err, file=sys.stderr)
            return 1

    # Topological sort
    try:
        sorted_modules = topological_sort(modules)
    except ValueError as e:
        print("ERROR: %s" % e, file=sys.stderr)
        return 1

    if args.verbose or args.dry_run:
        print("Module resolution order:", file=sys.stderr)
        for i, mod in enumerate(sorted_modules, 1):
            print("  %d. %s (%s)" % (i, mod.name, mod.source_file),
                  file=sys.stderr)

    if args.dry_run:
        return 0

    # Compose output
    output = compose(sorted_modules)

    if args.output:
        output_dir = os.path.dirname(args.output)
        if output_dir and not os.path.isdir(output_dir):
            print("ERROR: Output directory does not exist: %s" % output_dir,
                  file=sys.stderr)
            return 1
        with open(args.output, 'w') as f:
            f.write(output)
        if args.verbose:
            print("Wrote composed grammar to %s" % args.output,
                  file=sys.stderr)
    else:
        sys.stdout.write(output)

    return 0


if __name__ == '__main__':
    sys.exit(main())
