#!/usr/bin/env python3
#
# SPDX-FileCopyrightText: 2023-2026 Sébastien Helleu <flashcode@flashtux.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

"""Check Curl symbols defined in WeeChat source code.

Check if Curl symbols defined in src/core/core-url.c are matching symbols
defined in Curl (introduced/deprecated/last versions), using this file:
https://github.com/curl/curl/blob/master/docs/libcurl/symbols-in-versions.

File symbols-in-versions must be passed as stdin to the script.

Usage example:

```
URL=https://raw.githubusercontent.com/curl/curl/master/docs/libcurl/symbols-in-versions
curl $URL | ./check_curl_symbols.py
```

Or with Curl repository cloned locally:

```
./check_curl_symbols.py < /path/to/curl/docs/libcurl/symbols-in-versions
```

This script requires Python 3.7+.
"""

# ruff: noqa: COM812,T201

import io
import re
import sys
from dataclasses import dataclass
from pathlib import Path

SRC_PATH = Path(__file__).resolve().parent.parent / "src" / "core" / "core-url.c"

# NOTE: keep version in sync with CMakeLists.txt
CURL_MIN_VERSION_STR = "7.68.0"

WEECHAT_CURL_MIN_VERSION_RE = (
    r"#if LIBCURL_VERSION_NUM >= (?P<hex_min_version>0x[0-9A-F]+) "
    r"/\* (?P<str_min_version>[0-9][0-9.]+) \*/"
)

WEECHAT_CURL_MAX_VERSION_RE = (
    r"#if LIBCURL_VERSION_NUM < (?P<hex_max_version>0x[0-9A-F]+) "
    r"/\* < (?P<str_max_version>[0-9][0-9.]+) \*/"
)

WEECHAT_CURL_MIN_MAX_VERSION_RE = (
    r"#if LIBCURL_VERSION_NUM >= (?P<hex_min_version>0x[0-9A-F]+) "
    r"&& LIBCURL_VERSION_NUM < (?P<hex_max_version>0x[0-9A-F]+) "
    r"/\* (?P<str_min_version>[0-9][0-9.]+) < "
    r"(?P<str_max_version>[0-9][0-9.]+) \*/"
)
WEECHAT_ENDIF_RE = r"#endif"
WEECHAT_CURL_CONSTANT_RE = r"    URL_DEF_CONST\((?P<prefix>[A-Z0-9_]+), (?P<name>[A-Z0-9_]+)\),"
WEECHAT_CURL_OPTION_RE = r"    URL_DEF_OPTION\((?P<name>[A-Z0-9_]+), .*\),"
CURL_SYMBOL_RE = r"[A-Z][A-Z0-9_]"
CURL_VERSION_ITEMS = 3


@dataclass
class WeechatCurlSymbol:
    """A Curl symbol declared in WeeChat."""

    name: str
    min_curl: int = 0
    max_curl: int = 0
    line_no: int = 0


def curl_version_to_int(version: str) -> int:
    """Convert Curl version as string to integer.

    :param version: version as string (eg: "7.87.0")
    :return: version as integer, eg: 481024 (== 0x075700, 0x57 == 87)
    """
    if version == "-":
        return 0
    result = 0
    items = version.split(".")
    while len(items) < CURL_VERSION_ITEMS:
        items.append("0")
    factor = 0
    for item in reversed(items):
        result += int(item) << factor
        factor += 8
    return result


def curl_version_to_str(version: int) -> str:
    """Convert Curl version as integer to string.

    :param version: version as integer, eg: 481024 (0x075700)
    :return: version as string, eg: "7.87.0"
    """
    if version == 0:
        return "-"
    result = ""
    while version > 0:
        result = str(version & 0xFF) + "." + result
        version = version >> 8
    return result.rstrip(".")


def get_curl_symbols(symbols_file: io.TextIOBase) -> dict[str, tuple[int, int]]:
    """Parse file docs/libcurl/symbols-in-versions from Curl repository.

    :param symbols_file: file with Curl symbols
    :return: Curl symbols as dict: {name: (version_min, version_max)}
    """
    curl_symbol_pattern = re.compile(CURL_SYMBOL_RE)
    symbols: dict[str, tuple[int, int]] = {}
    if symbols_file.isatty():
        return symbols
    for line in symbols_file:
        match = re.match(curl_symbol_pattern, line)
        if match:
            name, intro, deprec, last, *_ = (line + " - -").split()
            v_max = last if last != "-" else deprec
            symbols[name] = (
                curl_version_to_int(intro),
                curl_version_to_int(v_max),
            )
    return symbols


def check_req_symbols(symbols: list[WeechatCurlSymbol]) -> int:
    """Check the symbols' min/max version, relative to min Curl version required.

    :return: errors
    """
    errors: int = 0
    req_curl = curl_version_to_int(CURL_MIN_VERSION_STR)
    for symbol in symbols:
        if symbol.min_curl and symbol.min_curl <= req_curl:
            print(
                f"{SRC_PATH}:{symbol.line_no}: min version for "
                f"symbol {symbol.name} older than minimal required "
                f"curl {req_curl}. Remove if guard."
            )
            errors += 1
        if symbol.max_curl and symbol.max_curl < req_curl:
            print(
                f"{SRC_PATH}:{symbol.line_no}: max version for "
                f"symbol {symbol.name} older than minimal required "
                f"curl {req_curl}. Remove the symbol."
            )
            errors += 1
    return errors


def get_weechat_curl_symbols() -> tuple[list[WeechatCurlSymbol], int]:  # noqa: C901,PLR0915
    """Parse Curl symbols declared in src/core/core-url.c.

    :return: tuple (list_symbols, errors)
    """
    min_version_pattern = re.compile(WEECHAT_CURL_MIN_VERSION_RE)
    max_version_pattern = re.compile(WEECHAT_CURL_MAX_VERSION_RE)
    min_max_version_pattern = re.compile(WEECHAT_CURL_MIN_MAX_VERSION_RE)
    endif_pattern = re.compile(WEECHAT_ENDIF_RE)
    constant_pattern = re.compile(WEECHAT_CURL_CONSTANT_RE)
    option_pattern = re.compile(WEECHAT_CURL_OPTION_RE)
    v_min: int = 0
    v_max: int = 0
    symbols: list[WeechatCurlSymbol] = []
    errors: int = 0
    line_no: int = 0
    with Path(SRC_PATH).open(encoding="utf-8") as src_file:
        for line in src_file:
            line_no += 1
            # min Curl version
            match = re.match(min_version_pattern, line)
            if match:
                hex_min_vers = match["hex_min_version"]
                str_min_vers = match["str_min_version"]
                v_min, v_max = int(hex_min_vers, 0), 0
                comment_min = curl_version_to_int(str_min_vers)
                if v_min != comment_min:
                    print(
                        f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}"
                    )
                    errors += 1
                continue
            # max Curl version
            match = re.match(max_version_pattern, line)
            if match:
                hex_max_vers = match["hex_max_version"]
                str_max_vers = match["str_max_version"]
                v_min, v_max = 0, int(hex_max_vers, 0)
                comment_max = curl_version_to_int(str_max_vers)
                if v_max != comment_max:
                    print(
                        f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}"
                    )
                    errors += 1
                continue
            # min + max Curl version
            match = re.match(min_max_version_pattern, line)
            if match:
                hex_min_vers = match["hex_min_version"]
                hex_max_vers = match["hex_max_version"]
                str_min_vers = match["str_min_version"]
                str_max_vers = match["str_max_version"]
                v_min, v_max = int(hex_min_vers, 0), int(hex_max_vers, 0)
                comment_min = curl_version_to_int(str_min_vers)
                comment_max = curl_version_to_int(str_max_vers)
                if v_min != comment_min:
                    print(
                        f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}"
                    )
                    errors += 1
                if v_max != comment_max:
                    print(
                        f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}"
                    )
                    errors += 1
                continue
            # end of min/max Curl version
            match = re.match(endif_pattern, line)
            if match:
                v_min, v_max = 0, 0
                continue
            # Curl constant
            match = re.match(constant_pattern, line)
            if match:
                name = f"CURL{match['prefix']}_{match['name']}"
                symbols.append(WeechatCurlSymbol(name, v_min, v_max, line_no))
                continue
            # Curl option
            match = re.match(option_pattern, line)
            if match:
                name = f"CURLOPT_{match['name']}"
                symbols.append(WeechatCurlSymbol(name, v_min, v_max, line_no))
                continue

    errors += check_req_symbols(symbols)
    return symbols, errors


def check_symbols(
    weechat_curl_symbols: list[WeechatCurlSymbol],
    curl_symbols: dict[str, tuple[int, int]],
) -> int:
    """Check that symbols declared are matching Curl symbols.

    :param weechat_curl_symbols: list of Curl symbols in WeeChat
    :param curl_symbols: list of all Curl symbols
    """
    req_curl = curl_version_to_int(CURL_MIN_VERSION_STR)
    to_str = curl_version_to_str
    errors = 0
    for symbol in weechat_curl_symbols:
        curl_symbol = curl_symbols.get(symbol.name)
        if not curl_symbol:
            print(f"{SRC_PATH}:{symbol.line_no}: symbol {symbol.name} not found in Curl")
            errors += 1
            continue
        if curl_symbol[0] > req_curl and symbol.min_curl != curl_symbol[0]:
            print(
                f"{SRC_PATH}:{symbol.line_no}: min version for "
                f"symbol {symbol.name} differs: "
                f"{to_str(symbol.min_curl)} in WeeChat, "
                f"{to_str(curl_symbol[0])} in Curl"
            )
            errors += 1
        if curl_symbol[1] >= req_curl and symbol.max_curl != curl_symbol[1]:
            print(
                f"{SRC_PATH}:{symbol.line_no}: max version for "
                f"symbol {symbol.name} differs: "
                f"{to_str(symbol.max_curl)} in WeeChat, "
                f"{to_str(curl_symbol[1])} in Curl"
            )
            errors += 1
    return errors


def main() -> int:
    """Check Curl symbols and return the number of errors found."""
    curl_symbols = get_curl_symbols(sys.stdin)
    if not curl_symbols:
        sys.exit("FATAL: failed to read Curl symbols on standard input")
    weechat_curl_symbols, errors = get_weechat_curl_symbols()
    errors += check_symbols(weechat_curl_symbols, curl_symbols)
    dict_err = {0: "all good!", 1: "1 error"}
    print("Curl symbols:", dict_err.get(errors, f"{errors} errors"))
    return errors


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