#!/usr/bin/python3

# 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 = []

DEFAULT_PYLINT_2 = '/usr/local/cpython-2.7/bin/pylint'
DEFAULT_PYLINT_3 = '/usr/local/cpython-3.6/bin/pylint'
PYLINT2_LIST = []
PYLINT3_LIST = []


def make_used(*variables):
    """Persuade linters that variables are used."""
    assert True or variables


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' % (DEFAULT_PYLINT_2, ))
    write('    --which-3 %s\n' % (DEFAULT_PYLINT_3, ))
    write('    --which-python-2 /path/to/python2\n')
    write('    --which-python-3 /path/to/python3\n')
    write('    --to-pylint args\n')
    write('\n')
    write('--which-2 and --which-3 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')
    write('\n')
    write('Instead of --which-2 or --which-3, you can give --which-python-2 or\n')
    write('--which-python-3 in which case pylint will be run via:\n')
    write('/path/to/python3 -m pylint (for example)\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()
    make_used(process.wait())
    lines = stdout.split(b'\n')
    return lines


def is_relevant_line(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(line):
    """Return True iff this line is a traceback."""
    if line.startswith(b'Traceback (most recent call last):'):
        return True
    return False


def has_traceback(lines):
    """Return True iff there is a traceback present in lines."""
    if any(is_traceback_line(line) for line in lines):
        return True
    return False


def is_usage_line(line):
    """Return True iff this line indicates a usage message."""
    # Note that this gets pylint and pylint3, because it's a substring search
    if b'Usage:  pylint' in line:
        return True
    return False


def has_usage_message(lines):
    """Return True iff there is a usage message present in lines."""
    if any(is_usage_line(line) for line in lines):
        return True
    return False


def is_score_line(line):
    """Return True iff this line has a score message."""
    if b'Your code has been rated at' in line:
        return True
    return False


def has_score_message(lines):
    """Return True iff there is a score message present in lines."""
    if any(is_score_line(line) for line in lines):
        return True
    return False


def remove_semi_relevant(lines):
    """Remove semi-relevant lines."""
    for line in lines:
        if is_semi_relevant_line(line):
            pass
        else:
            yield line


def is_semi_relevant_line(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

    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)
        mtch = ignore_regex.match(line)
        if mtch:
            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):
        """Initialize."""
        # pylint: disable=global-statement,too-many-branches,too-many-statements
        # too-many-branches: argument parsing almost always has "too many" branches
        global PYLINT2_LIST
        global PYLINT3_LIST
        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':
                    PYLINT2_LIST = None
                else:
                    if os.path.exists(sys.argv[2]):
                        if sys.argv[2].endswith('/python'):
                            tuple_ = (sys.argv[0], sys.argv[2])
                            sys.stderr.write('{}: {} ends with /python, but must be a pylint\n'.format(*tuple_))
                            sys.exit(1)
                        if sys.argv[2].endswith('/python3'):
                            tuple_ = (sys.argv[0], sys.argv[2])
                            sys.stderr.write('{}: {} ends with /python3, but must be a _pylint_ _2_\n'.format(*tuple_))
                            sys.exit(1)
                        PYLINT2_LIST = [sys.argv[2]]
                    else:
                        sys.stderr.write('{}: {} does not exist\n'.format(sys.argv[0], sys.argv[2]))
                        sys.exit(1)
                del sys.argv[1]
            elif sys.argv[1] == '--which-3':
                lower_argv_2 = sys.argv[2].lower()
                if lower_argv_2 == 'none':
                    PYLINT3_LIST = None
                else:
                    if os.path.exists(sys.argv[2]):
                        if sys.argv[2].endswith('/python') or sys.argv[2].endswith('/python3'):
                            tuple_ = (sys.argv[0], sys.argv[2])
                            string = '{}: {} ends with /python or /python3, but must be a pylint\n'
                            sys.stderr.write(string.format(*tuple_))
                            sys.exit(1)
                        PYLINT3_LIST = [sys.argv[2]]
                    else:
                        sys.stderr.write('{}: {} does not exist\n'.format(sys.argv[0], sys.argv[2]))
                        sys.exit(1)
                del sys.argv[1]
            elif sys.argv[1] == '--which-python-2':
                if os.path.exists(sys.argv[2]):
                    PYLINT2_LIST = [sys.argv[2], '-m', 'pylint']
                else:
                    sys.stderr.write('{}: {} does not exist\n'.format(sys.argv[0], sys.argv[2]))
                    sys.exit(1)
                del sys.argv[1]
            elif sys.argv[1] == '--which-python-3':
                if os.path.exists(sys.argv[2]):
                    PYLINT3_LIST = [sys.argv[2], '-m', 'pylint']
                else:
                    sys.stderr.write('{}: {} does not exist\n'.format(sys.argv[0], sys.argv[2]))
                    sys.exit(1)
                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 = list()

    def check(self):
        """Check command line options for suitability."""
        if PYLINT2_LIST is None:
            pass
        elif not PYLINT2_LIST:
            self.pylints.append([DEFAULT_PYLINT_2])
        else:
            self.pylints.append(PYLINT2_LIST)
        if PYLINT3_LIST is None:
            pass
        elif not PYLINT3_LIST:
            self.pylints.append([DEFAULT_PYLINT_3])
        else:
            self.pylints.append(PYLINT3_LIST)
        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 = pylint
    always_options = [
        ('--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(always_options)
    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{}: Detected {} traceback:\n'.format(sys.argv[0], pylint))
        sys.stderr.write('\n'.join(output_lines))
        traceback_count += 1
    elif has_usage_message(output_lines):
        sys.stderr.write('{}: {} gave usage message\n'.format(sys.argv[0], pylint))
        sys.stderr.write('\n'.join(output_lines))
        traceback_count += 1
    elif not has_score_message(output_lines):
        sys.stderr.write('{}: {} gave no score message\n'.format(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_line(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():
    """Run pylint with some secret sauce."""
    options = Options()
    options.check()

    traceback_count = 0

    for pylint in sorted(options.pylints):
        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()
