# Copyright 2018-2026 The Khronos Group Inc.
# Copyright (c) 2018-2019 Collabora, Ltd.
#
# SPDX-License-Identifier: Apache-2.0
#
# Author(s):    Rylie Pavlik <rylie.pavlik@collabora.com>
"""Provides a reusable command-line interface to a MacroChecker."""


import argparse
import logging
import re
from pathlib import Path

from .shared import MessageId


def checkerMain(default_enabled_messages, make_macro_checker,
                all_docs, available_messages=None):
    """Perform the bulk of the work for a command-line interface to a MacroChecker.

    Arguments:
    default_enabled_messages -- The MessageId values that should be enabled by default.
    make_macro_checker -- A function that can be called with a set of enabled MessageId to create a
      properly-configured MacroChecker.
    all_docs -- A list of all spec documentation files.
    available_messages -- a list of all MessageId values that can be generated for this project.
      Defaults to every value. (e.g. some projects don't have MessageId.LEGACY)
    """
    enabled_messages = set(default_enabled_messages)
    if not available_messages:
        available_messages = list(MessageId)

    disable_args = []
    enable_args = []

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--scriptlocation",
        help="Append the script location generated a message to the output.",
        action="store_true")
    parser.add_argument(
        "--verbose",
        "-v",
        help="Output 'info'-level development logging messages.",
        action="store_true")
    parser.add_argument(
        "--debug",
        "-d",
        help="Output 'debug'-level development logging messages (more verbose than -v).",
        action="store_true")
    parser.add_argument(
        "-Werror",
        "--warning_error",
        help="Make warnings act as errors, exiting with non-zero error code",
        action="store_true")
    parser.add_argument(
        "--include_warn",
        help="List all expected but unseen include files, not just those that are referenced.",
        action='store_true')
    parser.add_argument(
        "-Wmissing_refpages",
        help="List all entities with expected but unseen ref page blocks. NOT included in -Wall!",
        action='store_true')
    parser.add_argument(
        "--include_error",
        help="Make expected but unseen include files cause exiting with non-zero error code",
        action='store_true')
    parser.add_argument(
        "--broken_error",
        help="Make missing include/anchor for linked-to entities cause exiting with non-zero error code. Weaker version of --include_error.",
        action='store_true')
    parser.add_argument(
        "--dump_entities",
        help="Just dump the parsed entity data to entities.json and exit.",
        action='store_true')
    parser.add_argument(
        "--html",
        help="Output messages to the named HTML file instead of stdout.")
    parser.add_argument(
        "file",
        help="Only check the indicated file(s). By default, all chapters and extensions are checked.",
        nargs="*")
    parser.add_argument(
        "--ignore_count",
        type=int,
        help="Ignore up to the given number of errors without exiting with a non-zero error code.")
    parser.add_argument("-Wall",
                        help="Enable all warning categories.",
                        action='store_true')

    for message_id in MessageId:
        enable_arg = message_id.enable_arg()
        enable_args.append((message_id, enable_arg))

        disable_arg = message_id.disable_arg()
        disable_args.append((message_id, disable_arg))
        if message_id in enabled_messages:
            parser.add_argument(f"-{disable_arg}", action="store_true",
                                help=f"Disable message category {str(message_id)}: {message_id.desc()}")
            # Don't show the enable flag in help since it's enabled by default
            parser.add_argument(f"-{enable_arg}", action="store_true",
                                help=argparse.SUPPRESS)
        else:
            parser.add_argument(f"-{enable_arg}", action="store_true",
                                help=f"Enable message category {str(message_id)}: {message_id.desc()}")
            # Don't show the disable flag in help since it's disabled by
            # default
            parser.add_argument(f"-{disable_arg}", action="store_true",
                                help=argparse.SUPPRESS)

    args = parser.parse_args()

    arg_dict = vars(args)
    for message_id, arg in enable_args:
        if args.Wall or (arg in arg_dict and arg_dict[arg]):
            enabled_messages.add(message_id)

    for message_id, arg in disable_args:
        if arg in arg_dict and arg_dict[arg]:
            enabled_messages.discard(message_id)

    if args.verbose:
        logging.basicConfig(level='INFO')

    if args.debug:
        logging.basicConfig(level='DEBUG')

    checker = make_macro_checker(enabled_messages)

    if args.dump_entities:
        with open('entities.json', 'w', encoding='utf-8') as f:
            f.write(checker.getEntityJson())
            exit(0)

    if args.file:
        files = (str(Path(f).resolve()) for f in args.file)
    else:
        files = all_docs

    for fn in files:
        checker.processFile(fn)

    if args.html:
        from .html_printer import HTMLPrinter
        printer = HTMLPrinter(args.html)
    else:
        from .console_printer import ConsolePrinter
        printer = ConsolePrinter()

    if args.scriptlocation:
        printer.show_script_location = True

    if args.file:
        printer.output("Only checked specified files.")
        for f in args.file:
            printer.output(f)
    else:
        printer.output("Checked all chapters and extensions.")

    if args.warning_error:
        numErrors = checker.numDiagnostics()
    else:
        numErrors = checker.numErrors()

    check_includes = args.include_warn
    check_broken = not args.file

    if args.file and check_includes:
        print('Note: forcing --include_warn off because only checking supplied files.')
        check_includes = False

    printer.outputResults(checker, broken_links=(not args.file),
                          missing_includes=check_includes)

    if check_broken:
        numErrors += len(checker.getBrokenLinks())

    if args.file and args.include_error:
        print('Note: forcing --include_error off because only checking supplied files.')
        args.include_error = False
    if args.include_error:
        numErrors += len(checker.getMissingUnreferencedApiIncludes())

    check_missing_refpages = args.Wmissing_refpages
    if args.file and check_missing_refpages:
        print('Note: forcing -Wmissing_refpages off because only checking supplied files.')
        check_missing_refpages = False

    if check_missing_refpages:
        missing = checker.getMissingRefPages()
        if missing:
            printer.output("Expected, but did not find, ref page blocks for the following {} entities: {}".format(
                len(missing),
                ', '.join(missing)
            ))
            if args.warning_error:
                numErrors += len(missing)

    printer.close()

    if args.broken_error and not args.file:
        numErrors += len(checker.getBrokenLinks())

    if checker.hasFixes():
        fixFn = 'applyfixes.sh'
        print(f'Saving shell script to apply fixes as {fixFn}')
        with open(fixFn, 'w', encoding='utf-8') as f:
            f.write('#!/bin/sh -e\n')
            for fileChecker in checker.files:
                wroteComment = False
                for msg in fileChecker.messages:
                    if msg.fix is not None:
                        if not wroteComment:
                            f.write(f'\n# {fileChecker.filename}\n')
                            wroteComment = True
                        search, replace = msg.fix
                        f.write(
                            r"sed -i -r 's~\b{}\b~{}~g' {}".format(
                                re.escape(search),
                                replace,
                                fileChecker.filename))
                        f.write('\n')

    print(f'Total number of errors with this run: {numErrors}')

    if args.ignore_count:
        if numErrors > args.ignore_count:
            # Exit with non-zero error code so that we "fail" CI, etc.
            print(f'Exceeded specified limit of {args.ignore_count}, so exiting with error')
            exit(1)
        else:
            print(f'At or below specified limit of {args.ignore_count}, so exiting with success')
            exit(0)

    if numErrors:
        # Exit with non-zero error code so that we "fail" CI, etc.
        print('Exiting with error')
        exit(1)
    else:
        print('Exiting with success')
        exit(0)
