#!/usr/bin/env python3
"""Checker to detect native platform defines and suggest FL_IS_* equivalents.

Two-phase scanning:
  1. Fast scan: skip files that don't contain any preprocessor conditionals
  2. Slow scan: line-by-line parse with comment tracking, only flags defines
     on #if/#ifdef/#ifndef/#elif lines.

Scope:
  - Checks all files under src/ (fl/, platforms/, lib8tion/, etc.)
  - Covers: hardware platform, OS, and compiler detection defines.
  - Excludes: is_*.h detection headers, root dispatch headers,
    compile test files, platform-level dispatch headers,
    third_party/ (external code), and compiler abstraction files.
"""

import re
from pathlib import Path

from ci.util.check_files import FileContent, FileContentChecker
from ci.util.paths import PROJECT_ROOT


SRC_ROOT = PROJECT_ROOT / "src"
PLATFORMS_ROOT = SRC_ROOT / "platforms"

# Root-level dispatch headers that MUST use native defines for coarse
# platform routing. These are the entry points that route to platform-specific
# code based on compiler-defined macros.
_ROOT_DISPATCH_HEADERS: set[str] = {
    "platforms.h",  # Routes to platform-specific FastLED headers
    "led_sysdefs.h",  # Routes to platform-specific system definitions
    "fastspi.h",  # Routes to platform-specific SPI implementations
    "fastled_progmem.h",  # Routes to platform-specific PROGMEM definitions
}

# Files that act as dispatch/config headers selecting platform-specific
# implementations at the architecture level (e.g., AVR asm vs ARM vs C).
_DISPATCH_CONFIG_FILES: set[str] = {
    "config.h",  # lib8tion/config.h — selects ASM vs C math implementations
}

# Compiler abstraction files that MUST use raw compiler defines (__clang__,
# __GNUC__, _MSC_VER) because they define macros wrapping compiler-specific
# features like pragmas, attributes, builtins, and export/import decorators.
_COMPILER_ABSTRACTION_FILES: set[str] = {
    "deprecated.h",  # Compiler-specific [[deprecated]] attributes
    "align.h",  # Compiler-specific alignment (__attribute__, __declspec)
    "export.h",  # DLL export/import (__declspec(dllexport), __attribute__((visibility)))
    "fltest.h",  # Test framework — compiler-specific type demangling
    "type_traits.h",  # Compiler builtins (__is_base_of, __builtin_*)
    "m0clockless_c.h",  # ARM inline asm — compiler-specific asm syntax
}

# Mapping of native compiler defines to their FL_IS_* equivalents.
# Each native define maps to the most specific FL_IS_* group it belongs to.
# Sources: src/platforms/*/is_*.h detection headers
NATIVE_TO_MODERN_DEFINES: dict[str, str] = {
    # ── General AVR ──────────────────────────────────────────────────
    "__AVR__": "FL_IS_AVR",
    # ── ATmega328P family (Arduino UNO/Nano) → FL_IS_AVR_ATMEGA_328P
    "__AVR_ATmega328P__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega328PB__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega328__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega168P__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega168__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega8__": "FL_IS_AVR_ATMEGA_328P",
    "__AVR_ATmega8A__": "FL_IS_AVR_ATMEGA_328P",
    # ── ATmega family (classic Timer1-based) → FL_IS_AVR_ATMEGA ─────
    "__AVR_ATmega2560__": "FL_IS_AVR_ATMEGA_2560",
    "__AVR_ATmega1280__": "FL_IS_AVR_ATMEGA_2560",
    "__AVR_ATmega32U4__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega1284__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega1284P__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega644P__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega644__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega32__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega16__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega128__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega64__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega32U2__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega16U2__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega8U2__": "FL_IS_AVR_ATMEGA",
    "__AVR_AT90USB1286__": "FL_IS_AVR_ATMEGA",
    "__AVR_AT90USB646__": "FL_IS_AVR_ATMEGA",
    "__AVR_AT90USB162__": "FL_IS_AVR_ATMEGA",
    "__AVR_AT90USB82__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega128RFA1__": "FL_IS_AVR_ATMEGA",
    "__AVR_ATmega256RFR2__": "FL_IS_AVR_ATMEGA",
    # ── megaAVR 0-series/1-series → FL_IS_AVR_MEGAAVR ───────────────
    "__AVR_ATmega4809__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega4808__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega3209__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega3208__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega1609__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega1608__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega809__": "FL_IS_AVR_MEGAAVR",
    "__AVR_ATmega808__": "FL_IS_AVR_MEGAAVR",
    # ── ATtiny family → FL_IS_AVR_ATTINY ─────────────────────────────
    # Classic ATtiny
    "__AVR_ATtiny13__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny13A__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny24__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny44__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny84__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny25__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny45__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny85__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny48__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny87__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny88__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny167__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny261__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny441__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny841__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny861__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny2313__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny2313A__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtiny4313__": "FL_IS_AVR_ATTINY",
    "__AVR_ATtinyX41__": "FL_IS_AVR_ATTINY",
    # Modern tinyAVR (0-series/1-series) with PORT_t registers
    "__AVR_ATtiny202__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny204__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny212__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny214__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny402__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny404__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny406__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny407__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny412__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny414__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny416__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny417__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny1604__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny1616__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny3216__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtiny3217__": "FL_IS_AVR_ATTINY_MODERN",
    # Generic tinyAVR wildcard defines
    "__AVR_ATtinyxy4__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtinyxy6__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtinyxy7__": "FL_IS_AVR_ATTINY_MODERN",
    "__AVR_ATtinyxy2__": "FL_IS_AVR_ATTINY_MODERN",
    # Arduino board defines for ATtiny-class boards
    "ARDUINO_AVR_DIGISPARK": "FL_IS_AVR_ATTINY",
    "ARDUINO_AVR_DIGISPARKPRO": "FL_IS_AVR_ATTINY",
    "IS_BEAN": "FL_IS_AVR_ATTINY",
    # ── ESP platforms ────────────────────────────────────────────────
    "ESP32": "FL_IS_ESP32",
    "ESP8266": "FL_IS_ESP8266",
    "ESP_PLATFORM": "FL_IS_ESP32",
    "ARDUINO_ARCH_ESP32": "FL_IS_ESP32",
    "ARDUINO_ARCH_ESP8266": "FL_IS_ESP8266",
    # ESP-IDF target defines (CONFIG_IDF_TARGET_*)
    "CONFIG_IDF_TARGET_ESP32": "FL_IS_ESP_32DEV",
    "CONFIG_IDF_TARGET_ESP32S2": "FL_IS_ESP_32S2",
    "CONFIG_IDF_TARGET_ESP32S3": "FL_IS_ESP_32S3",
    "CONFIG_IDF_TARGET_ESP32C2": "FL_IS_ESP_32C2",
    "CONFIG_IDF_TARGET_ESP32C3": "FL_IS_ESP_32C3",
    "CONFIG_IDF_TARGET_ESP32C5": "FL_IS_ESP_32C5",
    "CONFIG_IDF_TARGET_ESP32C6": "FL_IS_ESP_32C6",
    "CONFIG_IDF_TARGET_ESP32H2": "FL_IS_ESP_32H2",
    "CONFIG_IDF_TARGET_ESP32P4": "FL_IS_ESP_32P4",
    "CONFIG_IDF_TARGET_ESP8266": "FL_IS_ESP8266",
    # ESP-IDF architecture defines
    "CONFIG_IDF_TARGET_ARCH_XTENSA": "FL_IS_ESP32_XTENSA",
    "CONFIG_IDF_TARGET_ARCH_RISCV": "FL_IS_ESP32_RISCV",
    # Bare ESP32 variant defines (without CONFIG_IDF_TARGET_ prefix)
    "ESP32S3": "FL_IS_ESP_32S3",
    "ESP32C3": "FL_IS_ESP_32C3",
    "ESP32C6": "FL_IS_ESP_32C6",
    # ── Teensy platforms ─────────────────────────────────────────────
    "__MK20DX128__": "FL_IS_TEENSY_30",
    "__MK20DX256__": "FL_IS_TEENSY_3X",
    "__MKL26Z64__": "FL_IS_TEENSY_LC",
    "__MK64FX512__": "FL_IS_TEENSY_35",
    "__MK66FX1M0__": "FL_IS_TEENSY_36",
    "__IMXRT1062__": "FL_IS_TEENSY_4X",
    "__IMXRT1052__": "FL_IS_TEENSY_4X",
    # ── ARM general ──────────────────────────────────────────────────
    "__arm__": "FL_IS_ARM",
    "__SAM3X8E__": "FL_IS_SAM",
    # ── STM32 platforms ──────────────────────────────────────────────
    "STM32F10X_MD": "FL_IS_STM32_F1",
    "__STM32F1__": "FL_IS_STM32_F1",
    "STM32F1": "FL_IS_STM32_F1",
    "STM32F1xx": "FL_IS_STM32_F1",
    "STM32F2XX": "FL_IS_STM32_F2",
    "STM32F2xx": "FL_IS_STM32_F2",
    "STM32F4": "FL_IS_STM32_F4",
    "STM32F4xx": "FL_IS_STM32_F4",
    "STM32F7": "FL_IS_STM32_F7",
    "STM32F7xx": "FL_IS_STM32_F7",
    "STM32L4": "FL_IS_STM32_L4",
    "STM32L4xx": "FL_IS_STM32_L4",
    "STM32H7": "FL_IS_STM32_H7",
    "STM32H7xx": "FL_IS_STM32_H7",
    "STM32G4": "FL_IS_STM32_G4",
    "STM32G4xx": "FL_IS_STM32_G4",
    "STM32U5": "FL_IS_STM32_U5",
    "STM32U5xx": "FL_IS_STM32_U5",
    "STM32U585xx": "FL_IS_STM32_U5",
    "STM32U585XX": "FL_IS_STM32_U5",
    "ARDUINO_UNO_Q": "FL_IS_STM32_U5",
    "CONFIG_BOARD_ARDUINO_UNO_Q": "FL_IS_STM32_U5",
    "CONFIG_SOC_STM32U585XX": "FL_IS_STM32_U5",
    # ── NRF platforms ────────────────────────────────────────────────
    "NRF51": "FL_IS_NRF52",
    "NRF52_SERIES": "FL_IS_NRF52",
    "NRF52840_XXAA": "FL_IS_NRF52840",
    "NRF52833_XXAA": "FL_IS_NRF52833",
    "NRF52832_XXAA": "FL_IS_NRF52832",
    # Bare NRF variant defines (without _XXAA suffix)
    "NRF52": "FL_IS_NRF52",
    "NRF52840": "FL_IS_NRF52840",
    "NRF52833": "FL_IS_NRF52833",
    "NRF52832": "FL_IS_NRF52832",
    # ── SAMD platforms ───────────────────────────────────────────────
    "__SAMD21G18A__": "FL_IS_SAMD21",
    "__SAMD21J18A__": "FL_IS_SAMD21",
    "__SAMD21E17A__": "FL_IS_SAMD21",
    "__SAMD21E18A__": "FL_IS_SAMD21",
    "__SAMD21__": "FL_IS_SAMD21",
    "__SAMD51G19A__": "FL_IS_SAMD51",
    "__SAMD51J19A__": "FL_IS_SAMD51",
    "__SAME51J19A__": "FL_IS_SAMD51",
    "__SAMD51P19A__": "FL_IS_SAMD51",
    "__SAMD51P20A__": "FL_IS_SAMD51",
    "__SAMD51J20A__": "FL_IS_SAMD51",
    "__SAMD51__": "FL_IS_SAMD51",
    # ── RP2040/RP2350 platforms ──────────────────────────────────────
    "TARGET_RP2040": "FL_IS_RP2040",
    "ARDUINO_ARCH_RP2040": "FL_IS_RP2040",
    "PICO_RP2040": "FL_IS_RP2040",
    "ARDUINO_ARCH_RP2350": "FL_IS_RP2350",
    "PICO_RP2350": "FL_IS_RP2350",
    # ── Other Arduino architecture defines ───────────────────────────
    "ARDUINO_ARCH_SAMD": "FL_IS_SAMD",
    "ARDUINO_ARCH_STM32": "FL_IS_STM32",
    "ARDUINO_ARCH_NRF52": "FL_IS_NRF52",
    "ARDUINO_ARCH_APOLLO3": "FL_IS_APOLLO3",
    "ARDUINO_ARCH_RENESAS": "FL_IS_RENESAS",
    "ARDUINO_ARCH_RENESAS_UNO": "FL_IS_RENESAS",
    "ARDUINO_ARCH_RENESAS_PORTENTA": "FL_IS_RENESAS",
    "ARDUINO_ARCH_SILABS": "FL_IS_SILABS",
    "ARDUINO_ARCH_AVR": "FL_IS_AVR",
    "ARDUINO_ARCH_MBED": "FL_IS_STM32_MBED",
    "ARDUINO_SAM_DUE": "FL_IS_SAM",
    # ── Arduino board-level defines ─────────────────────────────────
    "ARDUINO_GIGA": "FL_IS_STM32_H7",
    "ARDUINO_GIGA_M7": "FL_IS_STM32_H7",
    "ARDUINO_NRF52_ADAFRUIT": "FL_IS_NRF52",
    # ── WASM / Emscripten ───────────────────────────────────────────
    "__EMSCRIPTEN__": "FL_IS_WASM",
    # ── Particle / Spark ────────────────────────────────────────────
    "SPARK": "FL_IS_STM32_F2",
    # ── Apollo3 bare define ─────────────────────────────────────────
    "APOLLO3": "FL_IS_APOLLO3",
    # ── Teensy SDK defines ────────────────────────────────────────────
    "TEENSYDUINO": "FL_IS_TEENSY",
    "CORE_TEENSY": "FL_IS_TEENSY",
    # ── FastLED Teensy convenience defines ──────────────────────────
    "FASTLED_TEENSY3": "FL_IS_TEENSY_3X",
    "FASTLED_TEENSY4": "FL_IS_TEENSY_4X",
    "FASTLED_TEENSYLC": "FL_IS_TEENSY_LC",
    # ── NRF51 legacy ─────────────────────────────────────────────────
    "__RFduino__": "FL_IS_NRF52",
    "__Simblee__": "FL_IS_NRF52",
    # ── OS detection — desktop/host platforms ──────────────────────
    "__APPLE__": "FL_IS_APPLE",
    "__linux__": "FL_IS_LINUX",
    "__unix__": "FL_IS_POSIX",
    "_WIN32": "FL_IS_WIN",
    "_WIN64": "FL_IS_WIN",
    "__CYGWIN__": "FL_IS_WIN",
    "__MINGW32__": "FL_IS_WIN_MINGW",
    "__MINGW64__": "FL_IS_WIN_MINGW",
    "_MSC_VER": "FL_IS_WIN_MSVC",
    "__FreeBSD__": "FL_IS_BSD",
    "__OpenBSD__": "FL_IS_BSD",
    "__NetBSD__": "FL_IS_BSD",
    "__DragonFly__": "FL_IS_BSD",
    "__wasm__": "FL_IS_WASM",
    "EMSCRIPTEN": "FL_IS_WASM",
    # ── Compiler detection ────────────────────────────────────────
    "__clang__": "FL_IS_CLANG",
    "__GNUC__": "FL_IS_GCC",
    "__GNUG__": "FL_IS_GCC",
}

# Regex matching preprocessor conditional directives:
#   #if, #ifdef, #ifndef, #elif (with optional whitespace after #)
_PREPROCESSOR_CONDITIONAL_RE = re.compile(r"^\s*#\s*(?:if|ifdef|ifndef|elif)\b")

# Fast-scan pattern: does the raw text contain any preprocessor conditional keyword?
# This is intentionally loose — false positives are fine, false negatives are not.
_FAST_HAS_CONDITIONAL_RE = re.compile(r"#\s*(?:if|ifdef|ifndef|elif)\b")

# Pre-compiled word-boundary patterns for each native define
# (was re.search(r"\b" + re.escape(native_define) + r"\b", ...) per line per define)
_NATIVE_DEFINE_PATTERNS: dict[str, re.Pattern[str]] = {
    native_define: re.compile(r"\b" + re.escape(native_define) + r"\b")
    for native_define in NATIVE_TO_MODERN_DEFINES
}


def _fast_scan_dominated(content: str) -> bool:
    """Return True if the file *might* contain a flaggable violation.

    This is the fast path. It may return True for files that ultimately have
    zero violations (false positive), but it must never return False for a
    file that has real violations (no false negatives).

    The check is: file contains at least one preprocessor conditional AND
    at least one of the native defines we care about.
    """
    if not _FAST_HAS_CONDITIONAL_RE.search(content):
        return False
    for native_define in NATIVE_TO_MODERN_DEFINES:
        if native_define in content:
            return True
    return False


def _is_in_platforms_dir(path_obj: Path) -> bool:
    """Check if a file is under src/platforms/."""
    try:
        path_obj.relative_to(PLATFORMS_ROOT)
        return True
    except ValueError:
        return False


class NativePlatformDefinesChecker(FileContentChecker):
    """Checker for native platform defines across all src/ directories.

    Scans all C++ files under src/ for native platform, OS, and compiler
    defines in preprocessor conditionals and suggests FL_IS_* equivalents.

    Exclusions:
      - third_party/ (external code we don't control)
      - is_*.h detection headers (they define FL_IS_* from native defines)
      - Root dispatch headers (platforms.h, led_sysdefs.h, etc.)
      - Platform-level dispatch headers (fastpin_*.h, led_sysdefs_*.h, etc.)
      - Compiler abstraction files (compiler_control.h, deprecated.h, etc.)
      - Compile test files
      - core_detection.h
      - lib8tion/config.h (architecture-level dispatch for ASM vs C)
    """

    def __init__(self):
        self.violations: dict[str, list[tuple[int, str]]] = {}

    def should_process_file(self, file_path: str) -> bool:
        """Check if file should be processed for native platform defines."""
        # Must be under src/
        if not file_path.startswith(str(SRC_ROOT)):
            return False

        # Check file extension - .h, .cpp, .hpp files
        if not file_path.endswith((".cpp", ".h", ".hpp")):
            return False

        path_obj = Path(file_path)
        file_name = path_obj.name

        # ── Third-party exclusion ────────────────────────────────────
        # External libraries use raw compiler/OS defines appropriately;
        # we don't control their code.
        try:
            path_obj.relative_to(SRC_ROOT / "third_party")
            return False
        except ValueError:
            pass

        # Skip the is_*.h detection headers — they MUST use native defines
        # These files convert native compiler defines into FL_IS_* defines
        if file_name.startswith("is_"):
            return False

        # Skip core detection headers — they detect which Arduino core is in use
        if "core_detection" in file_name.lower():
            return False

        # Skip compile test files
        if "compile_test" in file_name.lower():
            return False

        # ── Compiler abstraction files ───────────────────────────────
        # These files define macros wrapping compiler-specific features
        # (pragmas, attributes, builtins, DLL export/import).
        # They MUST use raw compiler defines.
        if file_name in _COMPILER_ABSTRACTION_FILES:
            return False

        # ── Root-level src/ dispatch headers ──────────────────────────
        # These files at src/*.h route to platform-specific implementations
        if path_obj.parent == SRC_ROOT and file_name in _ROOT_DISPATCH_HEADERS:
            return False

        # ── lib8tion/config.h — architecture-level dispatch ──────────
        if file_name in _DISPATCH_CONFIG_FILES:
            # Only skip if it's directly in lib8tion/
            if path_obj.parent.name == "lib8tion":
                return False

        # ── Platform-specific exclusions (for files under src/platforms/) ─
        if _is_in_platforms_dir(path_obj):
            # Skip dispatch headers at src/platforms/*.h root level
            if path_obj.parent == PLATFORMS_ROOT:
                return False

            # Pattern 1: fastpin_*.h / fastspi_*.h — pin and SPI dispatch/implementation headers
            if file_name.startswith("fastpin_") or file_name.startswith("fastspi_"):
                return False

            # Pattern 2: led_sysdefs_<platform>.h
            if file_name.startswith("led_sysdefs_") and file_name.count("_") <= 2:
                return False

            # Pattern 3: Explicit dispatcher files
            if "dispatch" in file_name.lower() or file_name == "fastpin_legacy.h":
                return False

        return True

    def check_file_content(self, file_content: FileContent) -> list[str]:
        """Check file content for native platform defines in preprocessor conditionals.

        Uses two-phase scanning:
          1. Fast scan — bail immediately if file can't possibly have violations.
          2. Slow scan — precise line-by-line parse with comment tracking.
        """
        # --- Phase 1: fast scan ---
        if not _fast_scan_dominated(file_content.content):
            return []

        # --- Phase 2: slow scan ---
        violations: list[tuple[int, str]] = []
        in_multiline_comment = False

        for line_number, line in enumerate(file_content.lines, 1):
            # Track multi-line comment state
            if "/*" in line:
                in_multiline_comment = True
            if "*/" in line:
                in_multiline_comment = False
                continue

            if in_multiline_comment:
                continue

            stripped = line.strip()

            # Skip single-line comment lines
            if stripped.startswith("//"):
                continue

            # Only check preprocessor conditional directives
            if not _PREPROCESSOR_CONDITIONAL_RE.match(stripped):
                continue

            # Remove trailing comment before checking
            code_part = line.split("//")[0]

            for native_define, modern_define in NATIVE_TO_MODERN_DEFINES.items():
                # Fast first pass: skip regex if keyword not in line as substring
                if native_define not in code_part:
                    continue
                # Accurate second pass with pre-compiled word-boundary regex
                if _NATIVE_DEFINE_PATTERNS[native_define].search(code_part):
                    message = (
                        f"Native platform define '{native_define}' found in "
                        f"preprocessor conditional. "
                        f"Use '#ifdef {modern_define}' or "
                        f"'#if defined({modern_define})' instead "
                        f"(FL_IS_* macros are defined/undefined, never use bare "
                        f"'#if {modern_define}')."
                    )
                    violations.append((line_number, message))

        if violations:
            self.violations[file_content.path] = violations

        return []  # We collect violations internally


def main() -> None:
    """Run native platform defines checker standalone."""
    from ci.util.check_files import run_checker_standalone

    checker = NativePlatformDefinesChecker()
    run_checker_standalone(
        checker,
        [str(SRC_ROOT)],
        "Found native platform defines in src/",
    )


if __name__ == "__main__":
    main()
