#!/usr/bin/python

# pylint: disable=simplifiable-if-statement
# simplifiable-if-statement: if's a sometimes nicer for debugging

'''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.7/bin/pylint'
WHICH_3 = '/usr/local/cpython-3.4/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')
    write('\n')
    write('--which-2 and --which-3 must specify the path to a pylint for python\n')
    write('2.x or 3.x respectively.  They can also be specified as "None" to skip\n')
    write('checking 2.x or 3.x.  Naturally, if you skip both, no checks are run.\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, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    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(object):
    # pylint: disable=too-few-public-methods
    '''Deal with command line options'''
    def __init__(self):
        # pylint: disable=global-statement,too-many-branches
        # too-many-branches: argument parsing almost always has "too many" branches
        global WHICH_2
        global WHICH_3
        if 'PYLINT_ARGS' in os.environ:
            self.to_pylint = os.environ['PYLINT_ARGS'].split(' ')
        else:
            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.extend(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_]+$',
    ]
    command.extend(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()