#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Storm Engine Script Linter v2.0
Проверяет синтаксис скриптов Storm Engine перед запуском игры:
  - else/switch/try (не существуют в Storm Engine)
  - необъявленные константы (#define)
  - вызовы неопределённых функций
  - баланс скобок ()
  - пропущенные точки с запятой
  - баланс фигурных скобок {}

Использование:
  python lint.py <путь_к_PROGRAM>                        # проверить все .c файлы в PROGRAM/
  python lint.py <путь_к_PROGRAM> file1.c file2.c        # проверить конкретные файлы
  python lint.py <путь_к_PROGRAM> --verbose               # подробные предупреждения
  python lint.py <путь_к_PROGRAM> --engine <путь_к_engine> # подключить константы движка

Примеры:
  python lint.py "C:/Games/SeaDogs/PROGRAM"
  python lint.py "C:/Games/SeaDogs/PROGRAM" quests/ChardLine_reaction.c
  python lint.py ./PROGRAM --verbose --engine ./storm-engine-develop
"""

import sys
import os
import re
from pathlib import Path

class Colors:
    RED = '\033[91m'
    YELLOW = '\033[93m'
    GREEN = '\033[92m'
    CYAN = '\033[96m'
    GRAY = '\033[90m'
    RESET = '\033[0m'

    @classmethod
    def disable(cls):
        """Отключить цвета (для перенаправления в файл)"""
        cls.RED = cls.YELLOW = cls.GREEN = cls.CYAN = cls.GRAY = cls.RESET = ''

error_count = 0
warning_count = 0
verbose_mode = False

def error(file_path, line_num, message):
    global error_count
    print(f"{Colors.RED}ERROR:{Colors.RESET} {Colors.YELLOW}{file_path}:{line_num}{Colors.RESET} {message}")
    error_count += 1

def warning(file_path, line_num, message):
    global warning_count
    print(f"{Colors.YELLOW}WARNING:{Colors.RESET} {Colors.GRAY}{file_path}:{line_num}{Colors.RESET} {message}")
    warning_count += 1

# ============================================================
# Встроенные функции Storm Engine (из internal_functions.cpp)
# ============================================================
BUILTIN_FUNCTIONS = {
    # Математика
    "rand", "frnd", "abs", "sqrt", "sqr", "sin", "cos", "tan",
    "atan", "atan2", "asin", "acos", "pow",
    # Сущности/Классы
    "CreateClass", "CreateEntity", "DeleteClass", "DeleteEntities",
    "DeleteEntitiesByType", "IsEntity", "GetEntity", "FindEntity",
    "FindEntityNext",
    # События
    "SetEventHandler", "DelEventHandler", "Event", "PostEvent",
    "GetEventData", "ClearEvents", "ClearPostEvents", "EventsBreak",
    # Сообщения
    "SendMessage", "Trace", "trace",
    # Атрибуты
    "GetAttributesNum", "GetAttributeN", "GetAttributeName",
    "GetAttributeValue", "CheckAttribute", "DeleteAttribute",
    "DumpAttributes", "CopyAttributes",
    # Массивы
    "SetArraySize", "GetArraySize",
    # Строки
    "sti", "stf", "fts", "strlen", "strcut", "findSubStr",
    "GetSymbol", "IsDigit",
    # Приведение типов
    "MakeInt", "MakeFloat", "Vartype",
    # Сегменты
    "LoadSegment", "UnloadSegment", "SegmentIsLoaded",
    # Слои
    "LayerDeleteContent", "LayerSetRealize", "LayerSetExecute",
    "LayerSetMessages", "LayerAddObject", "LayerDelObject",
    "LayerFreeze",
    # Управление
    "CreateControl", "DeleteControl", "MapControl",
    "SetControlFlags", "SetControlTreshold", "LockControl",
    # Обновление сущностей
    "EntityUpdate",
    # Состояние движка
    "SaveEngineState", "LoadEngineState", "SaveVariable", "LoadVariable",
    # Управление программой
    "ExitProgram", "Stop",
    # Битовые операции
    "shl", "shr", "and", "or",
    # Утилиты
    "argb", "ClearRef", "TestRef", "Breakpoint", "GetDeltaTime",
    "SetTimeScale", "GetTargetPlatform", "GetEngineVersion",
    "CheckFunction", "sort",
    # XI_ функции интерфейса (движок)
    "XI_ConvertString",
    # Лог-функции (движок)
    "Log_Info", "Log_SetStringToLog", "Log_SetActiveAction",
    "Log_TestRecovery", "Log_info",
}

# Ключевые слова языка и типы - НЕ являются вызовами функций
KEYWORDS = {
    "if", "for", "while", "return", "break", "continue",
    "void", "int", "float", "string", "String", "bool", "ref", "aref",
    "object", "extern", "true", "false", "null",
    # Встроенные ключевые слова (из token.cpp)
    "makearef", "makeref",
}


def strip_comments(line):
    """Удалить // и /* комментарии (но не внутри строк)"""
    result = []
    in_string = False
    string_char = None
    i = 0
    while i < len(line):
        ch = line[i]
        if in_string:
            result.append(ch)
            if ch == '\\' and i + 1 < len(line):
                result.append(line[i + 1])
                i += 2
                continue
            if ch == string_char:
                in_string = False
        elif ch in ('"', "'"):
            in_string = True
            string_char = ch
            result.append(ch)
        elif ch == '/' and i + 1 < len(line) and line[i + 1] == '/':
            break
        elif ch == '/' and i + 1 < len(line) and line[i + 1] == '*':
            break
        else:
            result.append(ch)
        i += 1
    return ''.join(result)


def strip_strings(line):
    """Удалить содержимое строковых литералов"""
    return re.sub(r'"[^"]*"', '""', line)


def scan_defines(program_dir):
    """Сканировать все #define в проекте, вернуть множество имён"""
    defines = set()
    for root, dirs, files in os.walk(program_dir):
        for fname in files:
            if fname.endswith(('.c', '.h')):
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        for line in f:
                            m = re.match(r'\s*#define\s+(\w+)', line)
                            if m:
                                defines.add(m.group(1))
                except:
                    pass
    return defines


def scan_engine_defines(engine_dir):
    """Сканировать #define из заголовков движка (messages.h, script_defines.h и т.д.)"""
    defines = set()
    shared_dir = os.path.join(engine_dir, "src", "libs", "shared_headers",
                              "include", "shared")
    if not os.path.isdir(shared_dir):
        return defines

    for root, dirs, files in os.walk(shared_dir):
        for fname in files:
            if fname.endswith('.h'):
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        for line in f:
                            m = re.match(r'\s*#define\s+(\w+)', line)
                            if m:
                                defines.add(m.group(1))
                except:
                    pass
    return defines


def scan_functions(program_dir):
    """Сканировать определения функций, вернуть множество имён в lowercase"""
    functions = set()
    func_pattern = re.compile(
        r'^\s*(?:void|int|bool|float|string|String|ref|aref|object)\s+(\w+)\s*\('
    )
    for root, dirs, files in os.walk(program_dir):
        for fname in files:
            if fname.endswith(('.c', '.h')):
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        for line in f:
                            m = func_pattern.match(line)
                            if m:
                                functions.add(m.group(1).lower())
                except:
                    pass
    return functions


def scan_global_variables(program_dir):
    """Сканировать глобальные переменные (int x = ...; и т.д. на уровне файла)"""
    variables = set()
    var_pattern = re.compile(
        r'^(?:int|bool|float|string|String)\s+(\w+)\s*[=;,]'
    )
    for root, dirs, files in os.walk(program_dir):
        for fname in files:
            if fname.endswith(('.c', '.h')):
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        for line in f:
                            m = var_pattern.match(line)
                            if m:
                                variables.add(m.group(1))
                except:
                    pass
    return variables


def check_missing_semicolon(stripped, prev_stripped, file_path, line_num):
    """Проверить пропущенную точку с запятой после вызовов функций"""
    if not stripped:
        return
    if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
        return
    if stripped.startswith('#'):
        return
    if stripped.endswith('{') or stripped.endswith('}') or stripped == '{' or stripped == '}':
        return
    if stripped == ';':
        return
    if stripped.startswith('extern'):
        return
    # Определения функций не нуждаются в ;
    if re.match(r'^(?:void|int|bool|float|string|String|ref|aref|object)\s+\w+\s*\(', stripped):
        if not stripped.endswith(';'):
            return

    clean = strip_comments(stripped).rstrip()
    if not clean:
        return

    # Строка заканчивается на ) без ; — потенциальная ошибка
    if clean.endswith(')'):
        bare = clean.lstrip()
        # Исключить управляющие конструкции
        if re.match(r'^(if|for|while)\s*\(', bare):
            return
        # Определение функции
        if re.match(r'^(?:void|int|bool|float|string|String|ref|aref|object)\s+\w+\s*\(', bare):
            return
        # Многострочное продолжение (предыдущая строка заканчивается на || или && или , или + )
        if prev_stripped:
            ps = strip_comments(prev_stripped).rstrip()
            if ps.endswith('||') or ps.endswith('&&') or ps.endswith(',') or ps.endswith('+'):
                return
        # Текущая строка — продолжение предыдущей (начинается с оператора)
        if bare.startswith('+') or bare.startswith('||') or bare.startswith('&&'):
            return
        error(file_path, line_num,
              "Пропущена точка с запятой ';' в конце строки")


def check_function_calls(line, stripped, file_path, line_num, known_functions_lower):
    """Проверить вызовы неопределённых функций (без учёта регистра)"""
    if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
        return
    if stripped.startswith('#'):
        return

    clean = strip_comments(line)
    clean = strip_strings(clean)

    for m in re.finditer(r'\b([A-Za-z_]\w*)\s*\(', clean):
        func_name = m.group(1)

        # Пропустить ключевые слова
        if func_name in KEYWORDS or func_name.lower() in {k.lower() for k in KEYWORDS}:
            continue

        # Пропустить если это объявление типа (void func(...), int func(...))
        prefix = clean[:m.start()].rstrip()
        if re.match(r'.*\b(?:void|int|bool|float|string|String|ref|aref|object)$', prefix):
            continue

        # Проверка без учёта регистра
        if func_name.lower() in known_functions_lower:
            continue

        warning(file_path, line_num,
                f"Вызов неизвестной функции: {func_name}()")


def check_undefined_constants(line, stripped, file_path, line_num, known_defines, known_variables):
    """Проверить необъявленные UPPER_CASE константы"""
    if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
        return
    if stripped.startswith('#'):
        return

    clean = strip_comments(line)
    clean = strip_strings(clean)

    # Найти UPPER_CASE идентификаторы вида ABC_DEF (минимум одно подчёркивание)
    for m in re.finditer(r'\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b', clean):
        const_name = m.group(1)

        if const_name in known_defines:
            continue
        if const_name in known_variables:
            continue

        error(file_path, line_num,
              f"Необъявленная константа: {const_name}")


def lint_file(file_path, known_defines=None, known_functions_lower=None, known_variables=None):
    """Проверить один файл"""
    if not os.path.exists(file_path):
        print(f"{Colors.RED}ОШИБКА: Файл не найден: {file_path}{Colors.RESET}")
        return

    print(f"\n{Colors.CYAN}Проверка: {file_path}{Colors.RESET}")

    try:
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            lines = f.readlines()
    except Exception as e:
        print(f"{Colors.RED}ОШИБКА: Не удалось прочитать файл: {e}{Colors.RESET}")
        return

    open_braces = 0
    close_braces = 0
    in_block_comment = False
    paren_balance = 0
    prev_stripped = ""

    for line_num, line in enumerate(lines, 1):
        stripped = line.strip()

        # Отслеживание блочных комментариев /* ... */
        if in_block_comment:
            if '*/' in stripped:
                in_block_comment = False
            continue
        if '/*' in stripped and '*/' not in stripped:
            in_block_comment = True
            idx = stripped.index('/*')
            stripped = stripped[:idx].rstrip()
            line = line[:line.index('/*')]
            if not stripped:
                prev_stripped = stripped
                continue

        if stripped.startswith('//'):
            # Не обновлять prev_stripped для чистых комментариев
            continue

        # =====================================================
        # 1. else/switch/try не существуют в Storm Engine
        # =====================================================
        if re.match(r'^\}?\s*else\b', stripped):
            error(file_path, line_num, "'else' не существует в Storm Engine! Используйте отдельный if")

        if re.match(r'^\s*switch\s*\(', stripped):
            error(file_path, line_num, "'switch' не существует в Storm Engine!")

        if re.match(r'^\s*case\s+', stripped):
            error(file_path, line_num, "'case' не существует в Storm Engine!")

        if re.match(r'^\s*try\s*\{', stripped):
            error(file_path, line_num, "'try/catch' не существует в Storm Engine!")

        if re.match(r'^\s*catch\s*\(', stripped):
            error(file_path, line_num, "'catch' не существует в Storm Engine!")

        # =====================================================
        # 2. Баланс круглых скобок ()
        # =====================================================
        clean = strip_strings(strip_comments(line))
        line_opens = clean.count('(')
        line_closes = clean.count(')')
        paren_balance += (line_opens - line_closes)

        if paren_balance < 0:
            error(file_path, line_num,
                  f"Лишняя закрывающая скобка ')' (баланс={paren_balance})")
            paren_balance = 0

        # Если строка заканчивается на ; но скобки не закрыты — ошибка
        if stripped.endswith(';') and paren_balance > 0:
            error(file_path, line_num,
                  f"Незакрытая скобка '(' (баланс={paren_balance})")
            paren_balance = 0

        # =====================================================
        # 3. Пропущенные точки с запятой
        # =====================================================
        if paren_balance == 0:
            check_missing_semicolon(stripped, prev_stripped, file_path, line_num)

        # =====================================================
        # 4. Необъявленные константы
        # =====================================================
        if known_defines is not None:
            check_undefined_constants(line, stripped, file_path, line_num,
                                       known_defines, known_variables or set())

        # =====================================================
        # 5. Вызовы неопределённых функций
        # =====================================================
        if known_functions_lower is not None:
            check_function_calls(line, stripped, file_path, line_num, known_functions_lower)

        # =====================================================
        # 6. Подробные предупреждения (--verbose)
        # =====================================================
        if verbose_mode:
            if re.search(r'CharacterFromID\s*\(\s*"[^"]+"\s*\)', line) and 'GetCharacterIndex' not in line:
                warning(file_path, line_num, "Нет проверки GetCharacterIndex() перед CharacterFromID()?")

            if re.search(r'pchar\.[a-zA-Z]+\.', line) and 'CheckAttribute' not in line:
                if not stripped.startswith('//') and not stripped.startswith('*'):
                    warning(file_path, line_num, "Нет CheckAttribute() перед доступом к атрибуту?")

        # Подсчёт фигурных скобок
        open_braces += clean.count('{')
        close_braces += clean.count('}')
        prev_stripped = stripped

    # Проверка баланса фигурных скобок
    if open_braces != close_braces:
        error(file_path, 0, f"Несовпадение фигурных скобок! Открывающих: {open_braces}, Закрывающих: {close_braces}")


def main():
    global verbose_mode

    # Проверка аргументов
    if len(sys.argv) < 2 or '--help' in sys.argv or '-h' in sys.argv:
        print(__doc__)
        return 0

    # Разбор аргументов
    program_dir = None
    engine_dir = None
    files_to_check = []
    no_color = False
    i = 1
    while i < len(sys.argv):
        arg = sys.argv[i]
        if arg in ('--verbose', '-v'):
            verbose_mode = True
        elif arg == '--no-color':
            no_color = True
        elif arg == '--engine' and i + 1 < len(sys.argv):
            i += 1
            engine_dir = sys.argv[i]
        elif program_dir is None:
            program_dir = arg
        else:
            files_to_check.append(arg)
        i += 1

    if no_color or not sys.stdout.isatty():
        Colors.disable()

    program_dir = os.path.abspath(program_dir)
    if not os.path.isdir(program_dir):
        print(f"ОШИБКА: Директория PROGRAM не найдена: {program_dir}")
        return 1

    print("")
    print(f"{Colors.CYAN}============================================{Colors.RESET}")
    print(f"{Colors.CYAN}  STORM ENGINE SCRIPT LINTER v2.0{Colors.RESET}")
    print(f"{Colors.CYAN}============================================{Colors.RESET}")

    # Фаза 1: сканирование проекта для определений
    known_defines = set()
    known_functions_lower = {f.lower() for f in BUILTIN_FUNCTIONS}
    known_variables = set()

    print(f"{Colors.CYAN}Сканирование проекта: {program_dir}{Colors.RESET}")
    known_defines = scan_defines(program_dir)
    project_funcs = scan_functions(program_dir)  # уже lowercase
    known_functions_lower.update(project_funcs)
    known_variables = scan_global_variables(program_dir)

    # Сканирование заголовков движка для MSG_*, FADER_*, RELATION_* и т.д.
    if engine_dir and os.path.isdir(engine_dir):
        engine_defines = scan_engine_defines(engine_dir)
        known_defines.update(engine_defines)
        print(f"  + {len(engine_defines)} констант движка (MSG_*, FADER_*, RELATION_* и т.д.)")

    print(f"  {len(known_defines)} констант (#define)")
    print(f"  {len(known_functions_lower)} функций ({len(BUILTIN_FUNCTIONS)} встроенных)")
    print(f"  {len(known_variables)} глобальных переменных")

    # Фаза 2: проверка файлов
    if files_to_check:
        for file_path in files_to_check:
            full_path = file_path
            if not os.path.isabs(file_path):
                full_path = os.path.join(program_dir, file_path)
            lint_file(full_path, known_defines, known_functions_lower, known_variables)
    else:
        # Проверить все .c файлы в PROGRAM/
        print(f"\n{Colors.YELLOW}Проверка всех .c файлов...{Colors.RESET}")
        c_files = sorted(Path(program_dir).rglob("*.c"))
        for fpath in c_files:
            lint_file(str(fpath), known_defines, known_functions_lower, known_variables)

    # РЕЗУЛЬТАТЫ
    print("")
    print(f"{Colors.CYAN}============================================{Colors.RESET}")
    print(f"{Colors.CYAN}  РЕЗУЛЬТАТЫ{Colors.RESET}")
    print(f"{Colors.CYAN}============================================{Colors.RESET}")

    if error_count == 0 and warning_count == 0:
        print(f"{Colors.GREEN}ПРОБЛЕМ НЕ НАЙДЕНО!{Colors.RESET}")
        return 0

    print(f"{Colors.RED}Ошибок: {error_count}{Colors.RESET}")
    print(f"{Colors.YELLOW}Предупреждений: {warning_count}{Colors.RESET}")

    if error_count > 0:
        print("")
        print(f"{Colors.RED}НАЙДЕНЫ КРИТИЧЕСКИЕ ОШИБКИ! Исправьте их перед запуском игры.{Colors.RESET}")
        return 1

    print("")
    print(f"{Colors.YELLOW}Найдены предупреждения. Рекомендуется проверить.{Colors.RESET}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
