#!/usr/bin/env python3
#
#  Simple DirectMedia Layer
#  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
#
#  This software is provided 'as-is', without any express or implied
#  warranty.  In no event will the authors be held liable for any damages
#  arising from the use of this software.
#
#  Permission is granted to anyone to use this software for any purpose,
#  including commercial applications, and to alter it and redistribute it
#  freely, subject to the following restrictions:
#
#  1. The origin of this software must not be misrepresented; you must not
#     claim that you wrote the original software. If you use this software
#     in a product, an acknowledgment in the product documentation would be
#     appreciated but is not required.
#  2. Altered source versions must be plainly marked as such, and must not be
#     misrepresented as being the original software.
#  3. This notice may not be removed or altered from any source distribution.
#
# This script detects use of stdlib function in SDL code

import argparse
import os
import pathlib
import re
import sys

SDL_ROOT = pathlib.Path(__file__).resolve().parents[1]

STDLIB_SYMBOLS = (
    'abs',
    'acos',
    'acosf',
    'asin',
    'asinf',
    'asprintf',
    'atan',
    'atan2',
    'atan2f',
    'atanf',
    'atof',
    'atoi',
    'bsearch',
    'calloc',
    'ceil',
    'ceilf',
    'copysign',
    'copysignf',
    'cos',
    'cosf',
    'crc32',
    'exp',
    'expf',
    'fabs',
    'fabsf',
    'floor',
    'floorf',
    'fmod',
    'fmodf',
    'free',
    'getenv',
    'isalnum',
    'isalpha',
    'isblank',
    'iscntrl',
    'isdigit',
    'isgraph',
    'islower',
    'isprint',
    'ispunct',
    'isspace',
    'isupper',
    'isxdigit',
    'itoa',
    'lltoa',
    'log10',
    'log10f',
    'logf',
    'lround',
    'lroundf',
    'ltoa',
    'malloc',
    'memalign',
    'memcmp',
    'memcpy',
    'memcpy4',
    'memmove',
    'memset',
    'pow',
    'powf',
    'qsort',
    'qsort_r',
    'qsort_s',
    'realloc',
    'round',
    'roundf',
    'scalbn',
    'scalbnf',
    'setenv',
    'sin',
    'sinf',
    'snprintf',
    'sqrt',
    'sqrtf',
    'sscanf',
    'strcasecmp',
    'strchr',
    'strcmp',
    'strdup',
    'strlcat',
    'strlcpy',
    'strlen',
    'strlwr',
    'strncasecmp',
    'strncmp',
    'strrchr',
    'strrev',
    'strstr',
    'strtod',
    'strtokr',
    'strtol',
    'strtoll',
    'strtoul',
    'strupr',
    'tan',
    'tanf',
    'tolower',
    'toupper',
    'trunc',
    'truncf',
    'uitoa',
    'ulltoa',
    'ultoa',
    'utf8strlcpy',
    'utf8strlen',
    'vasprintf',
    'vsnprintf',
    'vsscanf',
    'wcscasecmp',
    'wcscmp',
    'wcsdup',
    'wcslcat',
    'wcslcpy',
    'wcslen',
    'wcsncasecmp',
    'wcsncmp',
    'wcsstr',
)
RE_STDLIB_SYMBOL = re.compile(rf"(?<!->)\b(?P<symbol>{'|'.join(STDLIB_SYMBOLS)})\b\(")


def find_symbols_in_file(file: pathlib.Path) -> int:
    match_count = 0

    allowed_extensions = [ ".c", ".cpp", ".m", ".h",  ".hpp", ".cc" ]

    excluded_paths = [
        "src/stdlib",
        "src/libm",
        "src/hidapi",
        "src/video/khronos",
        "src/video/miniz.h",
        "src/video/stb_image.h",
        "include/SDL3",
        "build-scripts/gen_audio_resampler_filter.c",
        "build-scripts/gen_audio_channel_conversion.c",
        "test/win32/sdlprocdump.c",
    ]

    filename = pathlib.Path(file)

    for ep in excluded_paths:
        if ep in filename.as_posix():
            # skip
            return 0

    if filename.suffix not in allowed_extensions:
        # skip
        return 0

    # print("Parse %s" % file)

    try:
        with file.open("r", encoding="UTF-8", newline="") as rfp:
            parsing_comment = False
            for line_i, original_line in enumerate(rfp, start=1):
                line = original_line.strip()

                line_comment = ""

                # Get the comment block /* ... */ across several lines
                while True:
                    if parsing_comment:
                        pos_end_comment = line.find("*/")
                        if pos_end_comment >= 0:
                            line = line[pos_end_comment+2:]
                            parsing_comment = False
                        else:
                            break
                    else:
                        pos_start_comment = line.find("/*")
                        if pos_start_comment >= 0:
                            pos_end_comment = line.find("*/", pos_start_comment+2)
                            if pos_end_comment >= 0:
                                line_comment += line[pos_start_comment:pos_end_comment+2]
                                line = line[:pos_start_comment] + line[pos_end_comment+2:]
                            else:
                                line_comment += line[pos_start_comment:]
                                line = line[:pos_start_comment]
                                parsing_comment = True
                                break
                        else:
                            break
                if parsing_comment:
                    continue
                pos_line_comment = line.find("//")
                if pos_line_comment >= 0:
                    line_comment += line[pos_line_comment:]
                    line = line[:pos_line_comment]

                if matches := tuple(RE_STDLIB_SYMBOL.finditer(line)):
                    text_string = " or ".join(f"SDL_{m.group(1)}" for m in matches)
                    first_quote = line.find("\"")
                    last_quote = line.rfind("\"")
                    first_occurrence = min(m.span()[0] for m in matches)
                    last_occurrence = max(m.span()[1] for m in matches)
                    if first_quote == -1 or not (first_quote < first_occurrence and last_quote > last_occurrence):
                        override_string = f"This should NOT be {text_string}"
                        if override_string not in line_comment:
                            print(f"{filename}:{line_i}")
                            print(f"    {line}")
                            print(f"")
                            match_count += 1

    except UnicodeDecodeError:
        print(f"{file} is not text, skipping", file=sys.stderr)

    return match_count

def find_symbols_in_dir(path: pathlib.Path) -> int:
    match_count = 0
    for entry in path.iterdir():
        if entry.is_dir():
            match_count += find_symbols_in_dir(entry)
        else:
            match_count += find_symbols_in_file(entry)
    return match_count

def main():
    parser = argparse.ArgumentParser(fromfile_prefix_chars="@")
    parser.add_argument("path", default=SDL_ROOT, nargs="?", type=pathlib.Path, help="Path to look for stdlib symbols")
    args = parser.parse_args()

    print(f"Looking for stdlib usage in {args.path}...")

    if args.path.is_file():
        match_count = find_symbols_in_file(args.path)
    else:
        match_count = find_symbols_in_dir(args.path)

    if match_count:
        print("If the stdlib usage is intentional, add a '// This should NOT be SDL_<symbol>()' line comment.")
        print("")
        print("NOT OK")
    else:
        print("OK")
    return 1 if match_count else 0

if __name__ == "__main__":
    raise SystemExit(main())
