#!/usr/bin/env python3
"""Remove 'static' from ATK class method definitions.

The class preprocessor emits non-static extern declarations for all class
methods in .eh files. When the .c implementation defines a method as 'static',
modern clang rejects it: "static declaration follows non-static declaration."

Removing 'static' from the definition is correct: class methods need external
linkage so the dispatch table and dlopen-based dynamic loader can find them.

Two K&R definition patterns are handled:

  Pattern A (static on same line as method):
    static struct foo *classname__Method(self, a, b)
    → struct foo *classname__Method(self, a, b)

  Pattern B (K&R split: static+type on own line, name on next):
        static boolean
    classname__Method(self, a, b)
    →
        boolean
    classname__Method(self, a, b)

Only targets function names containing __ (double-underscore ATK class methods).
Does not touch single-name helpers like 'initself' or 'DrawWormHole' — those
need forward-declaration fixes instead.

Usage:
    fix-static-methods [--dry-run] file.c [file2.c ...]
    fix-static-methods [--dry-run] --dir src/atk/text
"""

import re
import sys
import argparse
import os

# Matches a class method name: prefix__Suffix or prefix_Suffix (ATK convention)
# Must start at column 0 (no leading whitespace) in K&R style definitions.
CLASS_METHOD = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*__[A-Za-z0-9_]+\s*\(')

# Pattern A: static keyword at line start, followed by return type and method name
# e.g. "static struct environment *text__AlwaysWrapViewChar("
PATTERN_A = re.compile(
    r'^(static\s+)'
    r'((?:struct\s+\w+\s*\*\s*|(?:unsigned\s+)?(?:long\s+)?(?:int|char|void|boolean|short|long|float|double)\s*\*?\s*)?)'
    r'([A-Za-z_][A-Za-z0-9_]*__[A-Za-z0-9_]+\s*\()',
    re.MULTILINE
)

def fix_file(path, dry_run=False):
    with open(path, 'r', errors='replace') as f:
        lines = f.readlines()

    changed = False
    out = []
    i = 0
    while i < len(lines):
        line = lines[i]

        # Pattern A: 'static ... classname__Method(' on one line
        m = re.match(r'^(static\s+)(?=.*\b[A-Za-z_][A-Za-z0-9_]*__[A-Za-z0-9_]+\s*\()', line)
        if m:
            # Strip only the 'static ' prefix -- keep everything after it
            # (parameter names, closing paren, etc.) untouched.
            out.append(line[m.end(1):])
            changed = True
            i += 1
            continue

        # Pattern B: 'static TYPE' on this line, class method name starts next line
        m = re.match(r'^(\s*)static\s+(\S.*\S|\S)\s*$', line.rstrip('\n'))
        if m and i + 1 < len(lines) and CLASS_METHOD.match(lines[i + 1]):
            indent = m.group(1)
            return_type = m.group(2)
            out.append(indent + return_type + '\n')
            changed = True
            i += 1
            continue

        out.append(line)
        i += 1

    if changed:
        if not dry_run:
            with open(path, 'w') as f:
                f.writelines(out)
        return True
    return False


# Platform-specific directories that aren't compiled on Darwin
SKIP_DIRS = {
    'hp300', 'hp700', 'hp800', 'pmax_3', 'pmax_41', 'pmax_42', 'pmax_ul4',
    'rt_aos4', 'rt_mach', 'sun3_35', 'sun3_4', 'sun3_41', 'sun4_40',
    'sun4_41', 'sun4_51', 'vax_3', 'vax_43', 'aix_i386', 'aix_rt', 'aos_rt',
    'dec_mips', 'dec_mips_42', 'dec_vax', 'hp_68k', 'i386_bsd', 'i386_bsdi',
    'i386_Linux', 'i386_mach', 'mach_rt', 'sco_i386', 'sgi_mips', 'sun_68k',
    'sun_sparc', 'sun_sparc_mach', 'rs_aix3',
}


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument('--dry-run', action='store_true',
                        help='Show what would change without modifying files')
    parser.add_argument('--dir', metavar='DIR',
                        help='Process all .c files under DIR recursively')
    parser.add_argument('files', nargs='*', metavar='file.c',
                        help='C source files to process')
    args = parser.parse_args()

    targets = list(args.files)
    if args.dir:
        for dirpath, dirnames, filenames in os.walk(args.dir):
            dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
            for fn in filenames:
                if fn.endswith('.c'):
                    targets.append(os.path.join(dirpath, fn))

    if not targets:
        parser.print_help()
        sys.exit(1)

    total = 0
    for path in targets:
        if fix_file(path, dry_run=args.dry_run):
            verb = 'Would modify' if args.dry_run else 'Modified'
            print(f'{verb}: {path}')
            total += 1

    print(f'\nTotal files {"that would be " if args.dry_run else ""}modified: {total}')


if __name__ == '__main__':
    main()
