# SPDX-FileCopyrightText: 2025 Florent Rougon
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileComment: pre-commit hook for checking XML PropertyList files

"""pre-commit hook for checking XML PropertyList files."""

import argparse
import locale
import lxml
import os
import sys
import xml

from flightgear.meta import sgprops
from flightgear.meta.sgprops import PropsHandlerMode

PROGNAME = os.path.basename(sys.argv[0])


def isPropertyListFile(file_):
    try:
        tree = lxml.etree.parse(file_)
    except lxml.etree.XMLSyntaxError as e:
        print(f"{e.filename}: XML syntax error: {e.msg}")
        return False
    else:
        root = tree.getroot()
        return root.tag == "PropertyList"


def checkXMLFile(params, file_, includePaths, /, *, baseDir=None):
    res = True

    if isPropertyListFile(file_):
        try:
            errors = sgprops.readProps(file_, includePaths=includePaths,
                                       mode=PropsHandlerMode.RESILIENT,
                                       baseDir=baseDir).errors
        except xml.sax._exceptions.SAXParseException as e:
            print(e)
            res = False
        else:
            if errors:
                for exc in errors:
                    assert isinstance(exc, sgprops.ParseError), repr(exc)
                    if not params.quiet:
                        print(exc.altStr())
                res = False
            elif params.verbose:
                print("{}: OK".format(file_))
    elif params.verbose:
        print("{}: skipped because it doesn't appear to be in "
              "PropertyList format".format(file_))

    return res


def checkXMLFilesFunction(params):
    results = [ checkXMLFile(params, file_, params.include)
                for file_ in params.files ]

    return 0 if all(results) else 1


def osWalkDumbErrorHandling(exception):
    raise exception

def recursiveDirCheckFunction(params):
    results = []
    firstDir = True

    for dir_ in params.directories:
        if not os.path.isdir(dir_):
            print(f"{dir_!r}: not a directory, {PROGNAME} will exit",
                  file=sys.stderr)
            return 2

        includePaths = params.include.copy()
        # If dir_ is not in the list of include paths, prepend it
        for d in includePaths:
            if os.path.samefile(d, dir_):
                break
        else:
            includePaths.insert(0, dir_)

        if params.verbose:
            if not firstDir: print() # separator line between dirs
            print("Entering directory {!r}...".format(dir_))

        for dirPath, dirs, files in os.walk(dir_,
                                            onerror=osWalkDumbErrorHandling):
            for file_ in files:
                if file_.lower().endswith(".xml"):
                    pathToFile = os.path.join(dirPath, file_)
                    results.append(checkXMLFile(params, pathToFile,
                                                includePaths, baseDir=dir_))

        firstDir = False

    return 0 if all(results) else 1


def processCommandLine():
    parser = argparse.ArgumentParser(
# Commented out because this doesn't work well for “PROGNAME SUBCOMMAND --help”
#         usage="""\
# %(prog)s [OPTION ...] SUBCOMMAND FILE_OR_DIRECTORY...
# Validate XML files that appear to be in PropertyList format.""",
        description=f"""\
Validate XML files that appear to be in PropertyList format.

Examples:

  $ %(prog)s check-files file1.xml file2.xml

  The files must be in XML format. Any of the specified files which
  appears to be in PropertyList format is checked.

  $ %(prog)s check-dirs directory1 directory2

  The directories are recursively explored. During this exploration, any
  file found whose extension is 'xml' (ignoring the case) and which
  appears to be in PropertyList format is checked. In this mode, each of
  the explored directories is prepended to the list of include paths while
  it is being explored. If --prepend-curdir-to-includes is used, the
  currently processed directory has higher precedence in the order of
  include paths than the one added by the option.

  $ %(prog)s -I/path/to/fgdata check-dirs /path/to/f-14b /path/to/other...

  This checks all PropertyList files in the specified aircraft
  directories (all the hierarchy, and this not limited to files
  reachable from aircraft -set.xml files).

Exit status: zero if no problem was found, non-zero otherwise.""",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        # I want --help but not -h (it might be useful for something else)
        add_help=False)

    parser.add_argument("-I", "--include", action="append", default=[],
                        help="""\
      add an include directory, used to find the target files of PropertyList
      'include' directives (passing this option to specify a path to FGData
      is useful when checking an aircraft or add-on)""")
    parser.add_argument("--prepend-curdir-to-includes", action="store_true",
                        help="""\
      prepend the current directory to the list of include paths; useful
      for having /path/to/fgdata added to the list of include paths when
      run from a CI job for fgdata""")
    parser.add_argument("--help", action="help",
                        help="""\
      display this message and exit; “%(prog)s SUBCOMMAND --help” shows
      help for the specified subcommand.""")

    verbosityGroup = parser.add_mutually_exclusive_group()
    verbosityGroup.add_argument("-q", "--quiet", action="store_true",
                                help="""\
      don't write information when files are checked (you still have the
      exit status, however it obviously concerns all files as a whole)""")
    verbosityGroup.add_argument("-v", "--verbose", action="store_true",
                                help="""enable verbose output""")

    subparsers = parser.add_subparsers(title='subcommand', required=True)
    parser_checkXMLFiles = subparsers.add_parser("check-files", help="""\
      check each argument assuming it is an XML file""")
    parser_checkXMLFiles.set_defaults(func=checkXMLFilesFunction)
    parser_checkXMLFiles.add_argument("files", metavar="FILE", nargs="+",
                                      help="""XML file to check""")

    parser_recursiveDirCheck = subparsers.add_parser("check-dirs", help="""\
      check all XML files found under the specified directories (recursive
      exploration)""")
    parser_recursiveDirCheck.set_defaults(func=recursiveDirCheckFunction)
    parser_recursiveDirCheck.add_argument("directories", metavar="DIRECTORY",
                                          nargs="+",
                                          help="""directory to explore""")

    params = parser.parse_args()

    if params.prepend_curdir_to_includes:
        params.include.insert(0, os.getcwd())

    return params


def main():
    locale.setlocale(locale.LC_ALL, '')
    params = processCommandLine()

    return params.func(params)


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