#!/usr/bin/env python3
"""Report blank lines in SGML files not adjacent to a block element."""

import argparse
import re
import sys
from pathlib import Path


BLOCK_TAGS = {
    "abstract",
    "appendix",
    "bibliography",
    "biblioentry",
    "blockquote",
    "bridgehead",
    "calloutlist",
    "chapter",
    "caution",
    "cmdsynopsis",
    "entry",
    "epigraph",
    "example",
    "figure",
    "formalpara",
    "funcsynopsis",
    "glossary",
    "glossdiv",
    "glossentry",
    "glosslist",
    "highlights",
    "important",
    "indexterm",
    "informalexample",
    "informalfigure",
    "informaltable",
    "itemizedlist",
    "listitem",
    "literallayout",
    "mediaobject",
    "note",
    "orderedlist",
    "para",
    "part",
    "preface",
    "procedure",
    "programlisting",
    "refentry",
    "refsect1",
    "refsect2",
    "refsect3",
    "refsection",
    "revhistory",
    "row",
    "segmentedlist",
    "sect1",
    "sect2",
    "sect3",
    "sect4",
    "sect5",
    "section",
    "sidebar",
    "simplelist",
    "simpara",
    "screen",
    "step",
    "synopsis",
    "table",
    "task",
    "tip",
    "toc",
    "title",
    "variablelist",
    "varlistentry",
    "warning",
}
VERBATIM_TAGS = {"literallayout", "programlisting", "screen", "synopsis"}
TAG_RE = re.compile(r"<\s*(/?)\s*([A-Za-z][\w.-]*)\b")
COMMENT_RE = re.compile(r"<!--|-->")
TAG_ONLY_RE = re.compile(r"(?:\s*<[^<>]+>)+\s*")
ENTITY_ONLY_RE = re.compile(r"\s*&[a-z]+;\s*")


def contains_block_tag(line):
    return any(
        tag.lower() in BLOCK_TAGS
        for _, tag in TAG_RE.findall(line)
    )


def contains_only_tags(line):
    return TAG_ONLY_RE.fullmatch(line) is not None


def is_ignored_adjacent_line(line):
    return (
        contains_block_tag(line)
        or contains_only_tags(line)
        or ENTITY_ONLY_RE.fullmatch(line) is not None
        or COMMENT_RE.search(line) is not None
    )


def find_candidates(path):
    lines = path.read_text(encoding="utf-8").splitlines()
    in_verbatim = False
    in_comment = False

    for index, line in enumerate(lines):
        comment_markers = list(COMMENT_RE.finditer(line))
        was_in_comment = in_comment
        for marker in comment_markers:
            if marker.group() == "<!--":
                in_comment = True
            else:
                in_comment = False

        if was_in_comment or comment_markers:
            continue

        if in_verbatim:
            if any(f"</{tag}" in line.lower() for tag in VERBATIM_TAGS):
                in_verbatim = False
            continue

        if any(f"<{tag}" in line.lower() for tag in VERBATIM_TAGS):
            if not any(f"</{tag}" in line.lower() for tag in VERBATIM_TAGS):
                in_verbatim = True
            continue

        if line.strip():
            continue

        previous_is_block = (
            index > 0 and is_ignored_adjacent_line(lines[index - 1])
        )
        following_is_block = (
            index + 1 < len(lines)
            and is_ignored_adjacent_line(lines[index + 1])
        )
        if not previous_is_block and not following_is_block:
            yield index + 1, lines[index - 1].strip() if index else "", (
                lines[index + 1].strip() if index + 1 < len(lines) else ""
            )


def main():
    sgml_dir = Path(__file__).resolve().parent
    parser = argparse.ArgumentParser(
        description=(
            "Find blank lines in SGML files that have no block tag on either "
            "adjacent line."
        )
    )
    parser.add_argument(
        "paths",
        nargs="*",
        type=Path,
        help="SGML files or directories to check (default: this directory)",
    )
    args = parser.parse_args()
    paths = args.paths or [sgml_dir]
    files = []
    for path in paths:
        if path.is_dir():
            files.extend(sorted(path.rglob("*.sgml")))
        elif path.is_file() and path.suffix == ".sgml":
            files.append(path)
        else:
            parser.error(f"not an SGML file or directory: {path}")

    findings = 0
    for path in sorted(set(files)):
        try:
            candidates = find_candidates(path)
            for line_number, previous, following in candidates:
                try:
                    display_path = path.relative_to(sgml_dir)
                except ValueError:
                    display_path = path
                print(f"{display_path}:{line_number}: ")
                print(f"  before: {previous}")
                print(f"  after:  {following}")
                findings += 1
        except (OSError, UnicodeError) as error:
            print(f"{path}: {error}", file=sys.stderr)
            return 2

    print(f"{findings} candidate blank line(s) in {len(set(files))} file(s)")
    return 1 if findings else 0


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