#!/usr/bin/python

'''Run pylint using a 2.x and a 3.x interpreter, and optionally ignore some messages'''

import os
import re
import sys
import subprocess

IGNORE_MESSAGES = []

WHICH_2 = '/usr/local/cpython-2.6/bin/pylint'
WHICH_3 = '/usr/local/cpython-3.3/bin/pylint'


def usage(retval):
    '''Output a usage message'''
    write = sys.stderr.write
    write('Usage: %s\n' % sys.argv[0])
    write('    --ignore-message re1 --ignore-message re2\n')
    write('    --verbose\n')
    write('    --which-2 %s\n' % (WHICH_2, ))
    write('    --which-3 %s\n' % (WHICH_3, ))
    write('    --to-pylint args\n')
    sys.exit(retval)


def get_output_ignore_exit_code(command):
    '''Run a subprocess.  Return its stdout.  Ignore the exit code'''
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    #process.communicate()
    stdout = process.stdout.read()
    dummy = process.wait()
    lines = stdout.split(b'\n')
    return lines


def is_relevant(line):
    '''Classify a pylint line as relevant or irrelevant'''
    for prefix in [b'***', b'C:', b'E:', b'F:', b'I:', b'R:', b'W:']:
        if line.startswith(prefix):
            return True
    return False


def is_traceback(line):
    '''Return True iff this is a traceback'''
    if line.startswith(b'Traceback (most recent call last):'):
        return True
    else:
        return False


def has_traceback(lines):
    '''Return True iff there is a traceback present in lines'''
    if any(is_traceback(line) for line in lines):
        return True
    else:
        return False


def remove_semi_relevant(lines):
    '''Remove semi-relevant lines'''
    for line in lines:
        if is_semi_relevant(line):
            pass
        else:
            yield line


def is_semi_relevant(line):
    '''Return True iff line is semi-relevant'''
    if b'FIXME' in line:
        return True
    elif line.startswith(b'I:') and b'Locally disabling' in line:
        return True
    elif line.startswith(b'***'):
        return True
    else:
        for ignore_message in IGNORE_MESSAGES:
            # We compile these more than necessary, but this isn't really much of a bottleneck
            ignore_regex = re.compile(ignore_message, re.IGNORECASE)
            match = ignore_regex.match(line)
            if match:
                return True
        return False


def to_bytes(string):
    '''Convert string to bytes'''
    try:
        result = bytes(string, 'ASCII')
    except TypeError:
        result = string
    return result


class Options:
    '''Deal with command line options'''
    def __init__(self):
        global WHICH_2
        global WHICH_3
        self.to_pylint = ''
        self.verbose = False
        while sys.argv[1:]:
            if sys.argv[1] == '--ignore-message':
                IGNORE_MESSAGES.append(to_bytes(sys.argv[2]))
                del sys.argv[1]
            elif sys.argv[1] == '--verbose':
                self.verbose = True
            elif sys.argv[1] == '--which-2':
                lower_argv_2 = sys.argv[2].lower()
                if lower_argv_2 == 'none':
                    WHICH_2 = None
                else:
                    WHICH_2 = sys.argv[2]
                del sys.argv[1]
            elif sys.argv[1] == '--which-3':
                lower_argv_2 = sys.argv[2].lower()
                if lower_argv_2 == 'none':
                    WHICH_3 = None
                else:
                    WHICH_3 = sys.argv[2]
                del sys.argv[1]
            elif sys.argv[1] in '--to-pylint':
                self.to_pylint = ' '.join(sys.argv[2:])
                del sys.argv[2:]
            elif sys.argv[1] in ['--help', '-h']:
                usage(0)
            else:
                sys.stderr.write('%s: Unrecognized option: %s\n' % (sys.argv[0], sys.argv[1]))
                usage(1)
            del sys.argv[1]

        self.significant_found = False
        self.messages_of_interest = []
        self.pylints = set()

    def check(self):
        '''Check command line options for suitability'''
        if WHICH_2 and WHICH_2.endswith('/python'):
            sys.stderr.write('{}: --which-2 needs a pylint, not a python'.format(sys.argv[0]))
            sys.exit(1)
        if WHICH_3 and (WHICH_3.endswith('/python') or WHICH_3.endswith('/python3')):
            sys.stderr.write('{}: --which-3 needs a pylint, not a python'.format(sys.argv[0]))
            sys.exit(1)
        if WHICH_2:
            self.pylints.add(WHICH_2)
        if WHICH_3:
            self.pylints.add(WHICH_3)
        if not self.pylints:
            sys.stderr.write('%s: No python 2.x /and/ no python 3.x.  Nothing to do.\n' % (sys.argv[0], ))
            sys.exit(1)


def check_one(options, pylint):
    '''Check one pylint'''
    traceback_count = 0
    command = ('%s ' % pylint) + \
        ('--init-hook=\'import sys; sys.path.append("%s"); sys.path.append(".")\' ' % os.path.expanduser('~/lib')) + \
        '--max-line-length=133 ' + \
        "--indent-string='    ' " + \
        '--module-rgx="[A-Za-z_][-a-zA-Z0-9_]+$" ' + \
        '--class-rgx="[A-Za-z_][-a-zA-Z0-9_]+$" ' + \
        options.to_pylint
    if options.verbose:
        sys.stderr.write('\n{}\n\n'.format(command))
    output_lines = get_output_ignore_exit_code(command)
    if options.verbose:
        sys.stderr.write('Output from {} was:\n'.format(pylint, ))
        for line in output_lines:
            sys.stderr.write('    {}\n'.format(line))
        sys.stderr.write('\n')
    if len(output_lines) == 1 and output_lines[0] == '':
        sys.stderr.write('Error, {} returned no output\n'.format(pylint))
        sys.exit(1)
    if has_traceback(output_lines):
        sys.stderr.write('\n%s: Detected %s traceback:\n' % (sys.argv[0], pylint))
        sys.stderr.write('\n'.join(output_lines))
        traceback_count += 1
    else:
        relevant_lines = [output_line for output_line in output_lines if is_relevant(output_line)]
        pruned_lines = set(remove_semi_relevant(relevant_lines))
        if pruned_lines:
            options.significant_found = True
            for relevant_line in relevant_lines:
                if relevant_line in pruned_lines:
                    prefix = b'relevant     '
                else:
                    prefix = b'semirelevant '
                options.messages_of_interest.append(prefix + relevant_line)
    return traceback_count


def main():
    '''Main function'''

    options = Options()
    options.check()

    traceback_count = 0

    for pylint in sorted(options.pylints):
        if not os.path.exists(pylint):
            sys.stderr.write('%s: pylint %s does not exist\n' % (sys.argv[0], pylint))
            sys.exit(1)
        traceback_count += check_one(options, pylint)

    if traceback_count:
        sys.stderr.write('\n%s pylint tracebacks detected\n' % traceback_count)

    if traceback_count:
        sys.exit(1)
    else:
        if options.significant_found:
            for message_of_interest in options.messages_of_interest:
                sys.stderr.write('%s\n' % message_of_interest.decode('ISO-8859-1'))
            sys.exit(1)
        else:
            sys.exit(0)


main()
