#!/usr/bin/env python3

"""
Check that member functions declared in .h files and implemented in .cpp files
appear in the same order in both files.

Copyright Forschungszentrum Jülich GmbH 2026.
License:  Public Domain
"""

import check_utils as cu
import os, re, sys

####################################################################################################
# Function extraction
####################################################################################################

def extract_class_functions_from_header(text):
    """
    Extract class member function declarations from a header file.
    Returns a list of tuples: (class_name, function_name, position)
    where position is the order index in the header (0-based).
    """
    functions = []

    # Remove comments to avoid false matches
    text_no_comments = re.sub(r'//.*', '', text)
    text_no_comments = re.sub(r'/\*.*?\*/', '', text_no_comments, flags=re.DOTALL)

    # Find class definitions
    class_pattern = r'class\s+(\w+)(?:\s*:\s*[^{]+)?\s*{'

    for class_match in re.finditer(class_pattern, text_no_comments, re.DOTALL):
        class_name = class_match.group(1)
        class_start = class_match.end()

        # Find the matching closing brace for this class
        brace_count = 1
        pos = class_start
        while pos < len(text_no_comments) and brace_count > 0:
            if text_no_comments[pos] == '{':
                brace_count += 1
            elif text_no_comments[pos] == '}':
                brace_count -= 1
            pos += 1

        class_body = text_no_comments[class_start:pos-1]

        # Find member function declarations
        # Match both declarations with semicolons and inline definitions
        # Capture the assignment part to detect pure virtual functions
        func_pattern = r'(?:virtual\s+|static\s+|inline\s+|explicit\s+|constexpr\s+)*[\w:]+[\s*&]+(\w+)\s*\([^)]*\)(?:\s+const)?(?:\s+noexcept)?(?:\s+override)?(?:\s*(=\s*(?:0|default|delete)))?(?:\s*;|\s*{)'

        position = 0
        for func_match in re.finditer(func_pattern, class_body):
            func_name = func_match.group(1)
            assignment = func_match.group(2)  # Captures "= 0", "= default", or "= delete"

            # Skip constructors, destructors, and operators
            if func_name == class_name or func_name.startswith('~') or func_name == 'operator':
                continue

            # Skip pure virtual functions (= 0)
            if assignment and '0' in assignment:
                continue

            functions.append((class_name, func_name, position))
            position += 1

    return functions

def extract_class_functions_from_cpp(text):
    """
    Extract class member function definitions from a cpp file.
    Returns a list of tuples: (class_name, function_name, position)
    where position is the order index in the cpp file (0-based).
    """
    functions = []

    # Remove comments to avoid false matches
    text_no_comments = re.sub(r'//.*', '', text)
    text_no_comments = re.sub(r'/\*.*?\*/', '', text_no_comments, flags=re.DOTALL)

    # Match member function definitions: ClassName::functionName
    # This pattern matches function definitions outside of class bodies
    func_pattern = r'(?:^|\n)[\w:]+[\s*&]+(\w+)::(\w+)\s*\([^)]*\)(?:\s+const)?(?:\s*noexcept)?(?:\s*->\s*[\w:]+)?\s*(?:\n|{)'

    position = 0
    for func_match in re.finditer(func_pattern, text_no_comments, re.MULTILINE):
        class_name = func_match.group(1)
        func_name = func_match.group(2)
        # Skip constructors/destructors and operators
        if func_name == class_name or func_name.startswith('~') or func_name == 'operator':
            continue
        functions.append((class_name, func_name, position))
        position += 1

    return functions

####################################################################################################
# Checks
####################################################################################################

def check_function_order(stem, th, tc, errs):
    """
    Check that member functions appear in the same order in .h and .cpp files.
    Only functions present in BOTH files are compared, and positions are renumbered
    to count only among those common functions.
    """
    h_functions = extract_class_functions_from_header(th)
    cpp_functions = extract_class_functions_from_cpp(tc)

    if not h_functions or not cpp_functions:
        return

    # Build dictionaries for quick lookup
    # Map (class_name, func_name) -> original position
    h_positions = {(cls, func): pos for cls, func, pos in h_functions}
    cpp_positions = {(cls, func): pos for cls, func, pos in cpp_functions}

    # Find functions that appear in both files
    common_functions = set(h_positions.keys()) & set(cpp_positions.keys())

    if not common_functions:
        return

    # Check order for each class
    classes = set(cls for cls, func in common_functions)

    for class_name in classes:
        # Get functions for this class that appear in both files
        class_functions = [(func, h_positions[(class_name, func)], cpp_positions[(class_name, func)])
                          for cls, func in common_functions if cls == class_name]

        if len(class_functions) < 2:
            continue  # Need at least 2 functions to check order

        # Sort by header position to get expected order
        class_functions.sort(key=lambda x: x[1])

        # Renumber positions to count only among common functions (0-based)
        renumbered = []
        for new_pos, (func, old_h_pos, old_cpp_pos) in enumerate(class_functions):
            renumbered.append((func, new_pos, old_cpp_pos))

        # Now sort by cpp position to find renumbered cpp positions
        cpp_sorted = sorted(renumbered, key=lambda x: x[2])
        cpp_renumbered = {func: new_pos for new_pos, (func, _, _) in enumerate(cpp_sorted)}

        # Now check if renumbered positions match
        for i in range(len(renumbered)):
            func, expected_pos, _ = renumbered[i]
            actual_pos = cpp_renumbered[func]

            if expected_pos != actual_pos:
                # Found function out of order
                # Report renumbered positions for clarity
                errs.append(f"In {stem}.h|cpp, function {class_name}::{func} "
                          f"is at position {expected_pos} vs {actual_pos}")
                return  # Report only first error per file pair

####################################################################################################
# Main
####################################################################################################

if __name__ == "__main__":

    assert len(sys.argv) == 1, "Check script must be called without arguments"

    # Tests on file pairs
    errs = []
    for fn in cu.all_files():
        stem, suffix = cu.split_fname(fn)
        if suffix != 'cpp':
            continue
        fh = stem + '.h'
        fc = fn
        if not os.path.exists(fh):
            continue
        with open(fh, 'r') as fd:
            th = fd.read()
        with open(fc, 'r') as fd:
            tc = fd.read()
        check_function_order(stem, th, tc, errs)

    if len(errs) > 0:
        print("Found error(s):")
        for err in errs:
            print("- " + err)
        sys.exit(1)

    sys.exit(0)
