#!/usr/bin/env python3

"""
Various checks for certain C++ style rules that we choose for BornAgain,
in addition to the rules imposed by clang-format.

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

import check_utils as cu
import os, re, sys

####################################################################################################
# checks
####################################################################################################

def rm_namespace(t):
    return re.sub(r'\nnamespace ([\w:]+ )?{\n(.*\n)*?}.*', r'\nnamespace /*...*/', t)

def check_block(fn, t, name, errs):

    if name == "include":
        rex = r'(#include) .*?'
    elif name == "using":
        rex = r'(using) [^;]*?;'
        t = rm_namespace(t)
    elif name == "fwd decl":
        rex = r'(class|enum class|struct) \w+;'
        t = rm_namespace(t)
    matches = [m for m in re.finditer(r'(\n*|template.*(\n\s*requires.*)*)\n('+ rex + r'([ ]*//.*)?\n)+(\n*)', t)]

    if len(matches) > 1:

        # tolerate multiple #include blocks if they are separated by some #if or #pragma
        if name == "include" and re.search('#include.*#(if|pragma).*#include', t, re.DOTALL):
            return

        errs.append(f"several {name} blocks in file {fn}")

        return

    elif len(matches) == 0:
        return

    m = matches[0]

    if re.match(r'^template', m.group(1)):
        return
    if m.group(1) == '':
        if not re.match(rex, t):
            errs.append(f"missing blank line above {name} block in file {fn}")
        else:
            print(f'In file {fn} block {name} at head')
    elif m.group(1) != '\n':
        errs.append(f"more than one blank line above {name} block in file {fn}")
    if m.group(6) == '':
        errs.append(f"missing blank line below {name} block in file {fn}")
    elif m.group(6) != '\n':
        errs.append(f"more than one blank line below {name} block in file {fn}")

def check_fwd_decl_sorted(fn, t, errs):
    for m in re.finditer(r'((class|struct) (\w+);([ ]*//.*)?\n)+', t):
        decls_txt = m.group(0)
        decls_arr = decls_txt.rstrip('\n').split('\n')
        if decls_arr != sorted(decls_arr):
            errs.append(f"forward declarations not sorted in file {fn}")
            return

def check_no_trivial_getters_in_cppfile(fc, t, errs):
    m = re.search(r'\n\S+ (\w+::[a-z]\w*)\(\) const\n{\s*return m_\w+;\s*}', tc)
    if m:
        errs.append(f"trivial getter {m.group(1)} in file {fc}, should go to header file")

####################################################################################################
# main
####################################################################################################

if __name__ == "__main__":

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

    errs = []

    # tests on each file:
    for fn in cu.all_files():
        stem, suffix = cu.split_fname(fn)
        with open(fn, 'r') as fd:
            t = fd.read()
        check_block(fn, t, 'include', errs)
        check_block(fn, t, 'using', errs)
        if suffix == 'h':
            check_block(fn, t, 'fwd decl', errs)
            check_fwd_decl_sorted(fn, t, errs)

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

    sys.exit(0)
