#!/usr/bin/python

# u1lint: Wrapper script for pylint or pyflakes
#
# Author: Rodney Dawes <rodney.dawes@canonical.com>
#
# Copyright 2009-2012 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the
# OpenSSL library under certain conditions as described in each
# individual source file, and distribute linked combinations
# including the two.
# You must obey the GNU General Public License in all respects
# for all of the code used other than OpenSSL.  If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so.  If you
# do not wish to do so, delete this exception statement from your
# version.  If you delete this exception statement from all source
# files in the program, then also delete it here.
"""Wrapper script for pylint command."""

import ConfigParser
import os
import subprocess
import sys

from dirspec.basedir import xdg_data_dirs

SRCDIR = os.environ.get('SRCDIR', os.getcwd())


class InvalidSetupException(Exception):
    """Raised when the env is not correctly setup."""


def find_python_installation_path():
    """Return the path where python was installed."""
    assert(sys.platform == 'win32')
    # To get the correct path of the script we need the installation path
    # of python. To get the installation path we first check on the path,
    # then read the registry.

    for path in os.getenv("Path", "").split(";"):
        if os.path.exists(os.path.join(path, "python.exe")):
            return path

    # pylint: disable=F0401
    import _winreg
    # pylint: enable=F0401
    software_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'Software')
    python_key = None
    try:
        python_key = _winreg.OpenKey(software_key, 'Python')
    # pylint: disable=E0602
    except WindowsError:
    # pylint: enable=E0602
        try:
            # look in the WoW6432node, we are running python
            # 32 on a 64 machine
            wow6432node_key = _winreg.OpenKey(software_key, 'WoW6432Node')
            python_key = _winreg.OpenKey(wow6432node_key, 'Python')
        # pylint: disable=E0602
        except WindowsError:
        # pylint: enable=E0602
            raise InvalidSetupException(
                'Could not located python installation path.')
    try:
        core_key = _winreg.OpenKey(python_key, 'PythonCore')
        # pylint: disable=E1101
        version_key = _winreg.OpenKey(core_key, sys.winver)
        # pylint: enable=E1101
        return _winreg.QueryValue(version_key, 'InstallPath')
    # pylint: disable=E0602
    except WindowsError:
    # pylint: enable=E0602
        raise InvalidSetupException(
            'Could not located python installation path.')


def find_script_path(script, python_path=None):
    """Return the path of the given script to be executed by subprocess."""
    if sys.platform == 'win32':
        if python_path is None:
            python_path = find_python_installation_path()
        # In a buildout the scripts go next to python.exe, no Scripts folder.
        if os.path.exists(os.path.join(python_path, script)):
            return os.path.join(python_path, script)
        else:
            return os.path.join(python_path, 'Scripts', script)
    else:
        # the default is to return the name of the script beacuse we expect it
        # to be executable and in the path.
        return script


def get_subprocess_start_info(script):
    """Return the basic info used by subprocess to start a script."""
    if sys.platform == 'win32':
        # the basic setup in windows is not to have python in the path and not
        # to have .py assigned to be ran with python, therefore we assume this
        # scenario
        python_path = find_python_installation_path()
        return [os.path.join(python_path, 'python.exe'),
                find_script_path(script, python_path)]
    else:
        # the default is to assume that the script is executable and that it
        # can be found in the path
        return [script, ]


def find_pylintrc():
    """Return the first pylintrc found."""
    # Use the pylintrc in the source tree if there is one
    full_name = os.path.join(SRCDIR, 'pylintrc')
    if os.path.exists(full_name):
        return full_name

    # If no pylintrc in the source tree, use the first in $XDG_DATA_DIRS
    # Hopefully this is the one we installed, and hasn't been overridden
    for name in xdg_data_dirs:
        full_name = os.path.join(name, 'ubuntuone-dev-tools', 'pylintrc')
        if os.path.exists(full_name):
            return full_name
    return None


PYLINTRC = find_pylintrc()


def _read_pylintrc_ignored():
    """Get the ignored files list from pylintrc"""
    try:
        config = ConfigParser.ConfigParser()
        config.read([PYLINTRC])

        # pylint: disable=E1103
        return [os.path.join(SRCDIR, item) for item in \
                    config.get("MASTER", "ignore").split(",")]
    except (TypeError, ConfigParser.NoOptionError):
        return []
    # pylint: enable=E1103


def _group_lines_by_file(data):
    """Format file:line:message output as lines grouped by file."""
    did_fail = False
    outputs = []
    filename = ""
    for line in data.splitlines():
        current = line.split(":", 3)
        if line.startswith("    "):
            outputs.append("    " + current[0] + "")
        elif line.startswith("build/") or len(current) < 3:
            pass
        elif filename == current[0]:
            # pylint warning W0511 is a custom note
            if not "[W0511]" in current[2]:
                did_fail = True
            outputs.append("    " + current[1] + ": " + current[2])
        elif filename != current[0]:
            filename = current[0]
            outputs.append("")
            outputs.append(filename + ":")
            # pylint warning W0511 is a custom note
            if not "[W0511]" in current[2]:
                did_fail = True
            outputs.append("    " + current[1] + ": " + current[2])

    return (did_fail, "\n".join(outputs))


def _find_files():
    """Find all Python files under the current tree."""
    pyfiles = []
    # pylint: disable=W0612
    for root, dirs, files in os.walk(SRCDIR, topdown=False):
        for filename in files:
            filepath = "%s/" % root

            # Skip files in build/
            if filepath.startswith("%s/build/" % SRCDIR):
                continue

            # Skip protobuf-generated and backup files
            if filename.endswith("_pb2.py") or filename.endswith("~"):
                continue

            if filename.endswith(".py") or filepath.endswith("bin/"):
                pyfiles.append(os.path.join(root, filename))

    pyfiles.sort()
    return pyfiles


def main(options=None, args=None):
    """Do the deed."""
    from optparse import OptionParser
    usage = '%prog [options]'
    parser = OptionParser(usage=usage)
    parser.add_option('-i', '--ignore', dest='ignored', default=None,
                       help='comma-separated paths or files, to ignore')
    (options, args) = parser.parse_args()

    failed = False
    ignored = _read_pylintrc_ignored()
    if options.ignored:
        ignored.extend([os.path.join(SRCDIR, item) for item in \
                            map(str.strip, options.ignored.split(','))])

    if os.environ.get('USE_PYFLAKES'):
        pylint_args = get_subprocess_start_info('pyflakes')
    else:
        pylint_args = get_subprocess_start_info('pylint')
        # append the extra args to the start info
        pylint_args.extend(['--output-format=parseable',
            '--include-ids=yes'])
        if PYLINTRC:
            pylint_args.append("--rcfile=" + PYLINTRC)

    for path in _find_files():
        is_build = path.startswith(os.path.join(SRCDIR, "_build"))
        is_ignored = False
        if path in ignored:
            continue
        for ignored_path in ignored:
            if path.startswith(ignored_path):
                is_ignored = True
                break
        if is_build or is_ignored:
            continue
        pylint_args.append(path)

    sp = subprocess.Popen(pylint_args,
                          bufsize=4096, stdout=subprocess.PIPE)
    notices = sp.stdout

    output = "".join(notices.readlines())
    if output != "":
        print "== Python Lint Notices =="
        (failed, grouped) = _group_lines_by_file(output)
        print grouped
        print ""

    returncode = sp.wait()
    # XXX Testing that W0511 does not cause a failure
    if failed:
        if returncode != 0:
            exit(returncode)
        else:
            exit(1)


if __name__ == '__main__':
    main()
