#!/usr/bin/env python3

#
# Copyright (C) 2021 Christoph Sommer <sommer@cms-labs.org>
#
# Documentation for these modules is at http://veins.car2x.org/
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

"""
Format code according to Veins code style
"""

from __future__ import print_function
import argparse
import fnmatch
import logging
import os
import re
import subprocess
import sys

def version_to_tuple(s):
    r = []
    for pt in re.split("[^0-9a-zA-Z]", s):
        r.append(pt.zfill(16))
    return tuple(r)

def check_version(name, cmd, regex, minimum_version_str):
    """
    check (minimum) version of command
    """
    minimum_version_str = minimum_version_str
    minimum_version=version_to_tuple(minimum_version_str)
    try:
        logging.debug("executing {}".format(" ".join(cmd)))
        s = subprocess.check_output(cmd)
        logging.debug("result: {}".format(s))
        s = s.decode()
        m = re.match(regex, s)
        if not m:
            logging.error("Cannot check if {} version is at least {}: Cannot parse \"{}\"".format(name, minimum_version_str, s))
            sys.exit(1)
        s = m.group('version')
        v = version_to_tuple(s)
        if v < minimum_version:
            logging.error("Could not ensure that {} version is at least {}: found version {}".format(name, minimum_version_str, s))
            sys.exit(1)
    except Exception as e:
        logging.exception("Cannot check if {} version is at least {}: {}".format(name, minimum_version_str, e))
        sys.exit(1)


def check_uncrustify():
    """
    check (minimum) version of uncrustify
    """

    min_version = "0.68"
    check_version("uncrustify", ["uncrustify", "--version"], "^Uncrustify-(?P<version>[0-9.]+)", min_version)


def check_clang_format():
    """
    check (minimum) version of uncrustify
    """

    min_version = "6.0"
    check_version("clang-format", ["clang-format", "--version"], "^clang-format version (?P<version>[0-9.]+)", min_version)


def format_file(fname, strict, uncrustify_style_path, clang_format_styles):

    if os.path.islink(fname):
        logging.debug("Ignoring link {}".format(fname))
        return

    if not os.path.isfile(fname):
        logging.warning("Ignoring non-file {}".format(fname))
        return

    # Read file
    logging.debug("Reading {}".format(fname))
    content = None
    with open(fname, 'rb') as fp:
        content = fp.read()
    content_org = content

    # Run clang-format
    if strict:
        logging.debug("Running clang-format")
        p = subprocess.Popen(['clang-format', '-style='+clang_format_styles+'', '-assume-filename='+fname], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        content = p.communicate(input=content)[0]

    # Run uncrustify
    logging.debug("Running uncrustify")
    p = subprocess.Popen(['uncrustify', '-q', '-l', 'CPP', '-c', uncrustify_style_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    content = p.communicate(input=content)[0]

    if content != content_org:
        logging.debug("Must change {}".format(fname))
        tname = fname + '.tmp'
        if os.path.exists(tname):
            logging.warning("Temp file exists, skipping {}".format(fname))
            return
        with open(tname, 'wb') as fp:
            fp.write(content)
        os.replace(tname, fname)
        logging.info("Changed {}".format(fname))

    else:
        logging.debug("No need to change {}".format(fname))


def main():
    """
    Program entry point when run interactively.
    """

    # Arguments
    parser = argparse.ArgumentParser('Format code according to Veins code style')
    parser.add_argument('-s', '--strict', action='store_true', help='be more strict about formatting')
    parser.add_argument("-v", "--verbose", dest="count_verbose", default=0, action="count", help="increase verbosity [default: don't log debug, info]")
    parser.add_argument("-q", "--quiet", dest="count_quiet", default=0, action="count", help="decrease verbosity [default: log warnings, errors]")
    parser.add_argument('--', dest='arguments', help='Files or directories to format; empty for current directory')
    args, file_args = parser.parse_known_args()
    if (len(file_args) > 0) and file_args[0] == '--':
        file_args = file_args[1:]
    if len(file_args) == 0:
        file_args = ['.']


    # Logging
    loglevels = (logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG)
    loglevel = loglevels[max(0, min(1 + args.count_verbose - args.count_quiet, len(loglevels)-1))]
    logging.basicConfig(level=loglevel, format="%(levelname)s: %(message)s")

    # Version checks
    if args.strict:
        check_clang_format()
    check_uncrustify()

    clang_format_style_path = None
    clang_format_styles = None
    if args.strict:
        # Do clang-format config check
        clang_format_style_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", ".clang-format")
        logging.debug("Checking if \"{}\" exists".format(clang_format_style_path))
        if not os.path.isfile(clang_format_style_path):
            logging.error("Failed to find .clang-format")
            sys.exit(1)

        # Read clang-format config
        with open(clang_format_style_path) as fp:
            ss = fp.readlines()
            ss = [s for s in ss if s.strip() != '---']
            clang_format_styles = "{" + ", ".join([s.strip() for s in ss]) + "}"

    # Do uncrustify config check
    uncrustify_style_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", ".uncrustify.cfg")
    logging.debug("Checking if \"{}\" exists".format(uncrustify_style_path))
    if not os.path.isfile(uncrustify_style_path):
        logging.error("Failed to find .uncrustify.cfg")
        sys.exit(1)

    # Collect all files
    file_names = []
    for file_arg in file_args:
        if not os.path.isdir(file_arg):
            file_names += [file_arg]
        else:
            # Search for files
            for root, _, filenames in os.walk(file_arg):
                for filename in fnmatch.filter(filenames, '*.cc'):
                    file_names.append(os.path.join(root, filename))
                for filename in fnmatch.filter(filenames, '*.h'):
                    file_names.append(os.path.join(root, filename))

    # Format all files
    for fname in file_names:
        strict = args.strict
        format_file(fname, strict, uncrustify_style_path, clang_format_styles)

# Start main() when run interactively
if __name__ == '__main__':
    main()

