#!/usr/bin/env python3
import glob, os, pathlib, re, sys, traceback

def warning(
        *,
        path="tools/lint",
        line=1,
        col=1,
        sev="warning",
        msg="unknown error",
):
    # format must match .github/problem_matcher.json
    return f"{path}:{line}:{col}: {sev}: {msg}\n"

def alternatives(patterns):
    return r"|".join(map("(?:{})".format, patterns))

def lint_file_for_line_length(path, contents):
    excl_regex = re.compile(alternatives([
        r"\s*--\s*\S*<[^>]*>\S*",
        r"\s+T\.isolatedRun\s+.*",
    ]))
    for i, line in enumerate(contents.splitlines()):
        if len(line) > 80 and not excl_regex.fullmatch(line):
            yield warning(
                path=path,
                line=i+1,
                col=81,
                msg="line exceeds 80 chars",
            )

def lint_file_for_partial_os_string_ctors(path, contents):
    """Checks that the 'os' and 'so' functions are only ever called on literal
    strings (with few exceptions). These functions are partial and so they
    should only be called on static strings, never on user-provided strings.

    Note that this can catch false positives if an identifier is named 'os' or
    'so', so don't ever name identifiers that."""
    ctor_regex = re.compile(r"(?s)\b({})\b\s*(\S*)".format(alternatives([
            r"os",
            r"so",
    ])))
    excl_regex = re.compile(alternatives([
        r'".*',
        r"=",
        r"::",
        r"EXE_EXTENSION",
        r".*exeExtension",
    ]))
    contents = re.sub(
        r"(?s)--[^\n]*|\{-(.*)-\}",
        lambda m: "".join(c for c in (m.group(1) or "") if c == "\n"),
        contents,
    )
    for m in ctor_regex.finditer(contents):
        func, next_token = m.groups()
        if not excl_regex.fullmatch(next_token):
            line = 1 + contents[:m.start()].count("\n")
            yield warning(
                path=path,
                line=line,
                msg=f"{func!r} should only be applied to literals",
            )

def lint_proj_for_file_lints():
    file_lints = [
        (
            re.compile(r"(System|tests)/.*\.hsc?").fullmatch,
            lint_file_for_line_length,
        ),
        (
            re.compile(r"System/.*\.hsc?").fullmatch,
            lint_file_for_partial_os_string_ctors,
        ),
    ]
    for path in glob.glob("**", recursive=True):
        contents = None
        for path_condition, file_lint in file_lints:
            if not path_condition(path):
                continue
            if contents is None:
                contents = pathlib.Path(path).read_text()
            yield from file_lint(path, contents)

def lint_proj_for_changelog_version():
    directory_cabal = pathlib.Path("directory.cabal").read_text()
    version, = re.search(r"(?m)^version:\s*(\S+)", directory_cabal).groups()
    changelog_md = pathlib.Path("changelog.md").read_text()
    expected_text = f"\n## {version}"
    if expected_text not in changelog_md:
        yield warning(
            path="changelog.md",
            line=3,
            msg=f"missing entry for {expected_text.strip()!r}",
        )

def lint_all():
    proj_lints = [
        lint_proj_for_changelog_version,
        lint_proj_for_file_lints,
    ]
    for proj_lint in proj_lints:
        yield from proj_lint()

def main():
    err_count = 0
    for line in lint_all():
        sys.stderr.write(line)
        err_count += 1
    return int(err_count != 0)

sys.exit(main())
