"""
check_non_ascii.py
Finds non-ASCII characters in MQL5 sources before uploading to the Market.

The automatic validation rejects programs whose messages are not in English
(NON_LATIN). Messages come from string literals (Print, Alert, Comment,
#property description) and from the trailing comment of each "input" line,
which MetaTrader shows as the parameter name. Ordinary comments are compiled
out, so they are reported as information only.

Usage:
    python check_non_ascii.py <file or folder> [more paths...]
Exit code 1 when at least one ERROR is found.
"""
import sys
from pathlib import Path

EXTENSIONS = {".mq5", ".mqh", ".mq4"}


def read_source(path: Path) -> str:
    raw = path.read_bytes()
    # MetaEditor may save as UTF-16 LE with BOM or as UTF-8
    if raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"):
        return raw.decode("utf-16")
    if raw.startswith(b"\xef\xbb\xbf"):
        return raw[3:].decode("utf-8", errors="replace")
    try:
        return raw.decode("utf-8")
    except UnicodeDecodeError:
        return raw.decode("cp1252", errors="replace")


def classify_line(line: str, in_block: bool):
    """Returns (list of (col, char, zone), in_block) for one line.
    zone is 'string', 'input_comment', 'comment' or 'code'."""
    findings = []
    is_input = line.lstrip().startswith(("input ", "sinput "))
    i, n = 0, len(line)
    in_str = False
    while i < n:
        ch = line[i]
        if in_block:
            if line.startswith("*/", i):
                in_block = False
                i += 2
                continue
            if ord(ch) > 127:
                findings.append((i + 1, ch, "comment"))
            i += 1
            continue
        if in_str:
            if ch == "\\":
                i += 2
                continue
            if ch == '"':
                in_str = False
            elif ord(ch) > 127:
                findings.append((i + 1, ch, "string"))
            i += 1
            continue
        if line.startswith("//", i):
            zone = "input_comment" if is_input else "comment"
            for j in range(i, n):
                if ord(line[j]) > 127:
                    findings.append((j + 1, line[j], zone))
            break
        if line.startswith("/*", i):
            in_block = True
            i += 2
            continue
        if ch == '"':
            in_str = True
        elif ord(ch) > 127:
            findings.append((i + 1, ch, "code"))
        i += 1
    return findings, in_block


def scan_file(path: Path):
    errors, infos = [], []
    in_block = False
    for num, line in enumerate(read_source(path).splitlines(), start=1):
        found, in_block = classify_line(line, in_block)
        for col, ch, zone in found:
            item = (path, num, col, ch, zone)
            (infos if zone == "comment" else errors).append(item)
    return errors, infos


def iter_sources(paths):
    for p in map(Path, paths):
        if p.is_dir():
            yield from (f for f in sorted(p.rglob("*")) if f.suffix.lower() in EXTENSIONS)
        elif p.suffix.lower() in EXTENSIONS:
            yield p


def main(argv):
    if not argv:
        print(__doc__)
        return 2
    total_err = total_info = files = 0
    for src in iter_sources(argv):
        files += 1
        errors, infos = scan_file(src)
        for path, num, col, ch, zone in errors:
            print(f"ERROR {path}:{num}:{col} U+{ord(ch):04X} '{ch}' in {zone}")
        for path, num, col, ch, zone in infos:
            print(f"info  {path}:{num}:{col} U+{ord(ch):04X} '{ch}' in comment")
        total_err += len(errors)
        total_info += len(infos)
    print(f"{files} file(s) scanned, {total_err} error(s), {total_info} comment note(s)")
    return 1 if total_err else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
