#! /usr/bin/env python

# Copyright (C) 2010 Christian Dywan <christian@twotoasts.de>
# Copyright (C) 2010 Arno Renevier <arno@renevier.net>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# See the file COPYING for the full license text.
#
# check-style: Verify C source code according to coding style.

import glob, re, string, subprocess, sys, os

if len (sys.argv) < 2:
    name = os.path.basename (sys.argv[0])
    print ('Usage:\n  ' + name + ' FILENAMES\n'
           '  Pass "-" to read stdin, eg. "cat my-feature.diff | ' + name + ' -"\n'
           '  Pass "." to mean "git diff ^HEAD | ' + name + ' -"')
    sys.exit (1)

# Coding style violations
violations = [
    ['.{101}', 'Line longer than 100 columns'],
    ['^[ ]{1,3}[^ ]+', 'Indentation is less than 4 spaces'],
    # FIXME: Don't match empty strings
    # FIXME: Don't match indented function arguments
    # ['^(?!(([ ]{4})*[^ ]+))', 'Indentation is not 4 spaces'],
    ['.*[ ]+$', 'Trailing whitespace'],
    [r"\t+", 'Tabs instead of spaces'],
    ["[^0-9],[^ ][^0-9]", 'No space after comma'],
    # ['(([A-Z][a-z]+)+)\*?[ ][a-z]{2,}', 'Good variable name'],
    # ['(g?char|g(boolean|pointer))\*?[ ][a-z]{2,}', 'Good variable name'],
    # ['(g?int|guint)[ ][a-z]+', 'Good iterator name'],
    # ['(struct)[ ]+[_]?([A-Z][a-z]+)+', 'Good type name'],
    ['^\s*\w+(?<!\\breturn)\s+\*\s*\w*\s*[;,]', 'Space between type and asterisk'],
    ["(\w[+|-|*|/|<|>|=]{1,2}\w)", 'No space around operators'],
    ["\/\*[^ *\n]", 'No space after open comment'],
    ['[^ *]\*\/', 'No space before close comment'],
    ['\)\{', 'No space between ) and {'],
    [';[^ \s]', 'No space or newline after semicolon'],
    # ['(if)( \([^ ].*[^ ]\))$', 'Good if style'],
    ['^#\s+(if(n?def)?|define|else|elif)[ ].*$', 'Space between # and cpp'],
    [r'^\s*\*\w+(\+\+|--);', 'Invalid increment, use (*i)++ or *i += 1'],
    ['asctime|ctime|getgrgid|getprgnam|getlogin \
     |getpwnam|getpwuid|gmtime|localtime \
     |rand|readdir|strtok|ttyname', 'Not thread-safe posix, use _r variant'],
]
# No validation for strings, comments, includes
omissions = [
    [r'["]{1}.*["]', 'STRING'],
    ["'\\\?.'", 'CHAR'],
    ["^\s*\/\*.*\*\/\s*$", 'COMMENT'],
    ['#include <.*>', 'INCLUDE'],
]

# Output format
fmt = '%s - %d: %s'

# Pre-compile expressions
for violation in violations:
    violation[0] = re.compile (violation[0])
for omission in omissions:
    omission[0] = re.compile (omission[0])

for filename_or_glob in sys.argv[1:]:
    if filename_or_glob == '-':
        handles = [sys.stdin]
    else:
        handles = []
        for filename in glob.glob (filename_or_glob):
            if os.path.isdir (filename):
                gitdiff = subprocess.Popen (['git', 'diff', '^HEAD',
                    '--relative', filename], stdout=subprocess.PIPE)
                handles.append (gitdiff.stdout)
            else:
                handles.append (open (filename))
    if not handles:
        print (filename_or_glob + ' not found')
        sys.exit (1)

    for handle in handles:
        previous = ''
        i = 0
        mode = ''
        filename = handle.name
        comment = False
        curly = []

        for line in handle:
            line = line[:-1]
            i += 1

            # Parse diff, only validate modified lines
            if i == 1 and 'diff' in line:
                mode = 'diff'
            if mode == 'diff':
                if line[:3] == '+++':
                    filename = line[6:]
                    comment = False
                    curly = []
                    continue
                if line[:2] == '@@':
                    i = int (line.split (' ')[2].split (',')[0][1:]) - 1
                    curly = []
                if line[0] == '-':
                    i = i -1
                if line[0] != '+':
                    continue
                line = line[1:]

            # Spurious blank lines
            if previous == line == '':
                print (fmt % (filename, i, 'Spurious blank line'))
                previous = line
                continue
            previous = line

            # Skip multi-line comment blocks
            if '/*' in line and not '*/' in line:
                comment = True
            if comment:
                if '*/' in line and not '/*' in line:
                    comment = False
                continue

            cleaned = line
            for omission in omissions:
                cleaned = omission[0].sub (omission[1], cleaned)

            # Validate curly bracket indentation
            if '{' in cleaned and not '}' in cleaned:
                curly.append ((cleaned.index ('{'), cleaned))
            if '}' in cleaned and not '{' in cleaned and not '},' in cleaned:
                if len (curly) == 0 or curly[-1][0] != cleaned.index ('}'):
                    print (fmt % (filename, i, 'Misindented curly bracket'))
                    print (curly[-1][1])
                    print (line)
                    curly.pop()
                    continue
                curly.pop()

            # Validate preprocessor indentation
            # FIXME: Don't warn if the *following* line is a curly
            cpp = cleaned.find ('#if')
            if cpp != -1:
                if len (curly) != 0 and cpp != curly[-1][0] + 4:
                    print (fmt % (filename, i, 'Misindented preprocessor #if'))
                    print (curly[-1][1])
                    print (line)

            violated = False
            for violation in violations:
                if violation[0].search (cleaned):
                    violated = True
                    print (fmt % (filename, i, violation[1]))
            if violated:
                print (line)

