#!/usr/bin/env python3
"""
Filters and processes warnings generated by Doxygen, which are
annoyingly inconsistent and verbose, for greater readability.

(Neo)vim commands to go to the file and linenumber listed on a 
line, in the reports this program generates:
    :exe "let linenumber =" split(getline("."))[1]
    :exe "edit" fnameescape(split(getline("."))[0]) "|" linenumber

It's easy to put a workflow together to clear up redundant doc 
comments (which generate "multiple @param docs" warnings), using
simple vim commands to move the cursor and close buffers, Neovim's 
support for the Language Server Protocol or related tooling, and 
the command shown above.

A useful sequence, for rapidly deleting a doc comment from its last
line, is, in normal mode, `$v%ddd`.

For setting up LSP integration in Neovim, refer to the lsp_config
plugin. 

You may additionally need to generate compile_commands.json in the 
repository root, to allow the language server to find everything. 
This can be done using Bear (found at https://github.com/rizsotto/Bear).

@author: willow <willow@howhill.com>
"""

import argparse as ap
import re

# Regular expression construction

def sep_re(field, separator):
    "Constructs regex for a list"
    return rf"{field}(?:{separator}{field})*"

# File names and paths
fileclass = r"[\w-]"
filename = rf"{fileclass}+"
# filename = rf"(/{fileclass}+)+\.\w"
filepath = rf"{sep_re(filename, '/')}\.(?:\w+)"
main_match = rf"(?P<path>/{filepath}|\[generated\]):(?P<linenumber>\d+): warning:"

# Symbols
type_name = rf"(?:const )?(?:unsigned (?:long )?|struct |enum )?(?:\w+)(?: \*?const)? \*{{0,3}}"
var_def = rf"{type_name}\w+(?:\[(?:\(\d+/\d+\))?\])?"
func_params = rf"\({sep_re(var_def, ', ')}(?:,\.\.\.)?\)"
simple_name = r"\w+"
func_name = simple_name
verbose_name = rf"{sep_re(simple_name, ' ')}"
command_re = "(?:</[^>]+>|\\\w+)"
macro_params = rf"\({sep_re(simple_name, ', ')}(?:,\.\.\.)?\)"


matches = {
    "not an input @file": re.compile(rf"{main_match} the name '(?P<name>{filepath}|{simple_name})' supplied as the argument in the \\file statement is not an input file"),
    "multiple @param docs": re.compile(rf"{main_match} argument '(?P<arg_name>\w+)' from the argument list of ({func_name}) has multiple @param documentation sections"),
    "undocumented param": re.compile(rf"{main_match} The following parameters? of ({func_name})(?:{func_params}|{macro_params}) (?:is|are) not documented:"),
    "undocumented param (name)": re.compile(r"  parameter '([\w.]+)'"),
    "explicit link not resolved": re.compile(rf"{main_match} explicit link request to '(\w+(?:\(\))?)' could not be resolved"),
    "unknown command": re.compile(rf"{main_match} Found unknown command '(\\\w+)'"),
    "missing argument": re.compile(rf"{main_match} argument '(\w+)' of command @param is not found in the argument list of ({func_name})(?:{func_params}|{macro_params})"),
    "eof inside group": re.compile(rf"{main_match} end of file while inside a group"),
    "eof inside comment": re.compile(rf"{main_match} Reached end of file while still inside a \(nested\) comment. Nesting level \d+ \(probable line reference: (\d+)\)"),
    "eof inside code block": re.compile(rf"{main_match} reached end of file while inside a 'code' block!"),
    "eof inside code block (line 2)": re.compile(rf"The command that should end the block seems to be missing!"),
    "title mismatch": re.compile(rf"{main_match} group (?P<group_id>\w+): ignoring title \"(?P<new_title>{verbose_name})\" that does not match old title \"(?P<old_title>{verbose_name})\""),
    "end of comment expecting command": re.compile(rf"{main_match} end of comment block while expecting command ({command_re})"),
    "no matching tag": re.compile(rf"{main_match} found </(?P<tag>[^>]+)> tag without matching <(?P=tag)>"),
    "documented empty return type": re.compile(rf"{main_match} documented empty return type of {func_name}"),
    "unsupported tag": re.compile(rf"{main_match} Unsupported xml/html tag <(?P<tag>[^>]+)> found"),
    "expected whitespace after command": re.compile(rf"{main_match} expected whitespace after \\(?P<command>\w+) command"),
    "illegal command": re.compile(rf"{main_match} Illegal command (?P<illegal_cmd>(?:@|\\)\w+) as part of a \\(?P<command>\w+) command"),
    "undeclared symbol": re.compile(rf"{main_match} documented symbol '(\w+)' was not declared or defined\."),
    "nameless member": re.compile(rf"{main_match} member with no name found."),
    "end of empty list": re.compile(rf"{main_match} End of list marker found without any preceding list items"),
    "blank": re.compile(rf"^\s*$"),
#    "": re.compile(rf"{main_match} "),
}

parser_choices = set(matches.keys()) - {"blank", 
                                        "eof inside code block (line 2)",
                                        "undocumented param (name)"}

parser = ap.ArgumentParser()
parser.add_argument("filename")
parser.add_argument("--summary", "-s", action="store_true")
parser.add_argument("--key", "-k", choices=parser_choices, action="append", dest="keys")
args = parser.parse_args()

sorted_lines = {k:[] for k in matches.keys()}
unsorted_lines = []

with open(args.filename, "r") as file:
    for line in file.readlines():
        for key, value in matches.items():
            if match := value.match(line):
                sorted_lines[key].append(match)
                break
        else:
            unsorted_lines.append(line.strip("\n"))


processed_lines = {k: [" ".join(g for g in match.groups())
                       for match in matches]
                   for k, matches in sorted_lines.items()}

# Combining multiline warnings
processed_lines["undocumented param"] = [
    l1+" "+l2 for l1, l2 in zip(processed_lines["undocumented param"],
                                processed_lines["undocumented param (name)"]) 
]

# Removing chaff
del processed_lines["blank"]
del processed_lines["eof inside code block (line 2)"]
del processed_lines["undocumented param (name)"]

# Preparing count dictionary and summarising the results
counts = {k: len(v) for k, v in processed_lines.items()}
if args.summary:
    for k, v in counts.items():
        print(k+":", v)
    print("")

if args.keys is not None:
    for key in args.keys:
        print(f"{key}: {counts[key]}")
        for line in processed_lines[key]:
            print(line)
        print("")

