#!/usr/bin/env python3
"""
Parse transactions.macro file to extract transaction information
and generate C++ classes for each transaction type.

Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
"""

import io
import argparse
from pathlib import Path
import pyparsing as pp

# Import common utilities
from macro_parser_common import (
    CppCleaner,
    parse_sfields_macro,
    parse_field_list,
    generate_cpp_class,
    generate_from_template,
    clear_output_directory,
)


def create_transaction_parser():
    """Create a pyparsing parser for TRANSACTION macros.

    This parser extracts the full TRANSACTION macro call and parses its arguments
    using pyparsing's nesting-aware delimited list parsing.
    """
    # Define nested structures so pyparsing protects them
    nested_braces = pp.original_text_for(pp.nested_expr("{", "}"))
    nested_parens = pp.original_text_for(pp.nested_expr("(", ")"))

    # Define standard text (anything that isn't a comma, parens, or braces)
    plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()")

    # A single argument is any combination of the above
    single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text))
    single_arg.set_parse_action(lambda t: t[0].strip())

    # The arguments are a delimited list
    args_list = pp.DelimitedList(single_arg)

    # The full macro: TRANSACTION(args)
    macro_parser = (
        pp.Keyword("TRANSACTION")
        + pp.Suppress("(")
        + pp.Group(args_list)("args")
        + pp.Suppress(")")
    )

    return macro_parser


def parse_transaction_args(args_list):
    """Parse the arguments of a TRANSACTION macro call.

    Args:
        args_list: A list of parsed arguments from pyparsing, e.g.,
                   ['ttPAYMENT', '0', 'Payment', 'Delegation::delegable',
                    'uint256{}', 'createAcct', '({...})']

    Returns:
        A dict with parsed transaction information.
    """
    if len(args_list) < 7:
        raise ValueError(
            f"Expected at least 7 parts in TRANSACTION, got {len(args_list)}: {args_list}"
        )

    tag = args_list[0]
    value = args_list[1]
    name = args_list[2]
    delegable = args_list[3]
    amendments = args_list[4]
    privileges = args_list[5]
    fields_str = args_list[-1]

    # Parse fields: ({field1, field2, ...})
    fields = parse_field_list(fields_str)

    return {
        "tag": tag,
        "value": value,
        "name": name,
        "delegable": delegable,
        "amendments": amendments,
        "privileges": privileges,
        "fields": fields,
    }


def parse_macro_file(filepath):
    """Parse the transactions.macro file.

    Uses pcpp to preprocess the file and pyparsing to parse the TRANSACTION macros.
    """
    with open(filepath, "r") as f:
        c_code = f.read()

    # Step 1: Clean the C++ code using pcpp
    cleaner = CppCleaner("TRANSACTION_INCLUDE", "TRANSACTION")
    cleaner.parse(c_code)

    out = io.StringIO()
    cleaner.write(out)
    clean_text = out.getvalue()

    # Step 2: Parse the clean text using pyparsing
    parser = create_transaction_parser()
    transactions = []

    for match, _, _ in parser.scan_string(clean_text):
        # Extract the macro name and arguments
        raw_args = match.args

        # Parse the arguments
        tx_data = parse_transaction_args(raw_args)
        transactions.append(tx_data)

    return transactions


# TransactionBase is a static file in the repository at:
# - include/xrpl/protocol/TransactionBase.h
# - src/libxrpl/protocol/TransactionBase.cpp
# It is NOT generated by this script.


def main():
    parser = argparse.ArgumentParser(
        description="Generate C++ transaction classes from transactions.macro"
    )
    parser.add_argument("macro_path", help="Path to transactions.macro")
    parser.add_argument(
        "--header-dir",
        help="Output directory for header files",
        default="include/xrpl/protocol_autogen/transactions",
    )
    parser.add_argument(
        "--test-dir",
        help="Output directory for test files (optional)",
        default=None,
    )
    parser.add_argument(
        "--sfields-macro",
        help="Path to sfields.macro (default: auto-detect from macro_path)",
    )
    parser.add_argument(
        "--list-outputs",
        action="store_true",
        help="List output files without generating (one per line)",
    )

    args = parser.parse_args()

    # Parse the macro file to get transaction names
    transactions = parse_macro_file(args.macro_path)

    # If --list-outputs, just print the output file paths and exit
    if args.list_outputs:
        header_dir = Path(args.header_dir)
        for tx in transactions:
            print(header_dir / f"{tx['name']}.h")
        if args.test_dir:
            test_dir = Path(args.test_dir)
            for tx in transactions:
                print(test_dir / f"{tx['name']}Tests.cpp")
        return

    # Auto-detect sfields.macro path if not provided
    if args.sfields_macro:
        sfields_path = Path(args.sfields_macro)
    else:
        # Assume sfields.macro is in the same directory as transactions.macro
        macro_path = Path(args.macro_path)
        sfields_path = macro_path.parent / "sfields.macro"

    # Parse sfields.macro to get field type information
    print(f"Parsing {sfields_path}...")
    field_types = parse_sfields_macro(sfields_path)
    print(
        f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n"
    )

    print(f"Found {len(transactions)} transactions\n")

    for tx in transactions:
        print(f"Transaction: {tx['name']}")
        print(f"  Tag: {tx['tag']}")
        print(f"  Value: {tx['value']}")
        print(f"  Fields: {len(tx['fields'])}")
        for field in tx["fields"]:
            print(f"    - {field['name']}: {field['requirement']}")
        print()

    # Set up output directory
    header_dir = Path(args.header_dir)
    header_dir.mkdir(parents=True, exist_ok=True)

    # Clear existing generated files before regenerating
    clear_output_directory(header_dir)

    print(f"\nGenerating header-only template classes...")
    print(f"  Headers: {header_dir}\n")

    # Set up template directory
    script_dir = Path(__file__).parent
    template_dir = script_dir / "templates"

    generated_files = []
    for tx_info in transactions:
        header_path = generate_cpp_class(
            tx_info, header_dir, template_dir, field_types, "Transaction.h.mako"
        )
        generated_files.append(header_path)
        print(f"  Generated: {tx_info['name']}.h")

    print(
        f"\nGenerated {len(transactions)} transaction classes ({len(generated_files)} header files)"
    )
    print(f"  Headers: {header_dir.absolute()}")

    # Generate unit tests if --test-dir is provided
    if args.test_dir:
        test_dir = Path(args.test_dir)
        test_dir.mkdir(parents=True, exist_ok=True)

        # Clear existing generated test files before regenerating
        clear_output_directory(test_dir)

        for tx_info in transactions:
            # Fields are already enriched from generate_cpp_class above
            generate_from_template(
                tx_info,
                test_dir,
                template_dir,
                "TransactionTests.cpp.mako",
                "Tests.cpp",
            )

        print(f"\nGenerated {len(transactions)} transaction test files")


if __name__ == "__main__":
    main()
